├── test ├── mocha.opts ├── fixture │ ├── deviceguider.js │ ├── framehandler.js │ ├── deviceloader.js │ ├── task.js │ ├── command.js │ ├── device.js │ └── connection.js ├── framehandlerTest.js ├── deviceloaderTest.js ├── connectionTest.js ├── deviceTest.js └── deviceguiderTest.js ├── .npmignore ├── util ├── index.js ├── string.js ├── buffer.js └── array.js ├── .travis.yml ├── .gitignore ├── stresstest ├── deviceTest.js ├── connectionTest.js ├── deviceloaderTest.js └── deviceguiderTest.js ├── lib ├── errors │ ├── deviceNotFound.js │ ├── portNotFound.js │ ├── validationError.js │ └── portAccessDenied.js ├── ftdiserial │ └── device.js ├── ftdi │ ├── deviceloader.js │ ├── eventeddeviceloader.js │ └── device.js ├── command.js ├── serial │ ├── eventeddeviceloader.js │ ├── deviceguider.js │ ├── device.js │ ├── deviceloader.js │ └── globaldeviceloader.js ├── deviceloader.js ├── task.js ├── device.js ├── usb │ └── deviceloader.js ├── framehandler.js ├── connection.js └── deviceguider.js ├── licence ├── index.js ├── package.json ├── releasenotes.md └── README.md /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -R spec -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | stresstest 2 | test 3 | coverage -------------------------------------------------------------------------------- /util/index.js: -------------------------------------------------------------------------------- 1 | require('./array'); 2 | require('./buffer'); 3 | require('./string'); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | #before_script: "npm install --dev" 2 | 3 | language: node_js 4 | node_js: 5 | - "0.10" 6 | - "0.12" 7 | - iojs 8 | 9 | branches: 10 | only: 11 | - master 12 | 13 | notifications: 14 | email: 15 | - adriano@raiano.ch 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | $ cat .gitignore 2 | 3 | # Can ignore specific files 4 | .settings.xml 5 | .monitor 6 | .DS_Store 7 | 8 | # Use wildcards as well 9 | *~ 10 | #*.swp 11 | 12 | # Can also ignore all directories and files in a directory. 13 | node_modules 14 | node_modules/**/* 15 | 16 | coverage 17 | coverage/**/* -------------------------------------------------------------------------------- /test/fixture/deviceguider.js: -------------------------------------------------------------------------------- 1 | var DeviceGuider = require('../../index').DeviceGuider, 2 | util = require('util'); 3 | 4 | function MyDeviceGuider() { 5 | 6 | // call super class 7 | DeviceGuider.call(this, require('./deviceloader').create()); 8 | } 9 | 10 | util.inherits(MyDeviceGuider, DeviceGuider); 11 | 12 | module.exports = new MyDeviceGuider(); -------------------------------------------------------------------------------- /stresstest/deviceTest.js: -------------------------------------------------------------------------------- 1 | var Device = require('../test/fixture/device'); 2 | 3 | var device = new Device(); 4 | 5 | var counterOpen = 0, 6 | counterClose = 0; 7 | 8 | (function run() { 9 | device.on('close', function() { 10 | counterClose++; 11 | console.log('closed ' + counterClose + ' times!'); 12 | }); 13 | device.on('open', function() { 14 | counterOpen++; 15 | console.log('opened ' + counterOpen + ' times!'); 16 | 17 | device.close(function() { 18 | run(); 19 | }); 20 | }); 21 | 22 | device.open(); 23 | })(); -------------------------------------------------------------------------------- /stresstest/connectionTest.js: -------------------------------------------------------------------------------- 1 | var Device = require('../test/fixture/device'); 2 | 3 | var device = new Device(); 4 | 5 | var counterCon = 0, 6 | counterDis = 0; 7 | 8 | (function run() { 9 | device.once('open', function() { 10 | device.connection.on('connect', function() { 11 | counterCon++; 12 | console.log('connected ' + counterCon + ' times!'); 13 | device.disconnect(function() { 14 | counterDis++; 15 | console.log('disconnected ' + counterDis + ' times!'); 16 | run(); 17 | }); 18 | }); 19 | }); 20 | device.connect(); 21 | })(); -------------------------------------------------------------------------------- /stresstest/deviceloaderTest.js: -------------------------------------------------------------------------------- 1 | var deviceloader = require('../test/fixture/deviceloader'); 2 | 3 | var counterPlug = 0, 4 | counterUnplug = 0; 5 | 6 | deviceloader.on('unplug', function(connection) { 7 | counterUnplug++; 8 | console.log('unplugged ' + counterUnplug + ' times!'); 9 | 10 | deviceloader.startDevices = [ new deviceloader.Device() ]; 11 | }); 12 | deviceloader.on('plug', function(connection) { 13 | counterPlug++; 14 | console.log('plugged ' + counterPlug + ' times!'); 15 | 16 | deviceloader.startDevices = []; 17 | }); 18 | 19 | deviceloader.startDevices = [ new deviceloader.Device() ]; 20 | deviceloader.startLookup(); -------------------------------------------------------------------------------- /test/fixture/framehandler.js: -------------------------------------------------------------------------------- 1 | var FrameHandler = require('../../index').FrameHandler, 2 | util = require('util'); 3 | 4 | function MyFrameHandler(device) { 5 | // call super class 6 | FrameHandler.call(this, device); 7 | } 8 | 9 | util.inherits(MyFrameHandler, FrameHandler); 10 | 11 | // MyFrameHandler.prototype.analyzeNextFrame = function(incomming) { 12 | // return incomming.splice(0); 13 | // }; 14 | 15 | // MyFrameHandler.prototype.unwrapFrame = function(frame) { 16 | // return frame; 17 | // }; 18 | 19 | // MyFrameHandler.prototype.wrapFrame = function(frame) { 20 | // return frame; 21 | // }; 22 | 23 | module.exports = MyFrameHandler; -------------------------------------------------------------------------------- /lib/errors/deviceNotFound.js: -------------------------------------------------------------------------------- 1 | // Grab the util module that's bundled with Node 2 | var util = require('util'); 3 | 4 | // Create a new custom Error constructor 5 | function DeviceNotFound(msg) { 6 | // Pass the constructor to V8's 7 | // captureStackTrace to clean up the output 8 | Error.captureStackTrace(this, DeviceNotFound); 9 | 10 | // If defined, store a custom error message 11 | if (msg) { 12 | this.message = msg; 13 | } 14 | } 15 | 16 | // Extend our custom Error from Error 17 | util.inherits(DeviceNotFound, Error); 18 | 19 | // Give our custom error a name property. Helpful for logging the error later. 20 | DeviceNotFound.prototype.name = DeviceNotFound.name; 21 | 22 | module.exports = DeviceNotFound; -------------------------------------------------------------------------------- /lib/errors/portNotFound.js: -------------------------------------------------------------------------------- 1 | // Grab the util module that's bundled with Node 2 | var util = require('util'); 3 | 4 | // Create a new custom Error constructor 5 | function PortNotFound(msg) { 6 | // Pass the constructor to V8's 7 | // captureStackTrace to clean up the output 8 | Error.captureStackTrace(this, PortNotFound); 9 | 10 | // If defined, store a custom error message 11 | if (msg) { 12 | this.message = msg; 13 | } 14 | } 15 | 16 | // Extend our custom Error from Error 17 | util.inherits(PortNotFound, Error); 18 | 19 | // Give our custom error a name property. Helpful for logging the error later. 20 | PortNotFound.prototype.name = PortNotFound.name; 21 | 22 | module.exports = PortNotFound; -------------------------------------------------------------------------------- /lib/errors/validationError.js: -------------------------------------------------------------------------------- 1 | // Grab the util module that's bundled with Node 2 | var util = require('util'); 3 | 4 | // Create a new custom Error constructor 5 | function ValidationError(msg) { 6 | // Pass the constructor to V8's 7 | // captureStackTrace to clean up the output 8 | Error.captureStackTrace(this, ValidationError); 9 | 10 | // If defined, store a custom error message 11 | if (msg) { 12 | this.message = msg; 13 | } 14 | } 15 | 16 | // Extend our custom Error from Error 17 | util.inherits(ValidationError, Error); 18 | 19 | // Give our custom error a name property. Helpful for logging the error later. 20 | ValidationError.prototype.name = ValidationError.name; 21 | 22 | module.exports = ValidationError; -------------------------------------------------------------------------------- /stresstest/deviceguiderTest.js: -------------------------------------------------------------------------------- 1 | var deviceguider = require('../test/fixture/deviceguider'); 2 | 3 | var counterCon = 0, 4 | counterDis = 0, 5 | counterPlug = 0; 6 | 7 | deviceguider.on('disconnect', function(connection) { 8 | counterDis++; 9 | console.log('disconnected ' + counterDis + ' times!'); 10 | }); 11 | 12 | deviceguider.on('plug', function(plug) { 13 | counterDis++; 14 | console.log('pluged ' + counterDis + ' times!'); 15 | }); 16 | 17 | deviceguider.on('connect', function(connection) { 18 | counterCon++; 19 | console.log('connected ' + counterCon + ' times!'); 20 | 21 | deviceguider.manualconnect(function() { 22 | deviceguider.autoconnectOne(true); 23 | }); 24 | }); 25 | 26 | deviceguider.autoconnectOne(true); -------------------------------------------------------------------------------- /lib/errors/portAccessDenied.js: -------------------------------------------------------------------------------- 1 | // Grab the util module that's bundled with Node 2 | var util = require('util'); 3 | 4 | // Create a new custom Error constructor 5 | function PortAccessDenied(msg) { 6 | // Pass the constructor to V8's 7 | // captureStackTrace to clean up the output 8 | Error.captureStackTrace(this, PortAccessDenied); 9 | 10 | // If defined, store a custom error message 11 | if (msg) { 12 | this.message = msg; 13 | } 14 | } 15 | 16 | // Extend our custom Error from Error 17 | util.inherits(PortAccessDenied, Error); 18 | 19 | // Give our custom error a name property. Helpful for logging the error later. 20 | PortAccessDenied.prototype.name = PortAccessDenied.name; 21 | 22 | module.exports = PortAccessDenied; -------------------------------------------------------------------------------- /util/string.js: -------------------------------------------------------------------------------- 1 | if (!String.prototype.toArray) { 2 | 3 | /** 4 | * Converts a hex string to a "byte" array. 5 | * @return {Array} The result array. 6 | * @example: '0100'.toArray(); // returns [0x01, 0x00] 7 | * '01-FA'.toArray(); // returns [0x01, 0xFA] 8 | */ 9 | String.prototype.toArray = function() { 10 | var str = this + ''; 11 | var gapLength = 0; 12 | 13 | if ((str.length >= 3) && (str.charAt(2) == '-')) { 14 | gapLength = 1; 15 | } else { 16 | if (str.length % 2 !== 0) { 17 | str = '0' + str; 18 | } 19 | } 20 | 21 | var result = []; 22 | for (var i = 0; i < str.length; i += (2 + gapLength)) { 23 | var val = (str[i] + str[i+1]); 24 | result.push(parseInt(val, 16)); 25 | } 26 | return result; 27 | }; 28 | } -------------------------------------------------------------------------------- /test/fixture/deviceloader.js: -------------------------------------------------------------------------------- 1 | var DeviceLoader = require('../../index').DeviceLoader, 2 | util = require('util'), 3 | _ = require('lodash'), 4 | Device = require('./device'); 5 | 6 | function MyDeviceLoader() { 7 | var self = this; 8 | 9 | // call super class 10 | DeviceLoader.call(this); 11 | 12 | this.Device = Device; 13 | 14 | this.startDevices = [ 15 | new Device(), 16 | new Device() 17 | ]; 18 | } 19 | 20 | util.inherits(MyDeviceLoader, DeviceLoader); 21 | 22 | MyDeviceLoader.prototype.lookup = function(callback) { 23 | var devices = this.startDevices; 24 | try { 25 | this.emit('lookup'); 26 | } catch(e) { 27 | } 28 | callback(null, devices); 29 | }; 30 | 31 | module.exports = new MyDeviceLoader(); 32 | module.exports.create = function() { 33 | return new MyDeviceLoader(); 34 | }; -------------------------------------------------------------------------------- /test/fixture/task.js: -------------------------------------------------------------------------------- 1 | var Task = require('../../index').Task, 2 | util = require('util'), 3 | Command = require('./command'); 4 | 5 | function MyTask(identifier) { 6 | // call super class 7 | Task.call(this, arguments); 8 | } 9 | 10 | util.inherits(MyTask, Task); 11 | 12 | MyTask.prototype.argumentsSchema = { 13 | type: 'array', 14 | minItems: 0, 15 | items: [ 16 | { 17 | }, 18 | { 19 | type: 'string' 20 | } 21 | ] 22 | }; 23 | 24 | MyTask.prototype.initialize = function(connection, identifier) { 25 | if (identifier === 111) { 26 | throw new Error('wrong value in task'); 27 | } 28 | 29 | this.command = new Command(identifier); 30 | }; 31 | 32 | MyTask.prototype.perform = function(connection, callback) { 33 | this.execute(this.command, connection, callback); 34 | }; 35 | 36 | module.exports = MyTask; -------------------------------------------------------------------------------- /util/buffer.js: -------------------------------------------------------------------------------- 1 | if (!Buffer.prototype.toHexDebug) { 2 | 3 | /** 4 | * Converts a buffer object to a readable hex string. 5 | * @return {String} The result hex string. 6 | * 7 | * @example: 8 | * (new Buffer([0x01, 0x00])).toHexDebug(); // returns '01-00' 9 | */ 10 | Buffer.prototype.toHexDebug = function() { 11 | var str = this.toString('hex'); 12 | var res = ''; 13 | for (var i = 0, len = str.length; i < len; i += 2) { 14 | if (i > 0) { 15 | res += '-'; 16 | } 17 | res += str[i] + str[i+ 1]; 18 | } 19 | return res.toUpperCase(); 20 | }; 21 | } 22 | 23 | if (!Buffer.prototype.toArray) { 24 | 25 | /** 26 | * Converts a buffer object to a "byte" array. 27 | * @return {Array} The result array. 28 | * 29 | * @example: 30 | * (new Buffer([0x01, 0x00])).toArray(); // returns [0x01, 0x00] 31 | */ 32 | Buffer.prototype.toArray = function() { 33 | return Array.prototype.slice.call(this, 0); 34 | }; 35 | } -------------------------------------------------------------------------------- /test/fixture/command.js: -------------------------------------------------------------------------------- 1 | var Command = require('../../index').Command, 2 | util = require('util'); 3 | 4 | function MyCommand(firstByte) { 5 | // call super class 6 | Command.call(this, arguments); 7 | } 8 | 9 | util.inherits(MyCommand, Command); 10 | 11 | MyCommand.prototype.argumentsSchema = { 12 | type: 'array', 13 | items: [ 14 | { 15 | anyOf: [ 16 | { 17 | type: 'number' 18 | }, 19 | { 20 | type: 'undefined' 21 | } 22 | ] 23 | }, 24 | { 25 | type: 'string' 26 | } 27 | ] 28 | }; 29 | 30 | MyCommand.prototype.initialize = function(connection, firstByte) { 31 | 32 | firstByte = firstByte || 0x01; 33 | 34 | if (firstByte < 0) { 35 | throw new Error('wrong value'); 36 | } 37 | 38 | this.data = [firstByte, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]; 39 | }; 40 | 41 | MyCommand.prototype.execute = function(connection, callback) { 42 | connection.executeCommand(this, callback); 43 | }; 44 | 45 | module.exports = MyCommand; -------------------------------------------------------------------------------- /licence: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Adriano Raiano 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 deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | 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 FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/fixture/device.js: -------------------------------------------------------------------------------- 1 | var Device = require('../../index').Device, 2 | Connection = require('./connection'), 3 | util = require('util'), 4 | _ = require('lodash'); 5 | 6 | function MyDevice(withoutConnection) { 7 | var Conn = withoutConnection ? null : Connection; 8 | // call super class 9 | Device.call(this, Conn); 10 | } 11 | 12 | util.inherits(MyDevice, Device); 13 | 14 | MyDevice.prototype.open = function(callback) { 15 | var self = this; 16 | 17 | if (this.log) this.log('open device with id ' + this.id); 18 | 19 | setTimeout(function() { 20 | self.emit('open', callback); 21 | if (!self.connection && callback) callback(); 22 | }, 10); 23 | 24 | this.on('send', function(data) { 25 | setTimeout(function() { 26 | self.emit('receive', data); 27 | }, 5); 28 | }); 29 | }; 30 | 31 | MyDevice.prototype.close = function(callback, fire) { 32 | var self = this; 33 | 34 | if (this.log) this.log('close device with id ' + this.id); 35 | 36 | setTimeout(function() { 37 | self.emit('close', callback); 38 | self.removeAllListeners(); 39 | if (callback && (!self.connection || fire)) callback(null); 40 | }, 10); 41 | }; 42 | 43 | module.exports = MyDevice; -------------------------------------------------------------------------------- /test/framehandlerTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'), 2 | FrameHandler = require('./fixture/framehandler'), 3 | Device = require('./fixture/device'); 4 | 5 | describe('FrameHandler', function() { 6 | 7 | after(function() { 8 | var pm; 9 | if ((pm = global['pm-notify'])) { 10 | pm.stopMonitoring(); 11 | } 12 | var detection; 13 | if ((detection = global['usb-detection'])) { 14 | detection.stopMonitoring(); 15 | } 16 | }); 17 | 18 | var device = new Device(); 19 | var framehandler = new FrameHandler(device); 20 | 21 | before(function(done) { 22 | device.open(done); 23 | }); 24 | 25 | describe('emitting send', function() { 26 | 27 | it('it should emit send on device', function(done) { 28 | 29 | device.once('send', function() { 30 | done(); 31 | }); 32 | framehandler.emit('send', []); 33 | 34 | }); 35 | 36 | }); 37 | 38 | describe('calling send', function() { 39 | 40 | it('it should emit send on device', function(done) { 41 | 42 | device.once('send', function() { 43 | done(); 44 | }); 45 | framehandler.send([]); 46 | 47 | }); 48 | 49 | }); 50 | 51 | describe('receiving data from device', function() { 52 | 53 | it('it should emit receive', function(done) { 54 | 55 | framehandler.once('receive', function() { 56 | done(); 57 | }); 58 | device.send([]); 59 | 60 | }); 61 | 62 | }); 63 | 64 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var index; 2 | 3 | if (typeof module.exports !== 'undefined') { 4 | index = module.exports; 5 | } else { 6 | index = root.index = {}; 7 | } 8 | 9 | index.version = require('./package.json').version; 10 | 11 | require('./util/index'); 12 | 13 | index.Device = require('./lib/device'); 14 | index.Connection = require('./lib/connection'); 15 | index.FrameHandler = require('./lib/framehandler'); 16 | index.DeviceLoader = require('./lib/deviceloader'); 17 | index.DeviceGuider = require('./lib/deviceguider'); 18 | index.Command = require('./lib/command'); 19 | index.Task = require('./lib/task'); 20 | 21 | var tv4 = require('tv4'); 22 | index.addAdditionalValidationSchema = function(ref, schema) { 23 | tv4.addSchema(ref, schema); 24 | }; 25 | index.addAdditionalFormatForValidationSchemas = function(ref, fn) { 26 | tv4.addFormat(ref, fn); 27 | }; 28 | 29 | 30 | try { 31 | index.FtdiDevice = require('./lib/ftdi/device'); 32 | index.FtdiDeviceLoader = require('./lib/ftdi/deviceloader'); 33 | index.EventedFtdiDeviceLoader = require('./lib/ftdi/eventeddeviceloader'); 34 | } catch(e) { 35 | console.log(e.message); 36 | } 37 | 38 | try { 39 | index.SerialDevice = require('./lib/serial/device'); 40 | index.SerialDeviceLoader = require('./lib/serial/deviceloader'); 41 | index.EventedSerialDeviceLoader = require('./lib/serial/eventeddeviceloader'); 42 | index.SerialDeviceGuider = require('./lib/serial/deviceguider'); 43 | } catch(e) { 44 | console.log(e.message); 45 | } 46 | 47 | try { 48 | index.FtdiSerialDevice = require('./lib/ftdiserial/device'); 49 | } catch(e) { 50 | console.log(e.message); 51 | } 52 | 53 | try { 54 | index.UsbDeviceLoader = require('./lib/usb/deviceloader'); 55 | } catch(e) { 56 | console.log(e.message); 57 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devicestack", 3 | "version": "1.10.11", 4 | "description": "This module helps you to represent a device and its protocol.", 5 | "private": false, 6 | "main": "index.js", 7 | "engines": { 8 | "node": ">=0.8.x", 9 | "npm": ">=1.1.x" 10 | }, 11 | "directories": { 12 | "lib": "./lib", 13 | "util": "./util" 14 | }, 15 | "dependencies": { 16 | "async": "=1.0.0", 17 | "debug": "=2.2.0", 18 | "eventemitter2": "=0.4.14", 19 | "lodash": "=3.9.3", 20 | "node-uuid": "=1.4.3", 21 | "tv4": "=1.1.9" 22 | }, 23 | "optionalDependencies": { 24 | "serialport": "=1.7.1", 25 | "ftdi": "=1.1.0", 26 | "usb-detection": "=1.1.0", 27 | "pm-notify": "=1.0.2" 28 | }, 29 | "devDependencies": { 30 | "mocha": ">=1.0.1", 31 | "expect.js": ">=0.1.2", 32 | "istanbul": ">=0.1.44" 33 | }, 34 | "scripts": { 35 | "test": "mocha", 36 | "cov": "istanbul cover _mocha" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git@github.com:adrai/devicestack.git" 41 | }, 42 | "keywords": [ 43 | "device", 44 | "devicestack", 45 | "stack", 46 | "connection", 47 | "communication", 48 | "serialport", 49 | "usb", 50 | "framehandler", 51 | "protocol", 52 | "framing", 53 | "batchprocessing", 54 | "frame", 55 | "batch", 56 | "command", 57 | "task" 58 | ], 59 | "author": "adrai", 60 | "maintainers": [ 61 | { 62 | "name": "adrai", 63 | "email": "adriano@raiano.ch" 64 | } 65 | ], 66 | "homepage": "https://github.com/adrai/devicestack", 67 | "bugs": { 68 | "url": "https://github.com/adrai/devicestack/issues" 69 | }, 70 | "license": { 71 | "type": "MIT", 72 | "url": "https://raw.github.com/adrai/devicestack/master/licence" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/fixture/connection.js: -------------------------------------------------------------------------------- 1 | var Connection = require('../../index').Connection, 2 | util = require('util'), 3 | _ = require('lodash'), 4 | FrameHandler = require('./framehandler'); 5 | 6 | function MyConnection(device) { 7 | // call super class 8 | Connection.call(this, device); 9 | 10 | var self = this; 11 | 12 | this.frameHandler = new FrameHandler(this.device); 13 | this.frameHandler.on('receive', function (frame) { 14 | // forward to appropriate command... 15 | var cmds = self.getWaitingCommands(frame); 16 | _.each(cmds, function(cmd) { 17 | if (cmd.callback) { 18 | cmd.callback(null, frame); 19 | delete cmd.callback; 20 | } 21 | self.commandQueue.splice(_.indexOf(self.commandQueue, cmd), 1); 22 | }); 23 | 24 | self.checkForNextExecution(); 25 | }); 26 | } 27 | 28 | util.inherits(MyConnection, Connection); 29 | 30 | // MyConnection.prototype.onConnecting = function(callback) { 31 | // // Need to send some commands before definitely connected? 32 | // if (callback) callback(); 33 | // }; 34 | 35 | // MyConnection.prototype.onDisconnecting = function(callback) { 36 | // // Need to send some commands before definitely closed? 37 | // if (callback) callback(); 38 | // }; 39 | 40 | MyConnection.prototype.getWaitingCommands = function(frame) { 41 | return _.filter(this.commandQueue, function(cmd) { 42 | return frame && frame.length > 0 && frame[0] === cmd.command.data[0]; 43 | }); 44 | }; 45 | 46 | MyConnection.prototype.sendCommand = function(command, callback) { 47 | command.data = command.data || []; 48 | if (!Array.isByteArray(command.data)) { 49 | this.dequeueCommand(command); 50 | if (this.log) { this.log('Wrong data for COMMAND: ' + command.constructor.name); } 51 | if (callback) { callback(new Error('Wrong command data!')); } 52 | return; 53 | } 54 | this.frameHandler.send(command.data); 55 | }; 56 | 57 | module.exports = MyConnection; -------------------------------------------------------------------------------- /util/array.js: -------------------------------------------------------------------------------- 1 | if (!Array.prototype.toHexDebug) { 2 | 3 | /** 4 | * Converts a "byte" array to a readable hex string. 5 | * @return {String} The result string. 6 | * 7 | * @example: 8 | * [0x01, 0x00].toHexDebug(); // returns '01-00' 9 | */ 10 | Array.prototype.toHexDebug = function() { 11 | var res = ''; 12 | for (var i = 0, len = this.length; i < len; i++) { 13 | if (i > 0) { 14 | res += '-'; 15 | } 16 | var hex = this[i].toString(16); 17 | hex = hex.length < 2 ? '0' + hex : hex; 18 | res += hex; 19 | } 20 | return res.toUpperCase(); 21 | }; 22 | } 23 | 24 | if (!Array.prototype.toHexString) { 25 | 26 | /** 27 | * Converts a "byte" array to a hex string. 28 | * @return {String} The result array. 29 | * 30 | * @example: 31 | * [0x01, 0x00].toHexString(); // returns '0100' 32 | */ 33 | Array.prototype.toHexString = function() { 34 | var res = ''; 35 | for (var i = 0, len = this.length; i < len; i++) { 36 | var hex = this[i].toString(16); 37 | hex = hex.length < 2 ? '0' + hex : hex; 38 | res += hex; 39 | } 40 | return res.toUpperCase(); 41 | }; 42 | } 43 | 44 | if (!Array.prototype.toBuffer) { 45 | 46 | /** 47 | * Converts a "byte" array to a buffer object. 48 | * @return {Buffer} The result buffer object. 49 | * 50 | * @example: 51 | * [0x01, 0x00].toBuffer(); 52 | */ 53 | Array.prototype.toBuffer = function() { 54 | return new Buffer(this); 55 | }; 56 | } 57 | 58 | if(!Array.isArray) { 59 | 60 | Array.isArray = function (vArg) { 61 | return Object.prototype.toString.call(vArg) === "[object Array]"; 62 | }; 63 | 64 | } 65 | 66 | if (!Array.isByteArray) { 67 | 68 | /** 69 | * Checks if the passed argument is an array that contains byte values. 70 | * 71 | * @param {Array} data The array to be checked. 72 | * @return {Boolean} True if it's a byte array, otherwise false. 73 | * 74 | * @example: 75 | * Array.isByteArray([0x01, 0x00]) // returns true 76 | */ 77 | Array.isByteArray = function(data) { 78 | 79 | if (!data || !Array.isArray(data)) { 80 | return false; 81 | } 82 | 83 | for (var i = 0, len = data.length; i < len; i++) { 84 | if (typeof(data[i]) !== 'number' || data[i] < 0x00 || data[i] > 0xFF) { 85 | return false; 86 | } 87 | } 88 | 89 | return true; 90 | 91 | }; 92 | 93 | } -------------------------------------------------------------------------------- /lib/ftdiserial/device.js: -------------------------------------------------------------------------------- 1 | var Device = require('../device'), 2 | SerialDevice = require('../serial/device'), 3 | FtdiDevice = require('../ftdi/device'), 4 | util = require('util'), 5 | _ = require('lodash'); 6 | 7 | /** 8 | * FtdiSerialDevice represents your physical device. 9 | * Extends Device. 10 | * @param {Object} deviceSettings The device settings (locationId, serial, index, description) or (portName). 11 | * @param {Object} connectionSettings The connection settings (baudrate, databits, stopbits, parity). 12 | * @param {Object} Connection The constructor function of the connection. 13 | */ 14 | function FtdiSerialDevice(deviceSettings, connectionSettings, Connection) { 15 | 16 | if (_.isString(deviceSettings) && (deviceSettings.indexOf('COM') === 0 || deviceSettings.indexOf('/') === 0)) { 17 | this.isSerialDevice = true; 18 | // call super class 19 | SerialDevice.call(this, 20 | deviceSettings, 21 | connectionSettings, 22 | Connection 23 | ); 24 | } else { 25 | this.isFtdiDevice = true; 26 | // call super class 27 | FtdiDevice.call(this, 28 | deviceSettings, 29 | connectionSettings, 30 | Connection 31 | ); 32 | } 33 | } 34 | 35 | util.inherits(FtdiSerialDevice, Device); 36 | 37 | /** 38 | * The open mechanism of the device. 39 | * On opened 'open' will be emitted and the callback will be called. 40 | * @param {Function} callback The function, that will be called when device is opened. [optional] 41 | * `function(err){}` 42 | */ 43 | FtdiSerialDevice.prototype.open = function(callback) { 44 | if (this.isFtdiDevice) { 45 | FtdiDevice.prototype.open.apply(this, _.toArray(arguments)); 46 | } else if (this.isSerialDevice) { 47 | SerialDevice.prototype.open.apply(this, _.toArray(arguments)); 48 | } 49 | }; 50 | 51 | /** 52 | * The close mechanism of the device. 53 | * @param {Function} callback The function, that will be called when device is closed. [optional] 54 | * `function(err){}` 55 | * @param {Boolean} fire Forces the callnack to be called. [optional] 56 | */ 57 | FtdiSerialDevice.prototype.close = function(callback, fire) { 58 | if (this.isFtdiDevice) { 59 | FtdiDevice.prototype.close.apply(this, _.toArray(arguments)); 60 | } else if (this.isSerialDevice) { 61 | SerialDevice.prototype.close.apply(this, _.toArray(arguments)); 62 | } 63 | }; 64 | 65 | module.exports = FtdiSerialDevice; -------------------------------------------------------------------------------- /lib/ftdi/deviceloader.js: -------------------------------------------------------------------------------- 1 | var ftdi = require("ftdi"), 2 | DeviceLoader = require('../deviceloader'), 3 | util = require('util'), 4 | _ = require('lodash'), 5 | async = require('async'); 6 | 7 | /** 8 | * An FtdiDeviceLoader can check if there are available some ftdi devices. 9 | * @param {Object} Device Device The constructor function of the device. 10 | * @param {Number || Array} vendorId The vendor id or an array of vid/pid pairs. 11 | * @param {Number} productId The product id or optional. 12 | */ 13 | function FtdiDeviceLoader(Device, vendorId, productId) { 14 | 15 | // call super class 16 | DeviceLoader.call(this); 17 | 18 | this.Device = Device; 19 | 20 | this.vidPidPairs = []; 21 | 22 | if (!productId && _.isArray(vendorId)) { 23 | this.vidPidPairs = vendorId; 24 | } else { 25 | this.vidPidPairs = [{vendorId: vendorId, productId: productId}]; 26 | } 27 | } 28 | 29 | util.inherits(FtdiDeviceLoader, DeviceLoader); 30 | 31 | /** 32 | * Calls the callback with an array of devices. 33 | * @param {Function} callback The function, that will be called when finished lookup. 34 | * `function(err, devices){}` devices is an array of Device objects. 35 | */ 36 | FtdiDeviceLoader.prototype.lookup = function(callback) { 37 | var self = this; 38 | 39 | var result = []; 40 | async.forEach(this.vidPidPairs, function(pair, callback) { 41 | ftdi.find(pair.vendorId, pair.productId, function(err, ports) { 42 | if (err) { 43 | if (!err.name) { 44 | err = new Error(err); 45 | } 46 | return callback(err); 47 | } 48 | 49 | var resPorts = ports; 50 | if (self.filter) { 51 | resPorts = self.filter(ports); 52 | } 53 | 54 | var devices = _.map(resPorts, function(p) { 55 | var found = _.find(self.oldDevices, function(dev) { 56 | return dev.get('locationId') === p.locationId && 57 | dev.get('vendorId') === p.vendorId && 58 | dev.get('productId') === p.productId && 59 | dev.get('serialNumber') === p.serialNumber; 60 | }); 61 | if (found) { 62 | return found; 63 | } else { 64 | return new self.Device(p); 65 | } 66 | }) || []; 67 | 68 | result = result.concat(devices); 69 | callback(null); 70 | }); 71 | }, function(err) { 72 | if (err && !err.name) { 73 | err = new Error(err); 74 | } 75 | callback(err, result); 76 | }); 77 | }; 78 | 79 | module.exports = FtdiDeviceLoader; -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | tv4 = require('tv4'), 3 | ValidationError = require('./errors/validationError'); 4 | 5 | function Command() { 6 | var self = this; 7 | 8 | this.args = arguments; 9 | if (arguments.length === 1 && _.isArguments(arguments[0])) { 10 | this.args = arguments[0]; 11 | } 12 | 13 | if (this.log) { 14 | this.log = _.wrap(this.log, function(func, msg) { 15 | func(self.constructor.name + ': ' + msg); 16 | }); 17 | } else if (Command.prototype.log) { 18 | Command.prototype.log = _.wrap(Command.prototype.log, function(func, msg) { 19 | func(self.constructor.name + ': ' + msg); 20 | }); 21 | this.log = Command.prototype.log; 22 | } else { 23 | var debug = require('debug')(this.constructor.name); 24 | this.log = function(msg) { 25 | debug(msg); 26 | }; 27 | } 28 | } 29 | 30 | /** 31 | * Executes this command. 32 | * 33 | * @param {Connection} connection The connection object. 34 | * @param {Function} callback The function, that should be called when the command's answer is received. 35 | * `function(err){}` 36 | */ 37 | Command.prototype.execute = function(connection, callback) { 38 | connection.executeCommand(this, callback); 39 | }; 40 | 41 | /** 42 | * If the initialize function is present it will pass in the arguments passed in the constructor. 43 | * If necessary for validation reason the initialize function can throw errors. 44 | * 45 | * @param {Connection} connection The connection object. 46 | */ 47 | Command.prototype._initialize = function(connection) { 48 | if (this.initialize) { 49 | var args = _.toArray(this.args); 50 | 51 | if (this.argumentsSchema) { 52 | var validation = tv4.validateResult(args, this.argumentsSchema); 53 | 54 | if (validation.missing.length > 0) { 55 | throw new Error('Validation schema "' + validation.missing[0] + '" missing!'); 56 | } 57 | 58 | if (!validation.valid) { 59 | if (validation.error.subErrors && validation.error.subErrors.length > 0) { 60 | validation.error.subErrors = _.sortBy(validation.error.subErrors, function(s) { 61 | return -s.code; 62 | }); 63 | throw new ValidationError(validation.error.subErrors[0].dataPath + ' => ' + validation.error.subErrors[0].message); 64 | } else { 65 | throw new ValidationError(validation.error.dataPath + ' => ' + validation.error.message); 66 | } 67 | } 68 | } 69 | 70 | this.initialize.apply(this, [connection].concat(args)); 71 | } 72 | }; 73 | 74 | module.exports = Command; -------------------------------------------------------------------------------- /lib/ftdi/eventeddeviceloader.js: -------------------------------------------------------------------------------- 1 | var FtdiDeviceLoader = require('./deviceloader'), 2 | util = require('util'), 3 | _ = require('lodash'), 4 | monitor; 5 | 6 | /** 7 | * An FtdiDeviceLoader can check if there are available some ftdi devices. 8 | * @param {Object} Device Device The constructor function of the device. 9 | * @param {Number || Array} vendorId The vendor id or an array of vid/pid pairs. 10 | * @param {Number} productId The product id or optional. 11 | */ 12 | function EventedFtdiDeviceLoader(Device, vendorId, productId) { 13 | 14 | // call super class 15 | FtdiDeviceLoader.call(this, Device, vendorId, productId); 16 | } 17 | 18 | util.inherits(EventedFtdiDeviceLoader, FtdiDeviceLoader); 19 | 20 | /** 21 | * Starts to lookup. 22 | * @param {Function} callback The function, that will be called when trigger has started. [optional] 23 | * `function(err, devices){}` devices is an array of Device objects. 24 | */ 25 | EventedFtdiDeviceLoader.prototype.startLookup = function(interval, callback) { 26 | this.isRunning = true; 27 | 28 | if (!monitor) { 29 | try { 30 | monitor = require('usb-detection'); 31 | } catch(e) { 32 | console.log(e.message); 33 | throw e; 34 | } 35 | } 36 | 37 | if (!callback && _.isFunction(interval)) { 38 | callback = interval; 39 | interval = null; 40 | } 41 | 42 | var self = this; 43 | interval = interval || 500; 44 | 45 | this.oldDevices = []; 46 | 47 | _.each(this.vidPidPairs, function(pair) { 48 | monitor.on('add:' + pair.vendorId + ':' + pair.productId, self.addHandle = function(dev) { 49 | var isTriggering = false; 50 | var intervalId = setInterval(function() { 51 | if (isTriggering) { 52 | return; 53 | } 54 | isTriggering = true; 55 | 56 | self.once('plug', function() { 57 | clearInterval(intervalId); 58 | }); 59 | self.trigger(function() { 60 | isTriggering = false; 61 | }); 62 | }, interval); 63 | }); 64 | 65 | monitor.on('remove:' + pair.vendorId + ':' + pair.productId, self.removeHandle = function(dev) { 66 | self.trigger(); 67 | }); 68 | }); 69 | 70 | this.trigger(callback); 71 | }; 72 | 73 | /** 74 | * Stops the interval that calls trigger function. 75 | */ 76 | EventedFtdiDeviceLoader.prototype.stopLookup = function() { 77 | _.each(this.vidPidPairs, function(pair) { 78 | monitor.removeListener('add:' + pair.vendorId + ':' + pair.productId, self.addHandle); 79 | monitor.removeListener('remove:' + pair.vendorId + ':' + pair.productId, self.removeHandle); 80 | }); 81 | this.isRunning = false; 82 | }; 83 | 84 | module.exports = EventedFtdiDeviceLoader; -------------------------------------------------------------------------------- /lib/serial/eventeddeviceloader.js: -------------------------------------------------------------------------------- 1 | var SerialDeviceLoader = require('./deviceloader'), 2 | util = require('util'), 3 | _ = require('lodash'), 4 | monitor; 5 | 6 | /** 7 | * An EventedSerialDeviceLoader can check if there are available some serial devices. 8 | * @param {Object} Device Device The constructor function of the device. 9 | * @param {Number || Array} vendorId The vendor id or an array of vid/pid pairs. 10 | * @param {Number} productId The product id or optional. 11 | */ 12 | function EventedSerialDeviceLoader(Device, vendorId, productId) { 13 | 14 | // call super class 15 | SerialDeviceLoader.call(this, Device, false); 16 | 17 | this.vidPidPairs = []; 18 | 19 | if (!productId && _.isArray(vendorId)) { 20 | this.vidPidPairs = vendorId; 21 | } else { 22 | this.vidPidPairs = [{vendorId: vendorId, productId: productId}]; 23 | } 24 | } 25 | 26 | util.inherits(EventedSerialDeviceLoader, SerialDeviceLoader); 27 | 28 | /** 29 | * Starts to lookup. 30 | * @param {Function} callback The function, that will be called when trigger has started. [optional] 31 | * `function(err, devices){}` devices is an array of Device objects. 32 | */ 33 | EventedSerialDeviceLoader.prototype.startLookup = function(interval, callback) { 34 | this.isRunning = true; 35 | 36 | if (!monitor) { 37 | try { 38 | monitor = require('usb-detection'); 39 | } catch(e) { 40 | console.log(e.message); 41 | throw e; 42 | } 43 | } 44 | 45 | if (!callback && _.isFunction(interval)) { 46 | callback = interval; 47 | interval = null; 48 | } 49 | 50 | var self = this; 51 | interval = interval || 500; 52 | 53 | this.oldDevices = []; 54 | 55 | _.each(this.vidPidPairs, function(pair) { 56 | monitor.on('add:' + pair.vendorId + ':' + pair.productId, self.addHandle = function(dev) { 57 | var isTriggering = false; 58 | var intervalId = setInterval(function() { 59 | if (isTriggering) { 60 | return; 61 | } 62 | isTriggering = true; 63 | 64 | self.once('plug', function() { 65 | clearInterval(intervalId); 66 | }); 67 | self.trigger(function() { 68 | isTriggering = false; 69 | }); 70 | }, interval); 71 | }); 72 | 73 | monitor.on('remove:' + pair.vendorId + ':' + pair.productId, self.removeHandle = function(dev) { 74 | self.trigger(); 75 | }); 76 | }); 77 | 78 | this.trigger(callback); 79 | }; 80 | 81 | /** 82 | * Stops the interval that calls trigger function. 83 | */ 84 | EventedSerialDeviceLoader.prototype.stopLookup = function() { 85 | _.each(this.vidPidPairs, function(pair) { 86 | monitor.removeListener('add:' + pair.vendorId + ':' + pair.productId, self.addHandle); 87 | monitor.removeListener('remove:' + pair.vendorId + ':' + pair.productId, self.removeHandle); 88 | }); 89 | this.isRunning = false; 90 | }; 91 | 92 | module.exports = EventedSerialDeviceLoader; -------------------------------------------------------------------------------- /test/deviceloaderTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'), 2 | deviceloader = require('./fixture/deviceloader').create(); 3 | 4 | describe('DeviceLoader', function() { 5 | 6 | after(function() { 7 | var pm; 8 | if ((pm = global['pm-notify'])) { 9 | pm.stopMonitoring(); 10 | } 11 | var detection; 12 | if ((detection = global['usb-detection'])) { 13 | detection.stopMonitoring(); 14 | } 15 | }); 16 | 17 | describe('having a deviceloader object', function() { 18 | 19 | it('it should have all expected values', function() { 20 | 21 | expect(deviceloader.lookup).to.be.a('function'); 22 | expect(deviceloader.trigger).to.be.a('function'); 23 | expect(deviceloader.startLookup).to.be.a('function'); 24 | expect(deviceloader.stopLookup).to.be.a('function'); 25 | 26 | }); 27 | 28 | describe('calling lookup', function() { 29 | 30 | it('it should return a device array', function(done) { 31 | 32 | deviceloader.lookup(function(err, devices) { 33 | expect(devices).to.be.an('array'); 34 | done(); 35 | }); 36 | 37 | }); 38 | 39 | }); 40 | 41 | describe('calling trigger', function() { 42 | 43 | it('it should call lookup', function(done) { 44 | 45 | deviceloader.once('lookup', function(err, devices) { 46 | done(); 47 | }); 48 | deviceloader.trigger(); 49 | 50 | }); 51 | 52 | it('it should return the devices', function(done) { 53 | 54 | deviceloader.trigger(function(err, devices) { 55 | expect(devices).to.be.an('array'); 56 | expect(devices).to.have.length(2); 57 | done(); 58 | }); 59 | 60 | }); 61 | 62 | }); 63 | 64 | describe('calling startLookup', function() { 65 | 66 | beforeEach(function() { 67 | deviceloader.stopLookup(); 68 | }); 69 | 70 | it('it should emit plug when a device is added', function(done) { 71 | 72 | deviceloader.once('plug', function(device) { 73 | done(); 74 | }); 75 | deviceloader.startLookup(10); 76 | 77 | }); 78 | 79 | it('it should emit unplug when a device is removed', function(done) { 80 | 81 | deviceloader.once('unplug', function(device) { 82 | done(); 83 | }); 84 | deviceloader.startLookup(10); 85 | setTimeout(function() { 86 | deviceloader.startDevices = [deviceloader.startDevices.slice(0, 1)]; 87 | }, 20); 88 | 89 | }); 90 | 91 | }); 92 | 93 | describe('calling stopLookup', function() { 94 | 95 | before(function() { 96 | deviceloader.stopLookup(); 97 | }); 98 | 99 | it('it should stop calling lookup', function(done) { 100 | 101 | deviceloader.once('lookup', function() { 102 | deviceloader.stopLookup(); 103 | deviceloader.once('lookup', function() { 104 | expect(undefined).to.be.ok(); 105 | }); 106 | setTimeout(function() { 107 | done(); 108 | }, 20); 109 | }); 110 | deviceloader.startLookup(10); 111 | 112 | }); 113 | 114 | }); 115 | 116 | }); 117 | 118 | }); -------------------------------------------------------------------------------- /lib/serial/deviceguider.js: -------------------------------------------------------------------------------- 1 | var DeviceGuider = require('../deviceguider'), 2 | util = require('util'), 3 | _ = require('lodash'), 4 | DeviceNotFound = require('../errors/deviceNotFound'); 5 | 6 | /** 7 | * A serialdeviceguider emits 'plug' for new attached serial devices, 8 | * 'unplug' for removed serial devices, emits 'connect' for connected serial devices 9 | * and emits 'disconnect' for disconnected serial devices. 10 | * @param {SerialDeviceLoader} deviceLoader The deviceloader object. 11 | */ 12 | function SerialDeviceGuider(deviceLoader) { 13 | var self = this; 14 | 15 | // call super class 16 | DeviceGuider.call(this, deviceLoader); 17 | 18 | this.currentState.getDeviceByPort = function(port) { 19 | return _.find(self.currentState.plugged, function(d) { 20 | return d.get('portName') && port && d.get('portName').toLowerCase() === port.toLowerCase(); 21 | }); 22 | }; 23 | 24 | this.currentState.getConnectedDeviceByPort = function(port) { 25 | return _.find(self.currentState.connected, function(d) { 26 | return d.get('portName') && port && d.get('portName').toLowerCase() === port.toLowerCase(); 27 | }); 28 | }; 29 | } 30 | 31 | util.inherits(SerialDeviceGuider, DeviceGuider); 32 | 33 | /** 34 | * It will connect that device and call the callback. 35 | * @param {String} port The Com Port path or name for Windows. 36 | * @param {Function} callback The function, that will be called when the device is connected. [optional] 37 | * `function(err, connection){}` connection is a Connection object. 38 | */ 39 | SerialDeviceGuider.prototype.connect = function(port, callback) { 40 | if (_.isObject(port) && port.portName) { 41 | port = port.portName; 42 | } 43 | if (!_.isString(port) || (port.indexOf('COM') !== 0 && port.indexOf('/') !== 0)) { 44 | return DeviceGuider.prototype.connect.apply(this, arguments); 45 | } 46 | 47 | var device = this.currentState.getDeviceByPort(port); 48 | if (!device) { 49 | device = new this.deviceLoader.Device(port); 50 | } 51 | if (!this.deviceErrorSubscriptions[device.id]) { 52 | this.deviceErrorSubscriptions[device.id] = function(err) { 53 | if (self.listeners('error').length) { 54 | self.emit('error', err); 55 | } 56 | }; 57 | this.on('error', this.deviceErrorSubscriptions[device.id]); 58 | } 59 | this.connect(device, callback); 60 | }; 61 | 62 | /** 63 | * It will connect that device and call the callback. 64 | * @param {String} port The Com Port path or name for Windows. 65 | * @param {Function} callback The function, that will be called when the device is disconnected. [optional] 66 | * `function(err, connection){}` connection is a Connection object. 67 | */ 68 | SerialDeviceGuider.prototype.disconnect = function(port, callback) { 69 | if (_.isObject(port) && port.portName) { 70 | port = port.portName; 71 | } 72 | if (!_.isString(port) || (port.indexOf('COM') !== 0 && port.indexOf('/') !== 0)) { 73 | return DeviceGuider.prototype.disconnect.apply(this, arguments); 74 | } 75 | 76 | var device = this.currentState.getConnectedDeviceByPort(port); 77 | if (!device) { 78 | var err = new DeviceNotFound('Device not found!'); 79 | if (this.log) { this.log(err.message); } 80 | if (callback) { callback(err); } 81 | return; 82 | } 83 | this.disconnect(device, callback); 84 | }; 85 | 86 | module.exports = SerialDeviceGuider; -------------------------------------------------------------------------------- /releasenotes.md: -------------------------------------------------------------------------------- 1 | #### v1.10.11 2 | - update some deps 3 | 4 | #### v1.10.10 5 | - added log fallback to [debug](https://github.com/visionmedia/debug) 6 | 7 | #### v1.10.9 8 | - updated dependencies 9 | 10 | #### v1.10.8 11 | - new function devicestack.addAdditionalFormatForValidationSchemas 12 | 13 | #### v1.10.7 14 | - DeviceGuider: fix autoconnect issue [#2](https://github.com/adrai/devicestack/issues/2) 15 | 16 | #### v1.10.6 17 | - connection: try to console log if error is throwing 18 | 19 | #### v1.10.5 20 | - use fix dependency versions 21 | 22 | #### v1.10.4 23 | - command and task: throw error if validation schema is missing 24 | 25 | #### v1.10.3 26 | - DeviceGuider: introduce DeviceNotFound error 27 | - SerialDevice: introduce PortNotFound and PortAccessDenied error 28 | 29 | #### v1.10.2 30 | - DeviceGuider: fix callback call for autoconnect 31 | 32 | #### v1.10.0 33 | - IMPORTANT: DeviceGuider: manualconnect default has been inverted from holdConnections=false to directlyDisconnect=false 34 | -DeviceGuider: new function setConnectionMode 35 | -DeviceLoader: new event plugChanged 36 | -DeviceGuider: new event plugChanged 37 | 38 | #### v1.9.7 39 | - emit disconnect after connect error only if connection is established 40 | 41 | #### v1.9.6 42 | - catch error of command callback and task callback 43 | 44 | #### v1.9.5 45 | - handle errors while connecting 46 | 47 | #### v1.9.4 48 | - do not call callback twice if error is throwing on consumer callback 49 | - stabilize frame handler 50 | 51 | #### v1.9.3 52 | - removed postinstall script for optionalDependencies 53 | 54 | #### v1.9.2 55 | - added postinstall script for optionalDependencies 56 | 57 | #### v1.9.1 58 | - delete connection if not needed anymore 59 | - better errors for example if calling connect twice 60 | 61 | #### v1.9.0 62 | - implemented optional arguments validation by JSON schema for commands and tasks 63 | 64 | #### v1.8.7 65 | - impemented has function for Device and Connection 66 | - Warning! initialize function now passes connection as first argument and after that all other arguments passed by the constructor! 67 | - refacotred error objects 68 | 69 | #### v1.8.6 70 | - Connection: moved function isByteArray to Array.isByteArray 71 | 72 | #### v1.8.5 73 | - Connection: added function isByteArray 74 | 75 | #### v1.8.4 76 | - print some error messages 77 | 78 | #### v1.8.3 79 | - connection: added dequeueCommand function 80 | 81 | #### v1.8.2 82 | - The Enum type is not included automatically in devicestack. Please use it directly from [enum](https://github.com/adrai/enum). 83 | 84 | #### v1.8.1 85 | - connection: try to better catch errors while sending commands 86 | 87 | #### v1.8.0 88 | - introduce connectionStateChanged on DeviceGuider 89 | 90 | #### v1.7.0 91 | - introduce command and task validation (initialize function) 92 | 93 | #### v1.6.4 94 | - updated dependencies 95 | 96 | #### v1.6.3 97 | - DeviceGuider now emits connecting and disconnecting events 98 | 99 | #### v1.6.2 100 | - added possibility to add multiple vid/pid pairs 101 | 102 | #### v1.6.1 103 | - SerialDeviceLoader compare devices by lowercase port name 104 | - emit error on device only if there are listeners 105 | 106 | #### v1.6.0 107 | - implemented EventedSerialDeviceLoader (for USB devices that virtualizes the COM port) 108 | - fix for SerialDeviceLoader (bug was only for non-global users) 109 | 110 | #### v1.5.1 111 | - optimization for hibernate/sleep/standby 112 | 113 | ATTENTION! Connection: executeCommand -> sendCommand 114 | 115 | #### v1.5.0 116 | - ftdi integration 117 | 118 | FtdiDevice 119 | FtdiSerialDevice 120 | FtdiDeviceLoader 121 | EventedFtdiDeviceLoader 122 | 123 | #### v1.4.0 124 | - default serial device loaders uses the global serial device loader under the hood 125 | -------------------------------------------------------------------------------- /lib/deviceloader.js: -------------------------------------------------------------------------------- 1 | var EventEmitter2 = require('eventemitter2').EventEmitter2, 2 | util = require('util'), 3 | _ = require('lodash'); 4 | 5 | /** 6 | * A deviceloader can check if there are available some devices. 7 | */ 8 | function DeviceLoader() { 9 | var self = this; 10 | 11 | // call super class 12 | EventEmitter2.call(this, { 13 | wildcard: true, 14 | delimiter: ':', 15 | maxListeners: 1000 // default would be 10! 16 | }); 17 | 18 | if (this.log) { 19 | this.log = _.wrap(this.log, function(func, msg) { 20 | func(self.constructor.name + ': ' + msg); 21 | }); 22 | } else if (DeviceLoader.prototype.log) { 23 | DeviceLoader.prototype.log = _.wrap(DeviceLoader.prototype.log, function(func, msg) { 24 | func(self.constructor.name + ': ' + msg); 25 | }); 26 | this.log = DeviceLoader.prototype.log; 27 | } else { 28 | var debug = require('debug')(this.constructor.name); 29 | this.log = function(msg) { 30 | debug(msg); 31 | }; 32 | } 33 | 34 | this.lookupIntervalId = null; 35 | this.oldDevices = []; 36 | 37 | this.isRunning = false; 38 | } 39 | 40 | util.inherits(DeviceLoader, EventEmitter2); 41 | 42 | /** 43 | * Calls lookup function with optional callback 44 | * and emits 'plug' for new attached devices 45 | * and 'unplug' for removed devices. 46 | * @param {Function} callback The function, that will be called when finished triggering. [optional] 47 | * `function(err, devices){}` devices is an array of Device objects. 48 | */ 49 | DeviceLoader.prototype.trigger = function(callback) { 50 | var self = this; 51 | this.lookup(function(err, devices) { 52 | if (err) { 53 | if (self.log) self.log(err); 54 | if (!err.name) { 55 | err = new Error(err); 56 | } 57 | if (callback) callback(err, devices); 58 | return; 59 | } 60 | 61 | var remDevs = _.difference(self.oldDevices, devices); 62 | _.each(remDevs, function(d) { 63 | if (self.log) self.log('unplug device with id ' + d.id); 64 | if (d.close) { 65 | d.close(); 66 | } 67 | self.emit('unplug', d); 68 | self.emit('plugChanged', { state: 'unplug', device: d }); 69 | }); 70 | 71 | var addDevs = _.difference(devices, self.oldDevices); 72 | _.each(addDevs, function(d) { 73 | if (self.log) self.log('plug device with id ' + d.id); 74 | self.emit('plug', d); 75 | self.emit('plugChanged', { state: 'plug', device: d }); 76 | }); 77 | 78 | self.oldDevices = devices; 79 | if (callback) callback(null, devices); 80 | }); 81 | }; 82 | 83 | /** 84 | * Starts to lookup. 85 | * @param {Number} interval The interval milliseconds. [optional] 86 | * @param {Function} callback The function, that will be called when trigger has started. [optional] 87 | * `function(err, devices){}` devices is an array of Device objects. 88 | */ 89 | DeviceLoader.prototype.startLookup = function(interval, callback) { 90 | if (this.lookupIntervalId) { 91 | this.stopLookup(); 92 | } 93 | 94 | this.isRunning = true; 95 | 96 | if (!callback && _.isFunction(interval)) { 97 | callback = interval; 98 | interval = null; 99 | } 100 | 101 | var self = this; 102 | interval = interval || 500; 103 | 104 | this.oldDevices = []; 105 | this.trigger(function(err, devices) { 106 | var triggering = false; 107 | self.lookupIntervalId = setInterval(function() { 108 | if (triggering) return; 109 | triggering = true; 110 | self.trigger(function() { 111 | triggering = false; 112 | }); 113 | }, interval); 114 | 115 | if (err && !err.name) { 116 | err = new Error(err); 117 | } 118 | 119 | if (callback) { callback(err, devices); } 120 | }); 121 | }; 122 | 123 | /** 124 | * Stops the interval that calls trigger function. 125 | */ 126 | DeviceLoader.prototype.stopLookup = function() { 127 | if (this.lookupIntervalId) { 128 | clearInterval(this.lookupIntervalId); 129 | this.lookupIntervalId = null; 130 | this.isRunning = false; 131 | } 132 | }; 133 | 134 | module.exports = DeviceLoader; 135 | -------------------------------------------------------------------------------- /lib/task.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | tv4 = require('tv4'), 3 | ValidationError = require('./errors/validationError'); 4 | 5 | function Task() { 6 | var self = this; 7 | 8 | this.args = arguments; 9 | if (arguments.length === 1 && _.isArguments(arguments[0])) { 10 | this.args = arguments[0]; 11 | } 12 | 13 | if (this.log) { 14 | this.log = _.wrap(this.log, function(func, msg) { 15 | func(self.constructor.name + ': ' + msg); 16 | }); 17 | } else if (Task.prototype.log) { 18 | Task.prototype.log = _.wrap(Task.prototype.log, function(func, msg) { 19 | func(self.constructor.name + ': ' + msg); 20 | }); 21 | this.log = Task.prototype.log; 22 | } else { 23 | var debug = require('debug')(this.constructor.name); 24 | this.log = function(msg) { 25 | debug(msg); 26 | }; 27 | } 28 | } 29 | 30 | /** 31 | * If the initialize function is present it will pass in the arguments passed in the constructor. 32 | * If necessary for validation reason the initialize function can throw errors. 33 | * 34 | * @param {Connection} connection The connection object. 35 | */ 36 | Task.prototype._initialize = function(connection) { 37 | if (this.initialize) { 38 | var args = _.toArray(this.args); 39 | 40 | if (this.argumentsSchema) { 41 | var validation = tv4.validateResult(args, this.argumentsSchema); 42 | 43 | if (validation.missing.length > 0) { 44 | throw new Error('Validation schema "' + validation.missing[0] + '" missing!'); 45 | } 46 | 47 | if (!validation.valid) { 48 | if (validation.error.subErrors && validation.error.subErrors.length > 0) { 49 | validation.error.subErrors = _.sortBy(validation.error.subErrors, function(s) { 50 | return -s.code; 51 | }); 52 | throw new ValidationError(validation.error.subErrors[0].dataPath + ' => ' + validation.error.subErrors[0].message); 53 | } else { 54 | throw new ValidationError(validation.error.dataPath + ' => ' + validation.error.message); 55 | } 56 | } 57 | } 58 | 59 | this.initialize.apply(this, [connection].concat(args)); 60 | } 61 | }; 62 | 63 | Task.prototype.perform = function(connection, callback) { 64 | throw new Error('Implement the perform function!'); 65 | }; 66 | 67 | /** 68 | * Executes this task or an other task or a command. 69 | * 70 | * @param {Function} callback The function, that should be called when the task's or command's answer is received. 71 | * `function(err, res){}` 72 | * 73 | * @example: 74 | * this.execute(myTask, connection, false, function(err, res) {}); 75 | * // or 76 | * this.execute(myTask, connection, function(err, res) {}); 77 | * // or 78 | * this.execute(myCommand, connection, function(err, res) {}); 79 | * // or 80 | * this.execute(connection, true, function(err, res) {}); 81 | * // or 82 | * this.execute(connection, function(err, res) {}); 83 | */ 84 | Task.prototype.execute = function(taskOrCommand, connection, ignoreQueue, callback) { 85 | if (arguments.length === 2) { 86 | 87 | ignoreQueue = false; 88 | callback = connection; 89 | connection = taskOrCommand; 90 | taskOrCommand= null; 91 | 92 | } else if (arguments.length === 3 && _.isBoolean(connection)) { 93 | 94 | callback = ignoreQueue; 95 | ignoreQueue = connection; 96 | connection = taskOrCommand; 97 | taskOrCommand= null; 98 | 99 | } else if (arguments.length === 3 && !_.isBoolean(connection)) { 100 | 101 | callback = ignoreQueue; 102 | ignoreQueue = true; 103 | 104 | } 105 | 106 | if (taskOrCommand && taskOrCommand instanceof Task) { 107 | 108 | if (this.log) { this.log('Start executing TASK: ' + taskOrCommand.constructor.name); } 109 | connection.executeTask(taskOrCommand, ignoreQueue, callback); 110 | 111 | } else if (taskOrCommand) { 112 | 113 | if (this.log) { this.log('Start executing COMMAND: ' + taskOrCommand.constructor.name); } 114 | taskOrCommand.execute(connection, callback); 115 | 116 | } else { 117 | 118 | if (this.log) { this.log('Start executing...'); } 119 | connection.executeTask(this, ignoreQueue, callback); 120 | 121 | } 122 | }; 123 | 124 | module.exports = Task; -------------------------------------------------------------------------------- /lib/ftdi/device.js: -------------------------------------------------------------------------------- 1 | var ftdi = require("ftdi"), 2 | Device = require('../device'), 3 | util = require('util'), 4 | _ = require('lodash'); 5 | 6 | /** 7 | * FtdiDevice represents your physical device. 8 | * Extends Device. 9 | * @param {Object} deviceSettings The device settings (locationId, serial, index, description). 10 | * @param {Object} connectionSettings The connection settings (baudrate, databits, stopbits, parity). 11 | * @param {Object} Connection The constructor function of the connection. 12 | */ 13 | function FtdiDevice(deviceSettings, connectionSettings, Connection) { 14 | 15 | // call super class 16 | Device.call(this, Connection); 17 | 18 | if (deviceSettings instanceof ftdi.FtdiDevice) { 19 | this.ftdiDevice = deviceSettings; 20 | this.set(this.ftdiDevice.deviceSettings); 21 | } else { 22 | this.set(deviceSettings); 23 | } 24 | 25 | this.set('connectionSettings', connectionSettings); 26 | 27 | this.set('state', 'close'); 28 | } 29 | 30 | util.inherits(FtdiDevice, Device); 31 | 32 | /** 33 | * The open mechanism of the device. 34 | * On opened 'open' will be emitted and the callback will be called. 35 | * @param {Function} callback The function, that will be called when device is opened. [optional] 36 | * `function(err){}` 37 | */ 38 | FtdiDevice.prototype.open = function(callback) { 39 | var self = this; 40 | 41 | if (this.get('state') !== 'close') { 42 | if (this.log) this.log('Calling open... Device is already "' + this.get('state') + '"!'); 43 | if (callback) callback(new Error('Device is already "' + this.get('state') + '"!')); 44 | return; 45 | } 46 | 47 | if (!this.ftdiDevice) { 48 | this.ftdiDevice = new ftdi.FtdiDevice(this.toJSON()); 49 | } 50 | 51 | this.set('state', 'opening'); 52 | this.emit('opening'); 53 | 54 | this.ftdiDevice.open(this.get('connectionSettings'), function(err) { 55 | self.ftdiDevice.on('error', function(err) { 56 | console.log(err); 57 | if (self.log) self.log(err); 58 | 59 | if (self.listeners('error').length) { 60 | self.emit('error', err); 61 | } 62 | }); 63 | 64 | if (err) { 65 | if (self.log) self.log('error while opening device'); 66 | if (!err.name) { 67 | err = new Error(err); 68 | } 69 | if (callback) callback(err); 70 | return; 71 | } 72 | if (self.log) self.log('open device with id ' + self.id); 73 | self.set('state', 'open'); 74 | self.emit('open', callback); 75 | if (!self.connection && callback) callback(null); 76 | }); 77 | 78 | this.ftdiDevice.on('close', function() { 79 | if (self.log) self.log('close device with id ' + self.id); 80 | self.set('state', 'close'); 81 | self.emit('close'); 82 | self.removeAllListeners(); 83 | self.ftdiDevice.removeAllListeners(); 84 | self.ftdiDevice.removeAllListeners('open'); 85 | }); 86 | 87 | this.ftdiDevice.on('data', function(data) { 88 | if (self.log) self.log('<< ' + data.toHexDebug()); 89 | self.emit('receive', data.toArray()); 90 | }); 91 | 92 | this.on('send', function(data) { 93 | if (self.log) self.log('>> ' + data.toHexDebug()); 94 | self.ftdiDevice.write(data.toBuffer()); 95 | }); 96 | }; 97 | 98 | /** 99 | * The close mechanism of the device. 100 | * @param {Function} callback The function, that will be called when device is closed. [optional] 101 | * `function(err){}` 102 | * @param {Boolean} fire Forces the callback to be called. [optional] 103 | */ 104 | FtdiDevice.prototype.close = function(callback, fire) { 105 | if (this.get('state') !== 'open') { 106 | if (this.log) this.log('Calling close... Device is already "' + this.get('state') + '"!'); 107 | if (callback) callback(new Error('Device is already "' + this.get('state') + '"!')); 108 | return; 109 | } 110 | 111 | this.set('state', 'closing'); 112 | this.emit('closing'); 113 | 114 | var self = this; 115 | if (!this.ftdiDevice) { 116 | if (callback && (!this.connection || fire)) callback(null); 117 | return; 118 | } 119 | this.ftdiDevice.close(function(err) { 120 | if (err && !err.name) { 121 | err = new Error(err); 122 | } 123 | if (callback && (!self.connection || fire)) callback(err); 124 | }); 125 | }; 126 | 127 | module.exports = FtdiDevice; -------------------------------------------------------------------------------- /lib/serial/device.js: -------------------------------------------------------------------------------- 1 | var sp = require("serialport"), 2 | Device = require('../device'), 3 | util = require('util'), 4 | _ = require('lodash'), 5 | PortNotFound = require('../errors/portNotFound'), 6 | PortAccessDenied = require('../errors/portAccessDenied'); 7 | 8 | /** 9 | * SerialDevice represents your physical device. 10 | * Extends Device. 11 | * @param {string} port The Com Port path or name for Windows. 12 | * @param {Object} settings The Com Port Settings. 13 | * @param {Object} Connection The constructor function of the connection. 14 | */ 15 | function SerialDevice(port, settings, Connection) { 16 | 17 | // call super class 18 | Device.call(this, Connection); 19 | 20 | this.set('portName', port); 21 | this.set('settings', settings); 22 | 23 | this.set('state', 'close'); 24 | } 25 | 26 | util.inherits(SerialDevice, Device); 27 | 28 | /** 29 | * The open mechanism of the device. 30 | * On opened 'open' will be emitted and the callback will be called. 31 | * @param {Function} callback The function, that will be called when device is opened. [optional] 32 | * `function(err){}` 33 | */ 34 | SerialDevice.prototype.open = function(callback) { 35 | var self = this; 36 | 37 | if (this.get('state') !== 'close') { 38 | if (this.log) this.log('Calling open... Device is already "' + this.get('state') + '"!'); 39 | if (callback) callback(new Error('Device is already "' + this.get('state') + '"!')); 40 | return; 41 | } 42 | 43 | if (!this.serialPort) { 44 | this.serialPort = new sp.SerialPort(this.get('portName'), this.get('settings'), false); 45 | } 46 | 47 | this.set('state', 'opening'); 48 | this.emit('opening'); 49 | 50 | this.serialPort.on('error', function(err) { 51 | if (self.log) self.log(err); 52 | 53 | if (self.listeners('error').length) { 54 | self.emit('error', err); 55 | } 56 | }); 57 | 58 | this.serialPort.open(function(err) { 59 | if (err) { 60 | if (!err.name) { 61 | err = new Error(err); 62 | } 63 | 64 | if (err.message && 65 | err.message.toLowerCase().indexOf('not') >= 0 && 66 | err.message.toLowerCase().indexOf('found') >= 0) { 67 | err = new PortNotFound(err.message); 68 | } else if (err.message && 69 | err.message.toLowerCase().indexOf('denied') >= 0) { 70 | err = new PortAccessDenied(err.message); 71 | } 72 | 73 | if (self.log) self.log('error while opening device'); 74 | if (callback) callback(err); 75 | return; 76 | } 77 | if (self.log) self.log('open device with id ' + self.id); 78 | self.set('state', 'open'); 79 | self.emit('open', callback); 80 | if (!self.connection && callback) callback(null); 81 | }); 82 | 83 | this.serialPort.on('close', function() { 84 | if (self.log) self.log('close device with id ' + self.id); 85 | self.set('state', 'close'); 86 | self.emit('close'); 87 | self.removeAllListeners(); 88 | self.serialPort.removeAllListeners(); 89 | self.serialPort.removeAllListeners('open'); 90 | }); 91 | 92 | this.serialPort.on('data', function(data) { 93 | if (self.log) self.log('<< ' + data.toHexDebug()); 94 | self.emit('receive', data.toArray()); 95 | }); 96 | 97 | this.on('send', function(data) { 98 | if (self.log) self.log('>> ' + data.toHexDebug()); 99 | self.serialPort.write(data.toBuffer()); 100 | }); 101 | }; 102 | 103 | /** 104 | * The close mechanism of the device. 105 | * @param {Function} callback The function, that will be called when device is closed. [optional] 106 | * `function(err){}` 107 | * @param {Boolean} fire Forces the callnack to be called. [optional] 108 | */ 109 | SerialDevice.prototype.close = function(callback, fire) { 110 | if (this.get('state') !== 'open') { 111 | if (this.log) this.log('Calling close... Device is already "' + this.get('state') + '"!'); 112 | if (callback) callback(new Error('Device is already "' + this.get('state') + '"!')); 113 | return; 114 | } 115 | 116 | this.set('state', 'closing'); 117 | this.emit('closing'); 118 | 119 | var self = this; 120 | if (!this.serialPort) { 121 | if (callback && (!this.connection || fire)) callback(null); 122 | return; 123 | } 124 | this.serialPort.close(function(err) { 125 | if (err && !err.name) { 126 | err = new Error(err); 127 | } 128 | if (callback && (!self.connection || fire)) callback(err); 129 | }); 130 | }; 131 | 132 | module.exports = SerialDevice; -------------------------------------------------------------------------------- /lib/serial/deviceloader.js: -------------------------------------------------------------------------------- 1 | var sp = require('serialport'), 2 | DeviceLoader = require('../deviceloader'), 3 | util = require('util'), 4 | _ = require('lodash'), 5 | globalSerialDeviceLoader = require('./globaldeviceloader'); 6 | 7 | /** 8 | * A serialdeviceloader can check if there are available some serial devices. 9 | * @param {Object} Device The constructor function of the device. 10 | */ 11 | function SerialDeviceLoader(Device, useGlobal) { 12 | 13 | // call super class 14 | DeviceLoader.call(this); 15 | 16 | this.Device = Device; 17 | 18 | this.useGlobal = (useGlobal === undefined || useGlobal === null) ? true : useGlobal; 19 | 20 | if (this.useGlobal) { 21 | this.globalSerialDeviceLoader = globalSerialDeviceLoader.create(Device, this.filter); 22 | } 23 | } 24 | 25 | util.inherits(SerialDeviceLoader, DeviceLoader); 26 | 27 | /* Override of EventEmitter. */ 28 | SerialDeviceLoader.prototype.on = function(eventname, callback) { 29 | if (this.useGlobal) { 30 | this.globalSerialDeviceLoader.on.apply(this.globalSerialDeviceLoader, _.toArray(arguments)); 31 | } else { 32 | DeviceLoader.prototype.on.apply(this, _.toArray(arguments)); 33 | } 34 | }; 35 | 36 | /* Override of EventEmitter. */ 37 | SerialDeviceLoader.prototype.removeListener = function(eventname, callback) { 38 | if (this.useGlobal) { 39 | this.globalSerialDeviceLoader.removeListener.apply(this.globalSerialDeviceLoader, _.toArray(arguments)); 40 | } else { 41 | DeviceLoader.prototype.removeListener.apply(this, _.toArray(arguments)); 42 | } 43 | }; 44 | 45 | /* Same as removeListener */ 46 | SerialDeviceLoader.prototype.off = SerialDeviceLoader.prototype.removeListener; 47 | 48 | /** 49 | * Calls the callback with an array of devices. 50 | * @param {Function} callback The function, that will be called when finished lookup. 51 | * `function(err, devices){}` devices is an array of Device objects. 52 | */ 53 | SerialDeviceLoader.prototype.lookup = function(callback) { 54 | if (this.useGlobal) { 55 | this.globalSerialDeviceLoader.lookup(callback); 56 | } else { 57 | var self = this; 58 | sp.list(function(err, ports) { 59 | if (err) { 60 | if (!err.name) { 61 | err = new Error(err); 62 | } 63 | return callback(err); 64 | } 65 | 66 | var resPorts = self.filter(ports); 67 | 68 | var devices = _.map(resPorts, function(p) { 69 | var found = _.find(self.oldDevices, function(dev) { 70 | return dev.get('portName') && p.comName && dev.get('portName').toLowerCase() === p.comName.toLowerCase(); 71 | }); 72 | 73 | if (found) { 74 | return found; 75 | } else { 76 | var newDev = new self.Device(p.comName); 77 | newDev.set(p); 78 | return newDev; 79 | } 80 | }) || []; 81 | 82 | callback(null, devices); 83 | }); 84 | } 85 | }; 86 | 87 | /** 88 | * Calls lookup function with optional callback 89 | * and emits 'plug' for new attached devices 90 | * and 'unplug' for removed devices. 91 | * @param {Function} callback The function, that will be called when finished triggering. [optional] 92 | * `function(err, devices){}` devices is an array of Device objects. 93 | */ 94 | SerialDeviceLoader.prototype.trigger = function(callback) { 95 | if (this.useGlobal) { 96 | this.globalSerialDeviceLoader.trigger(callback); 97 | } else { 98 | DeviceLoader.prototype.trigger.apply(this, _.toArray(arguments)); 99 | } 100 | }; 101 | 102 | /** 103 | * Starts to lookup. 104 | * @param {Number} interval The interval milliseconds. [optional] 105 | * @param {Function} callback The function, that will be called when trigger has started. [optional] 106 | * `function(err, devices){}` devices is an array of Device objects. 107 | */ 108 | SerialDeviceLoader.prototype.startLookup = function(interval, callback) { 109 | if (this.useGlobal) { 110 | this.globalSerialDeviceLoader.startLookup(interval, callback); 111 | } else { 112 | DeviceLoader.prototype.startLookup.apply(this, _.toArray(arguments)); 113 | } 114 | }; 115 | 116 | /** 117 | * Stops the interval that calls trigger function. 118 | */ 119 | SerialDeviceLoader.prototype.stopLookup = function() { 120 | if (this.useGlobal) { 121 | this.globalSerialDeviceLoader.stopLookup(); 122 | } else { 123 | DeviceLoader.prototype.stopLookup.apply(this, _.toArray(arguments)); 124 | } 125 | }; 126 | 127 | module.exports = SerialDeviceLoader; -------------------------------------------------------------------------------- /lib/device.js: -------------------------------------------------------------------------------- 1 | var EventEmitter2 = require('eventemitter2').EventEmitter2, 2 | util = require('util'), 3 | _ = require('lodash'), 4 | uuid = require('node-uuid').v4; 5 | 6 | /** 7 | * Device represents your physical device. 8 | * @param {Object} Connection The constructor function of the connection. 9 | */ 10 | function Device(Connection) { 11 | var self = this; 12 | 13 | // call super class 14 | EventEmitter2.call(this, { 15 | wildcard: true, 16 | delimiter: ':', 17 | maxListeners: 1000 // default would be 10! 18 | }); 19 | 20 | if (this.log) { 21 | this.log = _.wrap(this.log, function(func, msg) { 22 | func(self.constructor.name + ': ' + msg); 23 | }); 24 | } else if (Device.prototype.log) { 25 | Device.prototype.log = _.wrap(Device.prototype.log, function(func, msg) { 26 | func(self.constructor.name + ': ' + msg); 27 | }); 28 | this.log = Device.prototype.log; 29 | } else { 30 | var debug = require('debug')(this.constructor.name); 31 | this.log = function(msg) { 32 | debug(msg); 33 | }; 34 | } 35 | 36 | this.id = uuid(); 37 | this.Connection = Connection; 38 | 39 | this.attributes = { id: this.id }; 40 | 41 | this.on('disconnect', function() { 42 | self.connection = null; 43 | }); 44 | } 45 | 46 | util.inherits(Device, EventEmitter2); 47 | 48 | /** 49 | * Sets attributes for the device. 50 | * 51 | * @example: 52 | * device.set('firmwareVersion', '0.0.1'); 53 | * // or 54 | * device.set({ 55 | * firmwareVersion: '0.0.1', 56 | * bootloaderVersion: '0.0.1' 57 | * }); 58 | */ 59 | Device.prototype.set = function(data) { 60 | if (arguments.length === 2) { 61 | this.attributes[arguments[0]] = arguments[1]; 62 | } else { 63 | for(var m in data) { 64 | this.attributes[m] = data[m]; 65 | } 66 | } 67 | }; 68 | 69 | /** 70 | * Gets an attribute of the device. 71 | * @param {string} attr The attribute name. 72 | * @return {object} The result value. 73 | * 74 | * @example: 75 | * device.get('firmwareVersion'); // returns '0.0.1' 76 | */ 77 | Device.prototype.get = function(attr) { 78 | return this.attributes[attr]; 79 | }; 80 | 81 | /** 82 | * Returns `true` if the attribute contains a value that is not null 83 | * or undefined. 84 | * @param {string} attr The attribute name. 85 | * @return {boolean} The result value. 86 | * 87 | * @example: 88 | * device.has('firmwareVersion'); // returns true or false 89 | */ 90 | Device.prototype.has = function(attr) { 91 | return (this.get(attr) !== null && this.get(attr) !== undefined); 92 | }; 93 | 94 | /** 95 | * The connect mechanism of the device. 96 | * On connecting 'opening' will be emitted. 97 | * Creates a new connection instance and calls open by passing the callback. 98 | * @param {Function} callback The function, that will be called when device is connected. [optional] 99 | * `function(err, connection){}` connection is a Connection object. 100 | */ 101 | Device.prototype.connect = function(callback) { 102 | if (this.Connection) { 103 | if (this.log) this.log('opening device with id ' + this.id); 104 | this.emit('opening'); 105 | this.connection = new this.Connection(this); 106 | } 107 | this.open(callback); 108 | }; 109 | 110 | /** 111 | * The disconnect mechanism of the device. 112 | * On disconnecting 'closing' will be emitted. 113 | * @param {Function} callback The function, that will be called when device is disconnected. [optional] 114 | * `function(err){}` 115 | */ 116 | Device.prototype.disconnect = function(callback) { 117 | var self = this; 118 | 119 | if (this.connection && this.connection.close) { 120 | if (this.log) this.log('closing device with id ' + this.id); 121 | this.emit('closing', callback); 122 | } else { 123 | this.close(callback); 124 | } 125 | }; 126 | 127 | /** 128 | * The send mechanism. 129 | * @param {Array} data A "byte" array. 130 | */ 131 | Device.prototype.send = function(data) { 132 | this.emit('send', data); 133 | }; 134 | 135 | /* The toJSON function will be called when JSON.stringify(). */ 136 | Device.prototype.toJSON = function() { 137 | var parse = JSON.deserialize || JSON.parse; 138 | var json = parse(JSON.stringify(this.attributes)); 139 | json.connection = this.connection ? { id: this.connection.id } : undefined; 140 | return json; 141 | }; 142 | 143 | Device.prototype.copy = function() { 144 | var clone = new Device(this.Connection); 145 | if (this.connection) { 146 | clone.connection = this.connection.copy(this); 147 | } 148 | clone.set(this.attributes); 149 | clone.set('id', clone.id); 150 | return clone; 151 | }; 152 | 153 | module.exports = Device; -------------------------------------------------------------------------------- /lib/usb/deviceloader.js: -------------------------------------------------------------------------------- 1 | var DeviceLoader = require('../deviceloader'), 2 | util = require('util'), 3 | _ = require('lodash'), 4 | async = require('async'), 5 | monitor; 6 | 7 | /** 8 | * A usbdeviceloader can check if there are available some serial devices. 9 | * @param {Object} Device Device The constructor function of the device. 10 | * @param {Number || Array} vendorId The vendor id or an array of vid/pid pairs. 11 | * @param {Number} productId The product id or optional. 12 | */ 13 | function UsbDeviceLoader(Device, vendorId, productId) { 14 | 15 | // call super class 16 | DeviceLoader.call(this); 17 | 18 | this.Device = Device; 19 | 20 | this.vidPidPairs = []; 21 | 22 | if (!productId && _.isArray(vendorId)) { 23 | this.vidPidPairs = vendorId; 24 | } else { 25 | this.vidPidPairs = [{vendorId: vendorId, productId: productId}]; 26 | } 27 | } 28 | 29 | util.inherits(UsbDeviceLoader, DeviceLoader); 30 | 31 | /** 32 | * Maps the result of monitor.find by the Device objects. 33 | * @param {Array} ports Result of monitor.find 34 | * @return {Array} Mapped Device Array. 35 | */ 36 | UsbDeviceLoader.prototype.map = function(ports) { 37 | if (this.filter) { 38 | ports = this.filter(ports); 39 | } 40 | 41 | var self = this; 42 | 43 | var devices = _.map(ports, function(p) { 44 | var found = _.find(self.oldDevices, function(dev) { 45 | return dev.get('locationId') === p.locationId && 46 | dev.get('vendorId') === p.vendorId && 47 | dev.get('productId') === p.productId && 48 | dev.get('serialNumber') === p.serialNumber; 49 | }); 50 | if (found) { 51 | return found; 52 | } else { 53 | return new self.Device(p); 54 | } 55 | }) || []; 56 | 57 | return devices; 58 | }; 59 | 60 | /** 61 | * Calls the callback with an array of devices. 62 | * @param {Function} callback The function, that will be called when finished lookup. 63 | * `function(err, devices){}` devices is an array of Device objects. 64 | */ 65 | UsbDeviceLoader.prototype.lookup = function(callback) { 66 | var self = this; 67 | var result = []; 68 | async.forEach(this.vidPidPairs, function(pair, callback) { 69 | monitor.find(this.vendorId, this.productId, function(err, ports) { 70 | if (err) { 71 | if (!err.name) { 72 | err = new Error(err); 73 | } 74 | return callback(err); 75 | } 76 | 77 | var devices = self.map(ports); 78 | result = result.concat(devices); 79 | 80 | callback(null); 81 | }); 82 | }, function(err) { 83 | if (err && !err.name) { 84 | err = new Error(err); 85 | } 86 | callback(err, result); 87 | }); 88 | }; 89 | 90 | /** 91 | * Starts to lookup. 92 | * @param {Function} callback The function, that will be called when trigger has started. [optional] 93 | * `function(err, devices){}` devices is an array of Device objects. 94 | */ 95 | UsbDeviceLoader.prototype.startLookup = function(interval, callback) { 96 | this.isRunning = true; 97 | 98 | if (!monitor) { 99 | try { 100 | monitor = require('usb-detection'); 101 | } catch(e) { 102 | console.log(e.message); 103 | throw e; 104 | } 105 | } 106 | 107 | if (!callback && _.isFunction(interval)) { 108 | callback = interval; 109 | interval = null; 110 | } 111 | 112 | var self = this; 113 | 114 | this.oldDevices = []; 115 | 116 | _.each(this.vidPidPairs, function(pair) { 117 | monitor.on('add:' + pair.vendorId + ':' + pair.productId, self.addHandle = function(dev) { 118 | var device = self.map([dev])[0]; 119 | self.oldDevices.push(device); 120 | if (self.log) self.log('plug device with id ' + device.id); 121 | self.emit('plug', device); 122 | self.emit('plugChanged', { state: 'plug', device: device }); 123 | }); 124 | 125 | monitor.on('remove:' + pair.vendorId + ':' + pair.productId, self.removeHandle = function(dev) { 126 | var device = self.map([dev])[0]; 127 | self.oldDevices = _.reject(self.oldDevices, function(d) { 128 | return d === device; 129 | }); 130 | if (self.log) self.log('unplug device with id ' + device.id); 131 | self.emit('unplug', device); 132 | self.emit('plugChanged', { state: 'unplug', device: device }); 133 | }); 134 | }); 135 | 136 | this.trigger(callback); 137 | }; 138 | 139 | /** 140 | * Stops the interval that calls trigger function. 141 | */ 142 | UsbDeviceLoader.prototype.stopLookup = function() { 143 | _.each(this.vidPidPairs, function(pair) { 144 | monitor.removeListener('add:' + pair.vendorId + ':' + pair.productId, self.addHandle); 145 | monitor.removeListener('remove:' + pair.vendorId + ':' + pair.productId, self.removeHandle); 146 | }); 147 | this.isRunning = false; 148 | }; 149 | 150 | module.exports = UsbDeviceLoader; -------------------------------------------------------------------------------- /lib/framehandler.js: -------------------------------------------------------------------------------- 1 | var EventEmitter2 = require('eventemitter2').EventEmitter2, 2 | util = require('util'), 3 | _ = require('lodash'), 4 | async = require('async'); 5 | 6 | /** 7 | * You can have one or multiple framehandlers. 8 | * A framhandler receives data from the upper layer and sends it to the lower layer by wrapping some header or footer information. 9 | * A framehandler receives data from lower layer and sends it to the upper layer by unwrapping some header or footer information. 10 | * The lowest layer for a framehandler is the device and the topmost ist the connection. 11 | * - reacts on send of upper layer, calls wrapFrame function if exists and calls send function on lower layer 12 | * - reacts on receive of lower layer, calls unwrapFrame function if exists and emits receive 13 | * - automatically calls start function 14 | * @param {Device || FrameHandler} deviceOrFrameHandler Device or framahandler object. 15 | */ 16 | function FrameHandler(deviceOrFrameHandler) { 17 | var self = this; 18 | 19 | // call super class 20 | EventEmitter2.call(this, { 21 | wildcard: true, 22 | delimiter: ':', 23 | maxListeners: 1000 // default would be 10! 24 | }); 25 | 26 | if (this.log) { 27 | this.log = _.wrap(this.log, function(func, msg) { 28 | func(self.constructor.name + ': ' + msg); 29 | }); 30 | } else if (FrameHandler.prototype.log) { 31 | FrameHandler.prototype.log = _.wrap(FrameHandler.prototype.log, function(func, msg) { 32 | func(self.constructor.name + ': ' + msg); 33 | }); 34 | this.log = FrameHandler.prototype.log; 35 | } else { 36 | var debug = require('debug')(this.constructor.name); 37 | this.log = function(msg) { 38 | debug(msg); 39 | }; 40 | } 41 | 42 | this.analyzeInterval = 20; 43 | 44 | if (deviceOrFrameHandler) { 45 | this.incomming = []; 46 | deviceOrFrameHandler.on('receive', function (frame) { 47 | var unwrappedFrame; 48 | if (self.analyzeNextFrame) { 49 | self.incomming = self.incomming.concat(Array.prototype.slice.call(frame, 0)); 50 | self.trigger(); 51 | } else { 52 | if (self.unwrapFrame) { 53 | unwrappedFrame = self.unwrapFrame(_.clone(frame)); 54 | if (self.log) self.log('receive unwrapped frame: ' + unwrappedFrame.toHexDebug()); 55 | self.emit('receive', unwrappedFrame); 56 | } else { 57 | if (self.log) self.log('receive frame: ' + frame.toHexDebug()); 58 | self.emit('receive', frame); 59 | } 60 | } 61 | }); 62 | 63 | deviceOrFrameHandler.on('close', function() { 64 | if (self.log) self.log('close'); 65 | self.emit('close'); 66 | self.removeAllListeners(); 67 | deviceOrFrameHandler.removeAllListeners(); 68 | deviceOrFrameHandler.removeAllListeners('receive'); 69 | self.incomming = []; 70 | }); 71 | } 72 | 73 | this.on('send', function(frame) { 74 | if (self.wrapFrame) { 75 | var wrappedFrame = self.wrapFrame(_.clone(frame)); 76 | if (self.log) self.log('send wrapped frame: ' + wrappedFrame.toHexDebug()); 77 | deviceOrFrameHandler.send(wrappedFrame); 78 | } else { 79 | if (self.log) self.log('send frame: ' + frame.toHexDebug()); 80 | deviceOrFrameHandler.send(frame); 81 | } 82 | }); 83 | } 84 | 85 | util.inherits(FrameHandler, EventEmitter2); 86 | 87 | /** 88 | * Analyzes the incomming data. 89 | */ 90 | FrameHandler.prototype.analyze = function() { 91 | 92 | try { 93 | if (this.incomming.length === 0) return; 94 | 95 | var nextFrame; 96 | while ((nextFrame = this.analyzeNextFrame(this.incomming)) && nextFrame.length) { 97 | if (this.unwrapFrame) { 98 | var unwrappedFrame = this.unwrapFrame(_.clone(nextFrame)); 99 | if (this.log) this.log('receive unwrapped frame: ' + unwrappedFrame.toHexDebug()); 100 | this.emit('receive', unwrappedFrame); 101 | } else { 102 | if (this.log) this.log('receive frame: ' + nextFrame.toHexDebug()); 103 | this.emit('receive', nextFrame); 104 | } 105 | } 106 | } catch(err) { 107 | this.isAnalyzing = false; 108 | this.trigger(); 109 | throw err; 110 | } 111 | }; 112 | 113 | /** 114 | * Triggers for analyzing incomming bytes. 115 | */ 116 | FrameHandler.prototype.trigger = function() { 117 | 118 | if (this.isAnalyzing || this.incomming.length === 0) return; 119 | 120 | this.isAnalyzing = true; 121 | 122 | this.analyze(); 123 | 124 | if (this.incomming.length === 0) { 125 | this.isAnalyzing = false; 126 | return; 127 | } 128 | 129 | var self = this; 130 | 131 | async.whilst( 132 | function() { 133 | return self.incomming.length > 0; 134 | }, 135 | function(callback) { 136 | setTimeout(function() { 137 | self.analyze(); 138 | callback(null); 139 | }, self.analyzeInterval); 140 | }, function(err) { 141 | self.isAnalyzing = false; 142 | } 143 | ); 144 | 145 | }; 146 | 147 | /** 148 | * The send mechanism. 149 | * @param {Array} data A "byte" array. 150 | */ 151 | FrameHandler.prototype.send = function(data) { 152 | this.emit('send', data); 153 | }; 154 | 155 | module.exports = FrameHandler; -------------------------------------------------------------------------------- /test/connectionTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'), 2 | Connection = require('./fixture/connection'), 3 | Device = require('./fixture/device'), 4 | Task = require('./fixture/task'), 5 | Command = require('./fixture/command'), 6 | ValidationError = require('../lib/errors/validationError'); 7 | 8 | describe('Connection', function() { 9 | 10 | after(function() { 11 | var pm; 12 | if ((pm = global['pm-notify'])) { 13 | pm.stopMonitoring(); 14 | } 15 | var detection; 16 | if ((detection = global['usb-detection'])) { 17 | detection.stopMonitoring(); 18 | } 19 | }); 20 | 21 | var connection; 22 | var device = new Device(); 23 | 24 | describe('creating a connection object', function() { 25 | 26 | beforeEach(function() { 27 | connection = new Connection(device); 28 | }); 29 | 30 | it('it should have all expected values', function() { 31 | 32 | expect(connection.id).to.be.a('string'); 33 | expect(connection.close).to.be.a('function'); 34 | expect(connection.set).to.be.a('function'); 35 | expect(connection.get).to.be.a('function'); 36 | expect(connection.has).to.be.a('function'); 37 | expect(connection.toJSON).to.be.a('function'); 38 | expect(connection.executeCommand).to.be.a('function'); 39 | expect(connection.executeTask).to.be.a('function'); 40 | 41 | }); 42 | 43 | describe('calling connect on the device', function() { 44 | 45 | it('it should emit connecting', function(done) { 46 | 47 | connection.once('connecting', function() { 48 | done(); 49 | }); 50 | device.connect(); 51 | 52 | }); 53 | 54 | it('it should emit connect', function(done) { 55 | 56 | connection.once('connect', function() { 57 | done(); 58 | }); 59 | device.connect(); 60 | 61 | }); 62 | 63 | }); 64 | 65 | describe('calling executeCommand', function() { 66 | 67 | it('it should emit send on device', function(done) { 68 | 69 | device.once('send', function() { 70 | done(); 71 | }); 72 | connection.executeCommand(new Command(), function() {}); 73 | 74 | }); 75 | 76 | describe('of a command that validates by schema', function() { 77 | 78 | it('it should callback a ValidationError', function(done) { 79 | 80 | connection.executeCommand(new Command(0, true), function(err) { 81 | expect(err).to.be.ok(); 82 | expect(err).to.be.an(Error); 83 | expect(err).to.be.a(ValidationError); 84 | done(); 85 | }); 86 | 87 | }); 88 | 89 | }); 90 | 91 | describe('of a command that validates an error', function() { 92 | 93 | it('it should callback that error', function(done) { 94 | 95 | connection.executeCommand(new Command(-1), function(err) { 96 | expect(err).to.be.ok(); 97 | expect(err).to.be.an(Error); 98 | expect(err).not.to.be.a(ValidationError); 99 | done(); 100 | }); 101 | 102 | }); 103 | 104 | }); 105 | 106 | }); 107 | 108 | describe('calling executeTask', function() { 109 | 110 | it('it should emit send on device', function(done) { 111 | 112 | var send = false; 113 | 114 | device.once('send', function() { 115 | send = true; 116 | }); 117 | connection.executeTask(new Task(), function() { 118 | expect(send).to.be.ok(); 119 | done(); 120 | }); 121 | 122 | }); 123 | 124 | describe('with ignoreQueue set to true', function() { 125 | 126 | it('it should bypass the queue', function(done) { 127 | 128 | var task1Executed, 129 | task2Executed, 130 | task3Executed; 131 | 132 | var task1Clb = function(err, res) { 133 | task1Executed = true; 134 | 135 | if (task1Executed && task2Executed && task3Executed) { 136 | done(); 137 | } 138 | }; 139 | 140 | var task2Clb = function(err, res) { 141 | task2Executed = true; 142 | 143 | if (task1Executed && task2Executed && task3Executed) { 144 | done(); 145 | } 146 | }; 147 | 148 | var task3Clb = function(err, res) { 149 | task3Executed = true; 150 | expect(task2Executed).not.to.be.ok(); 151 | expect(task3Executed).to.be.ok(); 152 | 153 | if (task1Executed && task2Executed && task3Executed) { 154 | done(); 155 | } 156 | }; 157 | 158 | connection.executeTask(new Task(1), task1Clb); 159 | connection.executeTask(new Task(2), task2Clb); 160 | connection.executeTask(new Task(3), true, task3Clb); 161 | 162 | }); 163 | 164 | }); 165 | 166 | describe('of a task that validates by schema', function() { 167 | 168 | it('it should callback a ValidationError', function(done) { 169 | 170 | connection.executeTask(new Task(0, true), function(err) { 171 | expect(err).to.be.ok(); 172 | expect(err).to.be.an(Error); 173 | expect(err).to.be.a(ValidationError); 174 | done(); 175 | }); 176 | 177 | }); 178 | 179 | }); 180 | 181 | describe('of a task that validates an error', function() { 182 | 183 | it('it should callback that error', function(done) { 184 | 185 | connection.executeTask(new Task(111), function(err) { 186 | expect(err).to.be.ok(); 187 | expect(err).to.be.an(Error); 188 | expect(err).not.to.be.a(ValidationError); 189 | done(); 190 | }); 191 | 192 | }); 193 | 194 | }); 195 | 196 | describe('of a task that validates an error in a command that he executes', function() { 197 | 198 | it('it should callback that error', function(done) { 199 | 200 | connection.executeTask(new Task(-1), function(err) { 201 | expect(err).to.be.ok(); 202 | done(); 203 | }); 204 | 205 | }); 206 | 207 | }); 208 | 209 | }); 210 | 211 | describe('calling disconnect on the device', function() { 212 | 213 | var dev2, conn2; 214 | 215 | before(function(done) { 216 | dev2 = new Device(); 217 | dev2.connect(function() { 218 | conn2 = dev2.connection; 219 | done(); 220 | }); 221 | }); 222 | 223 | it('it should emit disconnecting', function(done) { 224 | 225 | conn2.once('disconnecting', function() { 226 | done(); 227 | }); 228 | dev2.disconnect(); 229 | 230 | }); 231 | 232 | it('it should emit disconnect', function(done) { 233 | 234 | conn2.once('disconnect', function() { 235 | done(); 236 | }); 237 | dev2.disconnect(); 238 | 239 | }); 240 | 241 | }); 242 | 243 | describe('calling close', function() { 244 | 245 | var dev2, conn2; 246 | 247 | before(function(done) { 248 | dev2 = new Device(); 249 | dev2.connect(function() { 250 | conn2 = dev2.connection; 251 | done(); 252 | }); 253 | }); 254 | 255 | it('it should emit disconnecting', function(done) { 256 | 257 | conn2.once('disconnecting', function() { 258 | done(); 259 | }); 260 | conn2.close(); 261 | 262 | }); 263 | 264 | it('it should emit disconnect', function(done) { 265 | 266 | conn2.once('disconnect', function() { 267 | done(); 268 | }); 269 | conn2.close(); 270 | 271 | }); 272 | 273 | describe('with a callback', function() { 274 | 275 | it('it should call the callback', function(done) { 276 | 277 | conn2.close(function() { 278 | done(); 279 | }); 280 | 281 | }); 282 | 283 | }); 284 | 285 | }); 286 | 287 | }); 288 | 289 | }); -------------------------------------------------------------------------------- /lib/serial/globaldeviceloader.js: -------------------------------------------------------------------------------- 1 | var sp = require('serialport'), 2 | _ = require('lodash'), 3 | async = require('async'), 4 | EventEmitter2 = require('eventemitter2').EventEmitter2, 5 | globalSerialDeviceLoader, 6 | subscribers = [], 7 | lookupIntervalId = null, 8 | isRunning = false; 9 | 10 | module.exports = globalSerialDeviceLoader = { 11 | 12 | /** 13 | * Creates a deviceloader. 14 | * @param {Object} Device The constructor function of the device. 15 | * @param {Function} filter The filter function that will filter the needed devices. 16 | * @return {Object} Represents a SerialDeviceLoader. 17 | */ 18 | create: function(Device, filter) { 19 | var sub = new EventEmitter2({ 20 | wildcard: true, 21 | delimiter: ':', 22 | maxListeners: 1000 // default would be 10! 23 | }); 24 | sub.Device = Device; 25 | sub.filter = filter; 26 | sub.oldDevices = []; 27 | sub.newDevices = []; 28 | 29 | /** 30 | * Calls the callback with an array of devices. 31 | * @param {Array} ports When called within this file this are the listed system ports. [optional] 32 | * @param {Function} callback The function, that will be called when finished lookup. 33 | * `function(err, devices){}` devices is an array of Device objects. 34 | */ 35 | sub.lookup = function(ports, callback) { 36 | if (!callback) { 37 | callback = ports; 38 | ports = null; 39 | } 40 | 41 | if (this.newDevices.length > 0) { 42 | if (callback) { callback(null, this.newDevices); } 43 | return; 44 | } else { 45 | var self = this; 46 | globalSerialDeviceLoader.lookup(function(err) { 47 | if (err && !err.name) { 48 | err = new Error(err); 49 | } 50 | if (callback) { callback(err, self.newDevices); } 51 | }); 52 | } 53 | }; 54 | 55 | /** 56 | * Calls lookup function with optional callback 57 | * and emits 'plug' for new attached devices 58 | * and 'unplug' for removed devices. 59 | * @param {Function} callback The function, that will be called when finished triggering. [optional] 60 | * `function(err, devices){}` devices is an array of Device objects. 61 | */ 62 | sub.trigger = function(callback) { 63 | var self = this; 64 | globalSerialDeviceLoader.trigger(function(err) { 65 | if (err && !err.name) { 66 | err = new Error(err); 67 | } 68 | if (callback) { callback(err, self.newDevices); } 69 | }); 70 | }; 71 | 72 | /** 73 | * Starts to lookup. 74 | * @param {Number} interval The interval milliseconds. [optional] 75 | * @param {Function} callback The function, that will be called when trigger has started. [optional] 76 | * `function(err, devices){}` devices is an array of Device objects. 77 | */ 78 | sub.startLookup = function(interval, callback) { 79 | if (!callback && _.isFunction(interval)) { 80 | callback = interval; 81 | interval = null; 82 | } 83 | 84 | subscribers.push(this); 85 | 86 | var self = this; 87 | if (isRunning) { 88 | if (callback) { callback(null, self.newDevices); } 89 | return; 90 | } else { 91 | globalSerialDeviceLoader.startLookup(interval, function(err) { 92 | if (err && !err.name) { 93 | err = new Error(err); 94 | } 95 | if (callback) { callback(err, self.newDevices); } 96 | }); 97 | } 98 | }; 99 | 100 | /** 101 | * Removes itself as subscriber. 102 | * If last stops the interval that calls trigger function. 103 | */ 104 | sub.stopLookup = function() { 105 | var self = this; 106 | if (!isRunning) { 107 | return; 108 | } else { 109 | subscribers = _.reject(function(s) { 110 | return s === self; 111 | }); 112 | if (subscribers.length === 0) { 113 | globalSerialDeviceLoader.stopLookup(); 114 | } 115 | return; 116 | } 117 | }; 118 | 119 | return sub; 120 | }, 121 | 122 | /** 123 | * Calls the callback when finished. 124 | * @param {Function} callback The function, that will be called when finished lookup. 125 | * `function(err){}` 126 | */ 127 | lookup: function(callback) { 128 | sp.list(function(err, ports) { 129 | if (err && !err.name) { 130 | err = new Error(err); 131 | } 132 | if (err && callback) { return callback(err); } 133 | 134 | async.forEach(subscribers, function(s, callback) { 135 | if (s) { 136 | var resPorts = s.filter(ports); 137 | 138 | var devices = _.map(resPorts, function(p) { 139 | var found = _.find(s.oldDevices, function(dev) { 140 | return dev.get('portName') && p.comName && dev.get('portName').toLowerCase() === p.comName.toLowerCase(); 141 | }); 142 | if (found) { 143 | return found; 144 | } else { 145 | var newDev = new s.Device(p.comName); 146 | newDev.set(p); 147 | return newDev; 148 | } 149 | }) || []; 150 | 151 | s.newDevices = devices; 152 | } 153 | 154 | callback(null); 155 | }, function(err) { 156 | if (err && !err.name) { 157 | err = new Error(err); 158 | } 159 | if (callback) { return callback(err); } 160 | }); 161 | }); 162 | }, 163 | 164 | /** 165 | * Calls lookup function with optional callback 166 | * and emits 'plug' for new attached devices 167 | * and 'unplug' for removed devices. 168 | * @param {Function} callback The function, that will be called when finished triggering. [optional] 169 | * `function(err){}` 170 | */ 171 | trigger: function(callback) { 172 | globalSerialDeviceLoader.lookup(function(err) { 173 | if (err && !err.name) { 174 | err = new Error(err); 175 | } 176 | if (err && callback) { return callback(err); } 177 | 178 | async.forEach(subscribers, function(s, callback) { 179 | if (s && s.oldDevices.length !== s.newDevices.length) { 180 | var devs = []; 181 | if (s.oldDevices.length > s.newDevices.length) { 182 | devs = _.difference(s.oldDevices, s.newDevices); 183 | _.each(devs, function(d) { 184 | if (s.log) s.log('unplug device with id ' + device.id); 185 | if (d.close) { 186 | d.close(); 187 | } 188 | s.emit('unplug', d); 189 | }); 190 | } else { 191 | devs = _.difference(s.newDevices, s.oldDevices); 192 | _.each(devs, function(d) { 193 | if (s.log) s.log('plug device with id ' + device.id); 194 | s.emit('plug', d); 195 | }); 196 | } 197 | 198 | s.oldDevices = s.newDevices; 199 | } 200 | 201 | callback(null); 202 | }, function(err) { 203 | if (err && !err.name) { 204 | err = new Error(err); 205 | } 206 | if (callback) { return callback(err); } 207 | }); 208 | 209 | }); 210 | }, 211 | 212 | /** 213 | * Starts to lookup. 214 | * @param {Number} interval The interval milliseconds. [optional] 215 | * @param {Function} callback The function, that will be called when trigger has started. [optional] 216 | * `function(err){}` 217 | */ 218 | startLookup: function(interval, callback) { 219 | if (lookupIntervalId) { 220 | return; 221 | } 222 | 223 | isRunning = true; 224 | 225 | if (!callback && _.isFunction(interval)) { 226 | callback = interval; 227 | interval = null; 228 | } 229 | 230 | interval = interval || 500; 231 | 232 | globalSerialDeviceLoader.trigger(function(err) { 233 | var triggering = false; 234 | lookupIntervalId = setInterval(function() { 235 | if (triggering) return; 236 | triggering = true; 237 | globalSerialDeviceLoader.trigger(function() { 238 | triggering = false; 239 | }); 240 | }, interval); 241 | 242 | if (err && !err.name) { 243 | err = new Error(err); 244 | } 245 | if (callback) { callback(err); } 246 | }); 247 | }, 248 | 249 | /** 250 | * Stops the interval that calls trigger function. 251 | */ 252 | stopLookup: function() { 253 | if (lookupIntervalId) { 254 | clearInterval(lookupIntervalId); 255 | lookupIntervalId = null; 256 | isRunning = false; 257 | } 258 | } 259 | 260 | }; -------------------------------------------------------------------------------- /test/deviceTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'), 2 | Device = require('./fixture/device'); 3 | 4 | describe('Device', function() { 5 | 6 | after(function() { 7 | var pm; 8 | if ((pm = global['pm-notify'])) { 9 | pm.stopMonitoring(); 10 | } 11 | var detection; 12 | if ((detection = global['usb-detection'])) { 13 | detection.stopMonitoring(); 14 | } 15 | }); 16 | 17 | describe('without Connection object', function() { 18 | 19 | var device; 20 | 21 | describe('creating a device object', function() { 22 | 23 | it('it should have all expected values', function() { 24 | 25 | device = new Device(true); 26 | expect(device.id).to.be.a('string'); 27 | expect(device.open).to.be.a('function'); 28 | expect(device.close).to.be.a('function'); 29 | expect(device.send).to.be.a('function'); 30 | expect(device.set).to.be.a('function'); 31 | expect(device.get).to.be.a('function'); 32 | expect(device.has).to.be.a('function'); 33 | expect(device.toJSON).to.be.a('function'); 34 | 35 | }); 36 | 37 | describe('calling open', function() { 38 | 39 | it('it should emit open', function(done) { 40 | 41 | device.once('open', function() { 42 | done(); 43 | }); 44 | device.open(); 45 | 46 | }); 47 | 48 | describe('with a callback', function() { 49 | 50 | it('it should call the callback', function(done) { 51 | 52 | device.open(function() { 53 | done(); 54 | }); 55 | 56 | }); 57 | 58 | }); 59 | 60 | }); 61 | 62 | describe('emitting send', function() { 63 | 64 | it('it should emit receive', function(done) { 65 | 66 | device.once('receive', function() { 67 | done(); 68 | }); 69 | device.emit('send', []); 70 | 71 | }); 72 | 73 | }); 74 | 75 | describe('calling send', function() { 76 | 77 | it('it should emit receive', function(done) { 78 | 79 | device.once('receive', function() { 80 | done(); 81 | }); 82 | device.send([]); 83 | 84 | }); 85 | 86 | }); 87 | 88 | describe('calling close', function() { 89 | 90 | it('it should emit close', function(done) { 91 | 92 | device.once('close', function() { 93 | done(); 94 | }); 95 | device.close(); 96 | 97 | }); 98 | 99 | describe('with a callback', function() { 100 | 101 | it('it should call the callback', function(done) { 102 | 103 | device.close(function() { 104 | done(); 105 | }); 106 | 107 | }); 108 | 109 | }); 110 | 111 | }); 112 | 113 | }); 114 | 115 | }); 116 | 117 | describe('with Connection object', function() { 118 | 119 | var device; 120 | 121 | describe('creating a device object', function() { 122 | 123 | it('it should have all expected values', function() { 124 | 125 | device = new Device(); 126 | expect(device.id).to.be.a('string'); 127 | expect(device.open).to.be.a('function'); 128 | expect(device.close).to.be.a('function'); 129 | expect(device.send).to.be.a('function'); 130 | 131 | }); 132 | 133 | describe('calling open', function() { 134 | 135 | it('it should emit open', function(done) { 136 | 137 | device.once('open', function() { 138 | done(); 139 | }); 140 | device.open(); 141 | 142 | }); 143 | 144 | describe('with a callback', function() { 145 | 146 | it('it should call the callback', function(done) { 147 | 148 | device.open(function() { 149 | done(); 150 | }); 151 | 152 | }); 153 | 154 | }); 155 | 156 | }); 157 | 158 | describe('emitting send', function() { 159 | 160 | it('it should emit receive', function(done) { 161 | 162 | device.once('receive', function() { 163 | done(); 164 | }); 165 | device.emit('send', []); 166 | 167 | }); 168 | 169 | }); 170 | 171 | describe('calling send', function() { 172 | 173 | it('it should emit receive', function(done) { 174 | 175 | device.once('receive', function() { 176 | done(); 177 | }); 178 | device.send([]); 179 | 180 | }); 181 | 182 | }); 183 | 184 | describe('calling close', function() { 185 | 186 | it('it should emit close', function(done) { 187 | 188 | device.once('close', function() { 189 | done(); 190 | }); 191 | device.close(); 192 | 193 | }); 194 | 195 | describe('with a callback', function() { 196 | 197 | it('it should call the callback', function(done) { 198 | 199 | device.close(function() { 200 | done(); 201 | }); 202 | 203 | }); 204 | 205 | }); 206 | 207 | }); 208 | 209 | describe('calling connect', function() { 210 | 211 | it('it should emit opening', function(done) { 212 | 213 | device.once('opening', function() { 214 | done(); 215 | }); 216 | device.connect(); 217 | 218 | }); 219 | 220 | it('it should emit open', function(done) { 221 | 222 | device.once('open', function() { 223 | done(); 224 | }); 225 | device.connect(); 226 | 227 | }); 228 | 229 | it('it should emit connecting', function(done) { 230 | 231 | device.once('connecting', function() { 232 | done(); 233 | }); 234 | device.connect(); 235 | 236 | }); 237 | 238 | it('it should emit connect', function(done) { 239 | 240 | device.once('connect', function() { 241 | done(); 242 | }); 243 | device.connect(); 244 | 245 | }); 246 | 247 | it('it should a new connection property with a generated id', function(done) { 248 | 249 | device.once('open', function() { 250 | expect(device.connection).to.be.an('object'); 251 | expect(device.connection.id).to.be.a('string'); 252 | 253 | done(); 254 | }); 255 | device.connect(); 256 | 257 | }); 258 | 259 | describe('with a callback', function() { 260 | 261 | var device2 = new Device(); 262 | 263 | it('it should call the callback', function(done) { 264 | 265 | device2.connect(function() { 266 | done(); 267 | }); 268 | 269 | }); 270 | 271 | }); 272 | 273 | }); 274 | 275 | describe('calling disconnect', function() { 276 | 277 | beforeEach(function(done) { 278 | device = new Device(); 279 | device.connect(done); 280 | }); 281 | 282 | it('it should emit closing', function(done) { 283 | 284 | device.once('closing', function() { 285 | done(); 286 | }); 287 | device.disconnect(); 288 | 289 | }); 290 | 291 | it('it should emit close', function(done) { 292 | 293 | device.once('close', function() { 294 | done(); 295 | }); 296 | device.disconnect(); 297 | 298 | }); 299 | 300 | it('it should emit disconnecting', function(done) { 301 | 302 | device.once('disconnecting', function() { 303 | done(); 304 | }); 305 | device.disconnect(); 306 | 307 | }); 308 | 309 | it('it should emit disconnect', function(done) { 310 | 311 | device.once('disconnect', function() { 312 | done(); 313 | }); 314 | device.disconnect(); 315 | 316 | }); 317 | 318 | describe('with a callback', function() { 319 | 320 | it('it should call the callback', function(done) { 321 | 322 | var device2 = new Device(); 323 | 324 | device2.disconnect(function() { 325 | done(); 326 | }); 327 | 328 | }); 329 | 330 | }); 331 | 332 | describe('having disconnected', function() { 333 | 334 | it('it should have deleted the connection object', function(done) { 335 | 336 | device.once('disconnect', function() { 337 | expect(device.connection).not.to.be.ok(); 338 | done(); 339 | }); 340 | device.disconnect(); 341 | 342 | }); 343 | 344 | }); 345 | 346 | describe('having closed', function() { 347 | 348 | it('it should have deleted the connection object', function(done) { 349 | 350 | device.once('close', function() { 351 | expect(device.connection).not.to.be.ok(); 352 | done(); 353 | }); 354 | device.disconnect(); 355 | 356 | }); 357 | 358 | }); 359 | 360 | }); 361 | 362 | }); 363 | 364 | }); 365 | 366 | }); -------------------------------------------------------------------------------- /test/deviceguiderTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'), 2 | deviceguider = require('./fixture/deviceguider'); 3 | 4 | describe('DeviceGuider', function() { 5 | 6 | after(function() { 7 | var pm; 8 | if ((pm = global['pm-notify'])) { 9 | pm.stopMonitoring(); 10 | } 11 | var detection; 12 | if ((detection = global['usb-detection'])) { 13 | detection.stopMonitoring(); 14 | } 15 | }); 16 | 17 | describe('having a deviceguider object', function() { 18 | 19 | it('it should have all expected values', function() { 20 | 21 | expect(deviceguider.getCurrentState).to.be.a('function'); 22 | expect(deviceguider.setConnectionMode).to.be.a('function'); 23 | expect(deviceguider.autoconnect).to.be.a('function'); 24 | expect(deviceguider.manualconnect).to.be.a('function'); 25 | expect(deviceguider.autoconnectOne).to.be.a('function'); 26 | expect(deviceguider.disconnectDevice).to.be.a('function'); 27 | expect(deviceguider.connectDevice).to.be.a('function'); 28 | expect(deviceguider.closeConnection).to.be.a('function'); 29 | expect(deviceguider.changeConnectionMode).to.be.a('function'); 30 | 31 | }); 32 | 33 | describe('calling getCurrentState', function() { 34 | 35 | it('it should callback the correct object', function(done) { 36 | 37 | deviceguider.getCurrentState(function(err, currentState) { 38 | expect(currentState.plugged).to.be.an('array'); 39 | expect(currentState.connected).to.be.an('array'); 40 | expect(currentState.connectionMode).to.be.a('string'); 41 | expect(currentState.getDevice).to.be.a('function'); 42 | expect(currentState.getConnection).to.be.a('function'); 43 | expect(currentState.getDeviceByConnection).to.be.a('function'); 44 | expect(currentState.getConnectionByDevice).to.be.a('function'); 45 | done(); 46 | }); 47 | 48 | }); 49 | 50 | }); 51 | 52 | describe('calling changeConnectionMode', function() { 53 | 54 | beforeEach(function(done) { 55 | deviceguider.manualconnect(true, done); 56 | }); 57 | 58 | describe('changing the mode', function() { 59 | 60 | it('it should emit connectionModeChanged', function(done) { 61 | 62 | deviceguider.once('connectionModeChanged', function(connectionMode) { 63 | expect(connectionMode).to.eql('autoconnect'); 64 | done(); 65 | }); 66 | 67 | deviceguider.changeConnectionMode('autoconnect'); 68 | 69 | }); 70 | 71 | it('it should return true', function() { 72 | 73 | var result = deviceguider.changeConnectionMode('autoconnect'); 74 | expect(result).to.eql(true); 75 | 76 | }); 77 | 78 | }); 79 | 80 | describe('not changing the mode', function() { 81 | 82 | it('it should return false', function() { 83 | 84 | var result = deviceguider.changeConnectionMode('manualconnect'); 85 | expect(result).to.eql(false); 86 | 87 | }); 88 | 89 | }); 90 | 91 | }); 92 | 93 | describe('calling autoconnect', function() { 94 | 95 | beforeEach(function(done) { 96 | deviceguider.manualconnect(true, done); 97 | }); 98 | 99 | it('it should emit connecting', function(done) { 100 | 101 | deviceguider.once('connecting', function(dev) { 102 | expect(dev).to.be.an('object'); 103 | 104 | done(); 105 | }); 106 | deviceguider.autoconnect(true); 107 | 108 | }); 109 | 110 | it('it should emit connectionStateChanged', function(done) { 111 | 112 | deviceguider.once('connectionStateChanged', function(res) { 113 | expect(res.state).to.eql('connecting'); 114 | expect(res.device).to.be.an('object'); 115 | 116 | done(); 117 | }); 118 | deviceguider.autoconnect(true); 119 | 120 | }); 121 | 122 | it('it should automatically connect plugged devices', function(done) { 123 | 124 | deviceguider.once('connect', function(connection) { 125 | expect(connection).to.be.an('object'); 126 | 127 | done(); 128 | }); 129 | deviceguider.autoconnect(true); 130 | 131 | }); 132 | 133 | it('it should emit connectionStateChanged', function(done) { 134 | 135 | var handle; 136 | deviceguider.on('connectionStateChanged', handle = function(res) { 137 | if (res.state === 'connect') { 138 | deviceguider.off('connectionStateChanged', handle); 139 | done(); 140 | } 141 | }); 142 | deviceguider.autoconnect(true); 143 | 144 | }); 145 | 146 | }); 147 | 148 | describe('calling autoconnectOne', function() { 149 | 150 | beforeEach(function(done) { 151 | deviceguider.manualconnect(true, done); 152 | }); 153 | 154 | it('it should automatically connect the first plugged device by emitting connect', function(done) { 155 | 156 | deviceguider.once('connect', function(connection) { 157 | expect(connection).to.be.an('object'); 158 | 159 | done(); 160 | }); 161 | deviceguider.autoconnectOne(true); 162 | 163 | }); 164 | 165 | it('it should emit connectionStateChanged', function(done) { 166 | 167 | var handle; 168 | deviceguider.on('connectionStateChanged', handle = function(res) { 169 | if (res.state === 'connect') { 170 | deviceguider.off('connectionStateChanged', handle); 171 | done(); 172 | } 173 | }); 174 | deviceguider.autoconnectOne(true); 175 | 176 | }); 177 | 178 | it('it should automatically connect the first plugged device', function(done) { 179 | 180 | deviceguider.autoconnectOne(true, function(err, connection) { 181 | expect(connection).to.be.ok(err); 182 | expect(connection).to.be.an('object'); 183 | 184 | done(); 185 | }); 186 | 187 | }); 188 | 189 | }); 190 | 191 | describe('calling disconnectDevice', function() { 192 | 193 | var toDisconnect; 194 | 195 | beforeEach(function(done) { 196 | deviceguider.manualconnect(true, function() { 197 | deviceguider.once('connect', function(connection) { 198 | toDisconnect = connection.device; 199 | 200 | done(); 201 | }); 202 | deviceguider.autoconnect(true); 203 | }); 204 | }); 205 | 206 | it('it should emit disconnecting', function(done) { 207 | 208 | var called = false; 209 | deviceguider.once('disconnecting', function(con) { 210 | expect(con).to.be.an('object'); 211 | called = true; 212 | }); 213 | deviceguider.disconnectDevice(toDisconnect, function() { 214 | expect(called).to.eql(true); 215 | done(); 216 | }); 217 | 218 | }); 219 | 220 | it('it should emit connectionStateChanged', function(done) { 221 | 222 | var called = false; 223 | deviceguider.once('connectionStateChanged', function(res) { 224 | expect(res.state).to.be.eql('disconnecting'); 225 | expect(res.device).to.be.an('object'); 226 | called = true; 227 | }); 228 | deviceguider.disconnectDevice(toDisconnect, function() { 229 | expect(called).to.eql(true); 230 | done(); 231 | }); 232 | 233 | }); 234 | 235 | it('it should emit disconnect', function(done) { 236 | 237 | deviceguider.once('disconnect', function(connection) { 238 | done(); 239 | }); 240 | deviceguider.disconnectDevice(toDisconnect); 241 | 242 | }); 243 | 244 | it('it should emit connectionStateChanged', function(done) { 245 | 246 | var handle; 247 | deviceguider.on('connectionStateChanged', handle = function(res) { 248 | if (res.state === 'disconnect') { 249 | deviceguider.off('connectionStateChanged', handle); 250 | done(); 251 | } 252 | }); 253 | deviceguider.disconnectDevice(toDisconnect); 254 | 255 | }); 256 | 257 | describe('with a callback', function() { 258 | 259 | it('it should call the callback', function(done) { 260 | 261 | deviceguider.disconnectDevice(toDisconnect, function(connection) { 262 | done(); 263 | }); 264 | 265 | }); 266 | 267 | }); 268 | 269 | }); 270 | 271 | describe('calling closeConnection with the id', function() { 272 | 273 | var toClose; 274 | 275 | beforeEach(function(done) { 276 | deviceguider.manualconnect(true, function() { 277 | deviceguider.once('connect', function(connection) { 278 | toClose = connection; 279 | 280 | done(); 281 | }); 282 | deviceguider.autoconnect(true); 283 | }); 284 | }); 285 | 286 | it('it should emit disconnect', function(done) { 287 | 288 | deviceguider.once('disconnect', function(connection) { 289 | done(); 290 | }); 291 | deviceguider.closeConnection(toClose.id); 292 | 293 | }); 294 | 295 | it('it should emit connectionStateChanged', function(done) { 296 | 297 | var handle; 298 | deviceguider.on('connectionStateChanged', handle = function(res) { 299 | if (res.state === 'disconnect') { 300 | deviceguider.off('connectionStateChanged', handle); 301 | done(); 302 | } 303 | }); 304 | deviceguider.closeConnection(toClose.id); 305 | 306 | }); 307 | 308 | describe('with a callback', function() { 309 | 310 | it('it should call the callback', function(done) { 311 | 312 | deviceguider.closeConnection(toClose.id, function(connection) { 313 | done(); 314 | }); 315 | 316 | }); 317 | 318 | }); 319 | 320 | }); 321 | 322 | }); 323 | 324 | }); -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | var EventEmitter2 = require('eventemitter2').EventEmitter2, 2 | util = require('util'), 3 | _ = require('lodash'), 4 | uuid = require('node-uuid').v4, 5 | pm; 6 | 7 | if (!pm) { 8 | try { 9 | pm = require('pm-notify'); 10 | } catch(e) { 11 | console.log(e.message); 12 | } 13 | } 14 | 15 | /** 16 | * Connection will be created from connect of device. 17 | * -reacts on open of device, calls onConnecting function if exists and emits 'connecting' and 'connect' 18 | * -reacts on closing of device and calls close on device 19 | * -reacts on close of device and cleans up 20 | * In extended constuctor create the framehandler(s) and subscribe to receive on the last framehandler. 21 | * 22 | * @param {Device} device The device object. 23 | */ 24 | function Connection(device) { 25 | var self = this; 26 | 27 | // call super class 28 | EventEmitter2.call(this, { 29 | wildcard: true, 30 | delimiter: ':', 31 | maxListeners: 1000 // default would be 10! 32 | }); 33 | 34 | if (this.log) { 35 | this.log = _.wrap(this.log, function(func, msg) { 36 | func(self.constructor.name + ': ' + msg); 37 | }); 38 | } else if (Connection.prototype.log) { 39 | Connection.prototype.log = _.wrap(Connection.prototype.log, function(func, msg) { 40 | func(self.constructor.name + ': ' + msg); 41 | }); 42 | this.log = Connection.prototype.log; 43 | } else { 44 | var debug = require('debug')(this.constructor.name); 45 | this.log = function(msg) { 46 | debug(msg); 47 | }; 48 | } 49 | 50 | this.taskQueue = []; 51 | 52 | this.commandQueue = []; 53 | 54 | this.isTaskRunning = false; 55 | 56 | this.id = uuid(); 57 | 58 | this.device = device; 59 | 60 | this.attributes = { id: this.id, device: this.device }; 61 | 62 | this.set('state', 'disconnect'); 63 | 64 | this.device.on('open', this.openHandle = function(callback) { 65 | if (self.log) self.log('connecting connection with id ' + self.id); 66 | 67 | if (pm) { 68 | pm.on('wake', self.wakeHandler = function() { 69 | if (self.log) self.log('waking up'); 70 | if (self.onWakeUp) { 71 | self.onWakeUp(); 72 | } 73 | }); 74 | pm.on('sleep', self.sleepHandler = function() { 75 | if (self.log) self.log('going to sleep'); 76 | if (self.onSleep) { 77 | self.onSleep(); 78 | } 79 | }); 80 | } 81 | 82 | self.set('state', 'connecting'); 83 | self.device.emit('connecting', self.device); 84 | self.emit('connecting', self); 85 | if (self.onConnecting) { 86 | self.onConnecting(function(err) { 87 | if (err) { 88 | if (self.log) self.log('could not connect connection with id ' + self.id); 89 | self.close(function() { 90 | if (callback) callback(err, self); 91 | }); 92 | } else { 93 | if (self.log) self.log('connect connection with id ' + self.id); 94 | self.device.emit('connect', self.device); 95 | self.set('state', 'connect'); 96 | self.emit('connect', self); 97 | if (callback) callback(null, self); 98 | } 99 | }); 100 | } else { 101 | if (self.log) self.log('connect connection with id ' + self.id); 102 | self.device.emit('connect', self.device); 103 | self.set('state', 'connect'); 104 | self.emit('connect', self); 105 | if (callback) callback(null, self); 106 | } 107 | }); 108 | 109 | this.device.on('closing', this.closingHandle = function(callback) { 110 | self.close(callback); 111 | }); 112 | 113 | this.device.on('close', this.closeHandle = function(callback) { 114 | if (self.log) self.log('disconnect connection with id ' + self.id); 115 | self.device.emit('disconnect', self.device); 116 | self.set('state', 'disconnect'); 117 | self.emit('disconnect', self); 118 | self.removeAllListeners(); 119 | self.removeAllListeners('connect'); 120 | self.removeAllListeners('connecting'); 121 | self.removeAllListeners('disconnect'); 122 | self.removeAllListeners('disconnecting'); 123 | self.device.removeListener('open', self.openHandle); 124 | self.device.removeListener('closing', self.closingHandle); 125 | self.device.removeListener('close', self.closeHandle); 126 | if (self.device && self.device.connection) { 127 | delete self.device.connection; 128 | delete self.device; 129 | } 130 | // if (callback) callback(null, self); 131 | 132 | if (pm) { 133 | if (self.wakeHandler) pm.removeListener('wake', self.wakeHandler); 134 | if (self.sleepHandler) pm.removeListener('sleep', self.sleepHandler); 135 | } 136 | 137 | if (self.taskQueue) { 138 | self.taskQueue = []; 139 | } 140 | if (self.commandQueue) { 141 | self.commandQueue = []; 142 | } 143 | }); 144 | } 145 | 146 | util.inherits(Connection, EventEmitter2); 147 | 148 | /** 149 | * Sets attributes for the connection. 150 | * 151 | * @example: 152 | * connection.set('firmwareVersion', '0.0.1'); 153 | * // or 154 | * connection.set({ 155 | * firmwareVersion: '0.0.1', 156 | * bootloaderVersion: '0.0.1' 157 | * }); 158 | */ 159 | Connection.prototype.set = function(data) { 160 | if (arguments.length === 2) { 161 | this.attributes[arguments[0]] = arguments[1]; 162 | } else { 163 | for(var m in data) { 164 | this.attributes[m] = data[m]; 165 | } 166 | } 167 | }; 168 | 169 | /** 170 | * Gets an attribute of the connection. 171 | * @param {string} attr The attribute name. 172 | * @return {object} The result value. 173 | * 174 | * @example: 175 | * connection.get('firmwareVersion'); // returns '0.0.1' 176 | */ 177 | Connection.prototype.get = function(attr) { 178 | return this.attributes[attr]; 179 | }; 180 | 181 | /** 182 | * Returns `true` if the attribute contains a value that is not null 183 | * or undefined. 184 | * @param {string} attr The attribute name. 185 | * @return {boolean} The result value. 186 | * 187 | * @example: 188 | * connection.has('firmwareVersion'); // returns true or false 189 | */ 190 | Connection.prototype.has = function(attr) { 191 | return (this.get(attr) !== null && this.get(attr) !== undefined); 192 | }; 193 | 194 | /** 195 | * The close mechanism. 196 | * On closing 'disconnecting' will be emitted. 197 | * onDisconnecting function will be called if it exists, 198 | * @param {Function} callback The function, that will be called when connection is closed. [optional] 199 | * `function(err){}` 200 | */ 201 | Connection.prototype.close = function(callback) { 202 | if (this.get('state') !== 'connect' && this.get('state') !== 'connecting') { 203 | if (callback) callback(new Error('Connection is already "' + this.get('state') + '"!')); 204 | return; 205 | } 206 | var self = this; 207 | if (this.log) this.log('disconnecting connection with id ' + self.id); 208 | this.device.emit('disconnecting', this.device); 209 | this.set('state', 'disconnecting'); 210 | this.emit('disconnecting', this); 211 | if (this.onDisconnecting) { 212 | this.onDisconnecting(function() { 213 | self.device.close(callback, true); 214 | }); 215 | } else { 216 | this.device.close(callback, true); 217 | } 218 | }; 219 | 220 | /** 221 | * Called when system is waking up from sleeping. 222 | */ 223 | Connection.prototype.onWakeUp = function() { 224 | var self = this; 225 | if (this.commandQueue && this.commandQueue.length > 0) { 226 | var cmd = this.commandQueue[0]; 227 | if (this.isSleeping) { 228 | this.isSleeping = false; 229 | if (this.log) this.log('restart sending commands'); 230 | self.sendCommand(cmd.command, cmd.callback); 231 | } else { 232 | setTimeout(function() { 233 | if (self.commandQueue && self.commandQueue.length > 0 && self.commandQueue[0] === cmd) { 234 | if (self.log) self.log('resend last command'); 235 | self.sendCommand(cmd.command, cmd.callback); 236 | } 237 | }, 1000); 238 | } 239 | } 240 | }; 241 | 242 | /** 243 | * Called when system is going to sleep. 244 | */ 245 | Connection.prototype.onSleep = function() { 246 | this.isSleeping = true; 247 | }; 248 | 249 | /** 250 | * Dequeue command 251 | * @param {Command} cmd The command that should be dequeued. 252 | */ 253 | Connection.prototype.dequeueCommand = function(cmd) { 254 | this.commandQueue = _.reject(this.commandQueue, function(c) { 255 | return c.command === cmd; 256 | }); 257 | }; 258 | 259 | /** 260 | * Dequeue task 261 | * @param {Task} cmd The task that should be dequeued. 262 | */ 263 | Connection.prototype.dequeueTask = function(task) { 264 | this.taskQueue = _.reject(this.taskQueue, function(t) { 265 | return t.task === task; 266 | }); 267 | }; 268 | 269 | /** 270 | * Executes the passing command. 271 | * If the initialize function is present it will validate the arguments of the command. 272 | * If necessary for validation reason the initialize function can throw errors. 273 | * @param {Command} command The object that represents the command. Should have a named constructor. 274 | * @param {Function} callback The function, that should be called when the command's answer is received. 275 | * `function(err){}` 276 | */ 277 | Connection.prototype.executeCommand = function(command, callback) { 278 | try { 279 | var self = this; 280 | 281 | function run() { 282 | self.commandQueue.push({ command: command, callback: function() { 283 | try { 284 | callback.apply(callback, _.toArray(arguments)); 285 | } catch(e) { 286 | var addon = ''; 287 | if (e.message) { 288 | addon = ' > ' + e.message; 289 | } 290 | if (self.log) self.log(e.name + ' while calling callback of command: ' + command.constructor.name + '!' + addon); 291 | self.dequeueCommand(command); 292 | console.log(e.stack); 293 | throw e; 294 | } 295 | }}); 296 | 297 | if (self.log) { self.log('>> COMMAND: ' + command.constructor.name); } 298 | 299 | if (!self.isSleeping) { 300 | process.nextTick(function() { 301 | try { 302 | self.sendCommand(command, callback); 303 | } catch (err) { 304 | var addon = ''; 305 | if (err.message) { 306 | addon = ' > ' + err.message; 307 | } 308 | if (self.log) self.log(err.name + ' while sending command: ' + command.constructor.name + '!' + addon); 309 | self.dequeueCommand(command); 310 | console.log(err.stack); 311 | throw err; 312 | } 313 | }); 314 | } 315 | } 316 | 317 | if (command.initialize) { 318 | try { 319 | command._initialize(this); 320 | } catch (err) { 321 | if (this.log) this.log(err.name + ' while initializing command: ' + command.constructor.name + '!'); 322 | if (callback) { callback(err); } 323 | return; 324 | } 325 | } 326 | run(); 327 | 328 | } catch (err) { 329 | var addon = ''; 330 | if (err.message) { 331 | addon = ' > ' + err.message; 332 | } 333 | if (this.log) this.log(err.name + ' while executing command: ' + command.constructor.name + '!' + addon); 334 | this.dequeueCommand(command); 335 | console.log(err.stack); 336 | throw err; 337 | } 338 | }; 339 | 340 | /** 341 | * Sends the passing command. 342 | * Define your own command handling mechanism in extended connection! 343 | * @param {Command} command The object that represents the command. Should have a named constructor. 344 | * @param {Function} callback The function, that should be called when the command's answer is received. 345 | * `function(err){}` 346 | */ 347 | Connection.prototype.sendCommand = function(command, callback) { 348 | var err = new Error('Implement the sendCommand function!'); 349 | console.log(err.stack); 350 | throw err; 351 | // this.frameHandler.send(command.data); 352 | }; 353 | 354 | /** 355 | * Checks if there is a task in the queue and runs it. 356 | * @return {Boolean} The check result. 357 | */ 358 | Connection.prototype.executeNextTask = function() { 359 | 360 | if (this.taskQueue.length > 0) { 361 | var task = this.taskQueue.splice(0, 1)[0]; 362 | this.runTask(task.task, task.callback); 363 | return true; 364 | } 365 | return false; 366 | 367 | }; 368 | 369 | /** 370 | * Checks if there is something to execute and executes it. 371 | */ 372 | Connection.prototype.checkForNextExecution = function() { 373 | var commandsFinished = true; 374 | if (this.commandQueue && _.isArray(this.commandQueue)) { 375 | commandsFinished = this.commandQueue.length === 0; 376 | } 377 | if (!this.isTaskRunning && commandsFinished) { 378 | this.executeNextTask(); 379 | } 380 | }; 381 | 382 | /** 383 | * Runs the passing task. 384 | * @param {Object} task The object that represents the task. Should have a named constructor and the function perform. 385 | * @param {Function} callback The function, that will be called when the task has started. 386 | * `function(err){}` 387 | */ 388 | Connection.prototype.runTask = function(task, callback) { 389 | 390 | if (this.log) { this.log('>> TASK ' + task.constructor.name); } 391 | 392 | this.isTaskRunning = true; 393 | 394 | var self = this; 395 | 396 | (function(currentTask, clb) { 397 | currentTask.perform(self, function() { 398 | if (self.log) { self.log('<< TASK ' + currentTask.constructor.name); } 399 | 400 | self.isTaskRunning = false; 401 | 402 | clb.apply(currentTask, _.toArray(arguments)); 403 | }); 404 | })(task, callback); 405 | 406 | }; 407 | 408 | /** 409 | * Executes the passing task. 410 | * If the initialize function is present it will validate the arguments of the task. 411 | * If necessary for validation reason the initialize function can throw errors. 412 | * @param {Task} task The object that represents the task. Should have a named constructor and the function perform. 413 | * @param {Boolean} ignoreQueue If set to true it will not put it in the queue but it will be immediately executed. 414 | * @param {Function} callback The function, that will be called when the task has started. 415 | * `function(err){}` 416 | */ 417 | Connection.prototype.executeTask = function(task, ignoreQueue, callback) { 418 | 419 | if (!callback) { 420 | callback = ignoreQueue; 421 | ignoreQueue = false; 422 | } 423 | 424 | try { 425 | var self = this; 426 | 427 | function run() { 428 | if (ignoreQueue) { 429 | self.runTask(task, callback); 430 | } else { 431 | self.taskQueue.push({ task: task, callback: function() { 432 | try { 433 | callback.apply(callback, _.toArray(arguments)); 434 | } catch(e) { 435 | var addon = ''; 436 | if (e.message) { 437 | addon = ' > ' + e.message; 438 | } 439 | if (self.log) self.log(e.name + ' while calling callback of task: ' + task.constructor.name + '!' + addon); 440 | console.log(e.stack); 441 | throw e; 442 | } 443 | }}); 444 | 445 | if (!self.isTaskRunning) { 446 | self.checkForNextExecution(); 447 | } 448 | } 449 | } 450 | 451 | if (task.initialize) { 452 | try { 453 | task._initialize(this); 454 | } catch (err) { 455 | if (this.log) this.log(err.name + ' while initializing task: ' + task.constructor.name + '!'); 456 | if (callback) { callback(err); } 457 | return; 458 | } 459 | } 460 | run(); 461 | 462 | } catch (err) { 463 | var addon = ''; 464 | if (err.message) { 465 | addon = ' > ' + err.message; 466 | } 467 | if (this.log) this.log(err.name + ' while executing task: ' + task.constructor.name + '!' + addon); 468 | this.dequeueTask(task); 469 | console.log(err.stack); 470 | throw err; 471 | } 472 | 473 | }; 474 | 475 | /* The toJSON function will be called when JSON.stringify(). */ 476 | Connection.prototype.toJSON = function() { 477 | var parse = JSON.deserialize || JSON.parse; 478 | var json = parse(JSON.stringify(this.attributes)); 479 | json.device = this.device ? this.device.toJSON() : undefined; 480 | return json; 481 | }; 482 | 483 | Connection.prototype.copy = function(device) { 484 | var clone = new Connection(device || this.device); 485 | clone.set(this.attributes); 486 | clone.set('id', clone.id); 487 | return clone; 488 | }; 489 | 490 | module.exports = Connection; -------------------------------------------------------------------------------- /lib/deviceguider.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | EventEmitter2 = require('eventemitter2').EventEmitter2, 3 | _ = require('lodash'), 4 | async = require('async'), 5 | DeviceNotFound = require('./errors/deviceNotFound'); 6 | 7 | /** 8 | * A deviceguider emits 'plug' for new attached devices, 9 | * 'unplug' for removed devices, emits 'connect' for connected devices 10 | * and emits 'disconnect' for disconnected devices. 11 | * Emits 'connecting' and 'disconnecting' with device object. 12 | * Emits 'connectionStateChanged' with state and connection or device. 13 | * @param {DeviceLoader} deviceLoader The deviceloader object. 14 | */ 15 | function DeviceGuider(deviceLoader) { 16 | var self = this; 17 | 18 | // call super class 19 | EventEmitter2.call(this, { 20 | wildcard: true, 21 | delimiter: ':', 22 | maxListeners: 1000 // default would be 10! 23 | }); 24 | 25 | if (this.log) { 26 | this.log = _.wrap(this.log, function(func, msg) { 27 | func(self.constructor.name + ': ' + msg); 28 | }); 29 | } else if (DeviceGuider.prototype.log) { 30 | DeviceGuider.prototype.log = _.wrap(DeviceGuider.prototype.log, function(func, msg) { 31 | func(self.constructor.name + ': ' + msg); 32 | }); 33 | this.log = DeviceGuider.prototype.log; 34 | } else { 35 | var debug = require('debug')(this.constructor.name); 36 | this.log = function(msg) { 37 | debug(msg); 38 | }; 39 | } 40 | 41 | this.deviceErrorSubscriptions = {}; 42 | this.deviceLoader = deviceLoader; 43 | this.lookupStarted = false; 44 | this.currentState = { 45 | connectionMode: 'manualconnect', 46 | plugged: [], 47 | connected: [], 48 | getDevice: function(id) { 49 | return _.find(self.currentState.plugged, function(d) { 50 | return d.id === id; 51 | }); 52 | }, 53 | getConnection: function(id) { 54 | var found = _.find(self.currentState.connected, function(dc) { 55 | return dc.connection.id === id; 56 | }); 57 | 58 | return found ? found.connection : null; 59 | }, 60 | getConnectedDevice: function(id) { 61 | return _.find(self.currentState.connected, function(dc) { 62 | return dc.id === id; 63 | }); 64 | }, 65 | getDeviceByConnection: function(id) { 66 | return _.find(self.currentState.connected, function(dc) { 67 | return dc.connection.id === id; 68 | }); 69 | }, 70 | getConnectionByDevice: function(id) { 71 | var found = _.find(self.currentState.connected, function(dc) { 72 | return dc.id === id; 73 | }); 74 | 75 | return found ? found.connection : null; 76 | } 77 | }; 78 | 79 | this.deviceLoader.on('plug', function(device) { 80 | 81 | self.deviceErrorSubscriptions[device.id] = function(err) { 82 | if (self.listeners('error').length) { 83 | self.emit('error', err); 84 | } 85 | }; 86 | device.on('error', self.deviceErrorSubscriptions[device.id]); 87 | 88 | self.currentState.plugged.push(device); 89 | 90 | if (self.log) self.log('plug device with id ' + device.id); 91 | self.emit('plug', device); 92 | self.emit('plugChanged', { state: 'plug', device: device }); 93 | 94 | if (self.currentState.connectionMode !== 'manualconnect') { 95 | if (self.currentState.connectionMode === 'autoconnect' || (self.currentState.connectionMode === 'autoconnectOne' && self.currentState.connected.length === 0)) { 96 | self.connect(device); 97 | } 98 | } 99 | }); 100 | 101 | this.deviceLoader.on('unplug', function(device) { 102 | device.removeListener('error', self.deviceErrorSubscriptions[device.id]); 103 | delete self.deviceErrorSubscriptions[device.id]; 104 | 105 | self.currentState.plugged = _.reject(self.currentState.plugged, function(d) { 106 | return d.id === device.id; 107 | }); 108 | self.currentState.connected = _.reject(self.currentState.connected, function(dc) { 109 | return dc.id === device.id; 110 | }); 111 | 112 | if (self.log) self.log('unplug device with id ' + device.id); 113 | self.emit('unplug', device); 114 | self.emit('plugChanged', { state: 'unplug', device: device }); 115 | }); 116 | } 117 | 118 | util.inherits(DeviceGuider, EventEmitter2); 119 | 120 | 121 | /* Override of EventEmitter. */ 122 | DeviceGuider.prototype.on = function(eventname, callback) { 123 | if (this.deviceLoader && (eventname === 'plugChanged' || eventname === 'plug' || eventname === 'unplug')) { 124 | this.deviceLoader.on.apply(this.deviceLoader, _.toArray(arguments)); 125 | if (!this.deviceLoader.isRunning) { 126 | this.lookupStarted = true; 127 | this.deviceLoader.startLookup(); 128 | } 129 | } else { 130 | EventEmitter2.prototype.on.apply(this, _.toArray(arguments)); 131 | } 132 | }; 133 | 134 | /* Override of EventEmitter. */ 135 | DeviceGuider.prototype.removeListener = function(eventname, callback) { 136 | if (this.deviceLoader && (eventname === 'plugChanged' || eventname === 'plug' || eventname === 'unplug')) { 137 | this.deviceLoader.removeListener.apply(this.deviceLoader, _.toArray(arguments)); 138 | if (this.deviceLoader.listeners('plugChanged').length === 0 && this.deviceLoader.listeners('plug').length === 0 && this.deviceLoader.listeners('unplug').length === 0 ) { 139 | this.lookupStarted = false; 140 | this.deviceLoader.stopLookup(); 141 | } 142 | } else { 143 | EventEmitter2.prototype.removeListener.apply(this, _.toArray(arguments)); 144 | } 145 | }; 146 | 147 | /* Same as removeListener */ 148 | DeviceGuider.prototype.off = DeviceGuider.prototype.removeListener; 149 | 150 | /** 151 | * Checks if the connectionMode will change and returns true or false. 152 | * @param {String} mode The new connectionMode. 153 | * @return {Boolean} Returns true or false. 154 | */ 155 | DeviceGuider.prototype.checkConnectionMode = function(mode) { 156 | return mode !== this.currentState.connectionMode; 157 | }; 158 | 159 | /** 160 | * Checks if the connectionMode will change and emits 'connectionModeChanged'. 161 | * @param {String} mode The new connectionMode. 162 | * @return {Boolean} Returns true or false. 163 | */ 164 | DeviceGuider.prototype.changeConnectionMode = function(mode) { 165 | var self = this; 166 | 167 | var somethingChanged = this.checkConnectionMode(mode); 168 | this.currentState.connectionMode = mode; 169 | 170 | if (somethingChanged) { 171 | this.emit('connectionModeChanged', this.currentState.connectionMode); 172 | } 173 | 174 | return somethingChanged; 175 | }; 176 | 177 | /** 178 | * Calls the callback with the current state, containing the connectionMode, 179 | * the list of plugged devices and the list of connected devices. 180 | * @param {Function} callback The function, that will be called when state is fetched. 181 | * `function(err, currentState){}` currentState is an object. 182 | */ 183 | DeviceGuider.prototype.getCurrentState = function(callback) { 184 | var self = this; 185 | 186 | if (this.currentState.plugged.length > 0) { 187 | if (callback) { callback(null, this.currentState); } 188 | } else { 189 | if (!this.deviceLoader.isRunning && !this.lookupStarted) { 190 | this.deviceLoader.startLookup(function(err, devices) { 191 | if (callback) { callback(null, self.currentState); } 192 | }); 193 | } else { 194 | this.deviceLoader.trigger(function(err, devices) { 195 | if (callback) { callback(null, self.currentState); } 196 | }); 197 | } 198 | } 199 | }; 200 | 201 | /** 202 | * It emits 'connectionModeChanged'. 203 | * When plugging a device it will now automatically connect it and emit 'connect'. 204 | * Already plugged devices will connect immediately. 205 | * @param {Boolean} directlyConnect If set, connections will be opened. [optional] 206 | * @param {Function} callback The function, that will be called when function has finished. [optional] 207 | * `function(err){}` 208 | */ 209 | DeviceGuider.prototype.autoconnect = function(directlyConnect, callback) { 210 | var self = this; 211 | 212 | if (arguments.length === 1) { 213 | if (_.isFunction(directlyConnect)) { 214 | callback = directlyConnect; 215 | directlyConnect = false; 216 | } 217 | } 218 | 219 | if (this.changeConnectionMode('autoconnect')) { 220 | if (!this.deviceLoader.isRunning && !this.lookupStarted) { 221 | this.deviceLoader.startLookup(); 222 | } 223 | 224 | if (!directlyConnect) { 225 | if (callback) { callback(null); } 226 | return; 227 | } 228 | 229 | var toConnect = _.reject(this.currentState.plugged, function(d) { 230 | return _.find(self.currentState.connected, function(c) { 231 | return c.id == d.id; 232 | }); 233 | }); 234 | 235 | async.forEachSeries(toConnect, function(dc, clb) { 236 | self.connect(dc, clb); 237 | }, function(err) { 238 | if (err && !err.name) { 239 | err = new Error(err); 240 | } 241 | if (callback) { return callback(err); } 242 | }); 243 | } else { 244 | if (callback) { return callback(null); } 245 | } 246 | }; 247 | 248 | /** 249 | * Call with optional holdConnections flag and optional callback it emits 'connectionModeChanged'. 250 | * When plugging a device it will not connect it. 251 | * Dependent on the holdConnections flag already connected devices will disconnect immediately and call the callback. 252 | * @param {Boolean} disconnectDirectly If set established connections will be closed. [optional] 253 | * @param {Function} callback The function, that will be called when function has finished. [optional] 254 | * `function(err){}` 255 | */ 256 | DeviceGuider.prototype.manualconnect = function(disconnectDirectly, callback) { 257 | var self = this; 258 | 259 | if (arguments.length === 1) { 260 | if (_.isFunction(disconnectDirectly)) { 261 | callback = disconnectDirectly; 262 | disconnectDirectly = false; 263 | } 264 | } 265 | 266 | if (this.changeConnectionMode('manualconnect')) { 267 | if (this.currentState.connected.length === 0) { 268 | if (callback) { return callback(null); } 269 | } 270 | 271 | if (!disconnectDirectly) { 272 | if (callback) { callback(null); } 273 | return; 274 | } 275 | 276 | async.forEachSeries(this.currentState.connected, function(dc, clb) { 277 | self.closeConnection(dc.connection, clb); 278 | }, function(err) { 279 | if (err && !err.name) { 280 | err = new Error(err); 281 | } 282 | if (callback) { return callback(err); } 283 | }); 284 | } else { 285 | if (callback) { return callback(null); } 286 | } 287 | }; 288 | 289 | /** 290 | * It emits 'connectionModeChanged'. 291 | * When plugging one device it will now automatically connect one and emit 'connect'. 292 | * If there is already a plugged device, it will connect immediately and call the callback. 293 | * @param {Boolean} directlyConnect If set, connections will be opened. [optional] 294 | * @param {Function} callback The function, that will be called when one device is connected. [optional] 295 | * `function(err, connection){}` connection is a Connection object. 296 | */ 297 | DeviceGuider.prototype.autoconnectOne = function(directlyConnect, callback) { 298 | var self = this; 299 | 300 | if (arguments.length === 1) { 301 | if (_.isFunction(directlyConnect)) { 302 | callback = directlyConnect; 303 | directlyConnect = false; 304 | } 305 | } 306 | 307 | function doIt() { 308 | if (!directlyConnect) { 309 | if (callback) callback(null); 310 | return; 311 | } 312 | 313 | if (self.currentState.plugged.length > 0 && self.currentState.connected.length === 0) { 314 | self.connect(self.currentState.plugged[0], callback); 315 | } else { 316 | if (callback) callback(new Error('No device available!')); 317 | } 318 | } 319 | 320 | if (this.changeConnectionMode('autoconnectOne')) { 321 | if (!this.deviceLoader.isRunning && !this.lookupStarted) { 322 | this.deviceLoader.startLookup(doIt); 323 | } else { 324 | doIt(); 325 | } 326 | } else { 327 | if (callback) { return callback(null); } 328 | } 329 | }; 330 | 331 | /** 332 | * Changes the connection mode. 333 | * @param {String} mode 'autoconnect' || 'manualconnect' || 'autoconnectOne' 334 | * @param {Object} options Options for connection mode 335 | * @param {Function} callback The function, that will be called when function has finished [optional] 336 | * `function(err){}` connection is a Connection object. 337 | */ 338 | DeviceGuider.prototype.setConnectionMode = function(mode, options, callback) { 339 | if (arguments.length === 2) { 340 | if (_.isFunction(options)) { 341 | callback = options; 342 | options = false; 343 | } 344 | } 345 | 346 | if (mode === 'autoconnect') { 347 | this.autoconnect.apply(this, [options, callback]); 348 | } else if (mode === 'autoconnectOne') { 349 | this.autoconnectOne.apply(this, [options, callback]); 350 | } else if (mode === 'manualconnect') { 351 | this.manualconnect.apply(this, [options, callback]); 352 | } else { 353 | if (this.log) this.log('Connection Mode not valid!'); 354 | if (callback) callback(new Error('Connection Mode not valid!')); 355 | } 356 | }; 357 | 358 | /** 359 | * It will connect that device and call the callback. 360 | * @param {Device || String} deviceOrId The device object or the device id. 361 | * @param {Function} callback The function, that will be called when the device is connected. [optional] 362 | * `function(err, connection){}` connection is a Connection object. 363 | */ 364 | DeviceGuider.prototype.connect = function(deviceOrId, callback) { 365 | var self = this, 366 | device = deviceOrId; 367 | 368 | if (_.isString(deviceOrId)) { 369 | device = this.currentState.getDevice(deviceOrId); 370 | } else if (deviceOrId.id && !device.connect) { 371 | device = this.currentState.getDevice(deviceOrId.id); 372 | } else if (!deviceOrId.connect) { 373 | device = null; 374 | } 375 | 376 | if (!device) { 377 | var err = new DeviceNotFound('Device not found!'); 378 | if (this.log) { this.log(err.message); } 379 | if (callback) { callback(err); } 380 | return; 381 | } 382 | 383 | if (!this.deviceErrorSubscriptions[device.id]) { 384 | this.deviceErrorSubscriptions[device.id] = function(err) { 385 | if (self.listeners('error').length) { 386 | self.emit('error', err); 387 | } 388 | }; 389 | this.on('error', this.deviceErrorSubscriptions[device.id]); 390 | } 391 | 392 | device.on('connecting', function(dev) { 393 | if (self.log) self.log('connecting device with id ' + dev.id); 394 | self.emit('connecting', dev); 395 | self.emit('connectionStateChanged', { state: 'connecting', device: dev }); 396 | }); 397 | 398 | device.connect(function(err, connection) { 399 | if (err) { 400 | if (!err.name) { 401 | err = new Error(err); 402 | } 403 | if (self.log) self.log('Error while calling connect: ' + err.name + (err.message ? ': ' + err.message : '')); 404 | if (callback) { 405 | callback(err, connection); 406 | } 407 | 408 | self.emit('disconnect', connection); 409 | self.emit('connectionStateChanged', { state: 'disconnect', connection: connection, device: device }); 410 | return; 411 | } 412 | self.currentState.connected.push(device); 413 | 414 | connection.on('disconnecting', function(conn) { 415 | if (self.log) self.log('disconnecting device with id ' + conn.device.id + ' and connection id ' + conn.id); 416 | self.emit('disconnecting', conn.get('device')); 417 | self.emit('connectionStateChanged', { state: 'disconnecting', device: conn.get('device'), connection: conn }); 418 | }); 419 | 420 | connection.on('disconnect', function(conn) { 421 | self.currentState.connected = _.reject(self.currentState.connected, function(dc) { 422 | return dc.id === conn.device.id; 423 | }); 424 | 425 | if (self.log) self.log('disconnect device with id ' + conn.device.id + ' and connection id ' + conn.id); 426 | self.emit('disconnect', conn); 427 | self.emit('connectionStateChanged', { state: 'disconnect', connection: conn, device: conn.device }); 428 | }); 429 | 430 | if (self.log) self.log('connect device with id ' + device.id + ' and connection id ' + connection.id); 431 | self.emit('connect', connection); 432 | self.emit('connectionStateChanged', { state: 'connect', connection: connection, device: connection.device }); 433 | 434 | if (callback) callback(null, connection); 435 | }); 436 | }; 437 | 438 | /** 439 | * It will connect that device and call the callback. 440 | * @param {Device || String} deviceOrId The device object or the device id. 441 | * @param {Function} callback The function, that will be called when the device is connected. [optional] 442 | * `function(err, connection){}` connection is a Connection object. 443 | */ 444 | DeviceGuider.prototype.connectDevice = DeviceGuider.prototype.connect; 445 | 446 | /** 447 | * It will disconnect that device and call the callback. 448 | * @param {Device || String} deviceOrId The device object or the device id. 449 | * @param {Function} callback The function, that will be called when the device is disconnected. [optional] 450 | * `function(err, connection){}` connection is a Connection object. 451 | */ 452 | DeviceGuider.prototype.disconnect = function(deviceOrId, callback) { 453 | var self = this, 454 | device = deviceOrId; 455 | 456 | if (_.isString(deviceOrId)) { 457 | device = this.currentState.getConnectedDevice(deviceOrId); 458 | } else if (deviceOrId.id && !deviceOrId.disconnect) { 459 | device = this.currentState.getDevice(deviceOrId.id); 460 | } else if (!deviceOrId.disconnect) { 461 | device = null; 462 | } 463 | 464 | if (!device) { 465 | var err = new DeviceNotFound('Device not found!'); 466 | if (this.log) { this.log(err.message); } 467 | if (callback) { callback(err); } 468 | return; 469 | } 470 | 471 | device.disconnect(function(err) { 472 | if (err) { 473 | if (self.log) self.log('error calling disconnect' + JSON.stringify(err)); 474 | if (callback) { 475 | if (!err.name) { 476 | err = new Error(err); 477 | } 478 | callback(err, device.connection); 479 | } 480 | return; 481 | } 482 | 483 | if (callback) callback(null, device.connection); 484 | }); 485 | }; 486 | 487 | /** 488 | * It will disconnect that device and call the callback. 489 | * @param {Device || String} deviceOrId The device object or the device id. 490 | * @param {Function} callback The function, that will be called when the device is disconnected. [optional] 491 | * `function(err, connection){}` connection is a Connection object. 492 | */ 493 | DeviceGuider.prototype.disconnectDevice = DeviceGuider.prototype.disconnect; 494 | 495 | /** 496 | * It will close that connection and call the callback. 497 | * @param {Connection || String} connectionOrId The connection object or the connection id. 498 | * @param {Function} callback The function, that will be called when the connection is closed. [optional] 499 | * `function(err, connection){}` connection is a Connection object. 500 | */ 501 | DeviceGuider.prototype.closeConnection = function(connectionOrId, callback) { 502 | var self = this, 503 | connection = connectionOrId; 504 | 505 | if (_.isString(connectionOrId)) { 506 | connection = this.currentState.getConnection(connectionOrId); 507 | } 508 | 509 | if (!connection) { 510 | var error = new Error('Connection not found!'); 511 | if (this.log) this.log('Connection not found!'); 512 | if (callback) { callback(error); } 513 | return; 514 | } 515 | 516 | connection.close(function(err) { 517 | if (err) { 518 | if (self.log) self.log('error calling disconnect' + JSON.stringify(err)); 519 | if (callback) { 520 | if (!err.name) { 521 | err = new Error(err); 522 | } 523 | callback(err, connection); 524 | } 525 | return; 526 | } 527 | 528 | if (callback) callback(null, connection); 529 | }); 530 | }; 531 | 532 | module.exports = DeviceGuider; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
  2 |   eeeee eeeee eeeee eeee       e  eeeee 
  3 |   8   8 8  88 8   8 8          8  8   " 
  4 |   8e  8 8   8 8e  8 8eee       8e 8eeee 
  5 |   88  8 8   8 88  8 88      e  88    88 
  6 |   88  8 8eee8 88ee8 88ee 88 8ee88 8ee88
  7 | 
  8 |   eeeee eeee e   e e   eeee eeee eeeee eeeee eeeee  eeee e  ee 
  9 |   8   8 8    8   8 8  8e    8    8   "   8   8   8 8e    8 8   
 10 |   8e  8 8eee e   e 8e 8     8eee 8eeee   8e  8eee8 8     8eee 
 11 |   88  8 88    8 8  88 8e    88      88   88  88  8 8e    88 8e
 12 |   88ee8 88ee   8   88  eeee 88ee 8ee88   88  88  8  eeee 88  8
 13 | 
14 | 15 | # Introduction 16 | 17 | [![travis](https://img.shields.io/travis/adrai/devicestack.svg)](https://travis-ci.org/adrai/devicestack) [![npm](https://img.shields.io/npm/v/devicestack.svg)](https://npmjs.org/package/devicestack) 18 | 19 | This module helps you to represent a device and its protocol. 20 | 21 | [Release notes](https://github.com/adrai/devicestack/blob/master/releasenotes.md) 22 | 23 |
 24 |    ___________________________________________________   
 25 |   |                                   |          |    |  
 26 |   |                                   |   tasks  |    |  
 27 |   |________________    deviceguider   |__________|    |  
 28 |   |                |                  |               |  
 29 |   |  deviceloader  |                  |   commands    |  
 30 |   |________________|__________________|_______________|  
 31 |   |                |               ___________________|  
 32 |   |                |  connection  |                   |  
 33 |   |     device     |______________|  framehandler(s)  |  
 34 |   |                               |___________________|  
 35 |   |                                                   |  
 36 |   |___________________________________________________|  
 37 | 
 38 | 
39 | 40 | Each of the following software components can be used separately if you want... 41 | - require('devicestack').Device 42 | - require('devicestack').SerialDevice 43 | - require('devicestack').FtdiDevice 44 | - require('devicestack').FtdiSerialDevice 45 | - require('devicestack').Connection 46 | - require('devicestack').FrameHandler 47 | - require('devicestack').DeviceLoader 48 | - require('devicestack').SerialDeviceLoader 49 | - require('devicestack').EventedSerialDeviceLoader 50 | - require('devicestack').FtdiDeviceLoader 51 | - require('devicestack').EventedFtdiDeviceLoader 52 | - require('devicestack').DeviceGuider 53 | - require('devicestack').SerialDeviceGuider 54 | - require('devicestack').Command 55 | - require('devicestack').Task 56 | 57 | ## Prototype hierarchy 58 | 59 | Device -> SerialDevice -> FtdiSerialDevice 60 | Device -> FtdiDevice -> FtdiSerialDevice 61 | DeviceLoader -> SerialDeviceLoader -> EventedSerialDeviceLoader 62 | DeviceLoader -> FtdiDeviceLoader -> EventedFtdiDeviceLoader 63 | DeviceGuider -> SerialDeviceGuider 64 | 65 | ## device 66 | Device represents your physical device. 67 | 68 | ### open 69 | Implement the open mechanism to your device. 70 | Call with optional callback. On opened emit 'open' and call the callback. 71 | 72 | - If extending from `require('devicestack').SerialDevice` this mechanism is already defined! 73 | - If extending from `require('devicestack').FtdiDevice` this mechanism is already defined! 74 | - If extending from `require('devicestack').FtdiSerialDevice` this mechanism is already defined! 75 | 76 | ### close 77 | Implement the close mechanism to your device. 78 | Call with optional callback. On closed emit 'close' and call the callback. 79 | 80 | - If extending from `require('devicestack').SerialDevice` this mechanism is already defined! 81 | - If extending from `require('devicestack').FtdiDevice` this mechanism is already defined! 82 | - If extending from `require('devicestack').FtdiSerialDevice` this mechanism is already defined! 83 | 84 | ### send 85 | Implement the send mechanism to your device by subscribing the 'send' event. 86 | Call send or emit 'send' on the device with a byte array. 87 | 88 | - If extending from `require('devicestack').Device` the send function is already defined! 89 | 90 | ### receive 91 | Implement the receive mechanism from your device by emitting the 'receive' event. 92 | When you receive data from your device emit 'receive' with a byte array. 93 | 94 | - If extending from `require('devicestack').SerialDevice` this mechanism is already defined! 95 | - If extending from `require('devicestack').FtdiDevice` this mechanism is already defined! 96 | - If extending from `require('devicestack').FtdiSerialDevice` this mechanism is already defined! 97 | 98 | ### connect 99 | Implement the connect mechanism to your device. 100 | Call with optional callback. On connecting emit 'opening', create a new connection instance and call open by passing the callback. 101 | 102 | - If extending from `require('devicestack').Device` this mechanism is already defined! 103 | 104 | ### disconnect 105 | Implement the disconnect mechanism to your device. 106 | Call with optional callback. On disconnecting emit 'closing' and call close by passing the callback. 107 | 108 | - If extending from `require('devicestack').Device` this mechanism is already defined! 109 | 110 | ### set 111 | Sets attributes for the device. 112 | 113 | - If extending from `require('devicestack').Device` this mechanism is already defined! 114 | 115 | example: 116 | 117 | device.set('firmwareVersion', '0.0.1'); 118 | // or 119 | device.set({ 120 | firmwareVersion: '0.0.1', 121 | bootloaderVersion: '0.0.1' 122 | }); 123 | 124 | ### get 125 | Gets an attribute of the device. 126 | 127 | - If extending from `require('devicestack').Device` this mechanism is already defined! 128 | 129 | example: 130 | 131 | device.get('firmwareVersion'); // returns '0.0.1' 132 | 133 | ### has 134 | Returns `true` if the attribute contains a value that is not null or undefined. 135 | 136 | - If extending from `require('devicestack').Device` this mechanism is already defined! 137 | 138 | example: 139 | 140 | device.has('firmwareVersion'); // returns true or false 141 | 142 | 143 | ## connection 144 | Connection will be created from connect of device. 145 | 146 | - reacts on open of device, calls `onConnecting` function if exists and emits 'connecting' and 'connect' and emits 'connecting' on device 147 | - reacts on closing of device and calls close on device 148 | - reacts on close of device and cleans up 149 | 150 | In extended constuctor create the framehandler(s) and subscribe to receive on the last framehandler. 151 | 152 | ### close 153 | Implement the close mechanism. 154 | Call with optional callback. On closing emit 'disconnecting', call `onDisconnecting` function if exists, on disconnected emit 'disconnect' and call close on device by passing the callback. Emits 'disconnecting' on device. 155 | 156 | - If extending from `require('devicestack').Connection` this mechanism is already defined! 157 | 158 | ### onConnecting 159 | Define `onConnecting` function if you need to send some commands before definitely connected? 160 | 161 | ### onDisconnecting 162 | Define `onDisconnecting` function if you need to send some commands before definitely disconnected? 163 | 164 | ### set 165 | Sets attributes for the connection. 166 | 167 | - If extending from `require('devicestack').Connection` this mechanism is already defined! 168 | 169 | example: 170 | 171 | connection.set('firmwareVersion', '0.0.1'); 172 | // or 173 | connection.set({ 174 | firmwareVersion: '0.0.1', 175 | bootloaderVersion: '0.0.1' 176 | }); 177 | 178 | ### get 179 | Gets an attribute of the connection. 180 | 181 | - If extending from `require('devicestack').Connection` this mechanism is already defined! 182 | 183 | example: 184 | 185 | connection.get('firmwareVersion'); // returns '0.0.1' 186 | 187 | ### has 188 | Returns `true` if the attribute contains a value that is not null or undefined. 189 | 190 | - If extending from `require('devicestack').Connection` this mechanism is already defined! 191 | 192 | example: 193 | 194 | connection.has('firmwareVersion'); // returns true or false 195 | 196 | ### executeCommand 197 | Executes the passing command. 198 | If the initialize function is present it will validate the arguments of the command. 199 | If necessary for validation reason the initialize function can throw errors. 200 | Pushes the command in a queue and calls the sendCommand function. 201 | 202 | ### sendCommand 203 | Sends the passing command. 204 | Implement the executeCommand mechanism. 205 | 206 | ### executeTask 207 | Executes the passing task. 208 | If the initialize function is present it will validate the arguments of the task. 209 | If necessary for validation reason the initialize function can throw errors. 210 | 211 | - If extending from `require('devicestack').Connection` this mechanism is already defined! 212 | 213 | ### runTask 214 | Runs the passing task. 215 | 216 | - If extending from `require('devicestack').Connection` this mechanism is already defined! 217 | 218 | ### runTask 219 | Runs the passing task. 220 | 221 | - If extending from `require('devicestack').Connection` this mechanism is already defined! 222 | 223 | ### executeNextTask 224 | Checks if there is a task in the queue and runs it. 225 | 226 | - If extending from `require('devicestack').Connection` this mechanism is already defined! 227 | 228 | ### checkForNextExecution 229 | Checks if there is something to execute and executes it. 230 | IMPORTANT!!! Call this function when a command answer is handled! 231 | 232 | - If extending from `require('devicestack').Connection` this mechanism is already defined! 233 | 234 | 235 | ## framehandler(s) 236 | You can have one or multiple framehandlers. A framhandler receives data from the upper layer and sends it to the lower layer by wrapping some header or footer information. A framehandler receives data from lower layer and sends it to the upper layer by unwrapping some header or footer information. The lowest layer for a framehandler is the device and the topmost ist the connection. 237 | 238 | - reacts on send of upper layer, calls `wrapFrame` function if exists and calls `send` function on lower layer 239 | - reacts on receive of lower layer, calls `unwrapFrame` function if exists and emits `receive` 240 | - automatically analyzes incomming data 241 | 242 | ### analyze 243 | Calls `analyzeNextFrame` function in a loop. 244 | 245 | - If extending from `require('devicestack').FrameHandler` this mechanism is already defined! 246 | 247 | ### trigger 248 | Triggers the `analyze` function. 249 | 250 | - If extending from `require('devicestack').FrameHandler` this mechanism is already defined! 251 | 252 | ### send 253 | Call send or emit 'send' on the device with a byte array. 254 | 255 | - If extending from `require('devicestack').FrameHandler` this mechanism is already defined! 256 | 257 | ### analyzeNextFrame 258 | Implement the analyzeNextFrame mechanism that returns a frame as byte array or null of no frame found. 259 | Call with current incomming buffer. 260 | 261 | ### wrapFrame 262 | Define `wrapFrame` function if you need to wrap frames? 263 | 264 | ### unwrapFrame 265 | Define `unwrapFrame` function if you need to unwrap frames? 266 | 267 | 268 | ## deviceloader 269 | A deviceloader can check if there are available some devices. 270 | 271 | ### startLookup 272 | Creates an interval that calls `trigger` function. 273 | Call with optional interval in milliseconds and optional callback. 274 | 275 | - If extending from `require('devicestack').DeviceLoader` this mechanism is already defined! 276 | 277 | ### stopLookup 278 | Stops the interval that calls `trigger` function. 279 | 280 | - If extending from `require('devicestack').DeviceLoader` this mechanism is already defined! 281 | 282 | ### trigger 283 | Calls `lookup` function with optional callback and emits 'plug' for new attached devices and 'unplug' for removed devices. 284 | 285 | - If extending from `require('devicestack').DeviceLoader` this mechanism is already defined! 286 | 287 | ### lookup 288 | Call with optional callback and call callback with an array of devices. 289 | 290 | - If extending from `require('devicestack').SerialDeviceLoader` this mechanism is already defined! 291 | - If extending from `require('devicestack').FtdiDeviceLoader` this mechanism is already defined! 292 | 293 | 294 | ## deviceguider 295 | A deviceguider emits 'plug' for new attached devices, 'unplug' for removed devices, emits 'connect' for connected devices and emits 'disconnect' for disconnected devices. 296 | - Emits 'connecting' and 'disconnecting' with device object. 297 | - Emits 'connectionStateChanged' with state and connection or device. 298 | 299 | ### getCurrentState 300 | Call with callback and it calls the callback with the current state, containing the connectionMode, the list of plugged devices and the list of connected devices. 301 | 302 | - If extending from `require('devicestack').DeviceGuider` this mechanism is already defined! 303 | 304 | ### autoconnect 305 | Call with optional connectDirectly flag it emits 'connectionModeChanged'. When plugging a device it will now automatically connect it and emit 'connect'. Dependent on the connectDirectly flag already plugged devices will connect immediately. 306 | 307 | - If extending from `require('devicestack').DeviceGuider` this mechanism is already defined! 308 | 309 | ### autoconnectOne 310 | Call with optional connectDirectly flag and callback it emits 'connectionModeChanged'. When plugging one device it will now automatically connect one and emit 'connect'. Dependent on the connectDirectly flag if there is already a plugged device, it will connect immediately and call the callback. 311 | 312 | - If extending from `require('devicestack').DeviceGuider` this mechanism is already defined! 313 | 314 | ### manualconnect 315 | Call with optional disconnectDirectly flag and optional callback it emits 'connectionModeChanged'. When plugging a device it will not connect it. Dependent on the disconnectDirectly flag already connected devices will disconnect immediately and call the callback. 316 | 317 | - If extending from `require('devicestack').DeviceGuider` this mechanism is already defined! 318 | 319 | ### setConnectionMode 320 | Call with optional options and optional callback it emits 'connectionModeChanged'. 321 | 322 | - If extending from `require('devicestack').DeviceGuider` this mechanism is already defined! 323 | 324 | ### connect (alias connectDevice) 325 | Call with the deviceId or the device and optional callback it will connect that device and call the callback. 326 | 327 | - If extending from `require('devicestack').DeviceGuider` this mechanism is already defined! 328 | 329 | ### disconnect (alias disconnectDevice) 330 | Call with the deviceId or the device and optional callback it will disconnect that device and call the callback. 331 | 332 | - If extending from `require('devicestack').DeviceGuider` this mechanism is already defined! 333 | 334 | ### closeConnection 335 | Call with the connectionId or the connection and optional callback it will close that connection and call the callback. 336 | 337 | - If extending from `require('devicestack').DeviceGuider` this mechanism is already defined! 338 | 339 | ### checkConnectionMode 340 | Call with a connectionMode value it checks if the connectionMode will change and returns true or false. 341 | 342 | - If extending from `require('devicestack').DeviceGuider` this mechanism is already defined! 343 | 344 | ### changeConnectionMode 345 | Call with a connectionMode value it checks if the connectionMode will change and emits 'connectionModeChanged'. 346 | 347 | - If extending from `require('devicestack').DeviceGuider` this mechanism is already defined! 348 | 349 | ### connect 350 | Call with the portname and optional callback it will connect that device and call the callback. 351 | 352 | - If extending from `require('devicestack').SerialDeviceGuider` this mechanism is already defined! 353 | 354 | ### disconnect 355 | Call with the portname and optional callback it will disconnect that device and call the callback. 356 | 357 | - If extending from `require('devicestack').SerialDeviceGuider` this mechanism is already defined! 358 | 359 | 360 | ## commands 361 | Build your own commands looking like this: 362 | 363 | var Command = require('../../index').Command, 364 | util = require('util'); 365 | 366 | function MyCommand(firstByte) { 367 | // call super class 368 | Command.call(this, arguments); 369 | } 370 | 371 | util.inherits(MyCommand, Command); 372 | 373 | // if argumentsSchema is defined it will validates the passed constructor arguments 374 | MyCommand.prototype.argumentsSchema = { 375 | type: 'array', 376 | items: [ 377 | { 378 | anyOf: [ 379 | { 380 | type: 'number' 381 | }, 382 | { 383 | type: 'undefined' 384 | } 385 | ] 386 | }, 387 | { 388 | type: 'string' 389 | } 390 | ] 391 | }; 392 | 393 | MyCommand.prototype.initialize = function(connection, firstByte) { 394 | 395 | firstByte = firstByte || 0x01; 396 | 397 | if (firstByte < 0) { 398 | throw new Error('wrong value'); 399 | } 400 | 401 | this.data = [firstByte, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]; 402 | }; 403 | 404 | MyCommand.prototype.execute = function(connection, callback) { 405 | connection.executeCommand(this, callback); 406 | }; 407 | 408 | module.exports = MyCommand; 409 | 410 | ###BE SURE TO DEFINE JSON SCHEMAS! 411 | Hint: [http://jsonary.com/documentation/json-schema/](http://jsonary.com/documentation/json-schema/) 412 | 413 | 414 | ## tasks 415 | Build your own tasks looking like this: 416 | 417 | var Task = require('../../index').Task, 418 | util = require('util'), 419 | Command = require('./command'); 420 | 421 | function MyTask(identifier) { 422 | // call super class 423 | Task.call(this, arguments); 424 | } 425 | 426 | util.inherits(MyTask, Task); 427 | 428 | // if argumentsSchema is defined it will validates the passed constructor arguments 429 | MyTask.prototype.argumentsSchema = { 430 | type: 'array', 431 | minItems: 0, 432 | items: [ 433 | { 434 | }, 435 | { 436 | type: 'string' 437 | } 438 | ] 439 | }; 440 | 441 | MyTask.prototype.initialize = function(connection, identifier) { 442 | if (identifier === 111) { 443 | throw new Error('wrong value in task'); 444 | } 445 | 446 | this.command = new Command(identifier); 447 | }; 448 | 449 | MyTask.prototype.perform = function(connection, callback) { 450 | this.execute(this.command, connection, callback); 451 | }; 452 | 453 | module.exports = MyTask; 454 | 455 | ###BE SURE TO DEFINE JSON SCHEMAS! 456 | Hint: [http://jsonary.com/documentation/json-schema/](http://jsonary.com/documentation/json-schema/) 457 | 458 | 459 | ## utils 460 | Some utility functions are shipped with this module. 461 | 462 | ### array | toBuffer 463 | Converts a byte array to a buffer object. 464 | [0x01, 0x00].toBuffer() 465 | 466 | ### array | toHexDebug 467 | Converts a byte array to a readable hex string. 468 | [0x01, 0x00].toHexDebug() // returns '01-00' 469 | 470 | ### array | toHexString 471 | Converts a byte array to a hex string. 472 | [0x01, 0x00].toHexString() // returns '0100' 473 | 474 | ### buffer | toArray 475 | Converts a buffer object to a byte array. 476 | (new Buffer([0x01, 0x00])).toArray() // returns [0x01, 0x00] 477 | 478 | ### buffer | toHexDebug 479 | Converts a buffer object to a readable hex string. 480 | (new Buffer([0x01, 0x00])).toHexDebug() // returns '01-00' 481 | 482 | ### string | toArray 483 | Converts a hex string to a byte array. 484 | '0100'.toArray() // returns [0x01, 0x00] 485 | '01-FA'.toArray() // returns [0x01, 0xFA] 486 | 487 | ### array | isByteArray 488 | Checks if the passed argument is an array that contains byte values. 489 | Array.isByteArray([0x01, 0x00]) // returns true 490 | 491 | # Installation 492 | 493 | npm install devicestack 494 | 495 | 496 | # Usage 497 | 498 | ## Start from the device 499 | 500 | var Device = require('devicestack').Device 501 | , util = require('util'); 502 | 503 | function MyDevice() { 504 | // call super class 505 | Device.call(this); 506 | } 507 | 508 | util.inherits(MyDevice, Device); 509 | 510 | MyDevice.prototype.open = function(callback) { 511 | var self = this; 512 | 513 | setTimeout(function() { 514 | self.emit('open', callback); 515 | if (!self.connection && callback) callback(); 516 | }, 10); 517 | 518 | this.on('send', function(data) { 519 | setTimeout(function() { 520 | self.emit('receive', data); 521 | }, 5); 522 | }); 523 | }; 524 | 525 | MyDevice.prototype.close = function(callback, fire) { 526 | var self = this; 527 | 528 | setTimeout(function() { 529 | self.emit('close', callback); 530 | self.removeAllListeners(); 531 | if (callback && (!self.connection || fire)) callback(null); 532 | }, 10); 533 | }; 534 | 535 | module.exports = MyDevice; 536 | 537 | ### If it's a serial device extend from SerialDevice 538 | 539 | var SerialDevice = require('devicestack').SerialDevice 540 | , util = require('util'); 541 | 542 | function MyDevice(port) { 543 | // call super class 544 | SerialDevice.call(this, 545 | port, 546 | { 547 | baudrate: 38400, 548 | databits: 8, 549 | stopbits: 1, 550 | parity: 'none' 551 | } 552 | ); 553 | } 554 | 555 | util.inherits(MyDevice, SerialDevice); 556 | 557 | module.exports = MyDevice; 558 | 559 | 560 | ### If it's a ftdi device... 561 | 562 | var FtdiDevice = require('devicestack').FtdiDevice 563 | , util = require('util'); 564 | 565 | function MyDevice(ftdiSettings) { 566 | // call super class 567 | FtdiDevice.call(this, 568 | ftdiSettings, 569 | { 570 | baudrate: 38400, 571 | databits: 8, 572 | stopbits: 1, 573 | parity: 'none' 574 | } 575 | ); 576 | } 577 | 578 | util.inherits(MyDevice, SerialDevice); 579 | 580 | module.exports = MyDevice; 581 | 582 | ### ...and use it this way... 583 | var MyDevice = require('./myDevice'); 584 | 585 | var myDevice = new MyDevice({ 586 | locationId: 0x1234, 587 | serialNumber: 's2345' 588 | }); 589 | 590 | myDevice.open(function(err) { 591 | 592 | myDevice.on('receive', function(data) { 593 | console.log(data); 594 | }); 595 | 596 | myDevice.send([0x01, 0x02, 0x03]); 597 | 598 | }); 599 | 600 | 601 | ## Continue with the framehandler(s) 602 | 603 | var FrameHandler = require('devicestack').FrameHandler 604 | , util = require('util'); 605 | 606 | function MyFrameHandler(device) { 607 | // call super class 608 | FrameHandler.call(this, device); 609 | } 610 | 611 | util.inherits(MyFrameHandler, FrameHandler); 612 | 613 | MyFrameHandler.prototype.analyzeNextFrame = function(incomming) { 614 | return incomming.splice(0); 615 | }; 616 | 617 | MyFrameHandler.prototype.unwrapFrame = function(frame) { 618 | return frame; 619 | }; 620 | 621 | MyFrameHandler.prototype.wrapFrame = function(frame) { 622 | return frame; 623 | }; 624 | 625 | module.exports = MyFrameHandler; 626 | 627 | 628 | ## Now build your stack with the device and the framehandler(s) defining a connection 629 | 630 | var Connection = require('devicestack').Connection 631 | , util = require('util') 632 | , FrameHandler = require('./framehandler'); 633 | 634 | function MyConnection(device) { 635 | // call super class 636 | Connection.call(this, device); 637 | 638 | this.frameHandler = new FrameHandler(this.device); 639 | this.frameHandler.on('receive', function (frame) { 640 | // forward to appropriate command... 641 | }); 642 | } 643 | 644 | util.inherits(MyConnection, Connection); 645 | 646 | // define if needed 647 | MyConnection.prototype.onConnecting = function(callback) { 648 | // Need to send some commands before definitely connected? 649 | if (callback) callback(); 650 | }; 651 | 652 | // define if needed 653 | MyConnection.prototype.onDisconnecting = function(callback) { 654 | // Need to send some commands before definitely closed? 655 | if (callback) callback(); 656 | }; 657 | 658 | MyConnection.prototype.sendCommand = function(commandData, callback) { 659 | this.frameHandler.send('send', commandData); 660 | }; 661 | 662 | module.exports = MyConnection; 663 | 664 | ### Don't forget to extend the device with the connection 665 | 666 | var Device = require('devicestack').Device 667 | , util = require('util') 668 | , Connection = require('./connection'); // this line... 669 | 670 | function MyDevice() { 671 | // call super class 672 | Device.call(this, Connection); // ...and this line 673 | } 674 | 675 | util.inherits(MyDevice, Device); 676 | 677 | MyDevice.prototype.open = function(callback) { 678 | var self = this; 679 | 680 | setTimeout(function() { 681 | self.emit('open', callback); 682 | if (!self.connection && callback) callback(); 683 | }, 10); 684 | 685 | this.on('send', function(data) { 686 | setTimeout(function() { 687 | self.emit('receive', data); 688 | }, 5); 689 | }); 690 | }; 691 | 692 | MyDevice.prototype.close = function(callback, fire) { 693 | var self = this; 694 | 695 | setTimeout(function() { 696 | self.emit('close', callback); 697 | self.removeAllListeners(); 698 | if (callback && (!self.connection || fire)) callback(null); 699 | }, 10); 700 | }; 701 | 702 | module.exports = MyDevice; 703 | 704 | ### If it's a serial device... 705 | 706 | var SerialDevice = require('devicestack').SerialDevice 707 | , util = require('util') 708 | , Connection = require('./connection'); // this line...; 709 | 710 | function MyDevice(port) { 711 | // call super class 712 | SerialDevice.call(this, 713 | port, 714 | { 715 | baudrate: 38400, 716 | databits: 8, 717 | stopbits: 1, 718 | parity: 'none' 719 | }, 720 | Connection // ...and this line 721 | ); 722 | } 723 | 724 | util.inherits(MyDevice, SerialDevice); 725 | 726 | module.exports = MyDevice; 727 | 728 | ### If it's a ftdi device... 729 | 730 | var FtdiDevice = require('devicestack').FtdiDevice 731 | , util = require('util') 732 | , Connection = require('./connection'); // this line...; 733 | 734 | function MyDevice(ftdiSettings) { 735 | // call super class 736 | FtdiDevice.call(this, 737 | ftdiSettings, 738 | { 739 | baudrate: 38400, 740 | databits: 8, 741 | stopbits: 1, 742 | parity: 'none' 743 | }, 744 | Connection // ...and this line 745 | ); 746 | } 747 | 748 | util.inherits(MyDevice, SerialDevice); 749 | 750 | module.exports = MyDevice; 751 | 752 | ### ...and use it this way... 753 | var MyDevice = require('./myDevice'); 754 | 755 | var myDevice = new MyDevice({ 756 | locationId: 0x1234, 757 | serialNumber: 's2345' 758 | }); 759 | 760 | myDevice.open(function(err) { 761 | 762 | myDevice.on('receive', function(data) { 763 | console.log(data); 764 | }); 765 | 766 | myDevice.send([0x01, 0x02, 0x03]); 767 | 768 | }); 769 | 770 | 771 | ## Let's lookup 772 | 773 | var DeviceLoader = require('devicestack').DeviceLoader 774 | , util = require('util') 775 | , Device = require('./device'); 776 | 777 | function MyDeviceLoader() { 778 | // call super class 779 | DeviceLoader.call(this); 780 | } 781 | 782 | util.inherits(MyDeviceLoader, DeviceLoader); 783 | 784 | MyDeviceLoader.prototype.lookup = function(callback) { 785 | var devices = = [ 786 | new Device(), 787 | new Device() 788 | ]; 789 | try { 790 | this.emit('lookup'); 791 | } catch(e) { 792 | } 793 | callback(null, devices); 794 | }; 795 | 796 | module.exports = new MyDeviceLoader(); 797 | 798 | ### If it's a serial device extend from serialdeviceloader... 799 | 800 | var SerialDeviceLoader = require('devicestack').SerialDeviceLoader 801 | , _ = require('lodash') 802 | , util = require('util') 803 | , Device = require('./device'); 804 | 805 | function MyDeviceLoader() { 806 | // call super class 807 | MyDeviceLoader.call(this, Device); 808 | } 809 | 810 | util.inherits(MyDeviceLoader, SerialDeviceLoader); 811 | 812 | MyDeviceLoader.prototype.filter = function(ports) { 813 | var resPorts = _.filter(ports, function(item) { 814 | if (process.platform == 'win32') { 815 | return item.pnpId.indexOf('VID_1234+PID_5678') >= 0; 816 | } else if (process.platform == 'darwin') { 817 | return item.productId === '0x5678' && item.vendorId === '0x1234'; 818 | } else { 819 | return item.pnpId.indexOf('MyDeviceIdentification') >= 0; 820 | } 821 | }); 822 | return resPorts; 823 | }; 824 | 825 | module.exports = new MyDeviceLoader(); 826 | 827 | 828 | ## And finally help with a guider 829 | 830 | var DeviceGuider = require('devicestack').DeviceGuider 831 | , util = require('util') 832 | , deviceLoader = require('./deviceloader'); 833 | 834 | function MyDeviceGuider() { 835 | 836 | // call super class 837 | DeviceGuider.call(this, deviceLoader); 838 | } 839 | 840 | util.inherits(MyDeviceGuider, DeviceGuider); 841 | 842 | module.exports = new MyDeviceGuider(); 843 | 844 | 845 | ## And now? 846 | 847 | var myDeviceguider = require('./deviceguider'); 848 | 849 | myDeviceguider.on('err', function(err) { 850 | if (err) { console.log(err); return process.exit(); } 851 | }); 852 | 853 | myDeviceguider.on('plug', function(device) { 854 | console.log('\n--->>> plugged a device\n'); 855 | }); 856 | 857 | myDeviceguider.on('unplug', function(device) { 858 | console.log('\n--->>> unplugged a device\n'); 859 | }); 860 | 861 | myDeviceguider.on('connect', function(connection) { 862 | console.log('\n--->>> connected a device\n'); 863 | 864 | connection.executeCommand(/* your stuff */, callback); 865 | }); 866 | 867 | myDeviceguider.on('disconnect', function(connection) { 868 | console.log('\n--->>> disconnected a device\n'); 869 | }); 870 | 871 | myDeviceguider.getCurrentState(function(err, currentState) { 872 | if (currentState.plugged.length === 0) { console.log('No devices found!'); /*return process.exit();*/ } 873 | }); 874 | 875 | myDeviceguider.on('connectionStateChanged', function(evt) { 876 | // evt-> { state: 'connect', connection: /* connection */ } 877 | // evt-> { state: 'disconnect', connection: /* connection */ } 878 | // evt-> { state: 'connecting', device: /* device */ } 879 | // evt-> { state: 'disconnecting', device: /* device */ } 880 | }); 881 | 882 | 883 | 884 | // V1: autoconnect first device... 885 | myDeviceguider.on('connect', function(device, connection) { 886 | // when first device connected... 887 | }); 888 | myDeviceguider.autoconnectOne([function(err, device, connection) {}]); 889 | myDeviceguider.stopautoconnecting(); // will stop autoconnecting and will disconnect connected device 890 | 891 | // V2: autoconnect all devices... 892 | myDeviceguider.on('connect', function(device, connection) { 893 | // when a device connected... 894 | }); 895 | myDeviceguider.autoconnect(); 896 | myDeviceguider.stopautoconnecting(); // will stop autoconnecting and will disconnect all connected devices 897 | 898 | // V3: manualconnect devices... 899 | myDeviceguider.on('plug', function(device) { 900 | // when a device ready to be connected... 901 | device.connect(function(err, connection) { 902 | // device connected... 903 | }); 904 | }); 905 | 906 | // V4: manualconnect a device with port... 907 | myDeviceguider.on('connect', function(device, connection) { 908 | // when first device connected... 909 | }); 910 | myDeviceguider.connect(port[, function(err, device, connection) {}]); 911 | 912 | 913 | 914 | # License 915 | 916 | Copyright (c) 2015 Adriano Raiano 917 | 918 | Permission is hereby granted, free of charge, to any person obtaining a copy 919 | of this software and associated documentation files (the "Software"), to deal 920 | in the Software without restriction, including without limitation the rights 921 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 922 | copies of the Software, and to permit persons to whom the Software is 923 | furnished to do so, subject to the following conditions: 924 | 925 | The above copyright notice and this permission notice shall be included in 926 | all copies or substantial portions of the Software. 927 | 928 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 929 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 930 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 931 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 932 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 933 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 934 | THE SOFTWARE. 935 | --------------------------------------------------------------------------------