├── src ├── index.js ├── logger.js └── hyperdeck │ ├── parser.js │ ├── hyperdeck.js │ ├── response-handler.js │ └── hyperdeck-core.js ├── test ├── index.js └── hyperdeck │ ├── parser.js │ ├── response-handler.js │ └── hyperdeck-core.js ├── .github └── workflows │ └── nodejs.yaml ├── .gitignore ├── Gulpfile.js ├── LICENSE ├── package.json ├── README.md └── .jshintrc /src/index.js: -------------------------------------------------------------------------------- 1 | // Here we export what we want to be accessible from the library to the developer 2 | 3 | module.exports = { 4 | Hyperdeck: require('./hyperdeck/hyperdeck'), 5 | HyperdeckCore: require('./hyperdeck/hyperdeck-core'), 6 | Logger: require('./logger') 7 | }; -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | var JsLogger = require('js-logger'); 2 | 3 | JsLogger.useDefaults({ 4 | formatter: function(messages, context) { 5 | if (context.name) { 6 | messages.unshift('[' + context.name + ']'); 7 | } 8 | messages.unshift('[HyperdeckJSLib]'); 9 | } 10 | }); 11 | 12 | JsLogger.setLevel(JsLogger.OFF); 13 | 14 | module.exports = JsLogger; -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var index = require('../src/index'); 2 | 3 | describe('index', function() { 4 | it('should provide the Hyperdeck class', function() { 5 | index.Hyperdeck.should.be.ok(); 6 | }); 7 | 8 | it('should provide the HyperdeckCore class', function() { 9 | index.HyperdeckCore.should.be.ok(); 10 | }); 11 | 12 | it('should provide the Logger class', function() { 13 | index.Logger.should.be.ok(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yaml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [16.x, 18.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: npm install and test 20 | run: | 21 | npm ci 22 | npm test 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directories 27 | node_modules 28 | jspm_packages 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | var jshint = require('gulp-jshint'); 2 | var gulp = require('gulp'); 3 | var mocha = require('gulp-mocha'); 4 | 5 | gulp.task('lint', function() { 6 | return gulp.src(['src/*.js', 'src/**/*.js', 'test/*.js', 'test/**/*.js']) 7 | .pipe(jshint()) 8 | .pipe(jshint.reporter('jshint-stylish')) 9 | .pipe(jshint.reporter('fail')); 10 | }); 11 | 12 | gulp.task('test', function() { 13 | return gulp.src(['test/*.js', 'test/**/*.js'], {read: false}) 14 | // gulp-mocha needs filepaths so you can't have any plugins before it 15 | .pipe(mocha({ 16 | reporter: 'spec', 17 | require: ['should'] 18 | })); 19 | }); 20 | 21 | gulp.task('default', gulp.parallel('lint', 'test')); 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 LA1:TV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperdeck-js-lib", 3 | "version": "1.8.0", 4 | "description": "A javascript library for communication with the Blackmagic Hyperdeck.", 5 | "main": "src/index.js", 6 | "dependencies": { 7 | "js-logger": "^1.2.0", 8 | "promise": "^8.0.1" 9 | }, 10 | "devDependencies": { 11 | "gulp": "^4.0.2", 12 | "gulp-jshint": "^2.1.0", 13 | "gulp-mocha": "^8.0.0", 14 | "jshint": "^2.9.1", 15 | "jshint-stylish": "^2.1.0", 16 | "mocha": "^5.0.0", 17 | "proxyquire": "^2.0.0", 18 | "should": "^13.1.0", 19 | "sinon": "^6.0.0" 20 | }, 21 | "scripts": { 22 | "test": "./node_modules/.bin/gulp" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/LA1TV/Hyperdeck-JS-Lib.git" 27 | }, 28 | "keywords": [ 29 | "blackmagic", 30 | "hyperdeck", 31 | "node", 32 | "nodejs", 33 | "javascript", 34 | "api", 35 | "blackmagic design" 36 | ], 37 | "author": "LA1:TV", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/LA1TV/Hyperdeck-JS-Lib/issues" 41 | }, 42 | "homepage": "https://github.com/LA1TV/Hyperdeck-JS-Lib#readme" 43 | } 44 | -------------------------------------------------------------------------------- /src/hyperdeck/parser.js: -------------------------------------------------------------------------------- 1 | var FIRST_LINE_REGEX = /^([0-9]+) (.+?)(\:?)$/; 2 | var PARAMS_REGEX = /^(.*)\: (.*)$/; 3 | 4 | /** 5 | * Converts the data to a Object. 6 | * So the hyperdeck class can do things nicely with it. 7 | * @return dataObject, The data in a nice object. This will contain 'code', 'text' and 'params' keys, 8 | * (if there are parameters) where params is an object. 9 | **/ 10 | function convertDataToObject(lines) { 11 | var dataObject = { 12 | code: null, 13 | text: null 14 | }; 15 | 16 | var firstLine = lines.shift(); // should contain {Response code} {Response text} 17 | var firstLineMatches = FIRST_LINE_REGEX.exec(firstLine); 18 | var code = parseInt(firstLineMatches[1]); 19 | var text = firstLineMatches[2]; 20 | dataObject.code = code; 21 | dataObject.text = text; 22 | 23 | if (lines.length) { 24 | // provide the raw data in addition to attempting to parse the response into params 25 | dataObject.rawData = lines.join('\r\n'); 26 | } 27 | 28 | if (firstLineMatches[3] === ':') { 29 | // the response should have params on the next lines 30 | // (although sometimes it doesn't because of the responses (e.g 'commands'), do not return 31 | // the usual format, and in this case params will likely remain as {}) 32 | var params = {}; 33 | //Append the rest into an object for emitting. 34 | lines.forEach(function(line) { 35 | var lineData = PARAMS_REGEX.exec(line); 36 | //First element in array is the whole string. 37 | if(lineData) { 38 | params[lineData[1]] = lineData[2]; 39 | } 40 | }); 41 | dataObject.params = params; 42 | } 43 | return dataObject; 44 | } 45 | 46 | /** 47 | * Parses responses from the hyperdeck into a nice object. 48 | */ 49 | function failureResponseCode(lines) { 50 | return { 51 | type: 'synchronousFailure', 52 | data: convertDataToObject(lines) 53 | }; 54 | } 55 | 56 | function successResponseCode(lines) { 57 | return { 58 | type: 'synchronousSuccess', 59 | data: convertDataToObject(lines) 60 | }; 61 | } 62 | 63 | function asynchornousResponseCode(lines) { 64 | return { 65 | type: 'asynchronous', 66 | data: convertDataToObject(lines) 67 | }; 68 | } 69 | 70 | var Parser = { 71 | 72 | parse: function(lines) { 73 | // pass into the switch/case to decide which function to use. 74 | switch (lines[0].charAt(0)){ 75 | case '1': 76 | return failureResponseCode(lines); 77 | case '2': 78 | return successResponseCode(lines); 79 | case '5': 80 | return asynchornousResponseCode(lines); 81 | default: 82 | throw new Error('Invalid payload. Unknown response code.'); 83 | } 84 | } 85 | }; 86 | 87 | module.exports = Parser; -------------------------------------------------------------------------------- /src/hyperdeck/hyperdeck.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var HyperdeckCore = require('./hyperdeck-core.js'); 3 | 4 | var Hyperdeck = function(config) { 5 | 6 | // call constructor of HyperdeckCore 7 | HyperdeckCore.call(this, config); 8 | 9 | this.makeRequest('notify: remote: true'); 10 | this.makeRequest('notify: transport: true'); 11 | this.makeRequest('notify: slot: true'); 12 | this.makeRequest('notify: configuration: true'); 13 | 14 | // add Easy Access commands 15 | this.play = function(speed) { 16 | var commandString; 17 | if (Math.abs(speed) <= 1600) { 18 | commandString = 'play: speed: ' + speed; 19 | } else { 20 | if (speed) { 21 | throw new Error('Speed value invalid or out of range'); 22 | } else { 23 | commandString = 'play'; 24 | } 25 | } 26 | return this.makeRequest(commandString); 27 | }; 28 | 29 | this.stop = function() { 30 | return this.makeRequest('stop'); 31 | }; 32 | 33 | this.record = function(clipname) { 34 | if (typeof clipname === 'string') { 35 | return this.makeRequest('record: name: ' + clipname); 36 | } else { 37 | return this.makeRequest('record'); 38 | } 39 | }; 40 | 41 | this.goTo = function(timecode) { 42 | return this.makeRequest('goto: timecode: ' + timecode); 43 | }; 44 | 45 | this.jogTo = function(timecode) { 46 | return this.makeRequest('jog: timecode: ' + timecode); 47 | }; 48 | 49 | this.jogForward = function(timecode) { 50 | return this.makeRequest('jog: timecode: +' + timecode); 51 | }; 52 | 53 | this.jogBackwards = function(timecode) { 54 | return this.makeRequest('jog: timecode: -' + timecode); 55 | }; 56 | 57 | this.slotInfo = function (id) { 58 | if (typeof id === 'number') { 59 | return this.makeRequest('slot info: slot id: ' + id); 60 | } else{ 61 | return this.makeRequest('slot info'); 62 | } 63 | }; 64 | 65 | this.transportInfo = function(){ 66 | return this.makeRequest('transport info'); 67 | }; 68 | 69 | this.clipsGet = function(){ 70 | return this.makeRequest('clips get'); 71 | }; 72 | 73 | this.nextClip = function() { 74 | return this.makeRequest('goto: clip id: +1'); 75 | }; 76 | 77 | this.prevClip = function() { 78 | return this.makeRequest('goto: clip id: -1'); 79 | }; 80 | 81 | this.slotSelect = function(id){ 82 | return this.makeRequest('slot select: slot id: ' + id); 83 | }; 84 | 85 | this.format = function(format){ 86 | return this.makeRequest('format: prepare: ' + format).then(function(response){ 87 | if (response.code !== 216 || response.text !== 'format ready' || !response.rawData) { 88 | throw new Error('Unexpected response.'); 89 | } 90 | var token = response.rawData; 91 | return this.makeRequest('format: confirm: ' + token); 92 | }.bind(this)); 93 | }; 94 | }; 95 | 96 | // make this class extend HyperdeckCore 97 | // https://nodejs.org/docs/latest/api/util.html#util_util_inherits_constructor_superconstructor 98 | util.inherits(Hyperdeck, HyperdeckCore); 99 | 100 | module.exports = Hyperdeck; 101 | -------------------------------------------------------------------------------- /src/hyperdeck/response-handler.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var Parser = require('./parser'); 3 | var Logger = require('../logger'); 4 | 5 | var logger = Logger.get('hyperdeck.ResponseHandler'); 6 | 7 | var SINGLE_LINE_REGEX = /^(?:1\d{2}|200) /; 8 | 9 | /** 10 | * Handles responses from they hyperdeck. 11 | */ 12 | function ResponseHandler(clientSocket) { 13 | var destroyed = false; 14 | var notifier = new events.EventEmitter(); 15 | var buffer = []; 16 | var incompleteLastLine = ''; 17 | 18 | function isRespComplete() { 19 | var complete = false; 20 | if (buffer.length === 1) { 21 | // a single line response 22 | // 1XX and 200 is always single line response 23 | complete = SINGLE_LINE_REGEX.test(buffer[0]); 24 | } else { 25 | // multi line response, so waiting for a blank line to signify end 26 | complete = buffer[buffer.length - 1] === ''; 27 | } 28 | return complete; 29 | } 30 | 31 | function onData(rawData) { 32 | logger.debug('Got data on socket.\n', rawData); 33 | var resArray = (incompleteLastLine + rawData).split('\r\n'); 34 | incompleteLastLine = resArray.pop(); 35 | resArray.forEach(function (line) { 36 | // push to buffer till response is read completly 37 | // handle empty lines before the data 38 | // see https://github.com/LA1TV/Hyperdeck-JS-Lib/issues/44 39 | if (buffer.length > 0 || (buffer.length === 0 && line.trim() !== '')) { 40 | buffer.push(line); 41 | if (isRespComplete()) { 42 | if (buffer.length > 1) { 43 | // multiline response, remove empty line 44 | buffer.pop(); 45 | } 46 | 47 | logger.debug('Got complete data.\n', buffer.join('\n')); 48 | // reset buffer here and use clone (in case exception happens below) 49 | var bufferClone = buffer.splice(0); 50 | try { 51 | var data = Parser.parse(bufferClone); 52 | switch (data.type) { 53 | case 'synchronousFailure': 54 | case 'synchronousSuccess': 55 | var response = { 56 | success: data.type === 'synchronousSuccess', 57 | data: data.data 58 | }; 59 | notifier.emit('synchronousResponse', response); 60 | break; 61 | case 'asynchronous': 62 | notifier.emit('asynchronousResponse', data.data); 63 | break; 64 | default: 65 | throw new Error('Unknown response type.'); 66 | } 67 | } catch(e) { 68 | // defer exception so that we don't stop processing response 69 | setTimeout(function() { 70 | throw e; 71 | }, 0); 72 | } 73 | } 74 | } 75 | }); 76 | } 77 | 78 | clientSocket.on('data', onData); 79 | 80 | this.getNotifier = function () { 81 | return notifier; 82 | }; 83 | 84 | this.destroy = function () { 85 | if (destroyed) { 86 | return; 87 | } 88 | logger.debug('Destroying...'); 89 | destroyed = true; 90 | clientSocket.removeListener('data', onData); 91 | }; 92 | } 93 | 94 | module.exports = ResponseHandler; -------------------------------------------------------------------------------- /test/hyperdeck/parser.js: -------------------------------------------------------------------------------- 1 | var Parser = require('../../src/hyperdeck/parser'); 2 | 3 | var SUCCESS_RESPONSE = [ '200 ok' ]; 4 | var SUCCESS_RESPONSE_WITH_PARAMS = [ 5 | '201 Success with data:', 6 | 'something: 123', 7 | 'something else: test' 8 | ]; 9 | var SUCCESS_RESPONSE_WITH_UNPARSEABLE_PARAMS = [ 10 | '201 Success with data:', 11 | '' 12 | ]; 13 | var FAILURE_RESPONSE = [ '102 Failure' ]; 14 | var ASYNC_RESPONSE = [ 15 | '512 Async event:', 16 | 'protocol version: 9.5', 17 | 'model: xyz', 18 | 'time: 12:40:12' 19 | ]; 20 | var INVALID_RESPONSE = [ 'something invalid' ]; 21 | 22 | var SUCCESS_RESPONSE_DATA = { 23 | type: 'synchronousSuccess', 24 | data: { 25 | code: 200, 26 | text: 'ok' 27 | } 28 | }; 29 | 30 | var SUCCESS_PARAMS_RESPONSE_DATA = { 31 | type: 'synchronousSuccess', 32 | data: { 33 | code: 201, 34 | text: 'Success with data', 35 | rawData: 'something: 123\r\nsomething else: test', 36 | params: { 37 | something: '123', 38 | 'something else': 'test' 39 | } 40 | } 41 | }; 42 | 43 | var SUCCESS_UNPARSEABLE_PARAMS_RESPONSE_DATA = { 44 | type: 'synchronousSuccess', 45 | data: { 46 | code: 201, 47 | text: 'Success with data', 48 | rawData: '', 49 | params: {} 50 | } 51 | }; 52 | 53 | var FAILURE_RESPONSE_DATA = { 54 | type: 'synchronousFailure', 55 | data: { 56 | code: 102, 57 | text: 'Failure' 58 | } 59 | }; 60 | 61 | var ASYNC_RESPONSE_DATA = { 62 | type: 'asynchronous', 63 | data: { 64 | code: 512, 65 | text: 'Async event', 66 | rawData: 'protocol version: 9.5\r\nmodel: xyz\r\ntime: 12:40:12', 67 | params: { 68 | 'protocol version': '9.5', 69 | model: 'xyz', 70 | time: '12:40:12' 71 | } 72 | } 73 | }; 74 | 75 | describe('Parser', function() { 76 | 77 | it('should handle a response string with a success status code', function() { 78 | Parser.parse(SUCCESS_RESPONSE).should.eql(SUCCESS_RESPONSE_DATA); 79 | }); 80 | 81 | it('should handle a response string with a success status code and params', function() { 82 | Parser.parse(SUCCESS_RESPONSE_WITH_PARAMS).should.eql(SUCCESS_PARAMS_RESPONSE_DATA); 83 | }); 84 | 85 | it('should handle a response string with a failure status code', function() { 86 | Parser.parse(FAILURE_RESPONSE).should.eql(FAILURE_RESPONSE_DATA); 87 | }); 88 | 89 | it('should handle a response string with an async status code', function() { 90 | Parser.parse(ASYNC_RESPONSE).should.eql(ASYNC_RESPONSE_DATA); 91 | }); 92 | 93 | it('should throw an exception if the input string is not valid', function() { 94 | (function() { 95 | Parser.parse(INVALID_RESPONSE); 96 | }).should.throw(); 97 | }); 98 | 99 | it('should handle a response string with a success status code and unparseable params', function() { 100 | Parser.parse(SUCCESS_RESPONSE_WITH_UNPARSEABLE_PARAMS).should.eql(SUCCESS_UNPARSEABLE_PARAMS_RESPONSE_DATA); 101 | }); 102 | 103 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM](https://nodei.co/npm-dl/hyperdeck-js-lib.png?months=1)](https://nodei.co/npm/hyperdeck-js-lib/) 2 | 3 | [![Build Status](https://travis-ci.org/LA1TV/Hyperdeck-JS-Lib.svg?branch=master)](https://travis-ci.org/LA1TV/Hyperdeck-JS-Lib) 4 | [![npm version](https://badge.fury.io/js/hyperdeck-js-lib.svg)](https://badge.fury.io/js/hyperdeck-js-lib) 5 | [![Greenkeeper badge](https://badges.greenkeeper.io/LA1TV/Hyperdeck-JS-Lib.svg)](https://greenkeeper.io/) 6 | 7 | Hyperdeck-JS-Lib 8 | ---------------- 9 | A javascript library for communication with the Blackmagic Hyperdeck. 10 | 11 | # Installing 12 | `npm install --save hyperdeck-js-lib` 13 | 14 | 15 | # Using The Library 16 | The `makeRequest()` function returns a promise which will resolve with the response from the hyperdeck if it is a succcesful response. Otherwise it will reject. If the connection was lost the response object will be `null` otherwise it will be the error response from the hyperdeck. 17 | 18 | The response is an object with the following properties: 19 | - `code`: The nuemeric response code. 20 | - `text`: The response text. 21 | 22 | If the response from the hyperdeck also contains data the following keys will also exist: 23 | - `rawData`: A string which contains the unparsed data. 24 | - `params`: An object where the keys are the parameter keys in the response, and the values are the corresponding values in the response. This is best-effort, and if the response is not structured in the params format shown in the documentation, may be an empty object. It will try to parse each line in the response individually. 25 | 26 | ```javascript 27 | var HyperdeckLib = require("hyperdeck-js-lib"); 28 | 29 | var hyperdeck = new HyperdeckLib.Hyperdeck("192.168.1.12"); 30 | hyperdeck.onConnected().then(function() { 31 | // connected to hyperdeck 32 | // Note: you do not have to wait for the connection before you start making requests. 33 | // Requests are buffered until the connection completes. If the connection fails, any 34 | // buffered requests will be rejected. 35 | hyperdeck.makeRequest("device info").then(function(response) { 36 | console.log("Got response with code "+response.code+"."); 37 | console.log("Hyperdeck unique id: "+response.params["unique id"]); 38 | }).catch(function(errResponse) { 39 | if (!errResponse) { 40 | console.error("The request failed because the hyperdeck connection was lost."); 41 | } 42 | else { 43 | console.error("The hyperdeck returned an error with status code "+errResponse.code+"."); 44 | } 45 | }); 46 | 47 | hyperdeck.getNotifier().on("asynchronousEvent", function(response) { 48 | console.log("Got an asynchronous event with code "+response.code+"."); 49 | }); 50 | 51 | hyperdeck.getNotifier().on("connectionLost", function() { 52 | console.error("Connection lost."); 53 | }); 54 | }).catch(function() { 55 | console.error("Failed to connect to hyperdeck."); 56 | }); 57 | ``` 58 | 59 | There are a number of different predefined commands which can be called upon: 60 | 61 | ```javascript 62 | hyperdeck.play(); 63 | hyperdeck.play(35); //play at 35% 64 | hyperdeck.stop(); 65 | hyperdeck.record(); 66 | hyperdeck.goTo("00:13:03:55"); //goes to timecode in format hh:mm:ss:ff 67 | hyperdeck.slotSelect(2); 68 | hyperdeck.slotInfo(); //Gives info on currently selected slot 69 | hyperdeck.slotInfo(1); 70 | hyperdeck.clipsGet(); 71 | hyperdeck.nextClip(); 72 | hyperdeck.prevClip(); 73 | hyperdeck.transportInfo(); 74 | hyperdeck.format(format); 75 | ``` 76 | 77 | # API Documentation 78 | The hyperdeck API documentation can be found at "https://www.blackmagicdesign.com/uk/manuals/HyperDeck/HyperDeck_Manual.pdf". 79 | 80 | # Debugging 81 | You can enable logging: 82 | 83 | ```javascript 84 | var HyperdeckLib = require("hyperdeck-js-lib"); 85 | var Logger = HyperdeckLib.Logger; 86 | Logger.setLevel(Logger.DEBUG); 87 | Logger.setLevel(Logger.INFO); 88 | Logger.setLevel(Logger.WARN); 89 | Logger.setLevel(Logger.ERROR); 90 | Logger.setLevel(Logger.OFF); 91 | ``` -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 25, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 14 | "indent" : 4, // {int} Number of spaces to use for indentation 15 | "latedef" : false, // true: Require variables/functions to be defined before being used 16 | "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` 17 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 18 | "noempty" : true, // true: Prohibit use of empty blocks 19 | "nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment) 20 | "plusplus" : true, // true: Prohibit use of `++` & `--` 21 | "quotmark" : "single", // Quotation mark consistency: 22 | // false : do nothing (default) 23 | // true : ensure whatever is used is consistent 24 | // "single" : require single quotes 25 | // "double" : require double quotes 26 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 27 | "unused" : true, // true: Require all defined variables be used 28 | "strict" : false, // true: Requires all functions run in ES5 Strict Mode 29 | "maxparams" : false, // {int} Max number of formal params allowed per function 30 | "maxdepth" : 5, // {int} Max depth of nested blocks (within functions) 31 | "maxstatements" : false, // {int} Max number statements per function 32 | "maxcomplexity" : false, 33 | "maxlen" : 250, // {int} Max number of characters per line 34 | 35 | // Relaxing 36 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 37 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 38 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 39 | "eqnull" : false, // true: Tolerate use of `== null` 40 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 41 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 42 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 43 | // (ex: `for each`, multiple try/catch, function expression…) 44 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 45 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 46 | "funcscope" : false, // true: Tolerate defining variables inside control statements" 47 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 48 | "iterator" : false, // true: Tolerate using the `__iterator__` property 49 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 50 | "laxbreak" : true, // true: Tolerate possibly unsafe line breakings 51 | "laxcomma" : false, // true: Tolerate comma-first style coding 52 | "loopfunc" : false, // true: Tolerate functions being defined in loops 53 | "multistr" : false, // true: Tolerate multi-line strings 54 | "proto" : false, // true: Tolerate using the `__proto__` property 55 | "scripturl" : false, // true: Tolerate script-targeted URLs 56 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 57 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 58 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 59 | "validthis" : false, // true: Tolerate using this in a non-constructor function 60 | // Environments 61 | "browser" : true, // Web Browser (window, document, etc) 62 | "couch" : false, // CouchDB 63 | "devel" : true, // Development/debugging (alert, confirm, etc) 64 | "dojo" : false, // Dojo Toolkit 65 | "jquery" : true, // jQuery 66 | "mootools" : false, // MooTools 67 | "node" : false, // Node.js 68 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 69 | "prototypejs" : false, // Prototype and Scriptaculous 70 | "rhino" : false, // Rhino 71 | "worker" : false, // Web Workers 72 | "wsh" : false, // Windows Scripting Host 73 | "yui" : false, // Yahoo User Interface 74 | 75 | // Custom Globals 76 | // additional predefined global variables 77 | "globals" : { 78 | "module": true, 79 | "require": true, 80 | "exports": true, 81 | "define": true, 82 | // for when we lint tests 83 | "describe": true, 84 | "xdescribe": true, 85 | "xit": true, 86 | "it": true, 87 | "expect": true, 88 | "beforeEach": true, 89 | "waitsFor": true, 90 | "runs": true, 91 | "afterEach": true, 92 | "jasmine": true, 93 | "__base": true, 94 | "__dirname": true, 95 | "requirejs": true, 96 | "global": true, 97 | "process": true 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/hyperdeck/response-handler.js: -------------------------------------------------------------------------------- 1 | var ResponseHandler = require('../../src/hyperdeck/response-handler'); 2 | 3 | var SUCCESS_RESPONSE = '201 Success with data:\r\nsomething: 123\r\nsomething else: test\r\n\r\n'; 4 | var SUCCESS_RESPONSE_EVENT_PAYLOAD = { 5 | success: true, 6 | data: { 7 | code: 201, 8 | text: 'Success with data', 9 | rawData: 'something: 123\r\nsomething else: test', 10 | params: { 11 | something: '123', 12 | 'something else': 'test' 13 | } 14 | } 15 | }; 16 | 17 | // See format response 18 | var SUCCESS_RESPONSE_WITH_DATA_NO_BUT_NOT_PARAMS = '201 Success with data but not params and no colon\r\nabc\r\n\r\n'; 19 | var SUCCESS_RESPONSE_WITH_DATA_NO_BUT_NOT_PARAMS_EVENT_PAYLOAD = { 20 | success: true, 21 | data: { 22 | code: 201, 23 | text: 'Success with data but not params and no colon', 24 | rawData: 'abc' 25 | } 26 | }; 27 | 28 | var SINGLE_LINE_SUCCESS_RESPONSE = '200 ok\r\n'; 29 | var SINGLE_LINE_SUCCESS_RESPONSE_DATA = { 30 | success: true, 31 | data: { 32 | code: 200, 33 | text: 'ok' 34 | } 35 | }; 36 | 37 | var ASYNC_RESPONSE = '512 Async event:\r\nprotocol version: 9.5\r\nmodel: xyz\r\ntime: 12:40:12\r\n\r\n'; 38 | var ASYNC_RESPONSE_EVENT_PAYLOAD = { 39 | code: 512, 40 | text: 'Async event', 41 | rawData: 'protocol version: 9.5\r\nmodel: xyz\r\ntime: 12:40:12', 42 | params: { 43 | 'protocol version': '9.5', 44 | model: 'xyz', 45 | time: '12:40:12' 46 | } 47 | }; 48 | 49 | var COMBINED_RESPONSE = SUCCESS_RESPONSE + SINGLE_LINE_SUCCESS_RESPONSE + ASYNC_RESPONSE; 50 | var COMBINED_RESPONSE_EXTRA_LINES = ASYNC_RESPONSE + '\r\n' + SUCCESS_RESPONSE; 51 | 52 | describe('ResponseHandler', function() { 53 | 54 | var responseHandler = null; 55 | var socket = null; 56 | 57 | // create a new response handler (and fake socket) before each test 58 | beforeEach(function() { 59 | socket = new MockSocket(); 60 | responseHandler = new ResponseHandler(socket); 61 | }); 62 | 63 | afterEach(function() { 64 | // TODO call destroy() on responseHandler 65 | }); 66 | 67 | it('can be built', function() { 68 | responseHandler.should.be.ok(); 69 | }); 70 | 71 | it('emits a valid synchronous response event when receives a success response', function(done) { 72 | responseHandler.getNotifier().once('synchronousResponse', function(response) { 73 | response.should.eql(SUCCESS_RESPONSE_EVENT_PAYLOAD); 74 | done(); 75 | }); 76 | socket.write(SUCCESS_RESPONSE); 77 | }); 78 | 79 | it('emits a valid synchronous response event when receives a success response with data which is not params', function(done) { 80 | responseHandler.getNotifier().once('synchronousResponse', function(response) { 81 | response.should.eql(SUCCESS_RESPONSE_WITH_DATA_NO_BUT_NOT_PARAMS_EVENT_PAYLOAD); 82 | done(); 83 | }); 84 | socket.write(SUCCESS_RESPONSE_WITH_DATA_NO_BUT_NOT_PARAMS); 85 | }); 86 | 87 | it('emits a valid synchronous response event when receives a success response with data which is not params, after receiving an asynchronous response', function(done) { 88 | responseHandler.getNotifier().once('asynchronousResponse', function(response) { 89 | response.should.eql(ASYNC_RESPONSE_EVENT_PAYLOAD); 90 | responseHandler.getNotifier().once('synchronousResponse', function(response) { 91 | response.should.eql(SUCCESS_RESPONSE_WITH_DATA_NO_BUT_NOT_PARAMS_EVENT_PAYLOAD); 92 | done(); 93 | }); 94 | }); 95 | socket.write(ASYNC_RESPONSE + SUCCESS_RESPONSE_WITH_DATA_NO_BUT_NOT_PARAMS); 96 | }); 97 | 98 | it('emits a valid asynchronous response event when receives an aync response', function(done) { 99 | responseHandler.getNotifier().once('asynchronousResponse', function(response) { 100 | response.should.eql(ASYNC_RESPONSE_EVENT_PAYLOAD); 101 | done(); 102 | }); 103 | socket.write(ASYNC_RESPONSE); 104 | }); 105 | 106 | it('handles multiple responses arriving at the same time', function(done) { 107 | responseHandler.getNotifier().once('synchronousResponse', function(response) { 108 | response.should.eql(SUCCESS_RESPONSE_EVENT_PAYLOAD); 109 | responseHandler.getNotifier().once('synchronousResponse', function(response) { 110 | response.should.eql(SINGLE_LINE_SUCCESS_RESPONSE_DATA); 111 | responseHandler.getNotifier().once('asynchronousResponse', function(response) { 112 | response.should.eql(ASYNC_RESPONSE_EVENT_PAYLOAD); 113 | done(); 114 | }); 115 | }); 116 | }); 117 | socket.write(COMBINED_RESPONSE); 118 | }); 119 | 120 | // see https://github.com/LA1TV/Hyperdeck-JS-Lib/issues/44 121 | it('handles multiple responses arriving at the same time with extra lines inbetween', function(done) { 122 | responseHandler.getNotifier().once('asynchronousResponse', function(response) { 123 | response.should.eql(ASYNC_RESPONSE_EVENT_PAYLOAD); 124 | responseHandler.getNotifier().once('synchronousResponse', function(response) { 125 | response.should.eql(SUCCESS_RESPONSE_EVENT_PAYLOAD); 126 | done(); 127 | }); 128 | }); 129 | socket.write(COMBINED_RESPONSE_EXTRA_LINES); 130 | }); 131 | 132 | it('handles multiple responses arriving character by character', function(done) { 133 | responseHandler.getNotifier().once('synchronousResponse', function(response) { 134 | response.should.eql(SUCCESS_RESPONSE_EVENT_PAYLOAD); 135 | responseHandler.getNotifier().once('synchronousResponse', function(response) { 136 | response.should.eql(SINGLE_LINE_SUCCESS_RESPONSE_DATA); 137 | responseHandler.getNotifier().once('asynchronousResponse', function(response) { 138 | response.should.eql(ASYNC_RESPONSE_EVENT_PAYLOAD); 139 | done(); 140 | }); 141 | }); 142 | }); 143 | COMBINED_RESPONSE.split('').forEach(function(letter) { 144 | socket.write(letter); 145 | }); 146 | }); 147 | }); 148 | 149 | 150 | // incredibly basic implementation of a socket for testing. 151 | function MockSocket() { 152 | this._dataListeners = []; 153 | } 154 | 155 | MockSocket.prototype.write = function(data) { 156 | var _this = this; 157 | // make async 158 | setTimeout(function() { 159 | _this._dataListeners.forEach(function(listener) { 160 | listener(data); 161 | }); 162 | }, 0); 163 | }; 164 | 165 | MockSocket.prototype.on = function(evt, listener) { 166 | if (evt === 'data') { 167 | this._dataListeners.push(listener); 168 | } 169 | else { 170 | throw new Error('MockSocket doesn\'t support this!'); 171 | } 172 | }; 173 | -------------------------------------------------------------------------------- /test/hyperdeck/hyperdeck-core.js: -------------------------------------------------------------------------------- 1 | var proxyquire = require('proxyquire'); 2 | var events = require('events'); 3 | 4 | var responseHandlerNotifier = null; 5 | var onSocketWrite = null; 6 | var onConnectionCompleted = null; 7 | var ASYNCHRONOUS_EVENT_DATA = { 8 | code: 512, 9 | text: 'Async event', 10 | params: { 11 | 'protocol version': '9.5', 12 | model: 'xyz', 13 | time: '12:40:12' 14 | } 15 | }; 16 | var SUCCESS_DATA = { 17 | code: 201, 18 | text: 'Success with data', 19 | params: { 20 | something: '123', 21 | 'something else': 'test' 22 | } 23 | }; 24 | var FAILURE_DATA = { 25 | code: 102, 26 | text: 'Failure', 27 | params: { 28 | something: '123', 29 | 'something else': 'test' 30 | } 31 | }; 32 | 33 | // require Hyperdeck but overriding the require('net') and require('ResponseHandler') to use our stubs 34 | var Hyperdeck = proxyquire('../../src/hyperdeck/hyperdeck-core', { 35 | 'net': getNetStub(), 36 | './response-handler': getResponseHandlerStub() 37 | }); 38 | 39 | 40 | describe('Hyperdeck', function() { 41 | 42 | var hyperdeck = null; 43 | 44 | // create a new response handler (and fake socket) before each test 45 | beforeEach(function() { 46 | hyperdeck = new Hyperdeck('127.0.0.1'); 47 | }); 48 | 49 | afterEach(function() { 50 | // this is important to make sure nothing is still relying on the things 51 | // we've mocked after this. 52 | hyperdeck.destroy(); 53 | responseHandlerNotifier = null; 54 | }); 55 | 56 | it('can be constructed', function() { 57 | hyperdeck.should.be.ok(); 58 | }); 59 | 60 | it('throws an exception if request contains a new line', function() { 61 | (function() { 62 | hyperdeck.makeRequest('something\r'); 63 | }).should.throw(); 64 | (function() { 65 | hyperdeck.makeRequest('something\r'); 66 | }).should.throw(); 67 | (function() { 68 | hyperdeck.makeRequest('something\r\n'); 69 | }).should.throw(); 70 | }); 71 | 72 | it('triggers asynchronousEvent when the responseHandler gets an async response message.', function(done) { 73 | hyperdeck.getNotifier().once('asynchronousEvent', function(data) { 74 | data.should.eql(ASYNCHRONOUS_EVENT_DATA); 75 | done(); 76 | }); 77 | 78 | // when the response handler triggers this, the Hyperdeck should retrieve it and forward 79 | // it on with it's emitter. 80 | fakeAsyncResponse(); 81 | }); 82 | 83 | it('resolves a request promise correctly for a succesful response to a request', function(done) { 84 | queueFakeSuccesfulSynchronousResponse(); 85 | 86 | hyperdeck.makeRequest('a valid hyperdeck request').then(function(data) { 87 | data.should.eql(SUCCESS_DATA); 88 | done(); 89 | }); 90 | 91 | }); 92 | 93 | it('resolves a request promise correctly for a failure response to a request', function(done) { 94 | queueFakeFailureSynchronousResponse(); 95 | 96 | hyperdeck.makeRequest('a valid hyperdeck request').catch(function(data) { 97 | data.should.eql(FAILURE_DATA); 98 | done(); 99 | }); 100 | 101 | }); 102 | 103 | it('resolves a request promise correctly when there are asynchronous events inbetween', function(done) { 104 | // true means fake an async first 105 | queueFakeSuccesfulSynchronousResponse(true); 106 | 107 | hyperdeck.makeRequest('a valid hyperdeck request').then(function(data) { 108 | data.should.eql(SUCCESS_DATA); 109 | done(); 110 | }); 111 | 112 | fakeAsyncResponse(); 113 | }); 114 | 115 | // TODO tests to handle disconnection 116 | 117 | }); 118 | 119 | function getNetStub() { 120 | var netStub = { 121 | connect: function(opts, onSuccess) { 122 | var onCloseListeners = []; 123 | var destroyed = false; 124 | // async 125 | setTimeout(function() { 126 | if (destroyed) { 127 | return; 128 | } 129 | onSuccess(); 130 | setTimeout(function() { 131 | if (destroyed) { 132 | return; 133 | } 134 | // fake hyperdeck connection response 135 | responseHandlerNotifier.emit('asynchronousResponse', { 136 | code: 500, 137 | text: 'connection info', 138 | params: { 139 | 'protocol version': 'some protocol version', 140 | model: 'some model' 141 | } 142 | }); 143 | // allow tests to hook code that should run when connection completes 144 | if (onConnectionCompleted) { 145 | onConnectionCompleted(); 146 | } 147 | }, 0); 148 | }, 0); 149 | return { 150 | write: function(data) { 151 | if (onSocketWrite) { 152 | onSocketWrite(data); 153 | } 154 | }, 155 | on: function(evt, listener) { 156 | if (evt === 'close') { 157 | onCloseListeners.push(listener); 158 | } 159 | else if (evt !== 'error') { 160 | throw new Error('Not supported in mock net.'); 161 | } 162 | }, 163 | setEncoding: function() { 164 | // 165 | }, 166 | destroy: function() { 167 | if (destroyed) { 168 | throw new Error('Already destroyed.'); 169 | } 170 | destroyed = true; 171 | onCloseListeners.forEach(function(listener) { 172 | listener(); 173 | }); 174 | } 175 | }; 176 | } 177 | }; 178 | return netStub; 179 | } 180 | 181 | function getResponseHandlerStub() { 182 | 183 | function ResponseHandler() { 184 | if (responseHandlerNotifier) { 185 | throw new Error('responseHandlerNotifier should have been destroyed after each test.'); 186 | } 187 | var notifier = new events.EventEmitter(); 188 | // we can now use this event emitter to emit events from the mocked ResponseHandler 189 | responseHandlerNotifier = notifier; 190 | this.getNotifier = function() { 191 | return notifier; 192 | }; 193 | this.destroy = function() { 194 | // make sure nothing tries to emit events now 195 | responseHandlerNotifier = null; 196 | }; 197 | } 198 | return ResponseHandler; 199 | } 200 | 201 | // have the ResponseHandler behave as if it has received a succesfull synchronous response 202 | // once the Hyperdeck has written the request onto the socket 203 | function queueFakeSuccesfulSynchronousResponse(fakeAsync) { 204 | if (onSocketWrite) { 205 | throw new Error('onSocketWrite already set. Should be cleared after use.'); 206 | } 207 | // will be called when the Hyperdeck writes to the socket. 208 | onSocketWrite = function(/* data */) { 209 | onSocketWrite = null; 210 | 211 | // async 212 | setTimeout(function() { 213 | if (fakeAsync) { 214 | // fake an async response first 215 | responseHandlerNotifier.emit('asynchronousResponse', ASYNCHRONOUS_EVENT_DATA); 216 | } 217 | setTimeout(function() { 218 | responseHandlerNotifier.emit('synchronousResponse', { 219 | success: true, 220 | data: SUCCESS_DATA 221 | }); 222 | }, 0); 223 | }, 0); 224 | }; 225 | } 226 | 227 | // have the ResponseHandler behave as if it has received a failure synchronous response 228 | // once the Hyperdeck has written the request onto the socket 229 | function queueFakeFailureSynchronousResponse() { 230 | if (onSocketWrite) { 231 | throw new Error('onSocketWrite already set. Should be cleared after use.'); 232 | } 233 | // will be called when the Hyperdeck writes to the socket. 234 | onSocketWrite = function(/* data */) { 235 | onSocketWrite = null; 236 | 237 | // async 238 | setTimeout(function() { 239 | responseHandlerNotifier.emit('synchronousResponse', { 240 | success: false, 241 | data: FAILURE_DATA 242 | }); 243 | }, 0); 244 | }; 245 | } 246 | 247 | function fakeAsyncResponse() { 248 | if (onConnectionCompleted) { 249 | throw new Error('onConnectionCompleted already set. Should be cleared after use.'); 250 | } 251 | onConnectionCompleted = function() { 252 | onConnectionCompleted = null; 253 | responseHandlerNotifier.emit('asynchronousResponse', ASYNCHRONOUS_EVENT_DATA); 254 | }; 255 | } 256 | -------------------------------------------------------------------------------- /src/hyperdeck/hyperdeck-core.js: -------------------------------------------------------------------------------- 1 | /*jshint latedef: false */ 2 | var ResponseHandler = require('./response-handler'); 3 | var Promise = require('promise'); 4 | var net = require('net'); 5 | var events = require('events'); 6 | var Logger = require('../logger'); 7 | 8 | var logger = Logger.get('hyperdeck.HyperdeckCore'); 9 | 10 | /** 11 | * Represents a Hyperdeck. 12 | * Allows you to make requests to the hyperdeck and get its parsed responses. 13 | * This chains the requests so only one is sent at a time. 14 | * You can also listen for asynchronous events sent from the hyperdeck. 15 | * @param config hyperdeck configuration 16 | * config = 'ip address string[string]' 17 | * or 18 | * config = { 19 | * ip: 'ip address string[string]', 20 | * [ pingInterval: ping interval in miliseconds[int][default = 10000] ] 21 | * } 22 | **/ 23 | function HyperdeckCore(config) { 24 | 25 | /** 26 | * validate configuration 27 | * 28 | * @param {*} config 29 | */ 30 | function isConfigValid(config) { 31 | return ( 32 | config !== undefined && 33 | config !== null && 34 | (typeof config === 'string' || !!config.ip) 35 | ); 36 | } 37 | 38 | // check for valid configuration 39 | if (!isConfigValid(config)) { 40 | throw new Error('Invalid Configuration, please refer to documentations.'); 41 | } 42 | 43 | var self = this; 44 | 45 | if (typeof config === 'string') { 46 | config = { ip: config }; 47 | } 48 | config = Object.assign({ pingInterval: 10000 }, config); 49 | 50 | function onConnectionStateChange(state) { 51 | if (!state.connected) { 52 | publicNotifier.emit('connectionLost'); 53 | } 54 | } 55 | 56 | function handleConnectionResponse() { 57 | function removeListeners() { 58 | responseHandler.getNotifier().removeListener('asynchronousResponse', handler); 59 | responseHandler.getNotifier().removeListener('connectionStateChange', handleConnectionLoss); 60 | } 61 | 62 | function handler(response) { 63 | if (response.code === 500 && response.text === 'connection info') { 64 | removeListeners(); 65 | connected = true; 66 | connecting = false; 67 | registerAsyncResponseListener(); 68 | notifier.emit('connectionStateChange', {connected: true}); 69 | if (config.pingInterval > 0) { 70 | pingTimerId = setInterval(ping, config.pingInterval); 71 | } 72 | // a request might have been queued whilst the connection 73 | // was being made 74 | performNextRequest(); 75 | } 76 | else if (response.code === 120 && response.text === 'connection rejected') { 77 | removeListeners(); 78 | // close the socket, which should then result in onConnectionLost() being called 79 | client.destroy(); 80 | } 81 | else { 82 | throw new Error('Was expecting an async response stating whether the connection was succesful.'); 83 | } 84 | } 85 | 86 | function handleConnectionLoss(state) { 87 | if (state.connected) { 88 | throw new Error('Invalid connection state.'); 89 | } 90 | removeListeners(); 91 | } 92 | responseHandler.getNotifier().on('asynchronousResponse', handler); 93 | responseHandler.getNotifier().on('connectionStateChange', handleConnectionLoss); 94 | } 95 | 96 | function registerAsyncResponseListener() { 97 | responseHandler.getNotifier().on('asynchronousResponse', function(data) { 98 | // the developer will listen on the notifier for asynchronous events 99 | // fired from the hyperdeck 100 | publicNotifier.emit('asynchronousEvent', data); 101 | }); 102 | } 103 | 104 | // or the connection fails to be made 105 | function onConnectionLost() { 106 | if (!socketConnected && !connecting) { 107 | throw 'Must be connected (or connecting) in order to loose the connection!'; 108 | } 109 | connecting = false; 110 | connected = false; 111 | socketConnected = false; 112 | if (pingTimerId !== null) { 113 | clearTimeout(pingTimerId); 114 | pingTimerId = null; 115 | } 116 | notifier.emit('connectionStateChange', {connected: false}); 117 | performNextRequest(); 118 | } 119 | 120 | function isValidRequest(request) { 121 | // requests must not contain new lines 122 | return request.indexOf('\r') === -1 && request.indexOf('\n') === -1; 123 | } 124 | 125 | // write to the socket 126 | function write(data) { 127 | logger.debug('Writing to socket.', data); 128 | client.write(data); 129 | } 130 | 131 | function ping() { 132 | self.makeRequest('ping'); 133 | } 134 | 135 | /** 136 | * Checks the chain isn't empty or that the request is in progress. 137 | * Then takes from the bottom of the chain and do the request. 138 | * Once the request has finished do more things. 139 | * Performs the next request based of the chain, runs until the chain is empty. 140 | **/ 141 | function performNextRequest() { 142 | if (connecting || pendingRequests.length === 0 || requestInProgress) { 143 | // connection in progress or 144 | // there's nothing left in the chain or there's a request in progress. 145 | return; 146 | } 147 | 148 | requestInProgress = true; 149 | var request = pendingRequests.shift(); 150 | var requestCompletionPromise = requestCompletionPromises.shift(); 151 | var listenersRegistered = false; 152 | 153 | function onRequestCompleted() { 154 | requestInProgress = false; 155 | performNextRequest(); 156 | } 157 | 158 | function registerListeners() { 159 | listenersRegistered = true; 160 | responseHandler.getNotifier().on('synchronousResponse', handleResponse); 161 | notifier.on('connectionStateChange', handleConnectionLoss); 162 | } 163 | 164 | function removeListeners() { 165 | if (listenersRegistered) { 166 | responseHandler.getNotifier().removeListener('synchronousResponse', handleResponse); 167 | notifier.removeListener('connectionStateChange', handleConnectionLoss); 168 | } 169 | } 170 | 171 | function handleResponse(response) { 172 | logger.debug('Got response. Resolving provided promise with response.'); 173 | removeListeners(); 174 | if (response.success) { 175 | // response has a success status code 176 | requestCompletionPromise.resolve(response.data); 177 | } 178 | else { 179 | // response has a failure status code 180 | requestCompletionPromise.reject(response.data); 181 | } 182 | onRequestCompleted(); 183 | } 184 | 185 | function handleConnectionLoss(state) { 186 | if (state.connected) { 187 | throw new Error('Invalid connection state.'); 188 | } 189 | onConnectionLost(); 190 | } 191 | 192 | function onConnectionLost() { 193 | logger.debug('Connection lost. Rejecting provided promise to signify failure.'); 194 | removeListeners(); 195 | // null to signify connection loss 196 | requestCompletionPromise.reject(null); 197 | onRequestCompleted(); 198 | } 199 | 200 | if (!connected) { 201 | // connection has been lost 202 | // don't even attempt the request 203 | logger.debug('Not attempting request because connection lost.'); 204 | onConnectionLost(); 205 | } 206 | else { 207 | registerListeners(); 208 | // make the request 209 | // either the 'synchronousResponse' or 'connectionLost' event should be 210 | // fired at some point in the future. 211 | logger.info('Making request.', request); 212 | write(request+'\n'); 213 | } 214 | } 215 | 216 | var destroyed = false; 217 | var publicNotifier = new events.EventEmitter(); 218 | var notifier = new events.EventEmitter(); 219 | 220 | var pendingRequests = []; 221 | var requestCompletionPromises = []; 222 | var requestInProgress = false; 223 | 224 | var connecting = true; 225 | var socketConnected = false; 226 | // hyperdeck connection completed 227 | var connected = false; 228 | var pingTimerId = null; 229 | notifier.on('connectionStateChange', onConnectionStateChange); 230 | 231 | var client = net.connect({ 232 | host: config.ip, 233 | port: 9993 234 | }, function() { 235 | logger.info('Socket connected.'); 236 | socketConnected = true; 237 | // wait for the hyperdeck to confirm it's ready and connected. 238 | handleConnectionResponse(); 239 | }); 240 | client.setEncoding('utf8'); 241 | 242 | client.on('error', function (e) { 243 | logger.warn('Socket error.', e); 244 | }); 245 | // when the connection closes handle this 246 | // this should also happen if the connection fails at some point 247 | client.on('close', onConnectionLost); 248 | var responseHandler = new ResponseHandler(client); 249 | 250 | /** 251 | * Make a request to the hyperdeck. 252 | * - If the hyperdeck returns a success response the promise will be resolved 253 | * with the payload. 254 | * - If the hyperdeck returns a failure response the promise will be rejected 255 | * with the payload. 256 | * - If the hyperdeck looses connection or is not connected the promise will be 257 | * rejected and the payload will be `null`. 258 | * @return The promise which will resolve/reject when a response has been received 259 | * (or connection lost). 260 | */ 261 | this.makeRequest = function(requestToProcess) { 262 | if (!isValidRequest(requestToProcess)) { 263 | throw new Error('Invalid request.'); 264 | } 265 | 266 | var completionPromise = new Promise(function(resolve, reject) { 267 | requestCompletionPromises.push({ 268 | resolve: resolve, 269 | reject: reject 270 | }); 271 | }); 272 | 273 | pendingRequests.push(requestToProcess); 274 | logger.info('Queueing request.', requestToProcess); 275 | performNextRequest(); 276 | return completionPromise; 277 | }; 278 | 279 | /** 280 | * Returns a promise that resolves when they hyperdeck is connected, 281 | * or rejected if the connection fails. 282 | */ 283 | this.onConnected = function() { 284 | return new Promise(function(resolve, reject) { 285 | if (connected) { 286 | resolve(); 287 | } 288 | else if (!connecting) { 289 | reject(); 290 | } 291 | else { 292 | notifier.once('connectionStateChange', function(state) { 293 | if (state.connected) { 294 | resolve(); 295 | } 296 | else { 297 | reject(); 298 | } 299 | }); 300 | } 301 | }); 302 | }; 303 | 304 | /** 305 | * Determine if currently connected to the hyperdeck. 306 | * @return boolean true if connected, false otherwise. 307 | */ 308 | this.isConnected = function() { 309 | return connected; 310 | }; 311 | 312 | /** 313 | * Get the notifier. 314 | * Events with id 'asynchronousEvent' will be emitted from this for asynchronous events 315 | * from the hyperdeck. 316 | * 'connectionLost' is emitted if the hyperdeck connection is lost (or fails to connect) 317 | */ 318 | this.getNotifier = function() { 319 | return publicNotifier; 320 | }; 321 | 322 | /** 323 | * Destroy the hyperdeck instance, and disconnect if connected. 324 | */ 325 | this.destroy = function() { 326 | if (destroyed) { 327 | return; 328 | } 329 | logger.debug('Destroying...'); 330 | destroyed = true; 331 | write('quit\n'); 332 | responseHandler.destroy(); 333 | client.destroy(); 334 | }; 335 | } 336 | 337 | module.exports = HyperdeckCore; 338 | --------------------------------------------------------------------------------