├── 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 | [](https://nodei.co/npm/hyperdeck-js-lib/)
2 |
3 | [](https://travis-ci.org/LA1TV/Hyperdeck-JS-Lib)
4 | [](https://badge.fury.io/js/hyperdeck-js-lib)
5 | [](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 |
--------------------------------------------------------------------------------