From 9f8e9c8d731c6f4a7464b7dc1f68aa08bd66385f Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 9 Dec 2019 11:41:36 +0100 Subject: [PATCH 01/64] Added supported for Function on server side. Fixed invokationresult decoding --- ember.js | 169 ++++++++++++++++++++++++++++++++++++-------- package.json | 7 +- server.js | 90 +++++++++++++++++------ test/Server.test.js | 29 +++++++- test/client.js | 21 ------ test/utils.js | 24 ++++++- 6 files changed, 261 insertions(+), 79 deletions(-) delete mode 100755 test/client.js diff --git a/ember.js b/ember.js index 3e594bb..d82292d 100755 --- a/ember.js +++ b/ember.js @@ -101,7 +101,9 @@ Root.prototype.addElement = function(ele) { this.elements.push(ele); } - +Root.prototype.addResult = function(result) { + this.result = result; +} Root.prototype.addChild = function(child) { this.addElement(child); @@ -109,7 +111,7 @@ Root.prototype.addChild = function(child) { Root.prototype.encode = function(ber) { ber.startSequence(BER.APPLICATION(0)); - if(this.elements !== undefined) { + if(this.elements != null) { ber.startSequence(BER.APPLICATION(11)); for(var i=0; i 0) { - tag = seq.peek(); - var dataSeq = seq.getSequence(BER.CONTEXT(0)); - if (tag === BER.CONTEXT(0)) { - fc.arguments.push(FunctionArgument.decode(dataSeq)); - } + var dataSeq = seq.getSequence(BER.EMBER_SEQUENCE); + while(dataSeq.remain > 0) { + seq = dataSeq.getSequence(BER.CONTEXT(0)); + fc.arguments.push(FunctionArgument.decode(seq)); } } else if(tag == BER.CONTEXT(3)) { fc.result = []; @@ -1728,8 +1733,10 @@ FunctionContent.prototype.encode = function(ber) { if(this.arguments != null) { ber.startSequence(BER.CONTEXT(2)); ber.startSequence(BER.EMBER_SEQUENCE); - for(var i =0; i < this.arguments.length; i++) { + for(var i = 0; i < this.arguments.length; i++) { + ber.startSequence(BER.CONTEXT(0)); this.arguments[i].encode(ber); + ber.endSequence(); } ber.endSequence(); ber.endSequence(); // BER.CONTEXT(2) @@ -1756,11 +1763,12 @@ module.exports.FunctionContent = FunctionContent; * QualifiedFunction ***************************************************************************/ -function QualifiedFunction(path) { +function QualifiedFunction(path, func) { QualifiedFunction.super_.call(this); if (path != undefined) { this.path = path; } + this.func = func; } util.inherits(QualifiedFunction, TreeNode); @@ -1891,10 +1899,10 @@ module.exports.QualifiedFunction = QualifiedFunction; ***************************************************************************/ -function Function(number) { +function Function(number, func) { Function.super_.call(this); - if(number !== undefined) - this.number = number; + this.number = number; + this.func = func; }; util.inherits(Function, TreeNode); @@ -1953,8 +1961,11 @@ Function.prototype.encode = function(ber) { ber.endSequence(); // BER.APPLICATION(19) } -module.exports.Function = Function; - +Function.prototype.toQualified = function() { + const qf = new QualifiedFunction(this.getPath()); + qf.update(this); + return qf; +} Function.prototype.invoke = function(callback) { if(callback !== undefined) { @@ -1966,6 +1977,24 @@ Function.prototype.invoke = function(callback) { }); } +Function.prototype.update = function(other) { + callbacks = Function.super_.prototype.update.apply(this); + if ((other !== undefined) && (other.contents !== undefined)) { + if (this.contents == null) { + this.contents = other.contents; + } + else { + for (var key in other.contents) { + if (other.contents.hasOwnProperty(key)) { + this.contents[key] = other.contents[key]; + } + } + } + } + return callbacks; +} + +module.exports.Function = Function; /**************************************************************************** @@ -2077,7 +2106,7 @@ Command.decode = function(ber) { c.fieldFlags = FieldFlags.get(seq.readInt()); } else if(tag == BER.CONTEXT(2)) { - c.invocation = Invocation.decode(ber); + c.invocation = Invocation.decode(seq); } else { // TODO: options @@ -2123,20 +2152,29 @@ function Invocation(id = null) { Invocation._id = 1 -Invocation.prototype.decode = function(ber) { - let invocation = new Invocation(); +Invocation.decode = function(ber) { + let invocation = null; ber = ber.getSequence(BER.APPLICATION(22)); while(ber.remain > 0) { var tag = ber.peek(); var seq = ber.getSequence(tag); if(tag == BER.CONTEXT(0)) { - invocation.invocationId = seq.readInt(); + const invocationId = seq.readInt(); + invocation = new Invocation(invocationId); } - if(tag == BER.CONTEXT(1)) { + else if(tag == BER.CONTEXT(1)) { + if (invocation == null) { + throw new Error("Missing invocationID"); + } invocation.arguments = []; - const seq = ber.getSequence(BER.EMBER_SEQUENCE); + seq = seq.getSequence(BER.EMBER_SEQUENCE); while(seq.remain > 0) { - invocation.arguments.push(FunctionArgument.decode(seq)); + const dataSeq = seq.getSequence(BER.CONTEXT(0)); + tag = dataSeq.peek(); + const val = dataSeq.readValue(); + invocation.arguments.push( + new FunctionArgument(ParameterTypefromBERTAG(tag), val) + ); } } else { @@ -2171,15 +2209,66 @@ Invocation.prototype.encode = function(ber) { ber.endSequence(); ber.endSequence(); // BER.APPLICATION(22) - } /**************************************************************************** * InvocationResult ***************************************************************************/ -function InvocationResult() { + + function InvocationResult(invocationId = null) { + this.invocationId = invocationId; } + +util.inherits(InvocationResult, TreeNode); module.exports.InvocationResult = InvocationResult; +InvocationResult.prototype.setFailure = function() { + this.success = false; +} + +InvocationResult.prototype.setSuccess = function() { + this.success = true; +} + +/** + * @param{} + */ +InvocationResult.prototype.setResult = function(result) { + if (!Array.isArray(result)) { + throw new Error("Invalid inovation result. Should be array"); + } + this.result = result; +} + +InvocationResult.prototype.encode = function(ber) { + ber.startSequence(BER.APPLICATION(23)); + if (this.invocationId != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.invocationId); + ber.endSequence(); + } + if (this.success != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeBoolean(this.success); + ber.endSequence(); + } + if (this.result != null && this.result.length) { + ber.startSequence(BER.CONTEXT(2)); + ber.startSequence(BER.EMBER_SEQUENCE); + for (let i = 0; i < this.result.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeValue(this.result[i].value, ParameterTypetoBERTAG(this.result[i].type)); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); + } + ber.endSequence(); // BER.APPLICATION(23)} +} + +InvocationResult.prototype.toQualified = function() { + return this; +} + InvocationResult.decode = function(ber) { let invocationResult = new InvocationResult(); ber = ber.getSequence(BER.APPLICATION(23)); @@ -2195,9 +2284,14 @@ InvocationResult.decode = function(ber) { let res = seq.getSequence(BER.EMBER_SEQUENCE); while(res.remain > 0) { tag = res.peek(); - var resTag = res.getSequence(BER.CONTEXT(0)); if (tag === BER.CONTEXT(0)) { - invocationResult.result.push(resTag.readValue()); + var resTag = res.getSequence(BER.CONTEXT(0)); + tag = resTag.peek(); + invocationResult.result.push( + new FunctionArgument( + ParameterTypefromBERTAG(tag), + resTag.readValue() + )); } } continue @@ -2209,6 +2303,9 @@ InvocationResult.decode = function(ber) { return invocationResult; } + + + /**************************************************************************** * QualifiedParameter ***************************************************************************/ @@ -2495,6 +2592,18 @@ function ParameterTypetoBERTAG(type) { } } +function ParameterTypefromBERTAG(tag) { + switch (tag) { + case BER.EMBER_INTEGER: return ParameterType.integer; + case BER.EMBER_REAL: return ParameterType.real; + case BER.EMBER_STRING: return ParameterType.string; + case BER.EMBER_BOOLEAN: return ParameterType.boolean; + case BER.EMBER_OCTETSTRING: return ParameterType.octets; + default: + throw new Error(`Unhandled BER TAB ${tag}`); + } +} + module.exports.ParameterAccess = ParameterAccess; module.exports.ParameterType = ParameterType; diff --git a/package.json b/package.json index 270514d..8de1d96 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "emberplus", - "version": "1.8.0", + "version": "1.9.0", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { @@ -11,12 +11,13 @@ "contributors": [ { "name": "Gilles Dufour", - "email": "dufour.gilles@gmail.com" + "email": "dufour.gilles@gmail.com", + "web": "www.gdnet.be" } ], "repository": { "type": "git", - "url": "https://github.com/bmayton/node-emberplus" + "url": "https://github.com/dufourgilles/node-emberplus" }, "license": "MIT", "dependencies": { diff --git a/server.js b/server.js index 8bdfee5..d99daf5 100755 --- a/server.js +++ b/server.js @@ -106,7 +106,7 @@ TreeServer.prototype.handleRoot = function(client, root) { } else if (node instanceof ember.Command) { // Command on root element - this.handleCommand(client, this.tree, node.number); + this.handleCommand(client, this.tree, node); return "root"; } else { @@ -127,14 +127,14 @@ TreeServer.prototype.handleQualifiedNode = function(client, node) { // Find this element in our tree const element = this.tree.getElementByPath(path); - if ((element === null) || (element === undefined)) { + if (element == null) { this.emit("error", new Error(`unknown element at path ${path}`)); return this.handleError(client); } - if ((node.children !== undefined) && (node.children.length === 1) && + if ((node.children != null) && (node.children.length === 1) && (node.children[0] instanceof ember.Command)) { - this.handleCommand(client, element, node.children[0].number); + this.handleCommand(client, element, node.children[0]); } else { if (node instanceof ember.QualifiedMatrix) { @@ -142,7 +142,7 @@ TreeServer.prototype.handleQualifiedNode = function(client, node) { } else if (node instanceof ember.QualifiedParameter) { this.handleQualifiedParameter(client, element, node); - } + } } return path; } @@ -326,18 +326,28 @@ TreeServer.prototype.matrixSet = function(path, target, sources) { doMatrixOperation(this, path, target, sources, ember.MatrixOperation.absolute); } +TreeServer.prototype.handleQualifiedFunction = function(client, element, node) { + +} + + TreeServer.prototype.handleCommand = function(client, element, cmd) { - if (cmd === ember.GetDirectory) { - this.handleGetDirectory(client, element); - } - else if (cmd === ember.Subscribe) { - this.handleSubscribe(client, element); - } - else if (cmd === ember.Unsubscribe) { - this.handleUnSubscribe(client, element); - } - else { - this.emit("error", new Error(`invalid command ${cmd}`)); + switch(cmd.number) { + case ember.GetDirectory: + this.handleGetDirectory(client, element); + break; + case ember.Subscribe: + this.handleSubscribe(client, element); + break; + case ember.Unsubscribe: + this.handleUnSubscribe(client, element); + break; + case ember.Invoke: + this.handleInvoke(client, cmd.invocation, element); + break; + default: + this.emit("error", new Error(`invalid command ${cmd}`)); + break; } } @@ -357,7 +367,7 @@ TreeServer.prototype.getResponse = function(element) { } TreeServer.prototype.getQualifiedResponse = function(element) { - let res = new ember.Root(); + const res = new ember.Root(); let dup; if (element.isRoot() === false) { dup = element.toQualified(); @@ -374,6 +384,26 @@ TreeServer.prototype.getQualifiedResponse = function(element) { return res; } +TreeServer.prototype.handleInvoke = function(client, invocation, element) { + const result = new ember.InvocationResult(); + result.invocationId = invocation.id; + if (element == null || !element.isFunction()) { + result.setFailure(); + } + else { + try { + result.setResult(element.func(invocation.arguments)); + } + catch(e){ + this.emit("error", e); + result.setFailure(); + } + } + const res = new ember.Root(); + res.addResult(result); + client.sendBERNode(res); +} + TreeServer.prototype.handleGetDirectory = function(client, element) { if (client !== undefined) { if ((element.isMatrix() || element.isParameter()) && @@ -395,7 +425,7 @@ TreeServer.prototype.handleGetDirectory = function(client, element) { } } - let res = this.getQualifiedResponse(element); + const res = this.getQualifiedResponse(element); if (this._debug) { console.log("getDirectory response", res); } @@ -529,13 +559,12 @@ const parseMatrixContent = function(matrixContent, content) { } const parseObj = function(parent, obj) { - let path = parent.getPath(); for(let i = 0; i < obj.length; i++) { let emberElement; let content = obj[i]; let number = content.number !== undefined ? content.number : i; delete content.number; - if (content.value !== undefined) { + if (content.value != null) { emberElement = new ember.Parameter(number); emberElement.contents = new ember.ParameterContents(content.value); if (content.type) { @@ -553,7 +582,20 @@ const parseObj = function(parent, obj) { emberElement.contents.access = ember.ParameterAccess.read; } } - else if (content.targetCount !== undefined) { + else if (content.func != null) { + emberElement = new ember.Function(number, content.func); + emberElement.contents = new ember.FunctionContent(); + if (content.arguments != null) { + for(let argument of content.arguments) { + emberElement.contents.arguments.push(new ember.FunctionArgument( + argument.type, + argument.value, + argument.name + )); + } + } + } + else if (content.targetCount != null) { emberElement = new ember.MatrixNode(number); emberElement.contents = new ember.MatrixContents(); parseMatrixContent(emberElement.contents, content); @@ -583,7 +625,11 @@ const parseObj = function(parent, obj) { emberElement.contents = new ember.NodeContents(); } for(let id in content) { - if ((id !== "children") && (content.hasOwnProperty(id))) { + if (emberElement.isFunction() && id === "arguments") { + // we did it already. + continue; + } + if ((id !== "children") && (content.hasOwnProperty(id))) { emberElement.contents[id] = content[id]; } else { diff --git a/test/Server.test.js b/test/Server.test.js index ed44416..c163b32 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -27,7 +27,7 @@ describe("server", function() { expect(root.elements.length).toBe(1); console.log("root", root.elements[0].contents); expect(root.elements[0].contents.identifier).toBe("scoreMaster"); - expect(root.elements[0].children.length).toBe(2); + expect(root.elements[0].children.length).toBe(jsonTree[0].children.length); }); }); @@ -63,7 +63,7 @@ describe("server", function() { return client.getDirectory(client.root.elements[0]); }) .then(() => { - expect(client.root.elements[0].children.length).toBe(2); + expect(client.root.elements[0].children.length).toBe(jsonTree[0].children.length); return client.getDirectory(client.root.elements[0].children[0]); }) .then(() => { @@ -126,6 +126,31 @@ describe("server", function() { return client.disconnect(); }); }); + it("should be able to call a function with parameters", () => { + client = new DeviceTree(LOCALHOST, PORT); + //client._debug = true; + return Promise.resolve() + .then(() => client.connect()) + .then(() => { + return client.getDirectory(); + }) + .then(() => client.expand(client.root.elements[0])) + .then(() => { + const func = client.root.elements[0].children[2]; + return client.invokeFunction(func, [ + new ember.FunctionArgument(ember.ParameterType.integer, 1), + new ember.FunctionArgument(ember.ParameterType.integer, 7) + ]); + }) + .then(result => { + console.log(result); + expect(result).toBeDefined(); + expect(result.result).toBeDefined(); + expect(result.result.length).toBe(1); + expect(result.result[0].value).toBe(8); + return client.disconnect(); + }); + }); it("should be able to get child with getNodeByPath", function() { //server._debug = true; diff --git a/test/client.js b/test/client.js deleted file mode 100755 index 880e0cc..0000000 --- a/test/client.js +++ /dev/null @@ -1,21 +0,0 @@ -const DeviceTree = require("../").DeviceTree; -const ember = require("../ember"); - -const HOST = "192.168.4.4"; -const PORT = 9092; - -client = new DeviceTree(HOST, PORT); -client.connect() -.then(() => client.getDirectory()) -.then(() => client.expand(client.root.elements[0])) -.then(() => { - //console.log(client.root.elements[0].children[4].children[2].toJSON()); - console.log(JSON.stringify(client.root.elements[0].children[4].toJSON(), null, 4)); - return client.invokeFunction(client.root.elements[0].children[4].children[0], [ - new ember.FunctionArgument(ember.ParameterType.integer, 1), - new ember.FunctionArgument(ember.ParameterType.integer, 7) - ]); -}) -.then(result => { - console.log(result); -}); diff --git a/test/utils.js b/test/utils.js index 74b6eb9..103b542 100755 --- a/test/utils.js +++ b/test/utils.js @@ -1,4 +1,4 @@ -const {ParameterAccess} = require("../ember"); +const {ParameterType, FunctionArgument} = require("../ember"); const init = function(_src,_tgt) { const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; @@ -77,6 +77,28 @@ const init = function(_src,_tgt) { ] } ] + }, + { + // path "0.2" + identifier: "addFunction", + func: args => { + const res = new FunctionArgument(); + res.type = ParameterType.integer; + res.value = args[0].value + args[1].value; + return [res]; + }, + arguments: [ + { + type: ParameterType.integer, + value: null, + name: "arg1" + }, + { + type: ParameterType.integer, + value: null, + name: "arg2" + } + ] } ] } From ed2ff32c7d8f870a7b59bca7d347d48b12c36f73 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 9 Dec 2019 11:53:54 +0100 Subject: [PATCH 02/64] new documentation --- README.md | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/README.md b/README.md index 2464cc9..8f7f3aa 100755 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Server has been added in version 1.6.0. ## Example usage +### Client ```javascript const DeviceTree = require('emberplus').DeviceTree; var root; @@ -37,8 +38,33 @@ tree.connect() .catch((e) => { console.log(e.stack); }); +``` +### Function +```javascript +const client = new DeviceTree(HOST, PORT); +client.connect()) +.then(() => client.getDirectory()) +.then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) +.then(() => client.expand(client.root.elements[0])) +.then(() => { + console.log(JSON.stringify(client.root.elements[0].toJSON(), null, 4)); + let func; + if (TINYEMBER) { + func = client.root.elements[0].children[4].children[0]; + } + else { + func = client.root.elements[0].children[2]; + } + return client.invokeFunction(func, [ + new ember.FunctionArgument(ember.ParameterType.integer, 1), + new ember.FunctionArgument(ember.ParameterType.integer, 7) + ]); +}) +``` +### Packet decoder +```javascript // Simple packet decoder const Decoder = require('emberplus').Decoder; const fs = require("fs"); @@ -46,8 +72,125 @@ const fs = require("fs"); fs.readFile("tree.ember", (e,data) => { var root = Decoder(data); }); +``` +### Server +```javascript // Server const TreeServer = require("emberplus").TreeServer; const server = new TreeServer("127.0.0.1", 9000, root); +server.on("error", e => { + console.log("Server Error", e); +}); +server.on("clientError", info => { + console.log("clientError", info); +}); server.listen().then(() => { console.log("listening"); }).catch((e) => { console.log(e.stack); }); +``` + +### Construct Tree +```javascript +const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; +const sources = _src === undefined ? [ "src1", "src2", "src3" ] : _src; +const labels = function(endpoints) { + let labels = []; + for (let i = 0; i < endpoints.length; i++) { + let endpoint = endpoints[i]; + let l = { identifier: `Label-${i}` }; + if (endpoint) { + l.value = endpoint; + } + labels.push(l); + } + return labels; +}; + +const buildConnections = function(s, t) { + let connections = []; + for (let i = 0; i < t.length; i++) { + connections.push({target: `${i}`}); + } + return connections; +}; +const jsonTree = [ + { + // path "0" + identifier: "GDNet Tree", + children: [ + { + // path "0.0" + identifier: "identity", + children: [ + {identifier: "product", value: "gdnet core"}, + {identifier: "company", value: "GDNet", access: "readWrite"}, + {identifier: "version", value: "1.2.0"}, + {identifier: "author", value: "dufour.gilles@gmail.com"}, + ] + }, + { + // path "0.1" + identifier: "router", + children: [ + { + // path 0.1.0 + identifier: "matrix", + type: "oneToN", + mode: "linear", + targetCount: targets.length, + sourceCount: sources.length, + connections: buildConnections(sources, targets), + labels: ["0.1.1000"] + }, + { + identifier: "labels", + // path "0.1.1000" + number: 1000, + children: [ + { + identifier: "targets", + // Must be 1 + number: 1, + children: labels(targets) + }, + { + identifier: "sources", + // Must be 2 + number: 2, + children: labels(sources) + }, + { + identifier: "group 1", + children: [ {identifier: "sdp A", value: "A"}, {identifier: "sdp B", value: "B"}] + } + ] + } + ] + }, + { + // path "0.2" + identifier: "addFunction", + func: args => { + const res = new FunctionArgument(); + res.type = ParameterType.integer; + res.value = args[0].value + args[1].value; + return [res]; + }, + arguments: [ + { + type: ParameterType.integer, + value: null, + name: "arg1" + }, + { + type: ParameterType.integer, + value: null, + name: "arg2" + } + ] + } + ] + } +]; +const root = TreeServer.JSONtoTree(jsonTree); +``` + From 26380558f811aaedf38e8af120950ab7106ad97c Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 9 Dec 2019 11:58:52 +0100 Subject: [PATCH 03/64] new documentation --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 8f7f3aa..dfcf56f 100755 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ tree.connect() ### Function ```javascript +const DeviceTree = require('emberplus').DeviceTree; +const ember = require("emberplus").Ember; + const client = new DeviceTree(HOST, PORT); client.connect()) .then(() => client.getDirectory()) @@ -90,6 +93,9 @@ server.listen().then(() => { console.log("listening"); }).catch((e) => { console ### Construct Tree ```javascript +const TreeServer = require("emberplus").TreeServer; +const {ParameterType, FunctionArgument} = require("emberplus").Ember; + const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; const sources = _src === undefined ? [ "src1", "src2", "src3" ] : _src; const labels = function(endpoints) { From 46297db422aa98d934151694d9242ac78d9836d5 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 9 Dec 2019 17:11:22 +0100 Subject: [PATCH 04/64] Added all matrixConnect and Disconnect functions. Fixed all tests. Modified the Doc. --- README.md | 100 ++++++++++++++++------- device.js | 177 +++++++++++++++++++++++++++++++++------- ember.js | 63 +++++++++++++- package.json | 30 ++++--- server.js | 9 +- test/DeviceTree.test.js | 88 ++++++++++---------- test/Server.test.js | 170 +++++++++++++++++++------------------- 7 files changed, 435 insertions(+), 202 deletions(-) diff --git a/README.md b/README.md index dfcf56f..7a583c3 100755 --- a/README.md +++ b/README.md @@ -20,50 +20,88 @@ Server has been added in version 1.6.0. ## Example usage ### Client +Get Full tree: ```javascript const DeviceTree = require('emberplus').DeviceTree; var root; var tree = new DeviceTree("10.9.8.7", 9000); -tree.connect() -.then(() => { - return tree.getDirectory(); -}) -.then((r) => { - root = r ; - return tree.expand(r.elements[0]); -}) -.then(() => { - console.log("done"); -}) -.catch((e) => { - console.log(e.stack); +tree.on("error", e => { + console.log(e); }); +tree.connect() + .then(() => tree.getDirectory()) + .then((r) => { + root = r ; + return tree.expand(r.elements[0]); + }) + .then(() => { + console.log("done"); + }) + .catch((e) => { + console.log(e.stack); + }); +``` + +Get Specific Branch: +```javascript +const DeviceTree = require('emberplus').DeviceTree; +const ember = require("emberplus").Ember; + +const client = new DeviceTree(HOST, PORT); +client.connect()) + .then(() => client.getDirectory()) + .then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) + .then(() => client.getNodeByPath("scoreMaster/router/labels/group 1")) + .then(() => client.getNodeByPathnum("0.2")) + .then(() => { + console.log(JSON.stringify(client.root.elements[0].toJSON(), null, 4)); + }); ``` -### Function +### Invoking Function ```javascript const DeviceTree = require('emberplus').DeviceTree; const ember = require("emberplus").Ember; const client = new DeviceTree(HOST, PORT); client.connect()) -.then(() => client.getDirectory()) -.then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) -.then(() => client.expand(client.root.elements[0])) -.then(() => { - console.log(JSON.stringify(client.root.elements[0].toJSON(), null, 4)); - let func; - if (TINYEMBER) { - func = client.root.elements[0].children[4].children[0]; - } - else { - func = client.root.elements[0].children[2]; - } - return client.invokeFunction(func, [ - new ember.FunctionArgument(ember.ParameterType.integer, 1), - new ember.FunctionArgument(ember.ParameterType.integer, 7) - ]); -}) + .then(() => client.getDirectory()) + .then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) + .then(() => client.expand(client.root.elements[0])) + .then(() => { + console.log(JSON.stringify(client.root.elements[0].toJSON(), null, 4)); + let func; + if (TINYEMBER) { + func = client.root.elements[0].children[4].children[0]; + } + else { + func = client.root.elements[0].children[2]; + } + return client.invokeFunction(func, [ + new ember.FunctionArgument(ember.ParameterType.integer, 1), + new ember.FunctionArgument(ember.ParameterType.integer, 7) + ]); + }); +``` + +### Matrix Connection +```javascript +const DeviceTree = require('emberplus').DeviceTree; +const ember = require("emberplus").Ember; + +const client = new DeviceTree(HOST, PORT); +client.connect() + .then(() => client.getDirectory()) + .then(() => client.getNodeByPathnum("0.1.0")) + .then(matrix => { + console.log("Connecting source 1 to target 0); + return client.matrixConnect(matrix, 0, [1]); + }) + .then(() => client.matrixDisconnect(matrix, 0, [1])) + .then(() => client.matrixSetConnection(matrix, 0, [0,1])) + .then(matrix => client.getNodeByPathnum(matrix.getPath())) + .then(() => client.disconnect()); + ``` ### Packet decoder diff --git a/device.js b/device.js index a238066..c5e9272 100755 --- a/device.js +++ b/device.js @@ -224,6 +224,80 @@ DeviceTree.prototype.getDirectory = function (qnode) { }); }; +DeviceTree.prototype.matrixOPeration = function(matrixNode, targetID, sources, operation = ember.MatrixOperation.connect) { + return new Promise((resolve, reject) => { + if (!Array.isArray(sources)) { + return reject(new Error("Sources should be an array")); + } + try { + matrixNode.validateConnection(targetID, sources); + } + catch(e) { + return reject(e); + } + const connections = {} + const targetConnection = new ember.MatrixConnection(targetID); + targetConnection.operation = operation; + targetConnection.setSources(sources); + connections[targetID] = targetConnection; + + this.addRequest({node: matrixNode, func: (error) => { + if (error) { + this.finishRequest(); + reject(error); + return; + } + + this.callback = (error, node) => { + const requestedPath = matrixNode.getPath(); + if (node == null) { + if (this._debug) { + console.log(`received null response for ${requestedPath}`); + } + return; + } + if (error) { + if (this._debug) { + console.log("Received getDirectory error", error); + } + this.clearTimeout(); // clear the timeout now. The resolve below may take a while. + this.finishRequest(); + reject(error); + return; + } + let matrix = null; + if (node != null) { + matrix = node.elements[0]; + } + if (matrix != null && matrix.isMatrix() && matrix.getPath() === requestedPath) { + this.clearTimeout(); // clear the timeout now. The resolve below may take a while. + this.finishRequest(); + resolve(matrix); + } + else { + if (this._debug) { + console.log(`unexpected node response during matrix connect ${requestedPath}`, + JSON.stringify(matrix.toJSON(), null, 4)); + } + } + } + this.client.sendBERNode(matrixNode.connect(connections)); + }}); + }); +} + +DeviceTree.prototype.matrixConnect = function(matrixNode, targetID, sources) { + return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.connect) +} + +DeviceTree.prototype.matrixDisconnect = function(matrixNode, targetID, sources) { + return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.disconnect) +} + +DeviceTree.prototype.matrixSetConnection = function(matrixNode, targetID, sources) { + return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.absolute) +} + DeviceTree.prototype.invokeFunction = function (fnNode, params) { var self = this; return new Promise((resolve, reject) => { @@ -245,6 +319,7 @@ DeviceTree.prototype.invokeFunction = function (fnNode, params) { } resolve(result); } + // cleaning callback and making next request. self.finishRequest(); }; @@ -307,7 +382,11 @@ DeviceTree.prototype.finishRequest = function () { try { self.makeRequest(); } catch(e) { - console.log("warn:" + e.message) + if (self._debug) {console.log(e);} + if (self.callback != null) { + self.callback(e); + } + self.emit("error", e); } }; @@ -408,40 +487,80 @@ DeviceTree.prototype.handleNode = function (parent, node) { return callbacks; }; -DeviceTree.prototype.getNodeByPath = function (path, timeout = 2) { +DeviceTree.prototype.getNodeByPathnum = function (path) { var self = this; if (typeof path === 'string') { - path = path.split('/'); + path = path.split('.'); } - var timeoutError = new Error("Request timeout"); - return new Promise((resolve, reject) => { - self.addRequest({path: path, func: (error) => { - if (error) { - reject(error); - self.finishRequest(); - return; + var pos = 0; + var lastMissingPos = -1; + var currentNode = this.root; + const getNext = () => { + return Promise.resolve() + .then(() => { + const children = currentNode.getChildren(); + const number = Number(path[pos]); + if (children != null) { + for (let i = 0; i < children.length; i++) { + var node = children[i]; + if (node.getNumber() === number) { + // We have this part already. + pos++; + if (pos >= path.length) { + return node; + } + currentNode = node; + return getNext(); + } + } } - var timedOut = false; - var cb = (error, node) => { - if (timer) { - clearTimeout(timer); - } - if (timedOut) { return; } - if (error) { - reject(error); - } else { - resolve(node); + // We do not have that node yet. + if (lastMissingPos === pos) { + throw new Error(`Failed path discovery at ${path.slice(0, pos).join("/")}`); + } + lastMissingPos = pos; + return this.getDirectory(currentNode).then(() => getNext()); + }); + } + return getNext(); +}; + +DeviceTree.prototype.getNodeByPath = function (path) { + var self = this; + if (typeof path === 'string') { + path = path.split('/'); + } + var pos = 0; + var lastMissingPos = -1; + var currentNode = this.root; + const getNext = () => { + return Promise.resolve() + .then(() => { + const children = currentNode.getChildren(); + const identifier = path[pos]; + if (children != null) { + for (let i = 0; i < children.length; i++) { + var node = children[i]; + if (node.contents != null && node.contents.identifier === identifier) { + // We have this part already. + pos++; + if (pos >= path.length) { + return node; + } + currentNode = node; + return getNext(); + } } - self.finishRequest(); - }; - var cbTimeout = () => { - timedOut = true; - reject(timeoutError); } - var timer = timeout === 0 ? null : setTimeout(cbTimeout, timeout * 1000); - self.root.getNodeByPath(self.client, path, cb); - }}); - }); + // We do not have that node yet. + if (lastMissingPos === pos) { + throw new Error(`Failed path discovery at ${path.slice(0, pos + 1).join("/")}`); + } + lastMissingPos = pos; + return this.getDirectory(currentNode).then(() => getNext()); + }); + } + return getNext(); }; DeviceTree.prototype.subscribe = function (node, callback) { diff --git a/ember.js b/ember.js index d82292d..9ed41a4 100755 --- a/ember.js +++ b/ember.js @@ -828,6 +828,55 @@ function MatrixNode(number) { } +function validateConnection(matrixNode, targetID, sources) { + if (targetID < 0) { + throw new Error(`Invalid negative target index ${targetID}`); + } + for(let i = 0; i < sources.length; i++) { + if (sources[i] < 0) { + throw new Error(`Invalid negative source at index ${i}`); + } + } + if (matrixNode.contents.mode === MatrixMode.linear) { + if (targetID >= matrixNode.contents.targetCount) { + throw new Error(`targetID ${targetID} higher than max value ${matrixNode.contents.targetCount}`); + } + for(let i = 0; i < sources.length; i++) { + if (sources[i] >= matrixNode.contents.sourceCount) { + throw new Error(`Invalid source at index ${i}`); + } + } + } + else if ((matrixNode.targets == null) || (matrixNode.sources == null)) { + throw new Error("Non-Linear matrix should have targets and sources"); + } + else { + let found = false; + for(let i = 0; i < matrixNode.targets; i++) { + if (matrixNode.targets[i] === targetID) { + found = true; + break; + } + } + if (!found) { + throw new Error(`Unknown targetid ${targetID}`); + } + found = false; + for(let i = 0; i < sources.length; i++) { + for(let j = 0; i < matrixNode.sources; j++) { + if (matrixNode.sources[j] === sources[i]) { + found = true; + break; + } + } + if (!found) { + throw new Error(`Unknown source at index ${i}`); + } + } + } +} + + MatrixNode.decode = function(ber) { var m = new MatrixNode(); ber = ber.getSequence(BER.APPLICATION(13)); @@ -862,6 +911,9 @@ MatrixNode.decode = function(ber) { return m; }; +MatrixNode.prototype.validateConnection = function(targetID, sources) { + validateConnection(this, targetID, sources); +} MatrixNode.prototype.encode = function(ber) { ber.startSequence(BER.APPLICATION(13)); @@ -1238,7 +1290,13 @@ MatrixConnection.decode = function(ber) { c.target = seq.readInt(); } else if (tag == BER.CONTEXT(1)) { - c.sources = seq.readRelativeOID(BER.EMBER_RELATIVE_OID).split(".").map(i => Number(i)); + const sources = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); + if (sources === "") { + c .sources = []; + } + else { + c.sources = sources.split(".").map(i => Number(i)); + } } else if (tag == BER.CONTEXT(2)) { c.operation = MatrixOperation.get(seq.readInt()); @@ -1388,6 +1446,9 @@ QualifiedMatrix.prototype.getMinimal = function(complete = false) { return m; } +QualifiedMatrix.prototype.validateConnection = function(targetID, sources) { + validateConnection(this, targetID, sources); +} QualifiedMatrix.decode = function(ber) { var qm = new QualifiedMatrix(); diff --git a/package.json b/package.json index 8de1d96..c0d839d 100755 --- a/package.json +++ b/package.json @@ -1,27 +1,24 @@ { - "name": "emberplus", - "version": "1.9.0", + "name": "node-emberplus", + "version": "1.10.0", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { "test": "jest test", - "eslint": "eslint ./" + "eslint": "eslint ./", + "start": "node server.js" }, "author": "Brian Mayton (http://bdm.cc)", "contributors": [ - { - "name": "Gilles Dufour", - "email": "dufour.gilles@gmail.com", - "web": "www.gdnet.be" - } + "Gilles Dufour (www.gdnet.be)" ], "repository": { "type": "git", - "url": "https://github.com/dufourgilles/node-emberplus" + "url": "git+https://github.com/dufourgilles/node-emberplus.git" }, "license": "MIT", "dependencies": { - "asn1": "evs-broadcast/node-asn1#date_2018_01_02", + "asn1": "github:evs-broadcast/node-asn1#date_2018_01_02", "enum": "^2.4.0", "long": "^3.2.0", "smart-buffer": "^3.0.3", @@ -33,5 +30,16 @@ "jest": "^23.5.0", "jest-cli": "^24.9.0", "sinon": "^7.4.1" - } + }, + "bugs": { + "url": "https://github.com/dufourgilles/node-emberplus/issues" + }, + "homepage": "https://github.com/dufourgilles/node-emberplus#readme", + "directories": { + "test": "test" + }, + "keywords": [ + "emberplus", + "lawo" + ] } diff --git a/server.js b/server.js index d99daf5..ed0e506 100755 --- a/server.js +++ b/server.js @@ -223,7 +223,10 @@ TreeServer.prototype.handleQualifiedParameter = function(client, element, parame TreeServer.prototype.handleMatrixConnections = function(client, matrix, connections, response = true) { var res; var root; // ember message root - if (matrix.isQualified()) { + if (this._debug) { + console.log("Handling Matrix Connection"); + } + if (client.request.isQualified()) { root = new ember.Root(); res = new ember.QualifiedMatrix(matrix.getPath()); root.elements = [res]; // do not use addchild or the element will get removed from the tree. @@ -281,7 +284,9 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti if (client !== undefined) { client.sendBERNode(root); } - if (this._debug) { console.log("Updating subscribers for matrix change"); } + if (this._debug) { + console.log("Updating subscribers for matrix change"); + } this.updateSubscribers(matrix.getPath(), root, client); } diff --git a/test/DeviceTree.test.js b/test/DeviceTree.test.js index 85a0037..20c3995 100755 --- a/test/DeviceTree.test.js +++ b/test/DeviceTree.test.js @@ -27,9 +27,7 @@ describe("DeviceTree", () => { return server.listen(); }); }); - afterAll(() => server.close()); - }); it("should gracefully connect and disconnect", () => { return Promise.resolve() .then(() => { @@ -41,12 +39,52 @@ describe("DeviceTree", () => { .then(() => tree.connect()) .then(() => tree.getDirectory()) .then(() => tree.disconnect()) + }); + }); + + it("should not disconnect after 5 seconds of inactivity", () => { + return Promise.resolve() + .then(() => { + let tree = new DeviceTree(LOCALHOST, PORT); + + tree.on("error", error => { + throw error; + }); + + return Promise.resolve() + .then(() => tree.connect()) + .then(() => new Promise(resolve => setTimeout(resolve, 5000))) + .then(() => tree.disconnect()) }) - }); - + }, 7000); + + it("timeout should be taken into account when connecting to unknown host", () => { + let tree = new DeviceTree(UNKNOWN_HOST, PORT); + tree.on("error", () => { + }); + const expectedTimeoutInSec = 2; + const initialTime = performance.now(); + return tree.connect(expectedTimeoutInSec) + .then(() => { + throw new Error("Should have thrown"); + }, + error => { + const durationMs = performance.now() - initialTime; + const deltaMs = Math.abs(expectedTimeoutInSec * 1000 - durationMs); + expect(deltaMs).toBeLessThan(10); + expect(error.message).toBe(`Could not connect to ${UNKNOWN_HOST}:${PORT} after a timeout of ${expectedTimeoutInSec} seconds`) + }); + }); + + it("should gracefully connect and getDirectory", () => { let tree = new DeviceTree(LOCALHOST, PORT); + tree.on("error", e => { + console.log(e); + }) let stub = sinon.stub(tree.client, "sendBER"); + tree._debug = true; + server._debug = true; stub.onFirstCall().returns(); stub.onSecondCall().throws(new Error("blah")); stub.callThrough(); @@ -59,43 +97,9 @@ describe("DeviceTree", () => { tree.disconnect(); }, error => { stub.restore(); - tree.disconnect(); - throw error; - }) - - }, 10000); - it("should not disconnect after 5 seconds of inactivity", () => { - return Promise.resolve() - .then(() => { - let tree = new DeviceTree(LOCALHOST, PORT); - - tree.on("error", error => { - throw error; - }); - - return Promise.resolve() - .then(() => tree.connect()) - .then(() => new Promise(resolve => setTimeout(resolve, 5000))) - .then(() => tree.disconnect()) - }) - }, 7000); - }); - - it("timeout should be taken into account when connecting to unknown host", () => { - let tree = new DeviceTree(UNKNOWN_HOST, PORT); - tree.on("error", () => { - }); - const expectedTimeoutInSec = 2; - const initialTime = performance.now(); - return tree.connect(expectedTimeoutInSec) - .then(() => { - throw new Error("Should have thrown"); - }, - error => { - const durationMs = performance.now() - initialTime; - const deltaMs = Math.abs(expectedTimeoutInSec * 1000 - durationMs); - expect(deltaMs).toBeLessThan(10); - expect(error.message).toBe(`Could not connect to ${UNKNOWN_HOST}:${PORT} after a timeout of ${expectedTimeoutInSec} seconds`) + tree.disconnect(); + console.log(error); }); - }); + }, 10000); + }); }); diff --git a/test/Server.test.js b/test/Server.test.js index c163b32..4a63399 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -37,6 +37,12 @@ describe("server", function() { jsonTree = jsonRoot(); const root = TreeServer.JSONtoTree(jsonTree); server = new TreeServer(LOCALHOST, PORT, root); + server.on("error", e => { + console.log(e); + }); + server.on("clientError", e => { + console.log(e); + }); //server._debug = true; return server.listen().then(() => { console.log("server listening"); @@ -93,39 +99,7 @@ describe("server", function() { return client.disconnect(); }); }); - it("should be able to make a matrix connection", () => { - client = new DeviceTree(LOCALHOST, PORT); - //client._debug = true; - return Promise.resolve() - .then(() => client.connect()) - .then(() => { - return client.getDirectory(); - }) - .then(() => client.expand(client.root.elements[0])) - .then(() => { - console.log(client.root.elements[0].children[1].children[0]); - const matrix = client.root.elements[0].children[1].children[0]; - const connections = {} - const target0Connection = new ember.MatrixConnection(0); - target0Connection.operation = ember.MatrixOperation.connect; - target0Connection.setSources([1]); // connect with src 1 - connections[0] = target0Connection; - const p = new Promise(resolve => { - client.on("value-change", node => { - resolve(node); - }); - }); - client.client.sendBERNode(matrix.connect(connections)); - return p; - }) - .then(node => { - console.log(client.root.elements[0].children[1].children[0]); - expect(client.root.elements[0].children[1].children[0].connections['0'].sources).toBeDefined(); - expect(client.root.elements[0].children[1].children[0].connections['0'].sources.length).toBe(1); - expect(client.root.elements[0].children[1].children[0].connections['0'].sources[0]).toBe(1); - return client.disconnect(); - }); - }); + it("should be able to call a function with parameters", () => { client = new DeviceTree(LOCALHOST, PORT); //client._debug = true; @@ -152,49 +126,49 @@ describe("server", function() { }); }); - it("should be able to get child with getNodeByPath", function() { - //server._debug = true; - client = new DeviceTree(LOCALHOST, PORT); - //client._debug = true; + it("should be able to get child with getNodeByPath", function() { + //server._debug = true; + client = new DeviceTree(LOCALHOST, PORT); + //client._debug = true; //client._debug = true; return Promise.resolve() - .then(() => client.connect()) + .then(() => client.connect()) .then(() => { console.log("client connected"); - return client.getDirectory(); - }) - .then(() => { - return new Promise((resolve, reject) => { - client.root.getNodeByPath(client.client, ["scoreMaster", "identity", "product"], (err, child) => { - if (err) { reject(err) } - else { - resolve(child); - } - }); - }); + return client.getDirectory(); }) - .then(child => { - console.log(child); - }) - .then(() => { + .then(() => { return new Promise((resolve, reject) => { - client.root.getNodeByPath(client.client, ["scoreMaster", "router", "labels"], (err, child) => { - if (err) { reject(err) } - else { - resolve(child); - } - }); + client.root.getNodeByPath(client.client, ["scoreMaster", "identity", "product"], (err, child) => { + if (err) { reject(err) } + else { + resolve(child); + } + }); }); }) - .then(child => { - console.log(child); - client.disconnect(); + .then(child => { + console.log(child); + }) + .then(() => { + return new Promise((resolve, reject) => { + client.root.getNodeByPath(client.client, ["scoreMaster", "router", "labels"], (err, child) => { + if (err) { reject(err) } + else { + resolve(child); + } + }); }); - }); - it("should be able to get child with tree.getNodeByPath", function() { - //server._debug = true; - client = new DeviceTree(LOCALHOST, PORT); - //client._debug = true; + }) + .then(child => { + console.log(child); + client.disconnect(); + }); + }); + it("should be able to get child with tree.getNodeByPath", function() { + //server._debug = true; + client = new DeviceTree(LOCALHOST, PORT); + //client._debug = true; //client._debug = true; return Promise.resolve() .then(() => client.connect()) @@ -204,33 +178,57 @@ describe("server", function() { }) .then(() => client.getNodeByPath("scoreMaster/identity/product")) .then(child => { - console.log(child); - return client.getNodeByPath("scoreMaster/router/labels/group 1"); - }) + console.log(child); + return client.getNodeByPath("scoreMaster/router/labels/group 1"); + }) .then(child => { - console.log("router/labels", child); - client.disconnect(); + console.log("router/labels", child); + client.disconnect(); }); - }); - it("should throw an erro if getNodeByPath for unknown path", function() { - //server._debug = true; - client = new DeviceTree(LOCALHOST, PORT); - return Promise.resolve() + }); + it("should throw an error if getNodeByPath for unknown path", function() { + //server._debug = true; + client = new DeviceTree(LOCALHOST, PORT); + return Promise.resolve() .then(() => client.connect()) .then(() => { console.log("client connected"); return client.getDirectory(); }) - .then(() => client.getNodeByPath("scoreMaster/router/labels/group 1")) + .then(() => client.getNodeByPath("scoreMaster/router/labels/group")) .then(child => { - console.log("router/labels", child); - throw new Error("Should not succeed"); + console.log("router/labels", child); + throw new Error("Should not succeed"); + }) + .catch(e => { + client.disconnect(); + console.log(e); + expect(e.message).toMatch(/Failed path discovery/); + }); + }); + it("should be able to make a matrix connection", () => { + client = new DeviceTree(LOCALHOST, PORT); + //client._debug = true; + return Promise.resolve() + .then(() => client.connect()) + .then(() => { + return client.getDirectory(); + }) + .then(() => client.getNodeByPathnum("0.1.0")) + .then(matrix => { + console.log(matrix); + client._debug = true; + server._debug = true; + return client.matrixConnect(matrix, 0, [1]); }) - .catch(e => { - client.disconnect(); - console.log(e); - expect(e.message).toMatch(/timeout/); - }); - }); + .then(matrix => client.getNodeByPathnum(matrix.getPath())) + .then(matrix => { + console.log(matrix); + expect(matrix.connections['0'].sources).toBeDefined(); + expect(matrix.connections['0'].sources.length).toBe(1); + expect(matrix.connections['0'].sources[0]).toBe(1); + return client.disconnect(); + }); + }); }); }); From 3b0808197d559a3556795e151de1a6f1b7b037aa Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Fri, 13 Dec 2019 11:32:18 +0100 Subject: [PATCH 05/64] Add description to label when converting json to tree --- README.md | 2 +- ember.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++++--- package.json | 2 +- server.js | 17 ++++++++++--- test/utils.js | 2 +- 5 files changed, 82 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 7a583c3..9620414 100755 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ const jsonTree = [ targetCount: targets.length, sourceCount: sources.length, connections: buildConnections(sources, targets), - labels: ["0.1.1000"] + labels: [{basePath: "0.1.1000", description: "primary"}] }, { identifier: "labels", diff --git a/ember.js b/ember.js index 9ed41a4..1a99f45 100755 --- a/ember.js +++ b/ember.js @@ -827,6 +827,56 @@ function MatrixNode(number) { this.number = number; } +function canConnect(matrixNode, targetID, sources, operation) { + const type = matrixNode.contents.type == null ? MatrixType.oneToN : matrixNode.contents.type; + const mode = matrixNode.contents.mode == null ? MatrixConnection.linear : matrixNode.contents.mode; + const connection = matrixNode.connections[targetID];; + const oldSources = connection == null || connection.sources == null ? [] : connection.sources.slice(); + const newSources = operation === MatrixOperation.absolute ? sources : oldSources.concat(sources); + const sMap = new Set(newSources.map(i => Number(i))); + if (type === MatrixType.oneToN) { + return sMap.size < 2; + } + else if (type === MatrixType.oneToOne) { + if (sMap.size > 1) { + return false; + } + if (mode === MatrixMode.linear) { + for(let s = 0; s < sources.length; s++) { + for(let i = 0; i < matrixNode.targetCount; i++) { + const connection = matrixNode.connections[i]; + if (connection == null || connection.sources == null) { + continue; + } + for(let j = 0; j < connection.sources.length; j++) { + if (connection.sources[j] === sources[s] && targetID !== i) { + // This source is already connected to another target + return false; + } + } + } + } + } + else { + for(let s = 0; s < sources.length; s++) { + for(let t = 0; t < matrixNode.targets.length; t++) { + const target = matrixNode.targets[t]; + const connection = matrixNode.connections[target]; + if (connection == null || connection.sources == null) { + continue; + } + for(let j = 0; j < connection.sources.length; j++) { + if (connection.sources[j] === sources[s] && targetID !== target) { + // This source is already connected to another target + return false; + } + } + } + } + } + } + return true; +} function validateConnection(matrixNode, targetID, sources) { if (targetID < 0) { @@ -873,7 +923,7 @@ function validateConnection(matrixNode, targetID, sources) { throw new Error(`Unknown source at index ${i}`); } } - } + } } @@ -915,6 +965,10 @@ MatrixNode.prototype.validateConnection = function(targetID, sources) { validateConnection(this, targetID, sources); } +MatrixNode.prototype.canConnect = function(targetID, sources, operation) { + canConnect(this, targetID, sources, operation); +} + MatrixNode.prototype.encode = function(ber) { ber.startSequence(BER.APPLICATION(13)); @@ -1337,10 +1391,13 @@ MatrixConnection.prototype.encode = function(ber) { module.exports.MatrixConnection = MatrixConnection; -function Label(path) { +function Label(path, description) { if (path) { this.basePath = path; } + if (description) { + this.description = description; + } } Label.decode = function(ber) { @@ -1365,12 +1422,12 @@ Label.decode = function(ber) { Label.prototype.encode = function(ber) { ber.startSequence(BER.APPLICATION(18)); - if (this.basePath !== undefined) { + if (this.basePath != null) { ber.startSequence(BER.CONTEXT(0)); ber.writeRelativeOID(this.basePath, BER.EMBER_RELATIVE_OID); ber.endSequence(); } - if (this.description !== undefined) { + if (this.description != null) { ber.startSequence(BER.CONTEXT(1)); ber.writeString(this.description, BER.EMBER_STRING); ber.endSequence(); @@ -1450,6 +1507,10 @@ QualifiedMatrix.prototype.validateConnection = function(targetID, sources) { validateConnection(this, targetID, sources); } +QualifiedMatrix.prototype.canConnect = function(targetID, sources, operation) { + canConnect(this, targetID, sources, operation); +} + QualifiedMatrix.decode = function(ber) { var qm = new QualifiedMatrix(); ber = ber.getSequence(BER.APPLICATION(17)); diff --git a/package.json b/package.json index c0d839d..7812d6c 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.10.0", + "version": "1.10.1", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index ed0e506..322e530 100755 --- a/server.js +++ b/server.js @@ -528,9 +528,20 @@ const parseMatrixContent = function(matrixContent, content) { if (content.labels) { matrixContent.labels = []; for(let l = 0; l < content.labels.length; l++) { - matrixContent.labels.push( - new ember.Label(content.labels[l]) - ); + if (typeof (content.labels[l]) === "object") { + matrixContent.labels.push( + new ember.Label( + content.labels[l].basePath, + content.labels[l].description + ) + ); + } + else { + // for backward compatibility... Remove in the future + matrixContent.labels.push( + new ember.Label(content.labels[l]) + ); + } } delete content.labels; } diff --git a/test/utils.js b/test/utils.js index 103b542..84c3dca 100755 --- a/test/utils.js +++ b/test/utils.js @@ -51,7 +51,7 @@ const init = function(_src,_tgt) { targetCount: targets.length, sourceCount: sources.length, connections: buildConnections(sources, targets), - labels: ["0.1.1000"] + labels: [{basePath: "0.1.1000", description: "primary"}] }, { identifier: "labels", From dfbd5d14273832bca7a3e6410250bd399d384549 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Fri, 13 Dec 2019 16:09:46 +0100 Subject: [PATCH 06/64] Adding validation of connection request on server --- ember.js | 6 ++--- package.json | 2 +- server.js | 56 +++++++++++++++++++++++++++------------------ test/Server.test.js | 44 +++++++++++++++++++++++++++++++++-- 4 files changed, 80 insertions(+), 28 deletions(-) diff --git a/ember.js b/ember.js index 1a99f45..bf512c9 100755 --- a/ember.js +++ b/ember.js @@ -843,7 +843,7 @@ function canConnect(matrixNode, targetID, sources, operation) { } if (mode === MatrixMode.linear) { for(let s = 0; s < sources.length; s++) { - for(let i = 0; i < matrixNode.targetCount; i++) { + for(let i = 0; i < matrixNode.contents.targetCount; i++) { const connection = matrixNode.connections[i]; if (connection == null || connection.sources == null) { continue; @@ -966,7 +966,7 @@ MatrixNode.prototype.validateConnection = function(targetID, sources) { } MatrixNode.prototype.canConnect = function(targetID, sources, operation) { - canConnect(this, targetID, sources, operation); + return canConnect(this, targetID, sources, operation); } MatrixNode.prototype.encode = function(ber) { @@ -1508,7 +1508,7 @@ QualifiedMatrix.prototype.validateConnection = function(targetID, sources) { } QualifiedMatrix.prototype.canConnect = function(targetID, sources, operation) { - canConnect(this, targetID, sources, operation); + return canConnect(this, targetID, sources, operation); } QualifiedMatrix.decode = function(ber) { diff --git a/package.json b/package.json index 7812d6c..19a9981 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.10.1", + "version": "1.10.2", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index 322e530..3ca056c 100755 --- a/server.js +++ b/server.js @@ -226,7 +226,7 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti if (this._debug) { console.log("Handling Matrix Connection"); } - if (client.request.isQualified()) { + if (client != null && client.request.isQualified()) { root = new ember.Root(); res = new ember.QualifiedMatrix(matrix.getPath()); root.elements = [res]; // do not use addchild or the element will get removed from the tree. @@ -246,27 +246,37 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti res.connections[connection.target] = conResult; - // Apply changes - - if ((connection.operation === undefined) || - (connection.operation.value == ember.MatrixOperation.absolute)) { - matrix.connections[connection.target].setSources(connection.sources); - emitType = "matrix-change"; - } - else if (connection.operation == ember.MatrixOperation.connect) { - matrix.connections[connection.target].connectSources(connection.sources); - emitType = "matrix-connect"; - } - else { // Disconnect + if (((connection.operation === undefined) || + (connection.operation === ember.MatrixOperation.connect)) && + (matrix.canConnect(connection.target,connection.sources,connection.operation))) { + // Apply changes + if ((connection.operation === undefined) || + (connection.operation.value == ember.MatrixOperation.absolute)) { + matrix.connections[connection.target].setSources(connection.sources); + emitType = "matrix-change"; + } + else if (connection.operation == ember.MatrixOperation.connect) { + matrix.connections[connection.target].connectSources(connection.sources); + emitType = "matrix-connect"; + } + conResult.disposition = ember.MatrixDisposition.modified; + } + else if (connection.operarion === ember.MatrixOperation.disconnect) { // Disconnect matrix.connections[connection.target].disconnectSources(connection.sources); + conResult.disposition = ember.MatrixDisposition.modified; emitType = "matrix-disconnect"; } + else { + if (this._debug) { + console.log(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); + } + conResult.disposition = ember.MatrixDisposition.tally; + return; + } // Send response or update subscribers. - - if (response) { - conResult.sources = matrix.connections[connection.target].sources; - conResult.disposition = ember.MatrixDisposition.modified; + conResult.sources = matrix.connections[connection.target].sources; + if (response) { // We got a request so emit something. this.emit(emitType, { target: connection.target, @@ -277,17 +287,19 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti else { // the action has been applied. So we should either send the current state (absolute) // or send the action itself (connection.sources) - conResult.sources = matrix.connections[connection.target].sources; conResult.operation = ember.MatrixOperation.absolute; } } if (client !== undefined) { client.sendBERNode(root); } - if (this._debug) { - console.log("Updating subscribers for matrix change"); - } - this.updateSubscribers(matrix.getPath(), root, client); + + if (conResult.disposition !== ember.MatrixDisposition.tally) { + if (this._debug) { + console.log("Updating subscribers for matrix change"); + } + this.updateSubscribers(matrix.getPath(), root, client); + } } const validateMatrixOperation = function(matrix, target, sources) { diff --git a/test/Server.test.js b/test/Server.test.js index 4a63399..49570d1 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -14,7 +14,6 @@ const wait = function(t) { } describe("server", function() { - describe("JSONtoTree", function() { let jsonTree; beforeAll(function() { @@ -51,7 +50,7 @@ describe("server", function() { afterAll(function() { client.disconnect(); server.close(); - }) + }); it("should receive and decode the full tree", function () { client = new DeviceTree(LOCALHOST, PORT); //client._debug = true; @@ -231,4 +230,45 @@ describe("server", function() { }); }); }); + describe("Matrix Connect", function() { + let jsonTree; + let server; + beforeEach(function() { + jsonTree = jsonRoot(); + const root = TreeServer.JSONtoTree(jsonTree); + server = new TreeServer(LOCALHOST, PORT, root); + }); + it("should verify if connection allowed in 1-to-N", function() { + const matrix = server.tree.elements[0].children[1].children[0]; + const connection = new ember.MatrixConnection(0); + connection.setSources([1]); + connection.operation = ember.MatrixOperation.connect; + let res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + matrix.connections[0].sources = [0]; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + connection.operation = ember.MatrixOperation.absolute; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + }); + it("should verify if connection allowed in 1-to-1", function() { + const matrix = server.tree.elements[0].children[1].children[0]; + matrix.contents.type = ember.MatrixType.oneToOne; + const connection = new ember.MatrixConnection(0); + connection.setSources([1]); + connection.operation = ember.MatrixOperation.connect; + let res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + matrix.connections[0].sources = [0]; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + connection.operation = ember.MatrixOperation.absolute; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + matrix.connections[1].sources = [1]; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + }); + }); }); From 22fa47cc82f902e6af1dc1770f4d9f2ddc613d6d Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Fri, 13 Dec 2019 16:18:02 +0100 Subject: [PATCH 07/64] fix contributors --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 19a9981..d911c42 100755 --- a/package.json +++ b/package.json @@ -8,9 +8,10 @@ "eslint": "eslint ./", "start": "node server.js" }, - "author": "Brian Mayton (http://bdm.cc)", + "author": "Gilles Dufour (www.gdnet.be)", "contributors": [ - "Gilles Dufour (www.gdnet.be)" + "Gilles Dufour (www.gdnet.be)", + "Brian Mayton (http://bdm.cc)", ], "repository": { "type": "git", From 5a830dee324e113a0b4a83258bdaac1cb816784d Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Fri, 13 Dec 2019 16:18:38 +0100 Subject: [PATCH 08/64] fix contributors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d911c42..519fc1d 100755 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "author": "Gilles Dufour (www.gdnet.be)", "contributors": [ "Gilles Dufour (www.gdnet.be)", - "Brian Mayton (http://bdm.cc)", + "Brian Mayton (http://bdm.cc)" ], "repository": { "type": "git", From 876ed096d8697104b4060c918939bccf832d0658 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sat, 14 Dec 2019 12:08:34 +0100 Subject: [PATCH 09/64] Fixing getDirectory when root has more than 1 elements --- device.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/device.js b/device.js index c5e9272..7265ae6 100755 --- a/device.js +++ b/device.js @@ -141,7 +141,7 @@ DeviceTree.prototype.expand = function (node) { }; function isDirectSubPathOf(path, parent) { - return path.lastIndexOf('.') === parent.length && path.startsWith(parent) + return path === parent || (path.lastIndexOf('.') === parent.length && path.startsWith(parent)); } DeviceTree.prototype.getDirectory = function (qnode) { diff --git a/package.json b/package.json index 519fc1d..4cfe896 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.10.2", + "version": "1.10.3", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { From 42d351c52e8eabe75a0b4e2a1d7c367f3e297239 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sat, 14 Dec 2019 16:18:36 +0100 Subject: [PATCH 10/64] Added support for subscribe and unsubscribe of streams --- device.js | 42 +++++++++++++---- ember.js | 38 ++++++++++++++- package.json | 2 +- server.js | 12 +++-- test/Server.test.js | 111 ++++++++++++++++++++++++++++++++++++++++---- test/utils.js | 2 +- 6 files changed, 183 insertions(+), 24 deletions(-) diff --git a/device.js b/device.js index 7265ae6..2d02c72 100755 --- a/device.js +++ b/device.js @@ -563,19 +563,45 @@ DeviceTree.prototype.getNodeByPath = function (path) { return getNext(); }; -DeviceTree.prototype.subscribe = function (node, callback) { - if (node instanceof ember.Parameter && node.isStream()) { - // TODO: implement +DeviceTree.prototype.subscribe = function (qnode, callback) { + if (qnode.isParameter() && qnode.isStream()) { + var self = this; + if (qnode == null) { + self.root.clear(); + qnode = self.root; + } + return new Promise((resolve, reject) => { + self.addRequest({node: qnode, func: (error) => { + if (self._debug) { + console.log("Sending subscribe", qnode); + } + self.client.sendBERNode(qnode.subscribe(callback)); + self.finishRequest(); + resolve(); + }}); + }); } else { node.addCallback(callback); } }; -DeviceTree.prototype.unsubscribe = function (node, callback) { - if (node instanceof ember.Parameter && node.isStream()) { - // TODO: implement - } else { - node.addCallback(callback); +DeviceTree.prototype.unsubscribe = function (qnode, callback) { + if (qnode.isParameter() && qnode.isStream()) { + var self = this; + if (qnode == null) { + self.root.clear(); + qnode = self.root; + } + return new Promise((resolve, reject) => { + self.addRequest({node: qnode, func: (error) => { + if (self._debug) { + console.log("Sending subscribe", qnode); + } + self.client.sendBERNode(qnode.unsubscribe(callback)); + self.finishRequest(); + resolve(); + }}); + }); } }; diff --git a/ember.js b/ember.js index bf512c9..a134f3c 100755 --- a/ember.js +++ b/ember.js @@ -185,8 +185,8 @@ TreeNode.prototype.isQualified = function() { } TreeNode.prototype.isStream = function() { - return this.contents !== undefined && - this.contents.streamDescriptor !== undefined; + return this.contents != null && + this.contents.streamIdentifier != null; } TreeNode.prototype.addCallback = function(callback) { @@ -273,6 +273,9 @@ TreeNode.prototype.subscribe = function(callback) { if(callback !== undefined) { this._directoryCallbacks.push((error, node) => { callback(error, node) }); } + if (this.isParameter() && this.isStream()) { + this.contents.subscribers.add(callback); + } return this.getTreeBranch(new Command(COMMAND_SUBSCRIBE)); } @@ -280,6 +283,9 @@ TreeNode.prototype.unsubscribe = function(callback) { if(callback !== undefined) { this._directoryCallbacks.push((error, node) => { callback(error, node) }); } + if (this.isParameter() && this.isStream()) { + this.contents.subscribers.delete(callback); + } return this.getTreeBranch(new Command(COMMAND_UNSUBSCRIBE)); } @@ -680,6 +686,9 @@ QualifiedNode.prototype.subscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } + if (this.isStream()) { + this.contents.subscribers.add(callback); + } return QualifiedNodeCommand(this, COMMAND_SUBSCRIBE, callback) } @@ -687,6 +696,9 @@ QualifiedNode.prototype.unsubscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } + if (this.isStream()) { + this.contents.subscribers.delete(callback); + } return QualifiedNodeCommand(this, COMMAND_UNSUBSCRIBE, callback) } @@ -813,6 +825,9 @@ Node.prototype.subscribe = function(callback) { if(this._callbacks.indexOf(callback) < 0) { this._callbacks.push(callback); } + if (this.isStream()) { + this.contents.subscribers.add(callback); + } } module.exports.Node = Node; @@ -1624,6 +1639,9 @@ QualifiedMatrix.prototype.subscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } + if (this.isStream()) { + this.contents.subscribers.add(callback); + } return QualifiedMatrixCommand(this, COMMAND_SUBSCRIBE, callback); } @@ -1631,6 +1649,9 @@ QualifiedMatrix.prototype.unsubscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } + if (this.isStream()) { + this.contents.subscribers.delete(callback); + } return QualifiedMatrixCommand(this, COMMAND_UNSUBSCRIBE, callback); } @@ -2519,6 +2540,9 @@ QualifiedParameter.prototype.update = function(other) { this.contents[key] = other.contents[key]; } } + for(let cb of this.contents.subscribers) { + cb(); + } } } return callbacks; @@ -2550,6 +2574,9 @@ QualifiedParameter.prototype.subscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } + if (this.isStream()) { + this.contents.subscribers.add(callback); + } return QualifiedParameterCommand(this, COMMAND_SUBSCRIBE, callback); } @@ -2557,6 +2584,9 @@ QualifiedParameter.prototype.unsubscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } + if (this.isStream()) { + this.contents.subscribers.delete(callback); + } return QualifiedParameterCommand(this, COMMAND_UNSUBSCRIBE, callback); } @@ -2665,6 +2695,9 @@ Parameter.prototype.update = function(other) { if (other.contents.hasOwnProperty(key)) { this.contents[key] = other.contents[key]; } + } + for(let cb of this.contents.subscribers) { + cb(); } } } @@ -2730,6 +2763,7 @@ module.exports.ParameterAccess = ParameterAccess; module.exports.ParameterType = ParameterType; function ParameterContents(value, type) { + this.subscribers = new Set(); if(value !== undefined) { this.value = value; } diff --git a/package.json b/package.json index 4cfe896..11ead78 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.10.3", + "version": "1.10.4", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index 3ca056c..056d830 100755 --- a/server.js +++ b/server.js @@ -221,7 +221,7 @@ TreeServer.prototype.handleQualifiedParameter = function(client, element, parame TreeServer.prototype.handleMatrixConnections = function(client, matrix, connections, response = true) { - var res; + var res,conResult; var root; // ember message root if (this._debug) { console.log("Handling Matrix Connection"); @@ -241,7 +241,7 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti continue; } let connection = connections[id]; - let conResult = new ember.MatrixConnection(connection.target); + conResult = new ember.MatrixConnection(connection.target); let emitType; res.connections[connection.target] = conResult; @@ -294,7 +294,7 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti client.sendBERNode(root); } - if (conResult.disposition !== ember.MatrixDisposition.tally) { + if (conResult != null && conResult.disposition !== ember.MatrixDisposition.tally) { if (this._debug) { console.log("Updating subscribers for matrix change"); } @@ -451,10 +451,16 @@ TreeServer.prototype.handleGetDirectory = function(client, element) { } TreeServer.prototype.handleSubscribe = function(client, element) { + if (this._debug) { + console.log("subscribe"); + } this.subscribe(client, element); } TreeServer.prototype.handleUnSubscribe = function(client, element) { + if (this._debug) { + console.log("unsubscribe"); + } this.unsubscribe(client, element); } diff --git a/test/Server.test.js b/test/Server.test.js index 49570d1..2c0a0e5 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -48,8 +48,7 @@ describe("server", function() { }); }); afterAll(function() { - client.disconnect(); - server.close(); + return server.close(); }); it("should receive and decode the full tree", function () { client = new DeviceTree(LOCALHOST, PORT); @@ -154,14 +153,14 @@ describe("server", function() { client.root.getNodeByPath(client.client, ["scoreMaster", "router", "labels"], (err, child) => { if (err) { reject(err) } else { - resolve(child); + resolve(child); } }); }); }) .then(child => { console.log(child); - client.disconnect(); + return client.disconnect(); }); }); it("should be able to get child with tree.getNodeByPath", function() { @@ -182,7 +181,7 @@ describe("server", function() { }) .then(child => { console.log("router/labels", child); - client.disconnect(); + return client.disconnect(); }); }); it("should throw an error if getNodeByPath for unknown path", function() { @@ -200,9 +199,9 @@ describe("server", function() { throw new Error("Should not succeed"); }) .catch(e => { - client.disconnect(); console.log(e); expect(e.message).toMatch(/Failed path discovery/); + return client.disconnect(); }); }); it("should be able to make a matrix connection", () => { @@ -229,7 +228,7 @@ describe("server", function() { return client.disconnect(); }); }); - }); + }); describe("Matrix Connect", function() { let jsonTree; let server; @@ -240,7 +239,7 @@ describe("server", function() { }); it("should verify if connection allowed in 1-to-N", function() { const matrix = server.tree.elements[0].children[1].children[0]; - const connection = new ember.MatrixConnection(0); + let connection = new ember.MatrixConnection(0); connection.setSources([1]); connection.operation = ember.MatrixOperation.connect; let res = matrix.canConnect(connection.target,connection.sources,connection.operation); @@ -251,6 +250,11 @@ describe("server", function() { connection.operation = ember.MatrixOperation.absolute; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); + connection = new ember.MatrixConnection(1); + connection.operation = ember.MatrixOperation.absolute; + connection.setSources([1]); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); }); it("should verify if connection allowed in 1-to-1", function() { const matrix = server.tree.elements[0].children[1].children[0]; @@ -266,9 +270,98 @@ describe("server", function() { connection.operation = ember.MatrixOperation.absolute; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); + matrix.connections[0].sources = []; matrix.connections[1].sources = [1]; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeFalsy(); }); - }); + }); + describe("Parameters subscribe/unsubscribe", function( ){ + let jsonTree; + let server; + beforeAll(function() { + jsonTree = jsonRoot(); + const root = TreeServer.JSONtoTree(jsonTree); + server = new TreeServer(LOCALHOST, PORT, root); + server.on("error", e => { + console.log(e); + }); + server.on("clientError", e => { + console.log(e); + }); + return server.listen(); + }); + afterAll(function() { + return server.close(); + }); + it("should not auto subscribe stream parameter", function() { + const parameter = server.tree.elements[0].children[0].children[2]; + console.log(parameter); + expect(parameter.isStream()).toBeTruthy(); + expect(server.subscribers["0.0.2"]).not.toBeDefined(); + }); + it("should be able subscribe to parameter changes", function() { + const client = new DeviceTree(LOCALHOST, PORT); + const cb = () => { + return "updated"; + } + //client._debug = true; + return Promise.resolve() + .then(() => client.connect()) + .then(() => { + return client.getDirectory(); + }) + .then(() => client.getNodeByPathnum("0.0.2")) + .then(parameter => { + console.log(parameter); + expect(server.subscribers["0.0.2"]).not.toBeDefined(); + expect(parameter.contents.subscribers).toBeDefined(); + expect(parameter.contents.subscribers.size).toBe(0); + server._subscribe = server.subscribe; + let _resolve; + const p = new Promise((resolve, reject) => { + _resolve = resolve; + }); + server.subscribe = (c,e) => { + server._subscribe(c,e); + _resolve(); + }; + return client.subscribe(parameter, cb).then(() => (p)) + }) + .then(() => { + expect(server.subscribers["0.0.2"]).toBeDefined(); + expect(server.subscribers["0.0.2"].size).toBe(1); + return client.getNodeByPathnum("0.0.2"); + }) + .then(parameter => { + expect(parameter.contents.subscribers).toBeDefined(); + expect(parameter.contents.subscribers.size).toBe(1); + server._unsubscribe = server.unsubscribe; + let _resolve; + const p = new Promise((resolve, reject) => { + _resolve = resolve; + }); + server.unsubscribe = (c,e) => { + console.log("unsubscribe"); + server._unsubscribe(c,e); + _resolve(); + }; + console.log(parameter); + return client.unsubscribe(parameter, cb).then(() => (p)) + }) + .then(() => { + console.log(server.subscribers); + expect(server.subscribers["0.0.2"]).toBeDefined(); + return client.getNodeByPathnum("0.0.2"); + }) + .then(parameter => { + console.log(parameter); + expect(server.subscribers["0.0.2"]).toBeDefined(); + expect(server.subscribers["0.0.2"].size).toBe(0); + expect(parameter.contents.subscribers).toBeDefined(); + expect(parameter.contents.subscribers.size).toBe(0); + }) + .then(() => client.disconnect()); + }); + }); }); diff --git a/test/utils.js b/test/utils.js index 84c3dca..ad7a396 100755 --- a/test/utils.js +++ b/test/utils.js @@ -35,7 +35,7 @@ const init = function(_src,_tgt) { children: [ {identifier: "product", value: "S-CORE Master"}, {identifier: "company", value: "EVS", access: "readWrite"}, - {identifier: "version", value: "1.2.0"}, + {identifier: "version", value: "1.2.0", streamIdentifier: 1234567}, {identifier: "author", value: "g.dufour@evs.com"}, ] }, From 58bc2d856956800032301f9eefea76f36ddd8a86 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sat, 14 Dec 2019 16:42:42 +0100 Subject: [PATCH 11/64] fixing matrix absolute connect --- package.json | 2 +- server.js | 1 + test/Server.test.js | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 11ead78..c128d03 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.10.4", + "version": "1.10.5", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index 056d830..58ee1c8 100755 --- a/server.js +++ b/server.js @@ -247,6 +247,7 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti if (((connection.operation === undefined) || + (connection.operation === ember.MatrixOperation.absolute) || (connection.operation === ember.MatrixOperation.connect)) && (matrix.canConnect(connection.target,connection.sources,connection.operation))) { // Apply changes diff --git a/test/Server.test.js b/test/Server.test.js index 2c0a0e5..5a5f1b8 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -275,6 +275,40 @@ describe("server", function() { res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeFalsy(); }); + it("should return modified answer on absolute connect", function() { + let client; + server.on("error", e => { + console.log(e); + }); + server.on("clientError", e => { + console.log(e); + }); + //server._debug = true; + return server.listen() + .then(() => { + client = new DeviceTree(LOCALHOST, PORT); + return Promise.resolve() + }) + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => client.getNodeByPathnum("0.1.0")) + .then(matrix => { + console.log(matrix); + return client.matrixSetConnection(matrix, 0, [1]); + }) + .then(result => { + console.log(result); + expect(result).toBeDefined(); + expect(result.connections).toBeDefined(); + expect(result.connections[0]).toBeDefined(); + expect(result.connections[0].disposition).toBe(ember.MatrixDisposition.modified); + return client.disconnect(); + }) + .then(() => { + console.log("closing server"); + server.close(); + }); + }); }); describe("Parameters subscribe/unsubscribe", function( ){ let jsonTree; From fd31d8d0ebf5edd2895fdec7a515f46002bbeca1 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 16 Dec 2019 10:45:18 +0100 Subject: [PATCH 12/64] Better error when doing getNodeByPath --- README.md | 8 ++++---- device.js | 6 ++++-- package.json | 2 +- test/utils.js | 9 +++++---- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9620414..ca6ec73 100755 --- a/README.md +++ b/README.md @@ -136,11 +136,11 @@ const {ParameterType, FunctionArgument} = require("emberplus").Ember; const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; const sources = _src === undefined ? [ "src1", "src2", "src3" ] : _src; -const labels = function(endpoints) { +const labels = function(endpoints, type) { let labels = []; for (let i = 0; i < endpoints.length; i++) { let endpoint = endpoints[i]; - let l = { identifier: `Label-${i}` }; + let l = { identifier: `${type}-${i}` }; if (endpoint) { l.value = endpoint; } @@ -194,13 +194,13 @@ const jsonTree = [ identifier: "targets", // Must be 1 number: 1, - children: labels(targets) + children: labels(targets, "t") }, { identifier: "sources", // Must be 2 number: 2, - children: labels(sources) + children: labels(sources, "s") }, { identifier: "group 1", diff --git a/device.js b/device.js index 2d02c72..5233eee 100755 --- a/device.js +++ b/device.js @@ -492,6 +492,7 @@ DeviceTree.prototype.getNodeByPathnum = function (path) { if (typeof path === 'string') { path = path.split('.'); } + var pathnumError = new Error(`Failed path discovery at ${path.slice(0, pos).join("/")}`); var pos = 0; var lastMissingPos = -1; var currentNode = this.root; @@ -516,7 +517,7 @@ DeviceTree.prototype.getNodeByPathnum = function (path) { } // We do not have that node yet. if (lastMissingPos === pos) { - throw new Error(`Failed path discovery at ${path.slice(0, pos).join("/")}`); + throw pathnumError; } lastMissingPos = pos; return this.getDirectory(currentNode).then(() => getNext()); @@ -530,6 +531,7 @@ DeviceTree.prototype.getNodeByPath = function (path) { if (typeof path === 'string') { path = path.split('/'); } + var pathError = new Error(`Failed path discovery at ${path.slice(0, pos + 1).join("/")}`); var pos = 0; var lastMissingPos = -1; var currentNode = this.root; @@ -554,7 +556,7 @@ DeviceTree.prototype.getNodeByPath = function (path) { } // We do not have that node yet. if (lastMissingPos === pos) { - throw new Error(`Failed path discovery at ${path.slice(0, pos + 1).join("/")}`); + throw pathError; } lastMissingPos = pos; return this.getDirectory(currentNode).then(() => getNext()); diff --git a/package.json b/package.json index c128d03..ebf91b8 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.10.5", + "version": "1.10.6", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/test/utils.js b/test/utils.js index ad7a396..b0ae7c0 100755 --- a/test/utils.js +++ b/test/utils.js @@ -1,13 +1,14 @@ const {ParameterType, FunctionArgument} = require("../ember"); + const init = function(_src,_tgt) { const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; const sources = _src === undefined ? [ "src1", "src2", "src3" ] : _src; - const labels = function(endpoints) { + const labels = function(endpoints, type) { let labels = []; for (let i = 0; i < endpoints.length; i++) { let endpoint = endpoints[i]; - let l = { identifier: `Label-${i}` }; + let l = { identifier: `${type}-${i}` }; if (endpoint) { l.value = endpoint; } @@ -62,13 +63,13 @@ const init = function(_src,_tgt) { identifier: "targets", // Must be 1 number: 1, - children: labels(targets) + children: labels(targets, "t") }, { identifier: "sources", // Must be 2 number: 2, - children: labels(sources) + children: labels(sources, "s") }, { identifier: "group 1", From 88594780ecb22818b0a6b47de4ee97b1d1156e40 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Wed, 18 Dec 2019 17:46:23 +0100 Subject: [PATCH 13/64] handle connection for 1-to-1 matrix --- ember.js | 135 ++++++++++++++++++++++++++++++++++---------- package.json | 2 +- server.js | 67 +++++++++++++--------- test/Server.test.js | 61 +++++++++++++++++++- 4 files changed, 203 insertions(+), 62 deletions(-) diff --git a/ember.js b/ember.js index a134f3c..510d598 100755 --- a/ember.js +++ b/ember.js @@ -838,8 +838,11 @@ module.exports.Node = Node; function MatrixNode(number) { MatrixNode.super_.call(this); - if(number !== undefined) + if(number !== undefined) { this.number = number; + } + this._connectedSources = {}; + this._numConnections = 0; } function canConnect(matrixNode, targetID, sources, operation) { @@ -856,39 +859,26 @@ function canConnect(matrixNode, targetID, sources, operation) { if (sMap.size > 1) { return false; } - if (mode === MatrixMode.linear) { - for(let s = 0; s < sources.length; s++) { - for(let i = 0; i < matrixNode.contents.targetCount; i++) { - const connection = matrixNode.connections[i]; - if (connection == null || connection.sources == null) { - continue; - } - for(let j = 0; j < connection.sources.length; j++) { - if (connection.sources[j] === sources[s] && targetID !== i) { - // This source is already connected to another target - return false; - } - } - } - } + const sourceConnections = matrixNode._connectedSources[sources[0]]; + return sourceConnections == null || sourceConnections.size === 0 || sourceConnections.has(targetID); + } + else { + // N to N + if (matrixNode.contents.maximumConnectsPerTarget != null && + newSources.length > matrixNode.contents.maximumConnectsPerTarget) { + return false; } - else { - for(let s = 0; s < sources.length; s++) { - for(let t = 0; t < matrixNode.targets.length; t++) { - const target = matrixNode.targets[t]; - const connection = matrixNode.connections[target]; - if (connection == null || connection.sources == null) { - continue; - } - for(let j = 0; j < connection.sources.length; j++) { - if (connection.sources[j] === sources[s] && targetID !== target) { - // This source is already connected to another target - return false; - } - } - } + if (matrixNode.contents.maximumTotalConnects != null) { + let count = matrixNode._numConnections; + if (oldSources) { + count -= oldSources.length; } + if (newSources) { + count += newSources.length; + } + return count <= matrixNode.contents.maximumTotalConnects; } + } return true; } @@ -1063,6 +1053,74 @@ MatrixNode.prototype.encode = function(ber) { ber.endSequence(); // BER.APPLICATION(3) } +MatrixNode.prototype.setSources = function(targetID, sources) { + return MatrixNode.setSources(this, targetID, sources); +} +MatrixNode.prototype.connectSources = function(targetID, sources) { + return MatrixNode.connectSources(this, targetID, sources); +} +MatrixNode.prototype.disconnectSources = function(targetID, sources) { + return MatrixNode.disconnectSources(this, targetID, sources); +} +MatrixNode.prototype.getSourceConnections = function(source) { + return MatrixNode.getSourceConnections(this, source); +} + +MatrixNode.getSourceConnections = function(matrix, source) { + const targets = matrix._connectedSources[source]; + if (targets) { + return [...targets]; + } + return []; +} + +MatrixNode.setSources = function(matrix, targetID, sources) { + const currentSource = matrix.connections[targetID] == null || matrix.connections[targetID].sources == null ? + [] : matrix.connections[targetID].sources; + if (currentSource.length > 0) { + MatrixNode.disconnectSources(matrix, targetID, currentSource) + } + MatrixNode.connectSources(matrix, targetID, sources); +} + +MatrixNode.connectSources = function(matrix, targetID, sources) { + const target = Number(targetID); + if (matrix.connections[target] == null) { + matrix.connections[target] = new MatrixConnection(target); + } + matrix.connections[target].connectSources(sources); + if (sources != null) { + for(let source of sources) { + if (matrix._connectedSources[source] == null) { + matrix._connectedSources[source] = new Set(); + } + if (!matrix._connectedSources[source].has(target)) { + matrix._connectedSources[source].add(target); + matrix._numConnections++; + } + } + } +} + +MatrixNode.disconnectSources = function(matrix, targetID, sources) { + const target = Number(targetID); + if (matrix.connections[target] == null) { + matrix.connections[target] = new MatrixConnection(target); + } + matrix.connections[target].disconnectSources(sources); + if (sources != null) { + for(let source of sources) { + if (matrix._connectedSources[source] == null) { + continue; + } + if (matrix._connectedSources[source].has(target)) { + matrix._connectedSources[source].delete(target); + matrix._numConnections--; + } + } + } +} + MatrixNode.prototype.update = function(other) { callbacks = MatrixNode.super_.prototype.update.apply(this); MatrixUpdate(this, other); @@ -1494,6 +1552,8 @@ function QualifiedMatrix(path) { if (path != undefined) { this.path = path; } + this._connectedSources = {}; + this._numConnections = 0; } util.inherits(QualifiedMatrix, TreeNode); @@ -1526,6 +1586,19 @@ QualifiedMatrix.prototype.canConnect = function(targetID, sources, operation) { return canConnect(this, targetID, sources, operation); } +QualifiedMatrix.prototype.setSources = function(targetID, sources) { + return MatrixNode.setSources(this, targetID, sources); +} +QualifiedMatrix.prototype.connectSources = function(targetID, sources) { + return MatrixNode.connectSources(this, targetID, sources); +} +QualifiedMatrix.prototype.disconnectSources = function(targetID, sources) { + return MatrixNode.disconnectSources(this, targetID, sources); +} +QualifiedMatrix.prototype.getSourceConnections = function(source) { + return MatrixNode.getSourceConnections(this, source); +} + QualifiedMatrix.decode = function(ber) { var qm = new QualifiedMatrix(); ber = ber.getSequence(BER.APPLICATION(17)); diff --git a/package.json b/package.json index ebf91b8..f234479 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.10.6", + "version": "1.11.0", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index 58ee1c8..e36390d 100755 --- a/server.js +++ b/server.js @@ -70,7 +70,7 @@ util.inherits(TreeServer, EventEmitter); TreeServer.prototype.listen = function() { return new Promise((resolve, reject) => { this.callback = (e) => { - if (e === undefined) { + if (e == null) { return resolve(); } return reject(e); @@ -82,7 +82,7 @@ TreeServer.prototype.listen = function() { TreeServer.prototype.close = function () { return new Promise((resolve, reject) => { this.callback = (e) => { - if (e === undefined) { + if (e == null) { return resolve(); } return reject(e); @@ -92,7 +92,7 @@ TreeServer.prototype.close = function () { }; TreeServer.prototype.handleRoot = function(client, root) { - if ((root === undefined) || (root.elements === undefined) || (root.elements < 1)) { + if ((root == null) || (root.elements == null) || (root.elements < 1)) { this.emit("error", new Error("invalid request")); return; } @@ -153,7 +153,7 @@ TreeServer.prototype.handleNode = function(client, node) { let element = node; let path = []; while(element !== undefined) { - if (element.number === undefined) { + if (element.number == null) { this.emit("error", "invalid request"); return; } @@ -170,7 +170,7 @@ TreeServer.prototype.handleNode = function(client, node) { } let cmd = element; - if (cmd === undefined) { + if (cmd == null) { this.emit("error", "invalid request"); return this.handleError(client); } @@ -245,25 +245,40 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti let emitType; res.connections[connection.target] = conResult; - - if (((connection.operation === undefined) || - (connection.operation === ember.MatrixOperation.absolute) || - (connection.operation === ember.MatrixOperation.connect)) && - (matrix.canConnect(connection.target,connection.sources,connection.operation))) { + if (matrix.contents.type === ember.MatrixType.oneToOne && + connection.operation !== ember.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length === 1) { + // if the source is being used already, disconnect it. + const targets = matrix.getSourceConnections(connection.sources[0]); + if (targets.length === 1) { + const disconnect = new ember.MatrixConnection(targets[0]); + disconnect.setSources([]); + disconnect.disposition = ember.MatrixDisposition.modified; + res.connections[targets[0]] = disconnect; + matrix.setSources(targets[0], []); + } + // if the target is connected already, disconnect it + if (matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length === 1) { + matrix.setSources(connection.target, []); + } + } + if (connection.operation !== ember.MatrixOperation.disconnect && + matrix.canConnect(connection.target,connection.sources,connection.operation)) { // Apply changes - if ((connection.operation === undefined) || + if ((connection.operation == null) || (connection.operation.value == ember.MatrixOperation.absolute)) { - matrix.connections[connection.target].setSources(connection.sources); + matrix.setSources(connection.target, connection.sources); emitType = "matrix-change"; } else if (connection.operation == ember.MatrixOperation.connect) { - matrix.connections[connection.target].connectSources(connection.sources); + matrix.connectSources(connection.target, connection.sources); emitType = "matrix-connect"; } conResult.disposition = ember.MatrixDisposition.modified; } else if (connection.operarion === ember.MatrixOperation.disconnect) { // Disconnect - matrix.connections[connection.target].disconnectSources(connection.sources); + matrix.disconnectSources(connection.target, connection.sources); conResult.disposition = ember.MatrixDisposition.modified; emitType = "matrix-disconnect"; } @@ -291,7 +306,7 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti conResult.operation = ember.MatrixOperation.absolute; } } - if (client !== undefined) { + if (client != null) { client.sendBERNode(root); } @@ -304,19 +319,19 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti } const validateMatrixOperation = function(matrix, target, sources) { - if (matrix === undefined) { + if (matrix == null) { throw new Error(`matrix not found with path ${path}`); } - if (matrix.contents === undefined) { + if (matrix.contents == null) { throw new Error(`invalid matrix at ${path} : no contents`); } - if (matrix.contents.targetCount === undefined) { + if (matrix.contents.targetCount == null) { throw new Error(`invalid matrix at ${path} : no targetCount`); } if ((target < 0) || (target >= matrix.contents.targetCount)) { throw new Error(`target id ${target} out of range 0 - ${matrix.contents.targetCount}`); } - if (sources.length === undefined) { + if (sources.length == null) { throw new Error("invalid sources format"); } } @@ -468,7 +483,7 @@ TreeServer.prototype.handleUnSubscribe = function(client, element) { TreeServer.prototype.subscribe = function(client, element) { const path = element.getPath(); - if (this.subscribers[path] === undefined) { + if (this.subscribers[path] == null) { this.subscribers[path] = new Set(); } this.subscribers[path].add(client); @@ -476,7 +491,7 @@ TreeServer.prototype.subscribe = function(client, element) { TreeServer.prototype.unsubscribe = function(client, element) { const path = element.getPath(); - if (this.subscribers[path] === undefined) { + if (this.subscribers[path] == null) { return; } this.subscribers[path].delete(client); @@ -506,7 +521,7 @@ TreeServer.prototype.setValue = function(element, value, origin, key) { TreeServer.prototype.replaceElement = function(element) { let path = element.getPath(); let parent = this.tree.getElementByPath(path); - if ((parent === undefined)||(parent._parent === undefined)) { + if ((parent == null)||(parent._parent == null)) { throw new Error(`Could not find element at path ${path}`); } parent = parent._parent; @@ -525,7 +540,7 @@ TreeServer.prototype.replaceElement = function(element) { TreeServer.prototype.updateSubscribers = function(path, response, origin) { - if (this.subscribers[path] === undefined) { + if (this.subscribers[path] == null) { return; } @@ -640,10 +655,8 @@ const parseObj = function(parent, obj) { if (! content.connections.hasOwnProperty(c)) { continue; } - let t = content.connections[c].target !== undefined ? content.connections[c].target : 0; - let connection = new ember.MatrixConnection(t); - connection.setSources(content.connections[c].sources); - emberElement.connections[t] = connection; + const t = content.connections[c].target != null ? content.connections[c].target : 0; + emberElement.setSources(t, content.connections[c].sources); } delete content.connections; } diff --git a/test/Server.test.js b/test/Server.test.js index 5a5f1b8..61c45d8 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -264,16 +264,71 @@ describe("server", function() { connection.operation = ember.MatrixOperation.connect; let res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); - matrix.connections[0].sources = [0]; + matrix.setSources(0, [0]); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + // We can't connect but in 1-on-1 server should disconnect existing source and connect new one. + server.handleMatrixConnections(null, matrix, {0: connection}, false); + expect(matrix.connections[0].sources[0]).toBe(1); + + connection.operation = ember.MatrixOperation.absolute; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + matrix.setSources(0, []); + matrix.setSources(1, [1]); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + }); + it("should verify if connection allowed in N-to-N", function() { + const matrix = server.tree.elements[0].children[1].children[0]; + matrix.contents.type = ember.MatrixType.nToN; + matrix.contents.maximumTotalConnects = 2; + matrix.setSources(0, [0,1]); + + const connection = new ember.MatrixConnection(0); + connection.setSources([2]); + connection.operation = ember.MatrixOperation.connect; + let res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + + matrix.setSources(2, [2]); + matrix.setSources(1, [1]); + matrix.setSources(0, []); res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeFalsy(); + + matrix.setSources(1, []); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + + matrix.setSources(0, [1,2]); + matrix.setSources(1, []); connection.operation = ember.MatrixOperation.absolute; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); - matrix.connections[0].sources = []; - matrix.connections[1].sources = [1]; + + + matrix.contents.maximumTotalConnects = 20; + matrix.contents.maximumConnectsPerTarget = 1; + + matrix.setSources(2, [2]); + matrix.setSources(1, [1]); + matrix.setSources(0, [0]); + connection.setSources([2]); + connection.operation = ember.MatrixOperation.connect; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeFalsy(); + + matrix.setSources(0, []); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + + matrix.setSources(0, [0]); + connection.operation = ember.MatrixOperation.absolute; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + }); it("should return modified answer on absolute connect", function() { let client; From 4df675c69932f46fb322a728c03b9f31cc755e9a Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Wed, 18 Dec 2019 17:57:38 +0100 Subject: [PATCH 14/64] add maximumTotalConnects --- server.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index e36390d..81204f5 100755 --- a/server.js +++ b/server.js @@ -587,7 +587,11 @@ const parseMatrixContent = function(matrixContent, content) { matrixContent.type = ember.MatrixType.oneToOne; } else if (content.type == "nToN") { - matrixContent.type = ember.MatrixType.nToN; + matrixContent.type = ember.MatrixType.nToN; + matrixContent.maximumTotalConnects = content.maximumTotalConnects == null ? + Number(content.targetCount) * Number(content.sourceCount) : Number(content.maximumTotalConnects); + matrixContent.maximumConnectsPerTarget = content.maximumConnectsPerTarget == null ? + Number(content.sourceCount) : Number(content.maximumConnectsPerTarget); } else { throw new Error(`Invalid matrix type ${content.type}`); From 2011b647634c8b33e469eb6d8eb8b086fd80e6be Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Wed, 18 Dec 2019 22:33:07 +0100 Subject: [PATCH 15/64] fix 1 to N matrix connect --- ember.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ember.js b/ember.js index 510d598..b63e1a8 100755 --- a/ember.js +++ b/ember.js @@ -852,9 +852,14 @@ function canConnect(matrixNode, targetID, sources, operation) { const oldSources = connection == null || connection.sources == null ? [] : connection.sources.slice(); const newSources = operation === MatrixOperation.absolute ? sources : oldSources.concat(sources); const sMap = new Set(newSources.map(i => Number(i))); - if (type === MatrixType.oneToN) { + if (type === MatrixType.oneToN && + matrixNode.contents.maximumConnectsPerTarget == null && + matrixNode.contents.maximumConnectsPerTarget == null) { return sMap.size < 2; } + else if (type === MatrixType.oneToN && sMap.size >= 2) { + return false; + } else if (type === MatrixType.oneToOne) { if (sMap.size > 1) { return false; From 6d03d10d93e51ed0d38d13253fb60ea00e8bacb7 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Thu, 19 Dec 2019 09:18:28 +0100 Subject: [PATCH 16/64] Fix 1toN. Disconnect old source and connect to new one --- package.json | 2 +- server.js | 20 +++++++++++--------- test/Server.test.js | 6 +++++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index f234479..1555442 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.11.0", + "version": "1.11.1", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index 81204f5..072aceb 100755 --- a/server.js +++ b/server.js @@ -245,17 +245,19 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti let emitType; res.connections[connection.target] = conResult; - if (matrix.contents.type === ember.MatrixType.oneToOne && + if (matrix.contents.type !== ember.MatrixType.nToN && connection.operation !== ember.MatrixOperation.disconnect && connection.sources != null && connection.sources.length === 1) { - // if the source is being used already, disconnect it. - const targets = matrix.getSourceConnections(connection.sources[0]); - if (targets.length === 1) { - const disconnect = new ember.MatrixConnection(targets[0]); - disconnect.setSources([]); - disconnect.disposition = ember.MatrixDisposition.modified; - res.connections[targets[0]] = disconnect; - matrix.setSources(targets[0], []); + if (matrix.contents.type === ember.MatrixType.oneToOne) { + // if the source is being used already, disconnect it. + const targets = matrix.getSourceConnections(connection.sources[0]); + if (targets.length === 1) { + const disconnect = new ember.MatrixConnection(targets[0]); + disconnect.setSources([]); + disconnect.disposition = ember.MatrixDisposition.modified; + res.connections[targets[0]] = disconnect; + matrix.setSources(targets[0], []); + } } // if the target is connected already, disconnect it if (matrix.connections[connection.target].sources != null && diff --git a/test/Server.test.js b/test/Server.test.js index 61c45d8..e6da2a6 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -244,12 +244,16 @@ describe("server", function() { connection.operation = ember.MatrixOperation.connect; let res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); - matrix.connections[0].sources = [0]; + matrix.setSources(0, [0]); res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeFalsy(); connection.operation = ember.MatrixOperation.absolute; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); + // We can't connect. But server will disconnect existing source and connect new one. + server.handleMatrixConnections(null, matrix, {0: connection}, false); + expect(matrix.connections[0].sources[0]).toBe(1); + matrix.setSources(0, [0]); connection = new ember.MatrixConnection(1); connection.operation = ember.MatrixOperation.absolute; connection.setSources([1]); From 0283ce38126a06815bd44326b51f9aee21ed617a Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Thu, 19 Dec 2019 09:31:29 +0100 Subject: [PATCH 17/64] Server should emit disconnect when connecting target with new source --- package.json | 2 +- server.js | 16 ++++++++++++++-- test/Server.test.js | 20 +++++++++++++++++++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 1555442..d20eda5 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.11.1", + "version": "1.11.2", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index 072aceb..3459f3b 100755 --- a/server.js +++ b/server.js @@ -251,18 +251,30 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti if (matrix.contents.type === ember.MatrixType.oneToOne) { // if the source is being used already, disconnect it. const targets = matrix.getSourceConnections(connection.sources[0]); - if (targets.length === 1) { + if (targets.length === 1 && targets[0] !== connection.target) { const disconnect = new ember.MatrixConnection(targets[0]); disconnect.setSources([]); disconnect.disposition = ember.MatrixDisposition.modified; res.connections[targets[0]] = disconnect; matrix.setSources(targets[0], []); + this.emit("matrix-disconnect", { + target: targets[0], + sources: connection.sources, + client: client == null ? null : client.remoteAddress() + }); } } // if the target is connected already, disconnect it if (matrix.connections[connection.target].sources != null && - matrix.connections[connection.target].sources.length === 1) { + matrix.connections[connection.target].sources.length === 1 && + matrix.connections[connection.target].sources[0] !== connection.sources[0]) { + const source = matrix.connections[connection.target].sources[0]; matrix.setSources(connection.target, []); + this.emit("matrix-disconnect", { + target: connection.target, + sources: [source], + client: client == null ? null : client.remoteAddress() + }); } } if (connection.operation !== ember.MatrixOperation.disconnect && diff --git a/test/Server.test.js b/test/Server.test.js index e6da2a6..aaa6346 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -238,6 +238,11 @@ describe("server", function() { server = new TreeServer(LOCALHOST, PORT, root); }); it("should verify if connection allowed in 1-to-N", function() { + let disconnectCount = 0; + const handleDisconnect = info => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); const matrix = server.tree.elements[0].children[1].children[0]; let connection = new ember.MatrixConnection(0); connection.setSources([1]); @@ -253,6 +258,10 @@ describe("server", function() { // We can't connect. But server will disconnect existing source and connect new one. server.handleMatrixConnections(null, matrix, {0: connection}, false); expect(matrix.connections[0].sources[0]).toBe(1); + expect(disconnectCount).toBe(1); + // But if connecting same source and dest. Don't disconnect and reconnect. + server.handleMatrixConnections(null, matrix, {0: connection}, false); + expect(disconnectCount).toBe(1); matrix.setSources(0, [0]); connection = new ember.MatrixConnection(1); connection.operation = ember.MatrixOperation.absolute; @@ -262,6 +271,11 @@ describe("server", function() { }); it("should verify if connection allowed in 1-to-1", function() { const matrix = server.tree.elements[0].children[1].children[0]; + let disconnectCount = 0; + const handleDisconnect = info => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); matrix.contents.type = ember.MatrixType.oneToOne; const connection = new ember.MatrixConnection(0); connection.setSources([1]); @@ -274,7 +288,10 @@ describe("server", function() { // We can't connect but in 1-on-1 server should disconnect existing source and connect new one. server.handleMatrixConnections(null, matrix, {0: connection}, false); expect(matrix.connections[0].sources[0]).toBe(1); - + expect(disconnectCount).toBe(1); + // But if connecting same source and dest. Don't disconnect and reconnect. + server.handleMatrixConnections(null, matrix, {0: connection}, false); + expect(disconnectCount).toBe(1); connection.operation = ember.MatrixOperation.absolute; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); @@ -282,6 +299,7 @@ describe("server", function() { matrix.setSources(1, [1]); res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeFalsy(); + server.off("matrix-disconnect", handleDisconnect); }); it("should verify if connection allowed in N-to-N", function() { const matrix = server.tree.elements[0].children[1].children[0]; From a402f26f821d4aed6ba5c095cc1724b9be8da1bb Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Thu, 19 Dec 2019 09:38:15 +0100 Subject: [PATCH 18/64] Update doc --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index ca6ec73..696e011 100755 --- a/README.md +++ b/README.md @@ -126,6 +126,15 @@ server.on("error", e => { server.on("clientError", info => { console.log("clientError", info); }); +server.on("matrix-disconnect", info => { + console.log(`Client ${info.client} disconnected ${info.target} and ${info.sources}`); +} +server.on("matrix-connect", info => { + console.log(`Client ${info.client} connected ${info.target} and ${info.sources}`); +} +server.on("matrix-change", info => { + console.log(`Client ${info.client} changed ${info.target} and ${info.sources}`); +} server.listen().then(() => { console.log("listening"); }).catch((e) => { console.log(e.stack); }); ``` From b46b9176b775b9707e9481fb06e937548dc6a547 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Thu, 19 Dec 2019 12:11:08 +0100 Subject: [PATCH 19/64] Do a disconnect if attempting to connect same src and target --- package.json | 2 +- server.js | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index d20eda5..11f586e 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.11.2", + "version": "1.11.3", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index 3459f3b..9e48e92 100755 --- a/server.js +++ b/server.js @@ -266,15 +266,21 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti } // if the target is connected already, disconnect it if (matrix.connections[connection.target].sources != null && - matrix.connections[connection.target].sources.length === 1 && - matrix.connections[connection.target].sources[0] !== connection.sources[0]) { - const source = matrix.connections[connection.target].sources[0]; - matrix.setSources(connection.target, []); - this.emit("matrix-disconnect", { - target: connection.target, - sources: [source], - client: client == null ? null : client.remoteAddress() - }); + matrix.connections[connection.target].sources.length === 1) { + if (matrix.connections[connection.target].sources[0] !== connection.sources[0]) { + const source = matrix.connections[connection.target].sources[0]; + matrix.setSources(connection.target, []); + this.emit("matrix-disconnect", { + target: connection.target, + sources: [source], + client: client == null ? null : client.remoteAddress() + }); + } + else { + // let's change the request into a disconnect + connection.operation = ember.MatrixOperation.disconnect; + } + } } if (connection.operation !== ember.MatrixOperation.disconnect && From 61d9e01ed70445982258ce6c962223974d576874 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Thu, 19 Dec 2019 13:33:20 +0100 Subject: [PATCH 20/64] fix disconnect --- package.json | 2 +- server.js | 37 ++++++++++++++++++------------------- test/Server.test.js | 32 ++++++++++++++++++++++++-------- 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 11f586e..de77dbe 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.11.3", + "version": "1.11.4", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index 9e48e92..57ee655 100755 --- a/server.js +++ b/server.js @@ -257,11 +257,13 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti disconnect.disposition = ember.MatrixDisposition.modified; res.connections[targets[0]] = disconnect; matrix.setSources(targets[0], []); - this.emit("matrix-disconnect", { - target: targets[0], - sources: connection.sources, - client: client == null ? null : client.remoteAddress() - }); + if (response) { + this.emit("matrix-disconnect", { + target: targets[0], + sources: connection.sources, + client: client == null ? null : client.remoteAddress() + }); + } } } // if the target is connected already, disconnect it @@ -270,11 +272,13 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti if (matrix.connections[connection.target].sources[0] !== connection.sources[0]) { const source = matrix.connections[connection.target].sources[0]; matrix.setSources(connection.target, []); - this.emit("matrix-disconnect", { - target: connection.target, - sources: [source], - client: client == null ? null : client.remoteAddress() - }); + if (response) { + this.emit("matrix-disconnect", { + target: connection.target, + sources: [source], + client: client == null ? null : client.remoteAddress() + }); + } } else { // let's change the request into a disconnect @@ -297,7 +301,8 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti } conResult.disposition = ember.MatrixDisposition.modified; } - else if (connection.operarion === ember.MatrixOperation.disconnect) { // Disconnect + else if (connection.operation === ember.MatrixOperation.disconnect) { + // Disconnect matrix.disconnectSources(connection.target, connection.sources); conResult.disposition = ember.MatrixDisposition.modified; emitType = "matrix-disconnect"; @@ -307,7 +312,6 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti console.log(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); } conResult.disposition = ember.MatrixDisposition.tally; - return; } // Send response or update subscribers. @@ -317,14 +321,9 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti this.emit(emitType, { target: connection.target, sources: connection.sources, - client: client.remoteAddress() + client: client == null ? null : client.remoteAddress() }); - } - else { - // the action has been applied. So we should either send the current state (absolute) - // or send the action itself (connection.sources) - conResult.operation = ember.MatrixOperation.absolute; - } + } } if (client != null) { client.sendBERNode(root); diff --git a/test/Server.test.js b/test/Server.test.js index aaa6346..94c8873 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -256,12 +256,12 @@ describe("server", function() { res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); // We can't connect. But server will disconnect existing source and connect new one. - server.handleMatrixConnections(null, matrix, {0: connection}, false); + server.handleMatrixConnections(null, matrix, {0: connection}); expect(matrix.connections[0].sources[0]).toBe(1); expect(disconnectCount).toBe(1); - // But if connecting same source and dest. Don't disconnect and reconnect. - server.handleMatrixConnections(null, matrix, {0: connection}, false); - expect(disconnectCount).toBe(1); + // But if connecting same source and dest. just disconnect and don't reconnect. + server.handleMatrixConnections(null, matrix, {0: connection}); + expect(disconnectCount).toBe(2); matrix.setSources(0, [0]); connection = new ember.MatrixConnection(1); connection.operation = ember.MatrixOperation.absolute; @@ -286,12 +286,12 @@ describe("server", function() { res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeFalsy(); // We can't connect but in 1-on-1 server should disconnect existing source and connect new one. - server.handleMatrixConnections(null, matrix, {0: connection}, false); + server.handleMatrixConnections(null, matrix, {0: connection}); expect(matrix.connections[0].sources[0]).toBe(1); expect(disconnectCount).toBe(1); - // But if connecting same source and dest. Don't disconnect and reconnect. - server.handleMatrixConnections(null, matrix, {0: connection}, false); - expect(disconnectCount).toBe(1); + // But if connecting same source and dest. just disconnect and do not reconnect. + server.handleMatrixConnections(null, matrix, {0: connection}); + expect(disconnectCount).toBe(2); connection.operation = ember.MatrixOperation.absolute; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); @@ -301,6 +301,22 @@ describe("server", function() { expect(res).toBeFalsy(); server.off("matrix-disconnect", handleDisconnect); }); + it("should disconnect if trying to connect same source and target in 1-to-1", function() { + const matrix = server.tree.elements[0].children[1].children[0]; + let disconnectCount = 0; + const handleDisconnect = info => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); + matrix.contents.type = ember.MatrixType.oneToOne; + matrix.setSources(0, [1]); + const connection = new ember.MatrixConnection(0); + connection.setSources([1]); + connection.operation = ember.MatrixOperation.connect; + server.handleMatrixConnections(null, matrix, {0: connection}); + expect(matrix.connections[0].sources.length).toBe(0); + expect(disconnectCount).toBe(1); + }); it("should verify if connection allowed in N-to-N", function() { const matrix = server.tree.elements[0].children[1].children[0]; matrix.contents.type = ember.MatrixType.nToN; From c7126760dbf7fe963e0379f4cb08d0da8d4961ea Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Fri, 20 Dec 2019 10:21:36 +0100 Subject: [PATCH 21/64] handle connect with empty sources --- README.md | 2 ++ server.js | 27 ++++++++++++++++++++++++--- test/Server.test.js | 7 +++++-- test/utils.js | 4 +++- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 696e011..9f40cbd 100755 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ const {ParameterType, FunctionArgument} = require("emberplus").Ember; const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; const sources = _src === undefined ? [ "src1", "src2", "src3" ] : _src; +const defaultSources = [0, 0, 0]; const labels = function(endpoints, type) { let labels = []; for (let i = 0; i < endpoints.length; i++) { @@ -191,6 +192,7 @@ const jsonTree = [ mode: "linear", targetCount: targets.length, sourceCount: sources.length, + defaultSources: defaultSources, connections: buildConnections(sources, targets), labels: [{basePath: "0.1.1000", description: "primary"}] }, diff --git a/server.js b/server.js index 57ee655..7370ff1 100755 --- a/server.js +++ b/server.js @@ -280,14 +280,19 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti }); } } - else { + else if (matrix.contents.type === ember.MatrixType.oneToOne) { // let's change the request into a disconnect connection.operation = ember.MatrixOperation.disconnect; } - + else if (matrix.contents.type === ember.MatrixType.oneToN && + (matrix.defaultSources != null)) { + connection.sources = [matrix.defaultSources[connection.target]] + } } } + if (connection.operation !== ember.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length > 0 && matrix.canConnect(connection.target,connection.sources,connection.operation)) { // Apply changes if ((connection.operation == null) || @@ -300,7 +305,22 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti emitType = "matrix-connect"; } conResult.disposition = ember.MatrixDisposition.modified; - } + } + else if (connection.operation !== ember.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length === 0 && + matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length > 0) { + // let's disconnect + if (response) { + this.emit("matrix-disconnect", { + target: connection.target, + sources: matrix.connections[connection.target].sources, + client: client == null ? null : client.remoteAddress() + }); + } + matrix.setSources(connection.target, []); + conResult.disposition = ember.MatrixDisposition.modified; + } else if (connection.operation === ember.MatrixOperation.disconnect) { // Disconnect matrix.disconnectSources(connection.target, connection.sources); @@ -671,6 +691,7 @@ const parseObj = function(parent, obj) { else if (content.targetCount != null) { emberElement = new ember.MatrixNode(number); emberElement.contents = new ember.MatrixContents(); + emberElement.defaultSources = content.defaultSources; parseMatrixContent(emberElement.contents, content); if (content.connections) { emberElement.connections = {}; diff --git a/test/Server.test.js b/test/Server.test.js index 94c8873..5aa12c0 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -259,9 +259,12 @@ describe("server", function() { server.handleMatrixConnections(null, matrix, {0: connection}); expect(matrix.connections[0].sources[0]).toBe(1); expect(disconnectCount).toBe(1); - // But if connecting same source and dest. just disconnect and don't reconnect. + // But if connecting same source and dest this is a disconnect. But not possible in 1toN. + // instead connect with defaultSource or do nothing + matrix.defaultSources[0] = 222; server.handleMatrixConnections(null, matrix, {0: connection}); - expect(disconnectCount).toBe(2); + expect(disconnectCount).toBe(1); + expect(matrix.connections[0].sources[0]).toBe(222); matrix.setSources(0, [0]); connection = new ember.MatrixConnection(1); connection.operation = ember.MatrixOperation.absolute; diff --git a/test/utils.js b/test/utils.js index b0ae7c0..d1e0727 100755 --- a/test/utils.js +++ b/test/utils.js @@ -4,6 +4,7 @@ const {ParameterType, FunctionArgument} = require("../ember"); const init = function(_src,_tgt) { const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; const sources = _src === undefined ? [ "src1", "src2", "src3" ] : _src; + const defaultSources = [0, 0, 0]; const labels = function(endpoints, type) { let labels = []; for (let i = 0; i < endpoints.length; i++) { @@ -52,7 +53,8 @@ const init = function(_src,_tgt) { targetCount: targets.length, sourceCount: sources.length, connections: buildConnections(sources, targets), - labels: [{basePath: "0.1.1000", description: "primary"}] + labels: [{basePath: "0.1.1000", description: "primary"}], + defaultSources: defaultSources }, { identifier: "labels", From 84d325cf0075609488a51cb2f1aa17bacd6e7716 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sat, 21 Dec 2019 14:20:58 +0100 Subject: [PATCH 22/64] added lock/unlock of connection --- ember.js | 19 ++++++++++++++++++ server.js | 49 ++++++++++++++++++++++++++++++++++++--------- test/Server.test.js | 23 +++++++++++++++++++-- test/utils.js | 10 ++++++--- 4 files changed, 87 insertions(+), 14 deletions(-) diff --git a/ember.js b/ember.js index b63e1a8..c6adce6 100755 --- a/ember.js +++ b/ember.js @@ -393,6 +393,10 @@ TreeNode.prototype.toJSON = function() { return res; }; +TreeNode.prototype.getParent = function() { + return this._parent; +} + TreeNode.prototype.getElementByPath = function(path) { var children = this.getChildren(); if ((children === null)||(children === undefined)) { @@ -852,6 +856,10 @@ function canConnect(matrixNode, targetID, sources, operation) { const oldSources = connection == null || connection.sources == null ? [] : connection.sources.slice(); const newSources = operation === MatrixOperation.absolute ? sources : oldSources.concat(sources); const sMap = new Set(newSources.map(i => Number(i))); + + if (matrixNode.connections[targetID].isLocked()) { + return false; + } if (type === MatrixType.oneToN && matrixNode.contents.maximumConnectsPerTarget == null && matrixNode.contents.maximumConnectsPerTarget == null) { @@ -1348,6 +1356,7 @@ function MatrixConnection(target) { else { this.target = 0; } + this._locked = false; } // ConnectionOperation ::= @@ -1401,6 +1410,16 @@ MatrixConnection.prototype.connectSources = function(sources) { this.sources = [...s].sort(); } +MatrixConnection.prototype.lock = function() { + this._locked = true; +} +MatrixConnection.prototype.unlock = function() { + this._locked = false; +} +MatrixConnection.prototype.isLocked = function() { + return this._locked; +} + MatrixConnection.prototype.disconnectSources = function(sources) { if (sources === undefined) { return; diff --git a/server.js b/server.js index 7370ff1..66cf2e1 100755 --- a/server.js +++ b/server.js @@ -245,7 +245,10 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti let emitType; res.connections[connection.target] = conResult; - if (matrix.contents.type !== ember.MatrixType.nToN && + if (matrix.connections[connection.target].isLocked()) { + conResult.disposition = ember.MatrixDisposition.locked; + } + else if (matrix.contents.type !== ember.MatrixType.nToN && connection.operation !== ember.MatrixOperation.disconnect && connection.sources != null && connection.sources.length === 1) { if (matrix.contents.type === ember.MatrixType.oneToOne) { @@ -269,6 +272,19 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti // if the target is connected already, disconnect it if (matrix.connections[connection.target].sources != null && matrix.connections[connection.target].sources.length === 1) { + if (matrix.contents.type === ember.MatrixType.oneToN) { + const disconnectSource = this.getDisconnectSource(matrix, connection.target); + if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { + if (disconnectSource != null && + disconnectSource != connection.sources[0]) { + connection.sources = [disconnectSource]; + } + else { + // do nothing + connection.operarion = ember.MatrixOperation.tally; + } + } + } if (matrix.connections[connection.target].sources[0] !== connection.sources[0]) { const source = matrix.connections[connection.target].sources[0]; matrix.setSources(connection.target, []); @@ -284,10 +300,6 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti // let's change the request into a disconnect connection.operation = ember.MatrixOperation.disconnect; } - else if (matrix.contents.type === ember.MatrixType.oneToN && - (matrix.defaultSources != null)) { - connection.sources = [matrix.defaultSources[connection.target]] - } } } @@ -327,7 +339,7 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti conResult.disposition = ember.MatrixDisposition.modified; emitType = "matrix-disconnect"; } - else { + else if (conResult.disposition !== ember.MatrixDisposition.locked){ if (this._debug) { console.log(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); } @@ -577,6 +589,26 @@ TreeServer.prototype.replaceElement = function(element) { } } +TreeServer.prototype.getDisconnectSource = function(matrix, targetID) { + if (matrix.defaultSources) { + return matrix.defaultSources[targetID].contents.value; + } + if (matrix.contents.labels == null || matrix.contents.labels.length == 0) { + return null; + } + const basePath = matrix.contents.labels[0].basePath; + const labels = this.tree.getElementByPath(basePath); + const number = labels.getNumber() + 1; + const parent = labels.getParent(); + const children = parent.getChildren(); + for(let child of children) { + if (child.getNumber() === number) { + matrix.defaultSources = child.getChildren(); + return matrix.defaultSources[targetID].contents.value; + } + } + return null; +} TreeServer.prototype.updateSubscribers = function(path, response, origin) { if (this.subscribers[path] == null) { @@ -690,9 +722,8 @@ const parseObj = function(parent, obj) { } else if (content.targetCount != null) { emberElement = new ember.MatrixNode(number); - emberElement.contents = new ember.MatrixContents(); - emberElement.defaultSources = content.defaultSources; - parseMatrixContent(emberElement.contents, content); + emberElement.contents = new ember.MatrixContents(); + parseMatrixContent(emberElement.contents, content); if (content.connections) { emberElement.connections = {}; for (let c in content.connections) { diff --git a/test/Server.test.js b/test/Server.test.js index 5aa12c0..69050ac 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -261,9 +261,10 @@ describe("server", function() { expect(disconnectCount).toBe(1); // But if connecting same source and dest this is a disconnect. But not possible in 1toN. // instead connect with defaultSource or do nothing - matrix.defaultSources[0] = 222; + server.getDisconnectSource(matrix, 0); + matrix.defaultSources[0].contents.value = 222; server.handleMatrixConnections(null, matrix, {0: connection}); - expect(disconnectCount).toBe(1); + expect(disconnectCount).toBe(2); expect(matrix.connections[0].sources[0]).toBe(222); matrix.setSources(0, [0]); connection = new ember.MatrixConnection(1); @@ -320,6 +321,24 @@ describe("server", function() { expect(matrix.connections[0].sources.length).toBe(0); expect(disconnectCount).toBe(1); }); + it("should be able to lock a connection", function() { + const matrix = server.tree.elements[0].children[1].children[0]; + let disconnectCount = 0; + const handleDisconnect = info => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); + matrix.contents.type = ember.MatrixType.oneToOne; + matrix.setSources(0, [1]); + matrix.connections[0].lock(); + const connection = new ember.MatrixConnection(0); + connection.setSources([0]); + connection.operation = ember.MatrixOperation.connect; + server.handleMatrixConnections(null, matrix, {0: connection}); + expect(matrix.connections[0].sources.length).toBe(1); + expect(matrix.connections[0].sources[0]).toBe(1); + expect(disconnectCount).toBe(0); + }); it("should verify if connection allowed in N-to-N", function() { const matrix = server.tree.elements[0].children[1].children[0]; matrix.contents.type = ember.MatrixType.nToN; diff --git a/test/utils.js b/test/utils.js index d1e0727..1092e18 100755 --- a/test/utils.js +++ b/test/utils.js @@ -4,7 +4,7 @@ const {ParameterType, FunctionArgument} = require("../ember"); const init = function(_src,_tgt) { const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; const sources = _src === undefined ? [ "src1", "src2", "src3" ] : _src; - const defaultSources = [0, 0, 0]; + const defaultSources = [{identifier: "t-0", value: 0}, {identifier: "t-1", value: 0}, {identifier: "t-2", value: 0}]; const labels = function(endpoints, type) { let labels = []; for (let i = 0; i < endpoints.length; i++) { @@ -53,8 +53,7 @@ const init = function(_src,_tgt) { targetCount: targets.length, sourceCount: sources.length, connections: buildConnections(sources, targets), - labels: [{basePath: "0.1.1000", description: "primary"}], - defaultSources: defaultSources + labels: [{basePath: "0.1.1000", description: "primary"}], }, { identifier: "labels", @@ -78,6 +77,11 @@ const init = function(_src,_tgt) { children: [ {identifier: "sdp A", value: "A"}, {identifier: "sdp B", value: "B"}] } ] + }, + { + identifier: "disconnect sources", + number: 1001, + children: defaultSources } ] }, From b00b1b31991344b1df22bef1b15d79f9108e7edc Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sat, 21 Dec 2019 14:22:25 +0100 Subject: [PATCH 23/64] added lock/unlock of connection --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f40cbd..c30838f 100755 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ const {ParameterType, FunctionArgument} = require("emberplus").Ember; const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; const sources = _src === undefined ? [ "src1", "src2", "src3" ] : _src; -const defaultSources = [0, 0, 0]; +const defaultSources = [{identifier: "t-0", value: 0}, {identifier: "t-1", value: 0}, {identifier: "t-2", value: 0}]; const labels = function(endpoints, type) { let labels = []; for (let i = 0; i < endpoints.length; i++) { @@ -192,7 +192,6 @@ const jsonTree = [ mode: "linear", targetCount: targets.length, sourceCount: sources.length, - defaultSources: defaultSources, connections: buildConnections(sources, targets), labels: [{basePath: "0.1.1000", description: "primary"}] }, @@ -218,6 +217,12 @@ const jsonTree = [ children: [ {identifier: "sdp A", value: "A"}, {identifier: "sdp B", value: "B"}] } ] + }, + { + identifier: "disconnect sources", + // must be labels + 1 + number: 1001, + children: defaultSources } ] }, From 8b2109f2f79ed9769aafabb2a0e48d235e66d7f5 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sat, 21 Dec 2019 19:12:32 +0100 Subject: [PATCH 24/64] Fix disconnect for 1toN --- server.js | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index 66cf2e1..fac5511 100755 --- a/server.js +++ b/server.js @@ -241,6 +241,7 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti continue; } let connection = connections[id]; + console.log(connection); conResult = new ember.MatrixConnection(connection.target); let emitType; res.connections[connection.target] = conResult; @@ -335,9 +336,32 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti } else if (connection.operation === ember.MatrixOperation.disconnect) { // Disconnect - matrix.disconnectSources(connection.target, connection.sources); - conResult.disposition = ember.MatrixDisposition.modified; - emitType = "matrix-disconnect"; + if (matrix.contents.type === ember.MatrixType.oneToN) { + const disconnectSource = this.getDisconnectSource(matrix, connection.target); + if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { + if (disconnectSource != null && + disconnectSource != connection.sources[0]) { + if (response) { + this.emit("matrix-disconnect", { + target: connection.target, + sources: matrix.connections[connection.target].sources, + client: client == null ? null : client.remoteAddress() + }); + } + matrix.setSources(connection.target, [disconnectSource]); + connection.operarion = ember.MatrixOperation.modified; + } + else { + // do nothing + connection.operarion = ember.MatrixOperation.tally; + } + } + } + else { + matrix.disconnectSources(connection.target, connection.sources); + conResult.disposition = ember.MatrixDisposition.modified; + emitType = "matrix-disconnect"; + } } else if (conResult.disposition !== ember.MatrixDisposition.locked){ if (this._debug) { From 734c0ea5498d6d3fe28f6cf2e28cb1d4be1b58c8 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sun, 22 Dec 2019 10:19:37 +0100 Subject: [PATCH 25/64] Fix function result missing --- README.md | 7 +++++++ ember.js | 2 +- package.json | 2 +- server.js | 11 ++++++++++- test/utils.js | 7 +++++++ 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c30838f..095f679 100755 --- a/README.md +++ b/README.md @@ -246,6 +246,13 @@ const jsonTree = [ value: null, name: "arg2" } + ], + result: [ + { + type: ParameterType.integer, + value: null, + name: "changeCount" + } ] } ] diff --git a/ember.js b/ember.js index c6adce6..35e7e7b 100755 --- a/ember.js +++ b/ember.js @@ -1985,7 +1985,7 @@ FunctionContent.prototype.encode = function(ber) { if(this.result != null) { ber.startSequence(BER.CONTEXT(3)); ber.startSequence(BER.EMBER_SEQUENCE); - for(var i =0; i < this.result; i++) { + for(var i = 0; i < this.result.length; i++) { ber.startSequence(BER.CONTEXT(0)); this.result[i].encode(ber); ber.endSequence(); diff --git a/package.json b/package.json index de77dbe..1586315 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.11.4", + "version": "1.11.5", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index fac5511..f1b935c 100755 --- a/server.js +++ b/server.js @@ -241,7 +241,6 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti continue; } let connection = connections[id]; - console.log(connection); conResult = new ember.MatrixConnection(connection.target); let emitType; res.connections[connection.target] = conResult; @@ -743,6 +742,16 @@ const parseObj = function(parent, obj) { )); } } + if (content.result != null) { + for(let argument of content.result) { + emberElement.contents.result.push(new ember.FunctionArgument( + argument.type, + argument.value, + argument.name + )); + } + } + delete content.result; } else if (content.targetCount != null) { emberElement = new ember.MatrixNode(number); diff --git a/test/utils.js b/test/utils.js index 1092e18..b4cff7e 100755 --- a/test/utils.js +++ b/test/utils.js @@ -105,6 +105,13 @@ const init = function(_src,_tgt) { value: null, name: "arg2" } + ], + result: [ + { + type: ParameterType.integer, + value: null, + name: "changeCount" + } ] } ] From 341e88784eea0c199384d80a135f286bf3820b88 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sun, 22 Dec 2019 11:08:35 +0100 Subject: [PATCH 26/64] Added -1 for defaultSources to disable disconnect --- README.md | 10 +++++++--- server.js | 2 +- test/utils.js | 6 +++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 095f679..1d2052f 100755 --- a/README.md +++ b/README.md @@ -143,9 +143,13 @@ server.listen().then(() => { console.log("listening"); }).catch((e) => { console const TreeServer = require("emberplus").TreeServer; const {ParameterType, FunctionArgument} = require("emberplus").Ember; -const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; -const sources = _src === undefined ? [ "src1", "src2", "src3" ] : _src; -const defaultSources = [{identifier: "t-0", value: 0}, {identifier: "t-1", value: 0}, {identifier: "t-2", value: 0}]; +const targets = [ "tgt1", "tgt2", "tgt3" ]; +const sources = [ "src1", "src2", "src3" ]; +const defaultSources = [ + {identifier: "t-0", value: -1, access: "readWrite" }, + {identifier: "t-1", value: 0, access: "readWrite"}, + {identifier: "t-2", value: 0, access: "readWrite"} +]; const labels = function(endpoints, type) { let labels = []; for (let i = 0; i < endpoints.length; i++) { diff --git a/server.js b/server.js index f1b935c..0c8de4c 100755 --- a/server.js +++ b/server.js @@ -275,7 +275,7 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti if (matrix.contents.type === ember.MatrixType.oneToN) { const disconnectSource = this.getDisconnectSource(matrix, connection.target); if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { - if (disconnectSource != null && + if (disconnectSource != null && disconnectSource != -1 && disconnectSource != connection.sources[0]) { connection.sources = [disconnectSource]; } diff --git a/test/utils.js b/test/utils.js index b4cff7e..2324391 100755 --- a/test/utils.js +++ b/test/utils.js @@ -4,7 +4,11 @@ const {ParameterType, FunctionArgument} = require("../ember"); const init = function(_src,_tgt) { const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; const sources = _src === undefined ? [ "src1", "src2", "src3" ] : _src; - const defaultSources = [{identifier: "t-0", value: 0}, {identifier: "t-1", value: 0}, {identifier: "t-2", value: 0}]; + const defaultSources = [ + {identifier: "t-0", value: -1, access: "readWrite" }, + {identifier: "t-1", value: 0, access: "readWrite"}, + {identifier: "t-2", value: 0, access: "readWrite"} + ]; const labels = function(endpoints, type) { let labels = []; for (let i = 0; i < endpoints.length; i++) { From b11242e1a95b7bf096e694ca10359a2fc8265d24 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sun, 22 Dec 2019 11:52:28 +0100 Subject: [PATCH 27/64] fix disconnect when mismatch client server --- package.json | 2 +- server.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 1586315..03442dc 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.11.5", + "version": "1.11.6", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index 0c8de4c..1b3c03b 100755 --- a/server.js +++ b/server.js @@ -333,12 +333,14 @@ TreeServer.prototype.handleMatrixConnections = function(client, matrix, connecti matrix.setSources(connection.target, []); conResult.disposition = ember.MatrixDisposition.modified; } - else if (connection.operation === ember.MatrixOperation.disconnect) { - // Disconnect + else if (connection.operation === ember.MatrixOperation.disconnect && + matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length > 0) { + // Disconnect if (matrix.contents.type === ember.MatrixType.oneToN) { const disconnectSource = this.getDisconnectSource(matrix, connection.target); if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { - if (disconnectSource != null && + if (disconnectSource != null && disconnectSource != -1 && disconnectSource != connection.sources[0]) { if (response) { this.emit("matrix-disconnect", { From 3fdf5fe07e58a5d9170d8a312e4ad2e3004420c5 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sun, 22 Dec 2019 17:58:36 +0100 Subject: [PATCH 28/64] Fix bug in command handling --- server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index 1b3c03b..ff94e35 100755 --- a/server.js +++ b/server.js @@ -183,7 +183,7 @@ TreeServer.prototype.handleNode = function(client, node) { } if (cmd instanceof ember.Command) { - this.handleCommand(client, element, cmd.number); + this.handleCommand(client, element, cmd); } else if ((cmd instanceof ember.MatrixNode) && (cmd.connections !== undefined)) { this.handleMatrixConnections(client, element, cmd.connections); @@ -455,7 +455,7 @@ TreeServer.prototype.handleCommand = function(client, element, cmd) { this.handleInvoke(client, cmd.invocation, element); break; default: - this.emit("error", new Error(`invalid command ${cmd}`)); + this.emit("error", new Error(`invalid command ${cmd.number}`)); break; } } From 9efb3e3cf2f68f6125e7c4a6f92d42d9b1f3f3c5 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 23 Dec 2019 09:50:45 +0100 Subject: [PATCH 29/64] fix continuation message --- ember.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ember.js b/ember.js index 35e7e7b..87cab26 100755 --- a/ember.js +++ b/ember.js @@ -79,6 +79,7 @@ Root.decode = function(ber) { else if (tag === BER.CONTEXT(0)) { // continuation of previous message try { + var rootReader = ber.getSequence(BER.CONTEXT(0)); return RootElement.decode(rootReader) } catch (e) { From 60eab03f3e9e17d73f1d3427c4b5300c4e03c4c4 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Thu, 26 Dec 2019 10:24:35 +0100 Subject: [PATCH 30/64] Ignore empty request on server --- ember.js | 10 +++++++++- package.json | 2 +- server.js | 3 +-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ember.js b/ember.js index 87cab26..9f19b15 100755 --- a/ember.js +++ b/ember.js @@ -343,7 +343,7 @@ TreeNode.prototype.toJSON = function() { let res = {}; const node = this; if (node.number) { - res.number = node.number + res.number = node.number; } if (node.path) { res.path = node.path; @@ -2358,6 +2358,14 @@ Command.decode = function(ber) { return c; } +Command.prototype.toJSON = function() { + return { + number: this.number, + fieldFlags: this.fieldFlags, + invocation: this.invocation == null ? null : this.invocation.toJSON() + }; +} + Command.prototype.encode = function(ber) { ber.startSequence(BER.APPLICATION(2)); diff --git a/package.json b/package.json index 03442dc..5be2d70 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.11.6", + "version": "1.11.7", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index ff94e35..43e86db 100755 --- a/server.js +++ b/server.js @@ -93,11 +93,10 @@ TreeServer.prototype.close = function () { TreeServer.prototype.handleRoot = function(client, root) { if ((root == null) || (root.elements == null) || (root.elements < 1)) { - this.emit("error", new Error("invalid request")); + // ignore empty requests. return; } - const node = root.elements[0]; client.request = node; From cf182c4563e6bfc4115f0f0f0b1804cbdef4233b Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Thu, 26 Dec 2019 11:24:20 +0100 Subject: [PATCH 31/64] catch write error and disconnect --- client.js | 45 +++++++++++++++++++++++++++++++-------------- package.json | 2 +- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/client.js b/client.js index a60916a..7d41308 100755 --- a/client.js +++ b/client.js @@ -177,8 +177,7 @@ S101Socket.prototype.connect = function (timeout = 2) { timeout: 1000 * timeout }, () => { - winston.debug('socket connected'); - + winston.debug('socket connected'); // Disable connect timeout to hand-over to keepalive mechanism self.socket.removeListener("timeout", connectTimeoutListener); self.socket.setTimeout(0); @@ -220,14 +219,17 @@ S101Socket.prototype.connect = function (timeout = 2) { self.codec.dataIn(data); } }) - .on('close', () => { - clearInterval(self.keepaliveIntervalTimer); - self.emit('disconnected'); - self.status = "disconnected"; - self.socket = null; - }); + .on('close', self.handleClose) + .on("end", self.handleClose); } +S101Socket.prototype.handleClose = function() { + this.socket = null; + clearInterval(this.keepaliveIntervalTimer); + this.status = "disconnected"; + this.emit('disconnected'); +}; + S101Socket.prototype.isConnected = function () { return ((this.socket !== null) && (this.socket !== undefined)); } @@ -255,15 +257,25 @@ S101Socket.prototype.disconnect = function () { S101Socket.prototype.sendKeepaliveRequest = function () { var self = this; if (self.isConnected()) { - self.socket.write(self.codec.keepAliveRequest()); - winston.debug('sent keepalive request'); + try { + self.socket.write(self.codec.keepAliveRequest()); + winston.debug('sent keepalive request'); + } + catch(e){ + self.handleClose(); + } } } S101Socket.prototype.sendKeepaliveResponse = function () { var self = this; if (self.isConnected()) { - self.socket.write(self.codec.keepAliveResponse()); + try { + self.socket.write(self.codec.keepAliveResponse()); + } + catch(e){ + self.handleClose(); + } winston.debug('sent keepalive response'); } } @@ -271,9 +283,14 @@ S101Socket.prototype.sendKeepaliveResponse = function () { S101Socket.prototype.sendBER = function (data) { var self = this; if (self.isConnected()) { - var frames = self.codec.encodeBER(data); - for (var i = 0; i < frames.length; i++) { - self.socket.write(frames[i]); + try { + var frames = self.codec.encodeBER(data); + for (var i = 0; i < frames.length; i++) { + self.socket.write(frames[i]); + } + } + catch(e){ + self.handleClose(); } } } diff --git a/package.json b/package.json index 5be2d70..60508df 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.11.7", + "version": "1.11.8", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { From 2bb46c2bd0bbbd414ba9fddfb30f2324da1662f6 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sun, 29 Dec 2019 16:57:14 +0100 Subject: [PATCH 32/64] Removed all ref to callabcks...using different approach...Added support for subscribe --- README.md | 35 ++++++- client.js | 2 +- device.js | 86 +++++++--------- ember.js | 238 +++++++++++++++----------------------------- package.json | 2 +- server.js | 30 +++--- test/Server.test.js | 51 ++-------- test/utils.js | 2 +- 8 files changed, 176 insertions(+), 270 deletions(-) diff --git a/README.md b/README.md index 1d2052f..5b3d203 100755 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ tree.connect() }); ``` -Get Specific Branch: +Get Specific Node: ```javascript const DeviceTree = require('emberplus').DeviceTree; const ember = require("emberplus").Ember; @@ -53,11 +53,40 @@ client.connect()) .then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) .then(() => client.getNodeByPath("scoreMaster/router/labels/group 1")) .then(() => client.getNodeByPathnum("0.2")) - .then(() => { - console.log(JSON.stringify(client.root.elements[0].toJSON(), null, 4)); + .then(node => { + console.log(JSON.stringify(node.toJSON(), null, 4)); }); ``` +Subsribe to changes +```javascript +const DeviceTree = require('emberplus').DeviceTree; +const ember = require("emberplus").Ember; + +const client = new DeviceTree(HOST, PORT); +client.connect()) + .then(() => client.getDirectory()) + .then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) + .then(() => client.getNodeByPath("scoreMaster/router/labels/group 1")) + .then(node => { + // For streams, use subscribe + return client.subscribe(node, update => { + console.log(udpate); + }); + }) + .then(() => client.getNodeByPathnum("0.2")) + .then(node => { + // For non-streams a getDirectory will automatically subscribe for update + return client.getDirectory(node, update => { + console.log(udpate); + }); + }) + // You can also provide a callback to the getNodeNyPath + // Be carefull that subscription will be done for all elements in the path + .then(() => client.getNodeByPathnum("0.3", update => {console.log(update);})) + ; +``` + ### Invoking Function ```javascript const DeviceTree = require('emberplus').DeviceTree; diff --git a/client.js b/client.js index 7d41308..20bf7ad 100755 --- a/client.js +++ b/client.js @@ -287,7 +287,7 @@ S101Socket.prototype.sendBER = function (data) { var frames = self.codec.encodeBER(data); for (var i = 0; i < frames.length; i++) { self.socket.write(frames[i]); - } + } } catch(e){ self.handleClose(); diff --git a/device.js b/device.js index 5233eee..ba32402 100755 --- a/device.js +++ b/device.js @@ -103,7 +103,7 @@ DeviceTree.prototype.connect = function (timeout = 2) { }); }; -DeviceTree.prototype.expand = function (node) { +DeviceTree.prototype.expand = function (node, callback = null) { let self = this; if (node == null) { return Promise.reject(new Error("Invalid null node")); @@ -111,7 +111,7 @@ DeviceTree.prototype.expand = function (node) { if (node.isParameter() || node.isMatrix() || node.isFunction()) { return self.getDirectory(node); } - return self.getDirectory(node).then((res) => { + return self.getDirectory(node, callback).then((res) => { let children = node.getChildren(); if ((res === undefined) || (children === undefined) || (children === null)) { if (self._debug) { @@ -144,7 +144,7 @@ function isDirectSubPathOf(path, parent) { return path === parent || (path.lastIndexOf('.') === parent.length && path.startsWith(parent)); } -DeviceTree.prototype.getDirectory = function (qnode) { +DeviceTree.prototype.getDirectory = function (qnode, callback = null) { var self = this; if (qnode == null) { self.root.clear(); @@ -219,7 +219,7 @@ DeviceTree.prototype.getDirectory = function (qnode) { if (self._debug) { console.log("Sending getDirectory", qnode); } - self.client.sendBERNode(qnode.getDirectory()); + self.client.sendBERNode(qnode.getDirectory(callback)); }}); }); }; @@ -339,24 +339,18 @@ DeviceTree.prototype.disconnect = function () { }; DeviceTree.prototype.makeRequest = function () { - var self = this; + const self = this; if (self.activeRequest === null && self.pendingRequests.length > 0) { self.activeRequest = self.pendingRequests.shift(); - - const t = function (id) { - var path = self.activeRequest.path == null ? - self.activeRequest.node.getPath() : - self.activeRequest.path; - var req = `${id} - ${path}`; - if (self._debug) { - console.log(`Making request ${req}`, Date.now()); - } - self.timeout = setTimeout(() => { - self.timeoutRequest(req); - }, self.timeoutValue); - }; - - t(self.requestID++); + const req = `${ self.requestID++} - ${self.activeRequest.node.getPath()}`; + self.activeRequest.timeoutError = new errors.EmberTimeoutError(`Request ${req} timed out`) + + if (self._debug) { + console.log(`Making request ${req}`, Date.now()); + } + self.timeout = setTimeout(() => { + self.timeoutRequest(); + }, self.timeoutValue); self.activeRequest.func(); } }; @@ -376,7 +370,6 @@ DeviceTree.prototype.clearTimeout = function () { DeviceTree.prototype.finishRequest = function () { var self = this; - self.callback = undefined; self.clearTimeout(); self.activeRequest = null; try { @@ -391,9 +384,7 @@ DeviceTree.prototype.finishRequest = function () { }; DeviceTree.prototype.timeoutRequest = function (id) { - var self = this; - self.root.cancelCallbacks(); - self.activeRequest.func(new errors.EmberTimeoutError(`Request ${id !== undefined ? id : ""} timed out`)); + this.activeRequest.func(this.activeRequest.timeoutError); }; DeviceTree.prototype.handleRoot = function (root) { @@ -402,32 +393,28 @@ DeviceTree.prototype.handleRoot = function (root) { if (self._debug) { console.log("handling root", JSON.stringify(root)); } - var callbacks = self.root.update(root); + self.root.update(root); if (root.elements !== undefined) { for (var i = 0; i < root.elements.length; i++) { if (root.elements[i].isQualified()) { - callbacks = callbacks.concat(this.handleQualifiedNode(this.root, root.elements[i])); + this.handleQualifiedNode(this.root, root.elements[i]); } else { - callbacks = callbacks.concat(this.handleNode(this.root, root.elements[i])); + this.handleNode(this.root, root.elements[i]); } } - - // Fire callbacks once entire tree has been updated - for (var j = 0; j < callbacks.length; j++) { - //console.log('hr cb'); - callbacks[j](); - } + } + if (self.callback) { + self.callback(null, root); } }; DeviceTree.prototype.handleQualifiedNode = function (parent, node) { var self = this; - var callbacks = []; var element = parent.getElementByPath(node.path); if (element !== null) { self.emit("value-change", node); - callbacks = element.update(node); + element.update(node); } else { var path = node.path.split("."); @@ -439,10 +426,10 @@ DeviceTree.prototype.handleQualifiedNode = function (parent, node) { path.pop(); parent = this.root.getElementByPath(path.join(".")); if (parent === null) { - return callbacks; + return; } parent.addChild(node); - callbacks = parent.update(parent); + parent.update(parent); } element = node; } @@ -451,43 +438,40 @@ DeviceTree.prototype.handleQualifiedNode = function (parent, node) { if (children !== null) { for (var i = 0; i < children.length; i++) { if (children[i].isQualified()) { - callbacks = callbacks.concat(this.handleQualifiedNode(element, children[i])); + this.handleQualifiedNode(element, children[i]); } else { - callbacks = callbacks.concat(this.handleNode(element, children[i])); + this.handleNode(element, children[i]); } } } - return callbacks; + return; }; DeviceTree.prototype.handleNode = function (parent, node) { var self = this; - var callbacks = []; - var n = parent.getElementByNumber(node.getNumber()); if (n === null) { parent.addChild(node); n = node; } else { - callbacks = n.update(node); + n.update(node); } var children = node.getChildren(); if (children !== null) { for (var i = 0; i < children.length; i++) { - callbacks = callbacks.concat(this.handleNode(n, children[i])); + this.handleNode(n, children[i]); } } else { self.emit("value-change", node); } - - return callbacks; + return; }; -DeviceTree.prototype.getNodeByPathnum = function (path) { +DeviceTree.prototype.getNodeByPathnum = function (path, callback = null) { var self = this; if (typeof path === 'string') { path = path.split('.'); @@ -520,13 +504,13 @@ DeviceTree.prototype.getNodeByPathnum = function (path) { throw pathnumError; } lastMissingPos = pos; - return this.getDirectory(currentNode).then(() => getNext()); + return this.getDirectory(currentNode, callback).then(() => getNext()); }); } return getNext(); }; -DeviceTree.prototype.getNodeByPath = function (path) { +DeviceTree.prototype.getNodeByPath = function (path, callback = null) { var self = this; if (typeof path === 'string') { path = path.split('/'); @@ -559,14 +543,14 @@ DeviceTree.prototype.getNodeByPath = function (path) { throw pathError; } lastMissingPos = pos; - return this.getDirectory(currentNode).then(() => getNext()); + return this.getDirectory(currentNode, callback).then(() => getNext()); }); } return getNext(); }; DeviceTree.prototype.subscribe = function (qnode, callback) { - if (qnode.isParameter() && qnode.isStream()) { + if ((qnode.isParameter() || qnode.isMatrix()) && qnode.isStream()) { var self = this; if (qnode == null) { self.root.clear(); diff --git a/ember.js b/ember.js index 9f19b15..b1580a0 100755 --- a/ember.js +++ b/ember.js @@ -145,9 +145,7 @@ module.exports.Root = Root; ***************************************************************************/ function TreeNode() { - Object.defineProperty(this, '_parent', {value: null, enumerable: false, writable: true}); - Object.defineProperty(this, '_directoryCallbacks', {value: [], enumerable: false, writable: true}); - Object.defineProperty(this, '_callbacks', {value: [], enumerable: false, writable: true}); + Object.defineProperty(this, '_parent', {value: null, enumerable: false, writable: true}); } TreeNode.prototype.addChild = function(child) { @@ -190,23 +188,6 @@ TreeNode.prototype.isStream = function() { this.contents.streamIdentifier != null; } -TreeNode.prototype.addCallback = function(callback) { - if(this._callbacks.indexOf(callback) < 0) { - this._callbacks.push(callback); - } -} - -TreeNode.prototype.cancelCallbacks = function() { - var self=this; - self._directoryCallbacks = []; - var children = self.getChildren(); - if(children !== null) { - for(var i=0; i { callback(error, node) }); + if (callback != null && this.contents != null && !this.isStream()) { + this.contents._subscribers.add(callback); } return this.getTreeBranch(new Command(COMMAND_GETDIRECTORY)); } TreeNode.prototype.subscribe = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - if (this.isParameter() && this.isStream()) { - this.contents.subscribers.add(callback); + if (callback != null && this.isParameter() && this.isStream()) { + this.contents._subscribers.add(callback); } return this.getTreeBranch(new Command(COMMAND_SUBSCRIBE)); } TreeNode.prototype.unsubscribe = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - if (this.isParameter() && this.isStream()) { - this.contents.subscribers.delete(callback); + if (callback != null && this.isParameter() && this.isStream()) { + this.contents._subscribers.delete(callback); } return this.getTreeBranch(new Command(COMMAND_UNSUBSCRIBE)); } @@ -342,12 +317,8 @@ _getElementByPath = function(children, pathArray, path) { TreeNode.prototype.toJSON = function() { let res = {}; const node = this; - if (node.number) { - res.number = node.number; - } - if (node.path) { - res.path = node.path; - } + res.number = node.getNumber(); + res.path = node.getPath(); if (node.contents) { for(let prop in node.contents) { if (node.contents.hasOwnProperty(prop)) { @@ -460,26 +431,7 @@ TreeNode.prototype.getElement = function(id) { } TreeNode.prototype.update = function(other) { - var self=this; - var callbacks = []; - - while(self._directoryCallbacks.length > 0) { - (function(cb) { - callbacks.push(() => { - cb(null, self) - }); - })(self._directoryCallbacks.shift()); - } - - for(var i=0; i { - cb(self) - }); - })(self._callbacks[i]); - } - - return callbacks; + return; } TreeNode.prototype.getNodeByPath = function(client, path, callback) { @@ -490,7 +442,6 @@ TreeNode.prototype.getNodeByPath = function(client, path, callback) { return; } - var child = self.getElement(path[0]); if(child !== null) { child.getNodeByPath(client, path.slice(1), callback); @@ -649,7 +600,7 @@ QualifiedNode.prototype.getMinimal = function(complete = false) { } QualifiedNode.prototype.update = function(other) { - callbacks = QualifiedNode.super_.prototype.update.apply(this); + QualifiedNode.super_.prototype.update.apply(this); if((other !== undefined) && (other.contents !== undefined)) { if (this.contents == null) { this.contents = other.contents; @@ -662,49 +613,46 @@ QualifiedNode.prototype.update = function(other) { } } } - return callbacks; + return; } -function QualifiedNodeCommand(self, cmd, callback) { +function QualifiedNodeCommand(self, cmd) { var r = new Root(); var qn = new QualifiedNode(); qn.path = self.path; r.addElement(qn); qn.addChild(new Command(cmd)); - if(callback !== undefined) { - self._directoryCallbacks.push((error, node) => { callback(error, node) }); - } return r; } QualifiedNode.prototype.getDirectory = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } if (this.path === undefined) { throw new Error("Invalid path"); } - return QualifiedNodeCommand(this, COMMAND_GETDIRECTORY, callback) + if (callback != null && this.contents != null && !this.isStream()) { + this.contents._subscribers.add(callback); + } + return QualifiedNodeCommand(this, COMMAND_GETDIRECTORY) } QualifiedNode.prototype.subscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } - if (this.isStream()) { - this.contents.subscribers.add(callback); + if (callback != null && this.isStream()) { + this.contents._subscribers.add(callback); } - return QualifiedNodeCommand(this, COMMAND_SUBSCRIBE, callback) + return QualifiedNodeCommand(this, COMMAND_SUBSCRIBE) } QualifiedNode.prototype.unsubscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } - if (this.isStream()) { - this.contents.subscribers.delete(callback); + if (callback != null && this.isStream()) { + this.contents._subscribers.delete(callback); } - return QualifiedNodeCommand(this, COMMAND_UNSUBSCRIBE, callback) + return QualifiedNodeCommand(this, COMMAND_UNSUBSCRIBE) } QualifiedNode.prototype.encode = function(ber) { @@ -810,7 +758,7 @@ Node.prototype.toQualified = function() { } Node.prototype.update = function(other) { - callbacks = Node.super_.prototype.update.apply(this); + Node.super_.prototype.update.apply(this); if ((other !== undefined) && (other.contents !== undefined)) { if (this.contents == null) { this.contents = other.contents; @@ -823,15 +771,12 @@ Node.prototype.update = function(other) { } } } - return callbacks; + return; } Node.prototype.subscribe = function(callback) { - if(this._callbacks.indexOf(callback) < 0) { - this._callbacks.push(callback); - } - if (this.isStream()) { - this.contents.subscribers.add(callback); + if (callback != null && this.isStream()) { + this.contents._subscribers.add(callback); } } @@ -1136,9 +1081,9 @@ MatrixNode.disconnectSources = function(matrix, targetID, sources) { } MatrixNode.prototype.update = function(other) { - callbacks = MatrixNode.super_.prototype.update.apply(this); + MatrixNode.super_.prototype.update.apply(this); MatrixUpdate(this, other); - return callbacks; + return; } MatrixNode.prototype.toQualified = function() { @@ -1706,51 +1651,48 @@ function MatrixUpdate(matrix, newMatrix) { } QualifiedMatrix.prototype.update = function(other) { - callbacks = QualifiedMatrix.super_.prototype.update.apply(this); + QualifiedMatrix.super_.prototype.update.apply(this); MatrixUpdate(this, other); - return callbacks; + return; } -function QualifiedMatrixCommand(self, cmd, callback) { +function QualifiedMatrixCommand(self, cmd) { var r = new Root(); var qn = new QualifiedMatrix(); qn.path = self.path; r.addElement(qn); qn.addChild(new Command(cmd)); - if(callback !== undefined) { - self._directoryCallbacks.push((error, node) => { callback(error, node) }); - } return r; } QualifiedMatrix.prototype.getDirectory = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } if (this.path === undefined) { throw new Error("Invalid path"); } - return QualifiedMatrixCommand(this, COMMAND_GETDIRECTORY, callback); + if (callback != null && !this.isStream()) { + this.contents._subscribers.add(callback); + } + return QualifiedMatrixCommand(this, COMMAND_GETDIRECTORY); } QualifiedMatrix.prototype.subscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } - if (this.isStream()) { - this.contents.subscribers.add(callback); + if (callback != null && this.isStream()) { + this.contents._subscribers.add(callback); } - return QualifiedMatrixCommand(this, COMMAND_SUBSCRIBE, callback); + return QualifiedMatrixCommand(this, COMMAND_SUBSCRIBE); } QualifiedMatrix.prototype.unsubscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } - if (this.isStream()) { - this.contents.subscribers.delete(callback); + if (callback != null && this.isStream()) { + this.contents._subscribers.delete(callback); } - return QualifiedMatrixCommand(this, COMMAND_UNSUBSCRIBE, callback); + return QualifiedMatrixCommand(this, COMMAND_UNSUBSCRIBE); } QualifiedMatrix.prototype.connect = function(connections) { @@ -2042,7 +1984,7 @@ QualifiedFunction.decode = function(ber) { } QualifiedFunction.prototype.update = function(other) { - callbacks = QualifiedFunction.super_.prototype.update.apply(this); + QualifiedFunction.super_.prototype.update.apply(this); if ((other !== undefined) && (other.contents !== undefined)) { if (this.contents == null) { this.contents = other.contents; @@ -2055,54 +1997,48 @@ QualifiedFunction.prototype.update = function(other) { } } } - return callbacks; + return; } -function QualifiedFunctionCommand(self, cmd, callback) { +function QualifiedFunctionCommand(self, cmd) { var r = new Root(); var qf = new QualifiedFunction(); qf.path = self.path; r.addElement(qf); qf.addChild(new Command(cmd)); - if(callback !== undefined) { - self._directoryCallbacks.push((error, node) => { callback(error, node) }); - } return r; } -QualifiedFunction.prototype.invoke = function(params, callback) { +QualifiedFunction.prototype.invoke = function(params) { if (this.path === undefined) { throw new Error("Invalid path"); } - var QualifiedFunctionNode = QualifiedFunctionCommand(this, COMMAND_INVOKE, callback); + var QualifiedFunctionNode = QualifiedFunctionCommand(this, COMMAND_INVOKE); var invocation = new Invocation() invocation.arguments = params QualifiedFunctionNode.elements[0].children[0].invocation = invocation return QualifiedFunctionNode } -QualifiedFunction.prototype.getDirectory = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } +QualifiedFunction.prototype.getDirectory = function() { if (this.path === undefined) { throw new Error("Invalid path"); - } - return QualifiedFunctionCommand(this, COMMAND_GETDIRECTORY, callback); + } + return QualifiedFunctionCommand(this, COMMAND_GETDIRECTORY); } QualifiedFunction.prototype.subscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } - return QualifiedFunctionCommand(this, COMMAND_SUBSCRIBE, callback); + return QualifiedFunctionCommand(this, COMMAND_SUBSCRIBE); } QualifiedFunction.prototype.unsubscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } - return QualifiedFunctionCommand(this, COMMAND_UNSUBSCRIBE, callback); + return QualifiedFunctionCommand(this, COMMAND_UNSUBSCRIBE); } QualifiedFunction.prototype.encode = function(ber) { @@ -2208,18 +2144,14 @@ Function.prototype.toQualified = function() { return qf; } -Function.prototype.invoke = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - +Function.prototype.invoke = function() { return this.getTreeBranch(undefined, (m) => { m.addChild(new Command(COMMAND_INVOKE)) }); } Function.prototype.update = function(other) { - callbacks = Function.super_.prototype.update.apply(this); + Function.super_.prototype.update.apply(this); if ((other !== undefined) && (other.contents !== undefined)) { if (this.contents == null) { this.contents = other.contents; @@ -2232,7 +2164,7 @@ Function.prototype.update = function(other) { } } } - return callbacks; + return; } module.exports.Function = Function; @@ -2244,6 +2176,7 @@ module.exports.Function = Function; function NodeContents() { this.isOnline = true; + this._subscribers = new Set(); }; @@ -2635,72 +2568,68 @@ QualifiedParameter.prototype.encode = function(ber) { } QualifiedParameter.prototype.update = function(other) { - callbacks = QualifiedParameter.super_.prototype.update.apply(this); + QualifiedParameter.super_.prototype.update.apply(this); if ((other !== undefined) && (other.contents !== undefined)) { if (this.contents == null) { this.contents = other.contents; } else { for (var key in other.contents) { + if (key[0] === "_") { + continue; + } if (other.contents.hasOwnProperty(key)) { this.contents[key] = other.contents[key]; } } - for(let cb of this.contents.subscribers) { - cb(); + for(let cb of this.contents._subscribers) { + cb(this); } } } - return callbacks; + return; } -function QualifiedParameterCommand(self, cmd, callback) { +function QualifiedParameterCommand(self, cmd) { let r = new Root(); let qp = new QualifiedParameter(); qp.path = self.path; r.addElement(qp); qp.addChild(new Command(cmd)); - if(callback !== undefined) { - self._directoryCallbacks.push((error, node) => { callback(error, node) }); - } return r; } QualifiedParameter.prototype.getDirectory = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } if (this.path === undefined) { throw new Error("Invalid path"); } - return QualifiedParameterCommand(this, COMMAND_GETDIRECTORY, callback); + if (callback != null && !this.isStream()) { + this.contents._subscribers.add(callback); + } + return QualifiedParameterCommand(this, COMMAND_GETDIRECTORY); } QualifiedParameter.prototype.subscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } - if (this.isStream()) { - this.contents.subscribers.add(callback); + if (callback != null && this.isStream()) { + this.contents._subscribers.add(callback); } - return QualifiedParameterCommand(this, COMMAND_SUBSCRIBE, callback); + return QualifiedParameterCommand(this, COMMAND_SUBSCRIBE); } QualifiedParameter.prototype.unsubscribe = function(callback) { if (this.path === undefined) { throw new Error("Invalid path"); } - if (this.isStream()) { - this.contents.subscribers.delete(callback); + if (callback != null && this.isStream()) { + this.contents._subscribers.delete(callback); } - return QualifiedParameterCommand(this, COMMAND_UNSUBSCRIBE, callback); + return QualifiedParameterCommand(this, COMMAND_UNSUBSCRIBE); } -QualifiedParameter.prototype.setValue = function(value, callback) { - if(callback !== undefined) { - this._directoryCallbacks.push(callback); - } - +QualifiedParameter.prototype.setValue = function(value) { let r = new Root(); let qp = new QualifiedParameter(this.path); r.addElement(qp); @@ -2774,11 +2703,7 @@ Parameter.prototype.encode = function(ber) { ber.endSequence(); } -Parameter.prototype.setValue = function(value, callback) { - if(callback !== undefined) { - this._directoryCallbacks.push(callback); - } - +Parameter.prototype.setValue = function(value) { return this.getTreeBranch(undefined, (m) => { m.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); }); @@ -2791,23 +2716,24 @@ Parameter.prototype.toQualified = function() { } Parameter.prototype.update = function(other) { - callbacks = Parameter.super_.prototype.update.apply(this); + Parameter.super_.prototype.update.apply(this); if ((other !== undefined) && (other.contents !== undefined)) { if (this.contents == null) { this.contents = other.contents; } else { for (var key in other.contents) { + if (key[0] === "_") { continue; } if (other.contents.hasOwnProperty(key)) { this.contents[key] = other.contents[key]; } } - for(let cb of this.contents.subscribers) { - cb(); + for(let cb of this.contents._subscribers) { + cb(this); } } } - return callbacks; + return; } @@ -2869,7 +2795,7 @@ module.exports.ParameterAccess = ParameterAccess; module.exports.ParameterType = ParameterType; function ParameterContents(value, type) { - this.subscribers = new Set(); + this._subscribers = new Set(); if(value !== undefined) { this.value = value; } diff --git a/package.json b/package.json index 60508df..b2025bb 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.11.8", + "version": "1.12.0", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/server.js b/server.js index 43e86db..4d26789 100755 --- a/server.js +++ b/server.js @@ -575,21 +575,27 @@ TreeServer.prototype.unsubscribe = function(client, element) { TreeServer.prototype.setValue = function(element, value, origin, key) { return new Promise((resolve, reject) => { // Change the element value if write access permitted. - if (element.contents !== undefined) { - if (element.isParameter()) { - if ((element.contents.access !== undefined) && - (element.contents.access.value > 1)) { - element.contents.value = value; - this.emit("value-change", element); - } + if (element.contents == null) { + return resolve(); + } + if (element.isParameter()) { + if ((element.contents.access !== undefined) && + (element.contents.access.value > 1)) { + element.contents.value = value; + const res = this.getResponse(element); + this.updateSubscribers(element.getPath(),res, origin); + this.emit("value-change", element); } - else if (element.isMatrix()) { - if ((key !== undefined) && (element.contents.hasOwnProperty(key))) { - element.contents[key] = value; - this.emit("value-change", element); - } + } + else if (element.isMatrix()) { + if ((key !== undefined) && (element.contents.hasOwnProperty(key))) { + element.contents[key] = value; + const res = this.getResponse(element); + this.updateSubscribers(element.getPath(),res, origin); + this.emit("value-change", element); } } + return resolve(); }); } diff --git a/test/Server.test.js b/test/Server.test.js index 69050ac..898ef55 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -124,45 +124,6 @@ describe("server", function() { }); }); - it("should be able to get child with getNodeByPath", function() { - //server._debug = true; - client = new DeviceTree(LOCALHOST, PORT); - //client._debug = true; - //client._debug = true; - return Promise.resolve() - .then(() => client.connect()) - .then(() => { - console.log("client connected"); - return client.getDirectory(); - }) - .then(() => { - return new Promise((resolve, reject) => { - client.root.getNodeByPath(client.client, ["scoreMaster", "identity", "product"], (err, child) => { - if (err) { reject(err) } - else { - resolve(child); - } - }); - }); - }) - .then(child => { - console.log(child); - }) - .then(() => { - return new Promise((resolve, reject) => { - client.root.getNodeByPath(client.client, ["scoreMaster", "router", "labels"], (err, child) => { - if (err) { reject(err) } - else { - resolve(child); - } - }); - }); - }) - .then(child => { - console.log(child); - return client.disconnect(); - }); - }); it("should be able to get child with tree.getNodeByPath", function() { //server._debug = true; client = new DeviceTree(LOCALHOST, PORT); @@ -464,8 +425,8 @@ describe("server", function() { .then(parameter => { console.log(parameter); expect(server.subscribers["0.0.2"]).not.toBeDefined(); - expect(parameter.contents.subscribers).toBeDefined(); - expect(parameter.contents.subscribers.size).toBe(0); + expect(parameter.contents._subscribers).toBeDefined(); + expect(parameter.contents._subscribers.size).toBe(0); server._subscribe = server.subscribe; let _resolve; const p = new Promise((resolve, reject) => { @@ -483,8 +444,8 @@ describe("server", function() { return client.getNodeByPathnum("0.0.2"); }) .then(parameter => { - expect(parameter.contents.subscribers).toBeDefined(); - expect(parameter.contents.subscribers.size).toBe(1); + expect(parameter.contents._subscribers).toBeDefined(); + expect(parameter.contents._subscribers.size).toBe(1); server._unsubscribe = server.unsubscribe; let _resolve; const p = new Promise((resolve, reject) => { @@ -507,8 +468,8 @@ describe("server", function() { console.log(parameter); expect(server.subscribers["0.0.2"]).toBeDefined(); expect(server.subscribers["0.0.2"].size).toBe(0); - expect(parameter.contents.subscribers).toBeDefined(); - expect(parameter.contents.subscribers.size).toBe(0); + expect(parameter.contents._subscribers).toBeDefined(); + expect(parameter.contents._subscribers.size).toBe(0); }) .then(() => client.disconnect()); }); diff --git a/test/utils.js b/test/utils.js index 2324391..5393953 100755 --- a/test/utils.js +++ b/test/utils.js @@ -41,7 +41,7 @@ const init = function(_src,_tgt) { children: [ {identifier: "product", value: "S-CORE Master"}, {identifier: "company", value: "EVS", access: "readWrite"}, - {identifier: "version", value: "1.2.0", streamIdentifier: 1234567}, + {identifier: "version", value: "1.2.0", access: "readWrite", streamIdentifier: 1234567}, {identifier: "author", value: "g.dufour@evs.com"}, ] }, From f68f8347f05c500a8fd69365ece1cbfe403f2c18 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sun, 29 Dec 2019 18:04:00 +0100 Subject: [PATCH 33/64] Fix toJSON for root --- ember.js | 6 ++++-- package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ember.js b/ember.js index b1580a0..2201ae8 100755 --- a/ember.js +++ b/ember.js @@ -317,8 +317,10 @@ _getElementByPath = function(children, pathArray, path) { TreeNode.prototype.toJSON = function() { let res = {}; const node = this; - res.number = node.getNumber(); - res.path = node.getPath(); + if (!this.isRoot()) { + res.number = node.getNumber(); + res.path = node.getPath(); + } if (node.contents) { for(let prop in node.contents) { if (node.contents.hasOwnProperty(prop)) { diff --git a/package.json b/package.json index b2025bb..b1944fa 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "1.12.0", + "version": "1.12.1", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { From 46a35258fd0b9ce59a597cd85a225e3a5a24b40e Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sun, 5 Jan 2020 12:50:34 +0100 Subject: [PATCH 34/64] version 2 --- EmberLib/Command.js | 107 +++++ EmberLib/Elements.js | 8 + EmberLib/Function.js | 95 ++++ EmberLib/FunctionArgument.js | 79 ++++ EmberLib/FunctionContent.js | 99 ++++ EmberLib/Invocation.js | 79 ++++ EmberLib/InvocationResult.js | 115 +++++ EmberLib/Label.js | 58 +++ EmberLib/Matrix.js | 436 +++++++++++++++++ EmberLib/MatrixConnection.js | 150 ++++++ EmberLib/MatrixContents.js | 151 ++++++ EmberLib/MatrixDisposition.js | 18 + EmberLib/MatrixMode.js | 8 + EmberLib/MatrixNode.js | 110 +++++ EmberLib/MatrixOperation.js | 16 + EmberLib/MatrixType.js | 10 + EmberLib/Node.js | 90 ++++ EmberLib/NodeContents.js | 80 ++++ EmberLib/Parameter.js | 115 +++++ EmberLib/ParameterAccess.js | 12 + EmberLib/ParameterContents.js | 120 +++++ EmberLib/ParameterType.js | 52 +++ EmberLib/QualifiedFunction.js | 133 ++++++ EmberLib/QualifiedMatrix.js | 161 +++++++ EmberLib/QualifiedNode.js | 144 ++++++ EmberLib/QualifiedParameter.js | 186 ++++++++ EmberLib/TreeNode.js | 497 ++++++++++++++++++++ EmberLib/constants.js | 15 + EmberLib/index.js | 164 +++++++ EmberServer.js | 827 +++++++++++++++++++++++++++++++++ client.js | 6 +- device.js | 30 +- ember.js | 94 ++-- package.json | 2 +- server.js | 13 +- test/Ember.test.js | 3 +- 36 files changed, 4222 insertions(+), 61 deletions(-) create mode 100755 EmberLib/Command.js create mode 100755 EmberLib/Elements.js create mode 100755 EmberLib/Function.js create mode 100755 EmberLib/FunctionArgument.js create mode 100755 EmberLib/FunctionContent.js create mode 100755 EmberLib/Invocation.js create mode 100755 EmberLib/InvocationResult.js create mode 100755 EmberLib/Label.js create mode 100755 EmberLib/Matrix.js create mode 100755 EmberLib/MatrixConnection.js create mode 100755 EmberLib/MatrixContents.js create mode 100755 EmberLib/MatrixDisposition.js create mode 100755 EmberLib/MatrixMode.js create mode 100755 EmberLib/MatrixNode.js create mode 100755 EmberLib/MatrixOperation.js create mode 100755 EmberLib/MatrixType.js create mode 100755 EmberLib/Node.js create mode 100755 EmberLib/NodeContents.js create mode 100755 EmberLib/Parameter.js create mode 100755 EmberLib/ParameterAccess.js create mode 100755 EmberLib/ParameterContents.js create mode 100755 EmberLib/ParameterType.js create mode 100755 EmberLib/QualifiedFunction.js create mode 100755 EmberLib/QualifiedMatrix.js create mode 100755 EmberLib/QualifiedNode.js create mode 100755 EmberLib/QualifiedParameter.js create mode 100755 EmberLib/TreeNode.js create mode 100755 EmberLib/constants.js create mode 100755 EmberLib/index.js create mode 100755 EmberServer.js diff --git a/EmberLib/Command.js b/EmberLib/Command.js new file mode 100755 index 0000000..0ee5b77 --- /dev/null +++ b/EmberLib/Command.js @@ -0,0 +1,107 @@ +"use strict"; +const Enum = require('enum'); +const {COMMAND_GETDIRECTORY, COMMAND_INVOKE} = require("./constants"); +const BER = require('../ber.js'); + +const FieldFlags = new Enum({ + sparse: -2, + all: -1, + default: 0, + identifier: 1, + description: 2, + tree: 3, + value: 4, + connections: 5 +}); + +class Command { + constructor(number) { + if(number !== undefined) + this.number = number; + if(number == COMMAND_GETDIRECTORY) { + this.fieldFlags = FieldFlags.all; + } + } + + isCommand() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(2)); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.number); + ber.endSequence(); // BER.CONTEXT(0) + + if (this.number === COMMAND_GETDIRECTORY && this.fieldFlags) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeInt(this.fieldFlags.value); + ber.endSequence(); + } + + if (this.number === COMMAND_INVOKE && this.invocation) { + ber.startSequence(BER.CONTEXT(2)); + this.invocation.encode(ber); + ber.endSequence(); + } + // TODO: options + + ber.endSequence(); // BER.APPLICATION(2) + } + + /** + * @returns {number} + */ + getNumber() { + return this.number; + } + + /** + * + */ + toJSON() { + return { + number: this.number, + fieldFlags: this.fieldFlags, + invocation: this.invocation == null ? null : this.invocation.toJSON() + }; + } + + /** + * + * @param {BER} ber + * @returns {Command} + */ + static decode(ber) { + var c = new Command(); + ber = ber.getSequence(BER.APPLICATION(2)); + + while(ber.remain > 0) { + var tag = ber.peek(); + var seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + c.number = seq.readInt(); + } + else if(tag == BER.CONTEXT(1)) { + c.fieldFlags = FieldFlags.get(seq.readInt()); + } + else if(tag == BER.CONTEXT(2)) { + c.invocation = Invocation.decode(seq); + } + else { + // TODO: options + throw new errors.UnimplementedEmberTypeError(tag); + } + } + + return c; + } + +} + +module.exports = Command; \ No newline at end of file diff --git a/EmberLib/Elements.js b/EmberLib/Elements.js new file mode 100755 index 0000000..1826ae5 --- /dev/null +++ b/EmberLib/Elements.js @@ -0,0 +1,8 @@ +"use stricts"; + +class Elements { + constructor() { + this._elements = new Map(); + } + +} \ No newline at end of file diff --git a/EmberLib/Function.js b/EmberLib/Function.js new file mode 100755 index 0000000..ee53b7a --- /dev/null +++ b/EmberLib/Function.js @@ -0,0 +1,95 @@ +"use strict"; +const TreeNode = require("./TreeNode"); +const QualifiedFunction = require("./QualifiedFunction"); +const BER = require('../ber.js'); + +class Function extends TreeNode { + constructor(number, func) { + super(); + this.number = number; + this.func = func; + } + + isFunction() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(19)); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.number); + ber.endSequence(); // BER.CONTEXT(0) + + if(this.contents !== undefined) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + if(this.children !== undefined) { + ber.startSequence(BER.CONTEXT(2)); + ber.startSequence(BER.APPLICATION(4)); + for(var i=0; i { + m.addChild(new Command(COMMAND_INVOKE)) + }); + } + + /** + * @returns {QualifiedFunction} + */ + toQualified() { + const qf = new QualifiedFunction(this.getPath()); + qf.update(this); + return qf; + } + + + /** + * + * @param {BER} ber + * @returns {Function} + */ + static decode(ber) { + const f = new Function(); + ber = ber.getSequence(BER.APPLICATION(19)); + + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + f.number = seq.readInt(); + } else if(tag == BER.CONTEXT(1)) { + f.contents = FunctionContent.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + f.decodeChildren(seq); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + if (DEBUG) { console.log("Function", f); } + return f; + } +} + +module.exports = Function; \ No newline at end of file diff --git a/EmberLib/FunctionArgument.js b/EmberLib/FunctionArgument.js new file mode 100755 index 0000000..216e402 --- /dev/null +++ b/EmberLib/FunctionArgument.js @@ -0,0 +1,79 @@ +"use strict"; +const BER = require('../ber.js'); +const {ParameterType} = require("./ParameterType"); + +/* +TupleDescription ::= + SEQUENCE OF [0] TupleItemDescription +TupleItemDescription ::= + [APPLICATION 21] IMPLICIT + SEQUENCE { + type [0] ParameterType, + name [1] EmberString OPTIONAL + } +Invocation ::= + [APPLICATION 22] IMPLICIT + SEQUENCE { + invocationId [0] Integer32 OPTIONAL, + arguments [1] Tuple OPTIONAL + } +Tuple ::= + SEQUENCE OF [0] Value +*/ + +class FunctionArgument { + /** + * + * @param {ParameterType} type + * @param {number|string|null} value + * @param {string|null} name + */ + constructor (type = null, value = null, name = null) { + /** @type {ParameterType} */ + this.type = type; + this.value = value; + this.name = name; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(21)); + if (this.type != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.type.value); + ber.endSequence(); + } + if (this.name != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.name, BER.EMBER_STRING); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * + * @param {BER} ber + * @returns {FunctionArgument} + */ + static decode(ber) { + const tuple = new FunctionArgument(); + ber = ber.getSequence(BER.APPLICATION(21)); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if (tag === BER.CONTEXT(0)) { + tuple.type = ParameterType.get(seq.readInt()); + } + else if (tag === BER.CONTEXT(1)) { + tuple.name = seq.readString(BER.EMBER_STRING); + } + } + return tuple; + } +} + +module.exports = FunctionArgument; \ No newline at end of file diff --git a/EmberLib/FunctionContent.js b/EmberLib/FunctionContent.js new file mode 100755 index 0000000..aab06ee --- /dev/null +++ b/EmberLib/FunctionContent.js @@ -0,0 +1,99 @@ +"use strict"; +const BER = require('../ber.js'); +const FunctionArgument = require("./FunctionArgument"); + +class FunctionContent { + constructor() { + this.arguments = []; + this.result = []; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.EMBER_SET); + + if(this.identifier != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeString(this.identifier, BER.EMBER_STRING); + ber.endSequence(); // BER.CONTEXT(0) + } + + if(this.description != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.description, BER.EMBER_STRING); + ber.endSequence(); // BER.CONTEXT(1) + } + + if(this.arguments != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.startSequence(BER.EMBER_SEQUENCE); + for(var i = 0; i < this.arguments.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + this.arguments[i].encode(ber); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); // BER.CONTEXT(2) + } + + if(this.result != null) { + ber.startSequence(BER.CONTEXT(3)); + ber.startSequence(BER.EMBER_SEQUENCE); + for(var i = 0; i < this.result.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + this.result[i].encode(ber); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); // BER.CONTEXT(3) + } + + ber.endSequence(); // BER.EMBER_SET + } + + /** + * + * @param {BER} ber + * @returns {FunctionContent} + */ + static decode(ber) { + const fc = new FunctionContent(); + ber = ber.getSequence(BER.EMBER_SET); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + fc.identifier = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(1)) { + fc.description = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(2)) { + fc.arguments = []; + let dataSeq = seq.getSequence(BER.EMBER_SEQUENCE); + while(dataSeq.remain > 0) { + seq = dataSeq.getSequence(BER.CONTEXT(0)); + fc.arguments.push(FunctionArgument.decode(seq)); + } + } else if(tag == BER.CONTEXT(3)) { + fc.result = []; + while(seq.remain > 0) { + tag = seq.peek(); + let dataSeq = seq.getSequence(tag); + if (tag === BER.CONTEXT(0)) { + fc.result.push(FunctionArgument.decode(dataSeq)); + } + } + } else if(tag == BER.CONTEXT(4)) { + fc.templateReference = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + + return fc; + } +} + +module.exports = FunctionContent; \ No newline at end of file diff --git a/EmberLib/Invocation.js b/EmberLib/Invocation.js new file mode 100755 index 0000000..b7bf4f9 --- /dev/null +++ b/EmberLib/Invocation.js @@ -0,0 +1,79 @@ +"use strict"; +const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); +const BER = require('../ber.js'); + +let _id = 1; +class Invocation { + constructor(id = null) { + this.id = id == null ? _id++ : id; + this.arguments = []; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(22)); + // ber.startSequence(BER.EMBER_SEQUENCE); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.id) + ber.endSequence(); + + ber.startSequence(BER.CONTEXT(1)); + ber.startSequence(BER.EMBER_SEQUENCE); + for(var i = 0; i < this.arguments.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeValue( + this.arguments[i].value, + ParameterTypetoBERTAG(this.arguments[i].type + )); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); + + ber.endSequence(); // BER.APPLICATION(22) + } + + /** + * + * @param {BER} ber + * @returns {Invocation} + */ + static decode(ber) { + const invocation = null; + ber = ber.getSequence(BER.APPLICATION(22)); + while(ber.remain > 0) { + var tag = ber.peek(); + var seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + const invocationId = seq.readInt(); + invocation = new Invocation(invocationId); + } + else if(tag == BER.CONTEXT(1)) { + if (invocation == null) { + throw new Error("Missing invocationID"); + } + invocation.arguments = []; + seq = seq.getSequence(BER.EMBER_SEQUENCE); + while(seq.remain > 0) { + const dataSeq = seq.getSequence(BER.CONTEXT(0)); + tag = dataSeq.peek(); + const val = dataSeq.readValue(); + invocation.arguments.push( + new FunctionArgument(ParameterTypefromBERTAG(tag), val) + ); + } + } + else { + // TODO: options + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return invocation; + } +} + +module.exports = Invocation; \ No newline at end of file diff --git a/EmberLib/InvocationResult.js b/EmberLib/InvocationResult.js new file mode 100755 index 0000000..6fae4b5 --- /dev/null +++ b/EmberLib/InvocationResult.js @@ -0,0 +1,115 @@ +"use strict"; + +const BER = require('../ber.js'); + + +class InvocationResult { + /** + * + * @param {number|null} invocationId=null + */ + constructor(invocationId = null) { + this.invocationId = invocationId; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(23)); + if (this.invocationId != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.invocationId); + ber.endSequence(); + } + if (this.success != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeBoolean(this.success); + ber.endSequence(); + } + if (this.result != null && this.result.length) { + ber.startSequence(BER.CONTEXT(2)); + ber.startSequence(BER.EMBER_SEQUENCE); + for (let i = 0; i < this.result.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeValue(this.result[i].value, ParameterTypetoBERTAG(this.result[i].type)); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); + } + ber.endSequence(); // BER.APPLICATION(23)} + } + + /** + * + */ + setFailure() { + this.success = false; + } + + /** + * + */ + setSuccess() { + this.success = true; + } + + /** + * + * @param {} result + */ + setResult(result) { + if (!Array.isArray(result)) { + throw new Error("Invalid inovation result. Should be array"); + } + this.result = result; + } + + toQualified() { + return this; + } + + /** + * + * @param {BER} ber + * @returns {InvocationResult} + */ + static decode(ber) { + const invocationResult = new InvocationResult(); + ber = ber.getSequence(BER.APPLICATION(23)); + while(ber.remain > 0) { + tag = ber.peek(); + var seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { // invocationId + invocationResult.invocationId = seq.readInt(); + } else if(tag == BER.CONTEXT(1)) { // success + invocationResult.success = seq.readBoolean() + }else if(tag == BER.CONTEXT(2)) { + invocationResult.result = []; + let res = seq.getSequence(BER.EMBER_SEQUENCE); + while(res.remain > 0) { + tag = res.peek(); + if (tag === BER.CONTEXT(0)) { + let resTag = res.getSequence(BER.CONTEXT(0)); + tag = resTag.peek(); + invocationResult.result.push( + new FunctionArgument( + ParameterTypefromBERTAG(tag), + resTag.readValue() + )); + } + } + continue + } else { + // TODO: options + throw new errors.UnimplementedEmberTypeError(tag); + } + } + + return invocationResult; + } +} + +module.exports = InvocationResult; \ No newline at end of file diff --git a/EmberLib/Label.js b/EmberLib/Label.js new file mode 100755 index 0000000..d8fecde --- /dev/null +++ b/EmberLib/Label.js @@ -0,0 +1,58 @@ +"use strict"; + +class Label { + constructor(path, description) { + if (path) { + this.basePath = path; + } + if (description) { + this.description = description; + } + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(18)); + if (this.basePath != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeRelativeOID(this.basePath, BER.EMBER_RELATIVE_OID); + ber.endSequence(); + } + if (this.description != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.description, BER.EMBER_STRING); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * + * @param {BER} ber + * @returns {Label} + */ + static decode(ber) { + var l = new Label(); + + ber = ber.getSequence(BER.APPLICATION(18)); + + while (ber.remain > 0) { + var tag = ber.peek(); + var seq = ber.getSequence(tag); + if (tag == BER.CONTEXT(0)) { + l.basePath = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); + } else if (tag == BER.CONTEXT(1)) { + l.description = seq.readString(BER.EMBER_STRING); + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return l; + } +} + +module.exports = Label; \ No newline at end of file diff --git a/EmberLib/Matrix.js b/EmberLib/Matrix.js new file mode 100755 index 0000000..e697b74 --- /dev/null +++ b/EmberLib/Matrix.js @@ -0,0 +1,436 @@ +"use strict"; +const MatrixConnection = require("./MatrixConnection"); +const TreeNode = require("./TreeNode"); +const BER = require('../ber.js'); + +class Matrix extends TreeNode +{ + constructor() { + super(); + this._connectedSources = {}; + this._numConnections = 0; + } + + isMatrix() { + return true; + } + + /** + * + * @param {number} targetID + * @param {number[]} sources + * @param {Operation} operation + * @returns {boolean} + */ + canConnect(targetID, sources, operation) { + return Matrix.canConnect(this, targetID, sources, operation); + } + + /** + * + * @param {Object} connections + * @returns {root} + */ + connect(connections) { + const r = this.getTreeBranch(); + const m = r.getElementByPath(this.getPath()); + m.connections = connections; + return r; + } + + /** + * + * @param {number} targetID + * @param {number[]} sources + */ + connectSources(targetID, sources) { + return Matrix.connectSources(this, targetID, sources); + } + + /** + * + * @param {number} targetID + * @param {number[]} sources + */ + disconnectSources(targetID, sources) { + return Matrix.disconnectSources(this, targetID, sources); + } + + /** + * + * @param {BER} ber + */ + encodeConnections(ber) { + if (this.connections !== undefined) { + ber.startSequence(BER.CONTEXT(5)); + ber.startSequence(BER.EMBER_SEQUENCE); + + for(var id in this.connections) { + if (this.connections.hasOwnProperty(id)) { + ber.startSequence(BER.CONTEXT(0)); + this.connections[id].encode(ber); + ber.endSequence(); + } + } + ber.endSequence(); + ber.endSequence(); + } + } + + /** + * + * @param {BER} ber + */ + encodeSources(ber) { + if (this.sources != null) { + ber.startSequence(BER.CONTEXT(4)); + ber.startSequence(BER.EMBER_SEQUENCE); + + for(var i=0; i Number(i))); + + if (matrixNode.connections[targetID].isLocked()) { + return false; + } + if (type === MatrixType.oneToN && + matrixNode.contents.maximumConnectsPerTarget == null && + matrixNode.contents.maximumConnectsPerTarget == null) { + return sMap.size < 2; + } + else if (type === MatrixType.oneToN && sMap.size >= 2) { + return false; + } + else if (type === MatrixType.oneToOne) { + if (sMap.size > 1) { + return false; + } + const sourceConnections = matrixNode._connectedSources[sources[0]]; + return sourceConnections == null || sourceConnections.size === 0 || sourceConnections.has(targetID); + } + else { + // N to N + if (matrixNode.contents.maximumConnectsPerTarget != null && + newSources.length > matrixNode.contents.maximumConnectsPerTarget) { + return false; + } + if (matrixNode.contents.maximumTotalConnects != null) { + let count = matrixNode._numConnections; + if (oldSources) { + count -= oldSources.length; + } + if (newSources) { + count += newSources.length; + } + return count <= matrixNode.contents.maximumTotalConnects; + } + + } + return true; + } + + /** + * + * @param {MatrixNode} matrixNode + * @param {number} targetID + * @param {number[]} sources + */ + static connectSources(matrix, targetID, sources) { + const target = Number(targetID); + if (matrix.connections[target] == null) { + matrix.connections[target] = new MatrixConnection(target); + } + matrix.connections[target].connectSources(sources); + if (sources != null) { + for(let source of sources) { + if (matrix._connectedSources[source] == null) { + matrix._connectedSources[source] = new Set(); + } + if (!matrix._connectedSources[source].has(target)) { + matrix._connectedSources[source].add(target); + matrix._numConnections++; + } + } + } + } + + /** + * + * @param {BER} ber + * @returns {number[]} + */ + static decodeTargets(ber) { + const targets = []; + ber = ber.getSequence(BER.EMBER_SEQUENCE); + while(ber.remain > 0) { + var seq = ber.getSequence(BER.CONTEXT(0)); + seq = seq.getSequence(BER.APPLICATION(14)); + seq = seq.getSequence(BER.CONTEXT(0)); + targets.push(seq.readInt()); + } + return targets; + } + + /** + * + * @param {BER} ber + * @returns {number[]} + */ + static decodeSources(ber) { + const sources = []; + ber = ber.getSequence(BER.EMBER_SEQUENCE); + while(ber.remain > 0) { + var seq = ber.getSequence(BER.CONTEXT(0)); + seq = seq.getSequence(BER.APPLICATION(15)); + seq = seq.getSequence(BER.CONTEXT(0)); + sources.push(seq.readInt()); + } + return sources; + }; + + /** + * + * @param {BER} ber + * @returns {Object} + */ + static decodeConnections(ber) { + let connections = {}; + let seq = ber.getSequence(BER.EMBER_SEQUENCE); + while(seq.remain > 0) { + var conSeq = seq.getSequence(BER.CONTEXT(0)); + var con = MatrixConnection.decode(conSeq); + if (con.target !== undefined) { + connections[con.target] = (con); + } + } + return connections; + } + + /** + * + * @param {MatrixNode} matrixNode + * @param {number} targetID + * @param {number[]} sources + */ + static disconnectSources(matrix, targetID, sources) { + const target = Number(targetID); + if (matrix.connections[target] == null) { + matrix.connections[target] = new MatrixConnection(target); + } + matrix.connections[target].disconnectSources(sources); + if (sources != null) { + for(let source of sources) { + if (matrix._connectedSources[source] == null) { + continue; + } + if (matrix._connectedSources[source].has(target)) { + matrix._connectedSources[source].delete(target); + matrix._numConnections--; + } + } + } + } + + /** + * + * @param {MatrixNode} matrix + * @param {number} source + */ + static getSourceConnections(matrix, source) { + const targets = matrix._connectedSources[source]; + if (targets) { + return [...targets]; + } + return []; + } + + /** + * + * @param {QualifiedMatrix|MatrixNode} matrix + * @param {QualifiedMatrix|MatrixNode} newMatrix + */ + static MatrixUpdate(matrix, newMatrix) { + if (newMatrix.targets != null) { + matrix.targets = newMatrix.targets; + } + if (newMatrix.sources != null) { + matrix.sources = newMatrix.sources; + } + if (newMatrix.connections != null) { + if (matrix.connections == null) { + matrix.connections = {}; + } + for(let id in newMatrix.connections) { + if (newMatrix.connections.hasOwnProperty(id)) { + let connection = newMatrix.connections[id]; + if ((connection.target < matrix.contents.targetCount) && + (connection.target >= 0)) { + if (matrix.connections[connection.target] == null) { + matrix.connections[connection.target] = new MatrixConnection(connection.target); + } + matrix.connections[connection.target].setSources(connection.sources); + } + } + } + } + } + + /** + * + * @param {MatrixNode} matrixNode + * @param {number} targetID + * @param {number[]} sources + */ + static setSources(matrix, targetID, sources) { + const currentSource = matrix.connections[targetID] == null || matrix.connections[targetID].sources == null ? + [] : matrix.connections[targetID].sources; + if (currentSource.length > 0) { + MatrixNode.disconnectSources(matrix, targetID, currentSource) + } + Matrix.connectSources(matrix, targetID, sources); + } + + /** + * + * @param {MatrixNode} matrixNode + * @param {number} targetID + * @param {number[]} sources + */ + static validateConnection(matrixNode, targetID, sources) { + if (targetID < 0) { + throw new Error(`Invalid negative target index ${targetID}`); + } + for(let i = 0; i < sources.length; i++) { + if (sources[i] < 0) { + throw new Error(`Invalid negative source at index ${i}`); + } + } + if (matrixNode.contents.mode === MatrixMode.linear) { + if (targetID >= matrixNode.contents.targetCount) { + throw new Error(`targetID ${targetID} higher than max value ${matrixNode.contents.targetCount}`); + } + for(let i = 0; i < sources.length; i++) { + if (sources[i] >= matrixNode.contents.sourceCount) { + throw new Error(`Invalid source at index ${i}`); + } + } + } + else if ((matrixNode.targets == null) || (matrixNode.sources == null)) { + throw new Error("Non-Linear matrix should have targets and sources"); + } + else { + let found = false; + for(let i = 0; i < matrixNode.targets; i++) { + if (matrixNode.targets[i] === targetID) { + found = true; + break; + } + } + if (!found) { + throw new Error(`Unknown targetid ${targetID}`); + } + found = false; + for(let i = 0; i < sources.length; i++) { + for(let j = 0; i < matrixNode.sources; j++) { + if (matrixNode.sources[j] === sources[i]) { + found = true; + break; + } + } + if (!found) { + throw new Error(`Unknown source at index ${i}`); + } + } + } + } +} + +module.exports = Matrix; \ No newline at end of file diff --git a/EmberLib/MatrixConnection.js b/EmberLib/MatrixConnection.js new file mode 100755 index 0000000..5bd90c1 --- /dev/null +++ b/EmberLib/MatrixConnection.js @@ -0,0 +1,150 @@ +"use strict"; + +class MatrixConnection { + /** + * + * @param {number} target + */ + constructor(target) { + if (target) { + let _target = Number(target); + if (isNaN(_target)) { _target = 0; } + this.target = _target; + } + else { + this.target = 0; + } + this._locked = false; + } + + /** + * + * @param {number[]} sources + */ + connectSources(sources) { + if (sources == null) { + return; + } + let s = new Set(this.sources); + for(let item of sources) { + s.add(item); + } + this.sources = [...s].sort(); + } + + /** + * + * @param {number[]} sources + */ + disconnectSources(sources) { + if (sources == null) { + return; + } + let s = new Set(this.sources); + for(let item of sources) { + s.delete(item); + } + this.sources = [...s].sort(); + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(16)); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.target); + ber.endSequence(); + + if ((this.sources !== undefined)&& (this.sources.length > 0)) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeRelativeOID(this.sources.join("."), BER.EMBER_RELATIVE_OID); + ber.endSequence(); + } + if (this.operation !== undefined) { + ber.startSequence(BER.CONTEXT(2)); + ber.writeInt(this.operation.value); + ber.endSequence(); + } + if (this.disposition !== undefined) { + ber.startSequence(BER.CONTEXT(3)); + ber.writeInt(this.disposition.value); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * @returns {boolean} + */ + isLocked() { + return this._locked; + } + + /** + * + */ + lock() { + this._locked = true; + } + + /** + * + * @param {number[]} sources + */ + setSources(sources) { + if (sources == null) { + delete this.sources; + return; + } + let s = new Set(sources.map(i => Number(i))); + this.sources = [...s].sort(); // sources should be an array + } + + /** + * + */ + unlock() { + this._locked = false; + } + + /** + * + * @param {BER} ber + * @returns {MatrixConnection} + */ + static decode(ber) { + const c = new MatrixConnection(); + ber = ber.getSequence(BER.APPLICATION(16)); + while (ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if (tag == BER.CONTEXT(0)) { + c.target = seq.readInt(); + } + else if (tag == BER.CONTEXT(1)) { + const sources = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); + if (sources === "") { + c .sources = []; + } + else { + c.sources = sources.split(".").map(i => Number(i)); + } + } else if (tag == BER.CONTEXT(2)) { + c.operation = MatrixOperation.get(seq.readInt()); + + } else if (tag == BER.CONTEXT(3)) { + c.disposition = MatrixDisposition.get(seq.readInt()); + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return c; + } + +} + +module.exports = MatrixConnection; \ No newline at end of file diff --git a/EmberLib/MatrixContents.js b/EmberLib/MatrixContents.js new file mode 100755 index 0000000..7c11647 --- /dev/null +++ b/EmberLib/MatrixContents.js @@ -0,0 +1,151 @@ +"use strict"; + +const MatrixType = require("./MatrixType"); +const MatrixMode = require("./MatrixMode"); + +class MatrixContents { + constructor(type = MatrixType.oneToN, mode = MatrixMode.linear) { + this.type = type; + this.mode = mode; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.EMBER_SET); + if (this.identifier !== undefined) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeString(this.identifier, BER.EMBER_STRING); + ber.endSequence(); + } + if (this.description !== undefined) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.description, BER.EMBER_STRING); + ber.endSequence(); + } + if (this.type !== undefined) { + ber.startSequence(BER.CONTEXT(2)); + ber.writeInt(this.type.value); + ber.endSequence(); + } + if (this.mode !== undefined) { + ber.startSequence(BER.CONTEXT(3)); + ber.writeInt(this.mode.value); + ber.endSequence(); + } + if (this.targetCount !== undefined) { + ber.startSequence(BER.CONTEXT(4)); + ber.writeInt(this.targetCount); + ber.endSequence(); + } + if (this.sourceCount !== undefined) { + ber.startSequence(BER.CONTEXT(5)); + ber.writeInt(this.sourceCount); + ber.endSequence(); + } + if (this.maximumTotalConnects !== undefined) { + ber.startSequence(BER.CONTEXT(6)); + ber.writeInt(this.maximumTotalConnects); + ber.endSequence(); + } + if (this.maximumConnectsPerTarget !== undefined) { + ber.startSequence(BER.CONTEXT(7)); + ber.writeInt(this.maximumConnectsPerTarget); + ber.endSequence(); + } + if (this.parametersLocation !== undefined) { + ber.startSequence(BER.CONTEXT(8)); + let param = Number(this.parametersLocation) + if (isNaN(param)) { + ber.writeRelativeOID(this.parametersLocation, BER.EMBER_RELATIVE_OID); + } + else { + ber.writeInt(param); + } + ber.endSequence(); + } + if (this.gainParameterNumber !== undefined) { + ber.startSequence(BER.CONTEXT(9)); + ber.writeInt(this.gainParameterNumber); + ber.endSequence(); + } + if (this.labels !== undefined) { + ber.startSequence(BER.CONTEXT(10)); + ber.startSequence(BER.EMBER_SEQUENCE); + for(var i =0; i < this.labels.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + this.labels[i].encode(ber); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); + } + if (this.schemaIdentifiers !== undefined) { + ber.startSequence(BER.CONTEXT(11)); + ber.writeInt(this.schemaIdentifiers, BER.EMBER_STRING); + ber.endSequence(); + } + if (this.templateReference !== undefined) { + ber.startSequence(BER.CONTEXT(12)); + ber.writeRelativeOID(this.templateReference, BER.EMBER_RELATIVE_OID); + ber.endSequence(); + } + ber.endSequence(); + } + + static decode(ber) { + const mc = new MatrixContents(); + ber = ber.getSequence(BER.EMBER_SET); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + + if(tag == BER.CONTEXT(0)) { + mc.identifier = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(1)) { + mc.description = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(2)) { + mc.type = MatrixType.get(seq.readInt()); + } else if(tag == BER.CONTEXT(3)) { + mc.mode = MatrixMode.get(seq.readInt()); + } else if(tag == BER.CONTEXT(4)) { + mc.targetCount = seq.readInt(); + } else if(tag == BER.CONTEXT(5)) { + mc.sourceCount = seq.readInt(); + } else if(tag == BER.CONTEXT(6)) { + mc.maximumTotalConnects = seq.readInt(); + } else if(tag == BER.CONTEXT(7)) { + mc.maximumConnectsPerTarget = seq.readInt(); + } else if(tag == BER.CONTEXT(8)) { + tag = seq.peek(); + if (tag === BER.EMBER_RELATIVE_OID) { + mc.parametersLocation = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else { + mc.parametersLocation = seq.readInt(); + } + } else if(tag == BER.CONTEXT(9)) { + mc.gainParameterNumber = seq.readInt(); + } else if(tag == BER.CONTEXT(10)) { + mc.labels = []; + seq = seq.getSequence(BER.EMBER_SEQUENCE); + while(seq.remain > 0) { + let lSeq = seq.getSequence(BER.CONTEXT(0)); + mc.labels.push(Label.decode(lSeq)); + } + } else if(tag == BER.CONTEXT(11)) { + mc.schemaIdentifiers = seq.readInt(); + } else if(tag == BER.CONTEXT(12)) { + mc.templateReference = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return mc; + }; +} + +module.exports = MatrixContents; \ No newline at end of file diff --git a/EmberLib/MatrixDisposition.js b/EmberLib/MatrixDisposition.js new file mode 100755 index 0000000..1cce450 --- /dev/null +++ b/EmberLib/MatrixDisposition.js @@ -0,0 +1,18 @@ +const Enum = require('enum'); + +// ConnectionDisposition ::= +// INTEGER { +// tally (0), -- default +// modified (1), -- sources contains new current state +// pending (2), -- sources contains future state +// locked (3) -- error: target locked. sources contains current state +// -- more tbd. +// } +const MatrixDisposition = new Enum({ + tally: 0, + modified: 1, + pending: 2, + locked: 3 +}); + +module.exports = MatrixDisposition; \ No newline at end of file diff --git a/EmberLib/MatrixMode.js b/EmberLib/MatrixMode.js new file mode 100755 index 0000000..7101df0 --- /dev/null +++ b/EmberLib/MatrixMode.js @@ -0,0 +1,8 @@ +const Enum = require('enum'); + +const MatrixMode = new Enum({ + linear: 0, + nonLinear: 1 +}); + +module.exports = MatrixMode; \ No newline at end of file diff --git a/EmberLib/MatrixNode.js b/EmberLib/MatrixNode.js new file mode 100755 index 0000000..618b2a3 --- /dev/null +++ b/EmberLib/MatrixNode.js @@ -0,0 +1,110 @@ +"use strict"; + +const Matrix = require("./Matrix"); +const MatrixContents = require("./MatrixContents"); +const QualifiedMatrix = require("./QualifiedMatrix"); +const BER = require('../ber.js'); + +class MatrixNode extends Matrix { + constructor(number = undefined) { + super(); + this.number = number; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(13)); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.number); + ber.endSequence(); // BER.CONTEXT(0) + + if(this.contents !== undefined) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + this.encodeChildren(ber); + this.encodeTargets(ber); + this.encodeSources(ber); + this.encodeConnections(ber); + + ber.endSequence(); // BER.APPLICATION(3) + } + + + + /** + * + * @param {boolean} complete + * @returns {MatrixNode} + */ + getMinimal(complete = false) { + const number = this.getNumber(); + const m = new MatrixNode(number); + if (complete) { + if (this.contents != null) { + m.contents = this.contents; + } + if (this.targets != null) { + m.targets = this.targets; + } + if (this.sources != null) { + m.sources = this.sources; + } + if (this.connections != null) { + m.connections = this.connections; + } + } + return m; + } + + /** + * @returns {QualifiedMatrix} + */ + toQualified() { + const qm = new QualifiedMatrix(this.getPath()); + qm.update(this); + return qm; + } + + /** + * + * @param {BER} ber + * @returns {MatrixNode} + */ + static decode(ber) { + const m = new MatrixNode(); + ber = ber.getSequence(BER.APPLICATION(13)); + while (ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if (tag == BER.CONTEXT(0)) { + m.number = seq.readInt(); + } + else if (tag == BER.CONTEXT(1)) { + m.contents = MatrixContents.decode(seq); + + } else if (tag == BER.CONTEXT(2)) { + m.decodeChildren(seq); + } else if (tag == BER.CONTEXT(3)) { + m.targets = Matrix.decodeTargets(seq); + } else if (tag == BER.CONTEXT(4)) { + m.sources = Matrix.decodeSources(seq); + } else if (tag == BER.CONTEXT(5)) { + m.connections = Matrix.decodeConnections(seq); + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + if (DEBUG) { console.log("MatrixNode", m); } + return m; + } +} + +module.exports = MatrixNode; \ No newline at end of file diff --git a/EmberLib/MatrixOperation.js b/EmberLib/MatrixOperation.js new file mode 100755 index 0000000..c3a4879 --- /dev/null +++ b/EmberLib/MatrixOperation.js @@ -0,0 +1,16 @@ +const Enum = require('enum'); + +// ConnectionOperation ::= +// INTEGER { +// absolute (0), -- default. sources contains absolute information +// connect (1), -- nToN only. sources contains sources to add to connection +// disconnect (2) -- nToN only. sources contains sources to remove from +// connection +// } +const MatrixOperation = new Enum({ + absolute: 0, + connect: 1, + disconnect: 2 +}); + +module.exports = MatrixOperation; \ No newline at end of file diff --git a/EmberLib/MatrixType.js b/EmberLib/MatrixType.js new file mode 100755 index 0000000..8008bd4 --- /dev/null +++ b/EmberLib/MatrixType.js @@ -0,0 +1,10 @@ + +const Enum = require('enum'); + +const MatrixType = new Enum({ + oneToN: 0, + oneToOne: 1, + nToN: 2 +}); + +module.exports = MatrixType; \ No newline at end of file diff --git a/EmberLib/Node.js b/EmberLib/Node.js new file mode 100755 index 0000000..7b1d6a8 --- /dev/null +++ b/EmberLib/Node.js @@ -0,0 +1,90 @@ +"use strict"; + +const TreeNode = require("./TreeNode"); +const QualifiedNode = require("./QualifiedNode"); +const NodeContents = require("./NodeContents"); +const BER = require('../ber.js'); + +class Node extends TreeNode { + /** + * + * @param {number} number + */ + constructor(number) { + super(); + this.number = number; + } + + isNode() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(3)); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.number); + ber.endSequence(); // BER.CONTEXT(0) + + if(this.contents != null) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + this.encodeChildren(ber); + + ber.endSequence(); // BER.APPLICATION(3) + } + + /** + * + * @param {function} callback + */ + subscribe(callback) { + if (callback != null && this.isStream()) { + this.contents._subscribers.add(callback); + } + } + + /** + * @returns {QualifiedNode} + */ + toQualified() { + const qn = new QualifiedNode(this.getPath()); + qn.update(this); + return qn; + } + + /** + * + * @param {BER} ber + * @returns {Node} + */ + static decode(ber) { + const n = new Node(); + ber = ber.getSequence(BER.APPLICATION(3)); + + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + n.number = seq.readInt(); + } else if(tag == BER.CONTEXT(1)) { + n.contents = NodeContents.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + n.decodeChildren(seq); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + if (DEBUG) { console.log("Node", n); } + return n; + } +}; + +module.exports = Node; diff --git a/EmberLib/NodeContents.js b/EmberLib/NodeContents.js new file mode 100755 index 0000000..a34fc48 --- /dev/null +++ b/EmberLib/NodeContents.js @@ -0,0 +1,80 @@ +"use strict"; +const BER = require('../ber.js'); + +class NodeContents{ + constructor() { + this.isOnline = true; + this._subscribers = new Set(); + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.EMBER_SET); + + if(this.identifier !== undefined) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeString(this.identifier, BER.EMBER_STRING); + ber.endSequence(); // BER.CONTEXT(0) + } + + if(this.description !== undefined) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.description, BER.EMBER_STRING); + ber.endSequence(); // BER.CONTEXT(1) + } + + if(this.isRoot !== undefined) { + ber.startSequence(BER.CONTEXT(2)); + ber.writeBoolean(this.isRoot); + ber.endSequence(); // BER.CONTEXT(2) + } + + if(this.isOnline !== undefined) { + ber.startSequence(BER.CONTEXT(3)); + ber.writeBoolean(this.isOnline); + ber.endSequence(); // BER.CONTEXT(3) + } + + if(this.schemaIdentifiers !== undefined) { + ber.startSequence(BER.CONTEXT(4)); + ber.writeString(this.schemaIdentifiers, BER.EMBER_STRING); + ber.endSequence(); // BER.CONTEXT(4) + } + + ber.endSequence(); // BER.EMBER_SET + } + + /** + * + * @param {BER} ber + * @returns {NodeContents} + */ + static decode(ber) { + var nc = new NodeContents(); + ber = ber.getSequence(BER.EMBER_SET); + while(ber.remain > 0) { + var tag = ber.peek(); + var seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + nc.identifier = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(1)) { + nc.description = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(2)) { + nc.isRoot = seq.readBoolean(); + } else if(tag == BER.CONTEXT(3)) { + nc.isOnline = seq.readBoolean(); + } else if(tag == BER.CONTEXT(4)) { + nc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + + return nc; + } +} + +module.exports = NodeContents; \ No newline at end of file diff --git a/EmberLib/Parameter.js b/EmberLib/Parameter.js new file mode 100755 index 0000000..13e14d3 --- /dev/null +++ b/EmberLib/Parameter.js @@ -0,0 +1,115 @@ +"use strict"; + +const TreeNode = require("./TreeNode"); +const QualifiedParameter = require("./QualifiedParameter"); +const BER = require('../ber.js'); +const ParameterContents = require("./ParameterContents"); + +class Parameter extends TreeNode { + /** + * + * @param {number} number + */ + constructor(number) { + super(); + this.number = number; + } + + isParameter() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(1)); + + ber.writeIfDefined(this.number, ber.writeInt, 0); + + if(this.contents != null) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); + } + + this.encodeChildren(ber); + + ber.endSequence(); + } + + /** + * + * @param {string|number} value + * @returns {Root} + */ + setValue(value) { + return this.getTreeBranch(undefined, (m) => { + m.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); + }); + } + + /** + * @returns {QualifiedParameter} + */ + toQualified() { + let qp = new QualifiedParameter(this.getPath()); + qp.update(this); + return qp; + } + + /** + * + * @param {Parameter} other + */ + update(other) { + if ((other !== undefined) && (other.contents !== undefined)) { + if (this.contents == null) { + this.contents = other.contents; + } + else { + for (var key in other.contents) { + if (key[0] === "_") { continue; } + if (other.contents.hasOwnProperty(key)) { + this.contents[key] = other.contents[key]; + } + } + for(let cb of this.contents._subscribers) { + cb(this); + } + } + } + return; + } + + /** + * + * @param {BER} ber + * @returns {Parameter} + */ + static decode(ber) { + const p = new Parameter(); + ber = ber.getSequence(BER.APPLICATION(1)); + + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + p.number = seq.readInt(); + + } else if(tag == BER.CONTEXT(1)) { + p.contents = ParameterContents.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + p.decodeChildren(seq); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + if (DEBUG) { console.log("Parameter", p); } + return p; + } + +} + +module.exports = Parameter; \ No newline at end of file diff --git a/EmberLib/ParameterAccess.js b/EmberLib/ParameterAccess.js new file mode 100755 index 0000000..e6f7113 --- /dev/null +++ b/EmberLib/ParameterAccess.js @@ -0,0 +1,12 @@ + +const Enum = require('enum'); + +var ParameterAccess = new Enum({ + none: 0, + read: 1, + write: 2, + readWrite: 3 +}); + + +module.exports = ParameterAccess; \ No newline at end of file diff --git a/EmberLib/ParameterContents.js b/EmberLib/ParameterContents.js new file mode 100755 index 0000000..f068e46 --- /dev/null +++ b/EmberLib/ParameterContents.js @@ -0,0 +1,120 @@ +"use strict"; + +const {ParameterType} = require("./ParameterType"); +const ParameterAccess = require("./ParameterAccess"); + +const BER = require('../ber.js'); + +class ParameterContents { + constructor(value, type) { + this._subscribers = new Set(); + if(value !== undefined) { + this.value = value; + } + if(type !== undefined) { + if((type = ParameterType.get(type)) !== undefined){ + this.type = type + } + } + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.EMBER_SET); + + ber.writeIfDefined(this.identifier, ber.writeString, 0, BER.EMBER_STRING); + ber.writeIfDefined(this.description, ber.writeString, 1, BER.EMBER_STRING); + ber.writeIfDefined(this.value, ber.writeValue, 2); + ber.writeIfDefined(this.minimum, ber.writeValue, 3); + ber.writeIfDefined(this.maximum, ber.writeValue, 4); + ber.writeIfDefinedEnum(this.access, ParameterAccess, ber.writeInt, 5); + ber.writeIfDefined(this.format, ber.writeString, 6, BER.EMBER_STRING); + ber.writeIfDefined(this.enumeration, ber.writeString, 7, BER.EMBER_STRING); + ber.writeIfDefined(this.factor, ber.writeInt, 8); + ber.writeIfDefined(this.isOnline, ber.writeBoolean, 9); + ber.writeIfDefined(this.formula, ber.writeString, 10, BER.EMBER_STRING); + ber.writeIfDefined(this.step, ber.writeInt, 11); + ber.writeIfDefined(this.default, ber.writeValue, 12); + ber.writeIfDefinedEnum(this.type, ParameterType, ber.writeInt, 13); + ber.writeIfDefined(this.streamIdentifier, ber.writeInt, 14); + + if(this.emumMap !== undefined) { + ber.startSequence(BER.CONTEXT(15)); + StringIntegerCollection.encode(ber, this.enumMap); + ber.endSequence(); + } + + if(this.streamDescriptor !== undefined) { + ber.startSequence(BER.CONTEXT(16)); + this.streamDescriptor.encode(ber); + ber.endSequence(); + } + + ber.writeIfDefined(this.schemaIdentifiers, ber.writeString, 17, BER.EMBER_STRING); + + ber.endSequence(); + } + + /** + * + * @param {BER} ber + * @returns {ParameterContents} + */ + static decode(ber) { + const pc = new ParameterContents(); + ber = ber.getSequence(BER.EMBER_SET); + + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + pc.identifier = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(1)) { + pc.description = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(2)) { + pc.value = seq.readValue(); + } else if(tag == BER.CONTEXT(3)) { + pc.minimum = seq.readValue(); + } else if(tag == BER.CONTEXT(4)) { + pc.maximum = seq.readValue(); + } else if(tag == BER.CONTEXT(5)) { + pc.access = ParameterAccess.get(seq.readInt()); + } else if(tag == BER.CONTEXT(6)) { + pc.format = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(7)) { + pc.enumeration = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(8)) { + pc.factor = seq.readInt(); + } else if(tag == BER.CONTEXT(9)) { + pc.isOnline = seq.readBoolean(); + } else if(tag == BER.CONTEXT(10)) { + pc.formula = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(11)) { + pc.step = seq.readInt(); + } else if(tag == BER.CONTEXT(12)) { + pc.default = seq.readValue(); + } else if(tag == BER.CONTEXT(13)) { + pc.type = ParameterType.get(seq.readInt()); + } else if(tag == BER.CONTEXT(14)) { + pc.streamIdentifier = seq.readInt(); + } else if(tag == BER.CONTEXT(15)) { + pc.enumMap = StringIntegerCollection.decode(seq); + } else if(tag == BER.CONTEXT(16)) { + pc.streamDescriptor = StreamDescription.decode(seq); + } else if(tag == BER.CONTEXT(17)) { + pc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); + } else if (tag == null) { + break; + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return pc; + } +} + +module.exports = ParameterContents; diff --git a/EmberLib/ParameterType.js b/EmberLib/ParameterType.js new file mode 100755 index 0000000..1611237 --- /dev/null +++ b/EmberLib/ParameterType.js @@ -0,0 +1,52 @@ +const Enum = require('enum'); +const BER = require('../ber.js'); + +function ParameterTypetoBERTAG(type) { + switch (type.value) { + case 1: return BER.EMBER_INTEGER; + case 2: return BER.EMBER_REAL; + case 3: return BER.EMBER_STRING; + case 4: return BER.EMBER_BOOLEAN; + case 7: return BER.EMBER_OCTETSTRING; + default: + throw new Error(`Unhandled ParameterType ${type}`); + } +} + +function ParameterTypefromBERTAG(tag) { + switch (tag) { + case BER.EMBER_INTEGER: return ParameterType.integer; + case BER.EMBER_REAL: return ParameterType.real; + case BER.EMBER_STRING: return ParameterType.string; + case BER.EMBER_BOOLEAN: return ParameterType.boolean; + case BER.EMBER_OCTETSTRING: return ParameterType.octets; + default: + throw new Error(`Unhandled BER TAB ${tag}`); + } +} + +/* +BER VAlue +Value ::= + CHOICE { + integer Integer64, + real REAL, + string EmberString, + boolean BOOLEAN, + octets OCTET STRING, + null NULL + }*/ + + var ParameterType = new Enum({ + integer: 1, + real: 2, + string: 3, + boolean: 4, + trigger: 5, + enum: 6, + octets: 7 +}); + +module.exports = { + ParameterType, ParameterTypetoBERTAG, ParameterTypefromBERTAG +}; \ No newline at end of file diff --git a/EmberLib/QualifiedFunction.js b/EmberLib/QualifiedFunction.js new file mode 100755 index 0000000..cfccc54 --- /dev/null +++ b/EmberLib/QualifiedFunction.js @@ -0,0 +1,133 @@ +"use strict"; + +const TreeNode = require("./TreeNode"); +const FunctionContent = require("./FunctionContent"); +const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); +const BER = require('../ber.js'); +const Command = require("./Command"); + +class QualifiedFunction extends TreeNode { + /** + * + * @param {string} path + * @param {function} func + */ + constructor(path, func) { + super(); + this.path = path; + this.func = func; + } + + isFunction() { + return true; + } + isQualified() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(20)); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); + ber.endSequence(); // BER.CONTEXT(0) + + if(this.contents != null) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + this.encodeChildren(ber); + + ber.endSequence(); // BER.APPLICATION(3) + } + + /** + * + * @param {number} cmd + * @returns {TreeNode} + */ + getCommand(cmd) { + const r = new TreeNode(); + const qf = new QualifiedFunction(); + qf.path = this.getPath(); + r.addElement(qf); + qf.addChild(new Command(cmd)); + return r; + } + + /** + * + * @returns {TreeNode} + */ + getDirectory() { + return this.getCommand(COMMAND_GETDIRECTORY); + } + + /** + * + * @param {*} params + */ + invoke(params) { + if (this.path == null) { + throw new Error("Invalid path"); + } + var QualifiedFunctionNode = this.getCommand(COMMAND_INVOKE); + var invocation = new Invocation() + invocation.arguments = params; + QualifiedFunctionNode.getElementByPath(this.getPath()).getNumber(COMMAND_INVOKE).invocation = invocation + return QualifiedFunctionNode; + } + + /** + * + * @param {function} callback + * @returns {TreeNode} + */ + subscribe(callback) { + return this.getCommand(COMMAND_SUBSCRIBE); + } + + /** + * + * @param {function} callback + * @returns {TreeNode} + */ + unsubscribe(callback) { + return QualifiedFunctionCommand(COMMAND_UNSUBSCRIBE); + } + + + /** + * + * @param {BER} ber + * @returns {QualifiedFunction} + */ + static decode(ber) { + const qf = new QualifiedFunction(); + ber = ber.getSequence(BER.APPLICATION(20)); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + qf.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else if(tag == BER.CONTEXT(1)) { + qf.contents = FunctionContent.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + qf.decodeChildren(seq); + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return qf; + } +} + +module.exports = QualifiedFunction; \ No newline at end of file diff --git a/EmberLib/QualifiedMatrix.js b/EmberLib/QualifiedMatrix.js new file mode 100755 index 0000000..666320f --- /dev/null +++ b/EmberLib/QualifiedMatrix.js @@ -0,0 +1,161 @@ +"use strict"; + +const Matrix = require("./Matrix"); +const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); +const BER = require('../ber.js'); +const Command = require("./Command"); + +class QualifiedMatrix extends Matrix { + /** + * + * @param {string} path + */ + constructor(path) { + super(); + this.path = path; + } + + isQualified() { + return true; + } + /** + * + * @param {Object} connections + * @returns {Root} + */ + connect(connections) { + const r = new Root(); + const qn = new QualifiedMatrix(); + qn.path = this.path; + r.addElement(qn); + qn.connections = connections; + return r; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(17)); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); + ber.endSequence(); // BER.CONTEXT(0) + + if(this.contents != null) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + this.encodeChildren(ber); + this.encodeTargets(ber); + this.encodeSources(ber); + this.encodeConnections(ber); + + ber.endSequence(); // BER.APPLICATION(3) + } + + /** + * + * @param {number} cmd + * @returns {TreeNode} + */ + getCommand(cmd) { + const r = new TreeNode(); + const qn = new QualifiedMatrix(); + qn.path = this.getPath(); + r.addElement(qn); + qn.addChild(new Command(cmd)); + return r; + } + + /** + * + * @param {function} callback + * @returns {TreeNode} + */ + getDirectory(callback) { + if (this.path === undefined) { + throw new Error("Invalid path"); + } + if (callback != null && !this.isStream()) { + this.contents._subscribers.add(callback); + } + return this.getCommand(COMMAND_GETDIRECTORY); + } + + /** + * + * @param {function} callback + * @returns {TreeNode} + */ + subscribe(callback) { + if (this.path === undefined) { + throw new Error("Invalid path"); + } + if (callback != null && this.isStream()) { + this.contents._subscribers.add(callback); + } + return this.getCommand(COMMAND_SUBSCRIBE); + } + + /** + * + * @param {function} callback + * @returns {TreeNode} + */ + unsubscribe(callback) { + if (this.path === undefined) { + throw new Error("Invalid path"); + } + if (callback != null && this.isStream()) { + this.contents._subscribers.delete(callback); + } + return this.getCommand(COMMAND_UNSUBSCRIBE); + } + + /** + * + * @param {BER} ber + * @returns {QualifiedMatrix} + */ + static decode(ber) { + const qm = new QualifiedMatrix(); + ber = ber.getSequence(BER.APPLICATION(17)); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + qm.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else if(tag == BER.CONTEXT(1)) { + qm.contents = MatrixContents.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + qm.decodeChildren(seq); + } else if (tag == BER.CONTEXT(3)) { + qm.targets = decodeTargets(seq); + } else if (tag == BER.CONTEXT(4)) { + qm.sources = decodeSources(seq); + } else if (tag == BER.CONTEXT(5)) { + qm.connections = {}; + seq = seq.getSequence(BER.EMBER_SEQUENCE); + while(seq.remain > 0) { + let conSeq = seq.getSequence(BER.CONTEXT(0)); + let con = MatrixConnection.decode(conSeq); + if (con.target !== undefined) { + qm.connections[con.target] = con; + } + } + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + if (DEBUG) { console.log("QualifiedMatrix", qm); } + return qm; + } +} + +module.exports = QualifiedMatrix; \ No newline at end of file diff --git a/EmberLib/QualifiedNode.js b/EmberLib/QualifiedNode.js new file mode 100755 index 0000000..3ac19ba --- /dev/null +++ b/EmberLib/QualifiedNode.js @@ -0,0 +1,144 @@ +"user strict"; +const TreeNode = require("./TreeNode"); +const BER = require('../ber.js'); +const NodeContents = require("./NodeContents"); +const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); +const Command = require("./Command"); + +class QualifiedNode extends TreeNode { + constructor (path) { + super(); + if (path != undefined) { + this.path = path; + } + } + + isNode() { + return true; + } + isQualified() { + return true; + } + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(10)); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); + ber.endSequence(); // BER.CONTEXT(0) + + if(this.contents != null) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + this.encodeChildren(ber); + + ber.endSequence(); // BER.APPLICATION(3) + } + + /** + * + * @param {number} cmd + * @returns {TreeNode} + */ + getCommand(cmd) { + const r = new TreeNode(); + const qn = new QualifiedNode(); + qn.path = this.getPath(); + r.addElement(qn); + qn.addChild(new Command(cmd)); + return r; + } + + /** + * + * @param {function} callback + * @returns {TreeNode} + */ + getDirectory(callback) { + if (this.path === undefined) { + throw new Error("Invalid path"); + } + if (callback != null && this.contents != null && !this.isStream()) { + this.contents._subscribers.add(callback); + } + return this.getCommand(COMMAND_GETDIRECTORY); + } + + /** + * + * @param {boolean} complete + * @returns {QualifiedNode} + */ + getMinimal(complete = false) { + const number = this.getNumber(); + const n = new Node(number); + if (complete && (this.contents != null)) { + n.contents = this.contents; + } + return n; + } + + /** + * + * @param {function} callback + * @returns {TreeNode} + */ + subscribe(callback) { + if (this.path == null) { + throw new Error("Invalid path"); + } + if (callback != null && this.isStream()) { + this.contents._subscribers.add(callback); + } + return this.getCommand(COMMAND_SUBSCRIBE); + } + + /** + * + * @param {function} callback + * @returns {TreeNode} + */ + unsubscribe(callback) { + if (this.path == null) { + throw new Error("Invalid path"); + } + if (callback != null && this.isStream()) { + this.contents._subscribers.delete(callback); + } + return this.getCommand(COMMAND_UNSUBSCRIBE); + } + + /** + * + * @param {BER} ber + * @returns {QualifiedNode} + */ + static decode(ber) { + const qn = new QualifiedNode(); + ber = ber.getSequence(BER.APPLICATION(10)); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + qn.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else if(tag == BER.CONTEXT(1)) { + qn.contents = NodeContents.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + qn.decodeChildren(seq); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + if (DEBUG) { console.log("QualifiedNode", qn); } + return qn; + } +} + +module.exports = QualifiedNode; \ No newline at end of file diff --git a/EmberLib/QualifiedParameter.js b/EmberLib/QualifiedParameter.js new file mode 100755 index 0000000..05a0fe1 --- /dev/null +++ b/EmberLib/QualifiedParameter.js @@ -0,0 +1,186 @@ +"use strict"; + +const TreeNode = require("./TreeNode"); +const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); +const ParameterContents = require("./ParameterContents"); +const BER = require('../ber.js'); +const Command = require("./Command"); + + +class QualifiedParameter extends TreeNode { + /** + * + * @param {string} path + */ + constructor(path) { + super(); + this.path = path; + } + + isParameter() { + return true; + } + isQualified() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(9)); + ber.startSequence(BER.CONTEXT(0)); + ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); + ber.endSequence(); // BER.CONTEXT(0) + + if(this.contents != null) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + this.encodeChildren(ber); + + ber.endSequence(); // BER.APPLICATION(3) + } + + /** + * + * @param {number} cmd + * @returns {TreeNode} + */ + getCommand(cmd) { + const r = new TreeNode(); + const qp = new QualifiedParameter(); + qp.path = this.getPath(); + r.addElement(qp); + qp.addChild(new Command(cmd)); + return r; + } + + /** + * + * @param {function} callback + * @returns {TreeNode} + */ + getDirectory(callback) { + if (callback != null && !this.isStream()) { + this.contents._subscribers.add(callback); + } + return this.getCommand(COMMAND_GETDIRECTORY); + } + + /** + * + * @param {boolean} complete + * @returns {Parameter} + */ + getMinimal(complete = false) { + const number = this.getNumber(); + const p = new Parameter(number); + if (complete) { + if (this.contents != null) { + p = this.contents; + } + } + return p; + } + + /** + * + * @param {number|string} value + * @returns {TreeNode} + */ + setValue(value) { + let r = new TreeNode(); + let qp = new QualifiedParameter(this.path); + r.addElement(qp); + qp.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); + return r; + } + + /** + * + * @param {function} callback + * @returns {TreeNode} + */ + subscribe(callback) { + if (this.path === undefined) { + throw new Error("Invalid path"); + } + if (callback != null && this.isStream()) { + this.contents._subscribers.add(callback); + } + return this.getCommand(COMMAND_SUBSCRIBE); + } + + /** + * + * @param {function} callback + * @returns {TreeNode} + */ + unsubscribe(callback) { + if (this.path === undefined) { + throw new Error("Invalid path"); + } + if (callback != null && this.isStream()) { + this.contents._subscribers.delete(callback); + } + return this.getCommand(COMMAND_UNSUBSCRIBE); + } + + /** + * + * @param {QualifiedParameter} other + */ + update(other) { + if ((other !== undefined) && (other.contents !== undefined)) { + if (this.contents == null) { + this.contents = other.contents; + } + else { + for (var key in other.contents) { + if (key[0] === "_") { + continue; + } + if (other.contents.hasOwnProperty(key)) { + this.contents[key] = other.contents[key]; + } + } + for(let cb of this.contents._subscribers) { + cb(this); + } + } + } + return; + } + + /** + * + * @param {BER} ber + * @returns {QualifiedParameter} + */ + static decode(ber) { + var qp = new QualifiedParameter(); + ber = ber.getSequence(BER.APPLICATION(9)); + while(ber.remain > 0) { + var tag = ber.peek(); + var seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + qp.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else if(tag == BER.CONTEXT(1)) { + qp.contents = ParameterContents.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + qp.decodeChildren(seq); + } else { + return qp; + } + } + if (DEBUG) { console.log("QualifiedParameter", qp); } + return qp; + } +} + +module.exports = QualifiedParameter; \ No newline at end of file diff --git a/EmberLib/TreeNode.js b/EmberLib/TreeNode.js new file mode 100755 index 0000000..9992de9 --- /dev/null +++ b/EmberLib/TreeNode.js @@ -0,0 +1,497 @@ +"use strict"; +const BER = require('../ber.js'); +const Command = require("./Command"); +const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); + +class TreeNode { + constructor() { + /** @type {TreeNode} */ + this._parent = null; + } + + addChild(child) { + TreeNode.addElement(this, child); + } + + /** + * + * @param {TreeNode} element + */ + addElement(element) { + TreeNode.addElement(this, element); + } + /** + * + */ + addResult(result) { + this.result = result; + } + + /** + * + */ + clear() { + this.elements = undefined; + } + + get children() { + let it = {}; + const self = this; + it[Symbol.iterator] = function*() { + if (self.elements == null) { return null;} + for(let child of self.elements.entries()) { + yield child[1]; + } + } + return it; + } + + /** + * + * @param {BER} ber + */ + decodeChildren(ber) { + const seq = ber.getSequence(BER.APPLICATION(4)); + while(seq.remain > 0) { + const nodeSeq = seq.getSequence(BER.CONTEXT(0)); + this.addChild(TreeNode.decode(nodeSeq)); + } + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(0)); + if(this.elements != null) { + const elements = this.getChildren(); + ber.startSequence(BER.APPLICATION(11)); + for(var i=0; i < elements.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + elements[i].encode(ber); + ber.endSequence(); // BER.CONTEXT(0) + } + ber.endSequence(); + } + if (this.result != null) { + this.result.encode(ber); + } + ber.endSequence(); // BER.APPLICATION(0) + } + + /** + * + * @param {BER} ber + */ + encodeChildren(ber) { + const children = this.getChildren(); + if(children != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.startSequence(BER.APPLICATION(4)); + for(var i = 0; i < children.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + children[i].encode(ber); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); + } + } + + hasChildren() { + return this.elements != null && this.elements.size > 0; + } + + isCommand() { + return false; + } + + isNode() { + return false; + } + + isMatrix() { + return false; + } + + isParameter() { + return false; + } + + isFunction() { + return false; + } + + isRoot() { + return this._parent == null; + } + + isQualified() { + return false; + } + + isStream() { + return this.contents != null && + this.contents.streamIdentifier != null; + } + + getMinimalContent() { + let obj; + if (this.isQualified()) { + obj = new this.constructor(this.path); + } + else { + obj = new this.constructor(this.number); + } + if (this.contents !== undefined) { + obj.contents= this.contents; + } + return obj; + }; + + getDuplicate() { + const obj = this.getMinimal(); + obj.update(this); + return obj; + } + + getMinimal() { + if (this.isQualified()) { + return new this.constructor(this.path); + } + else { + return new this.constructor(this.number); + } + } + + getTreeBranch(child, modifier) { + const m = this.getMinimal(); + if(child !== undefined) { + m.addChild(child); + } + + if(modifier !== undefined) { + modifier(m); + } + + if(this._parent === null) { + return m; + } + else { + return this._parent.getTreeBranch(m); + } + } + + getRoot() { + if(this._parent == null) { + return this; + } else { + return this._parent.getRoot(); + } + } + + getDirectory(callback) { + if (callback != null && this.contents != null && !this.isStream()) { + this.contents._subscribers.add(callback); + } + return this.getTreeBranch(new Command(COMMAND_GETDIRECTORY)); + } + + subscribe(callback) { + if (callback != null && this.isParameter() && this.isStream()) { + this.contents._subscribers.add(callback); + } + return this.getTreeBranch(new Command(COMMAND_SUBSCRIBE)); + } + + unsubscribe(callback) { + if (callback != null && this.isParameter() && this.isStream()) { + this.contents._subscribers.delete(callback); + } + return this.getTreeBranch(new Command(COMMAND_UNSUBSCRIBE)); + } + + /** + * @returns {TreeNode[]} + */ + getChildren() { + if(this.elements != null) { + return [...this.elements.values()]; + } + return null; + } + + /** + * @returns {number} + */ + getNumber() { + if (this.isQualified()) { + return TreeNode.path2number(this.getPath()); + } + else { + return this.number; + } + } + + /** + * @returns {TreeNode} + */ + getParent() { + return this._parent; + } + + /** + * @returns {string} + */ + getPath() { + return ""; + } + + /** + * + * @param {string} path + * @returns {TreeNode} + */ + getElementByPath(path) { + if (this.elements == null || this.elements.size === 0) { + return null; + } + const myPath = this.getPath(); + if (path == myPath) { + return this; + } + const myPathArray = this.isRoot() ? [] : myPath.split("."); + let pathArray = path.split("."); + + if (pathArray.length <= myPathArray.length) { + // We are lower in the tree than the requested path + return null; + } + + // Verify that our path matches the beginning of the requested path + for(var i = 0; i < myPathArray.length; i++) { + if (pathArray[i] != myPathArray[i]) { + return null; + } + } + //Now add child by child to get the requested path + let node = this; + while(myPathArray.length != pathArray.length) { + const number = pathArray[myPathArray.length]; + node = node.getElementByNumber(number); + if (node == null) { + return null; + } + myPathArray.push(number); + } + return node; + } + + /** + * + * @param {number} number + * @returns {TreeNode} + */ + getElementByNumber(number) { + const n = Number(number); + if (this.elements != null) { + return this.elements.get(n); + } + return null; + } + /** + * + * @param {string} identifier + * @returns {TreeNode} + */ + getElementByIdentifier(identifier) { + const children = this.getChildren(); + if (children == null) return null; + for(var i = 0; i < children.length; i++) { + if(children[i].contents !== undefined && + children[i].contents.identifier == identifier) { + return children[i]; + } + } + return null; + } + + /** + * + * @param {number|string} id + * @returns {TreeNode} + */ + getElement(id) { + if(Number.isInteger(id)) { + return this.getElementByNumber(id); + } else { + return this.getElementByIdentifier(id); + } + } + + getNodeByPath(client, path, callback) { + if(path.length === 0) { + callback(null, this); + return; + } + + const child = this.getElement(path[0]); + if(child !== null) { + child.getNodeByPath(client, path.slice(1), callback); + } else { + var cmd = self.getDirectory((error, node) => { + if(error) { + callback(error); + } + child = node.getElement(path[0]); + if(child === null) { + //let e = new Error('invalid path: "' + path[0] + '"'); + // console.log(e.message ,"Self:", self, "Node:", node); + //callback(e); + //DO NOT REJECT !!!! We could still be updating the tree. + return; + } else { + child.getNodeByPath(client, path.slice(1), callback); + } + }); + if(cmd !== null) { + client.sendBERNode(cmd); + } + } + } + + /** + * @returns {string} + */ + getPath() { + if (this.path != null) { + return this.path; + } + if(this._parent == null) { + if(this.number == null) { + return ""; + } + else { + return this.number.toString(); + } + } else { + let path = this._parent.getPath(); + if(path.length > 0) { + path = path + "."; + } + return path + this.number; + } + } + + /** + * + */ + toJSON() { + const res = {}; + const node = this; + if (this.isRoot()) { + const elements = this.getChildren(); + return { + elements: elements.map(e => e.toJSON()) + }; + } + res.number = node.getNumber(); + res.path = node.getPath(); + if (node.contents) { + for(let prop in node.contents) { + if (node.contents.hasOwnProperty(prop)) { + const type = typeof node.contents[prop]; + if ((type === "string") || (type === "number")) { + res[prop] = node.contents[prop]; + } + else if (node.contents[prop].value !== undefined) { + res[prop] = node.contents[prop].value; + } + else { + res[prop] = node.contents[prop]; + } + } + } + } + if (node.isMatrix()) { + if (node.targets) { + res.targets = node.targets.slice(0); + } + if (node.sources) { + res.sources = node.sources.slice(0); + } + if (node.connections) { + res.connections = {}; + for (let target in node.connections) { + if (node.connections.hasOwnProperty(target)) { + res.connections[target] = {target: target, sources: []}; + if (node.connections[target].sources) { + res.connections[target].sources = node.connections[target].sources.slice(0); + } + } + } + + } + } + const children = node.getChildren(); + if (children) { + res.children = []; + for(let child of children) { + res.children.push(child.toJSON()); + } + } + return res; + }; + + /** + * + * @param {TreeNode} other + */ + update(other) { + if ((other != null) && (other.contents != null)) { + if (this.contents == null) { + this.contents = other.contents; + } + else { + for (var key in other.contents) { + if (other.contents.hasOwnProperty(key)) { + this.contents[key] = other.contents[key]; + } + } + } + } + return; + } + + static decode(ber) { + + } + + /** + * + * @param {TreeNode} parent + * @param {TreeNode} element + */ + static addElement(parent, element) { + element._parent = parent; + if(parent.elements == null) { + parent.elements = new Map(); + } + parent.elements.set(element.getNumber(), element); + } + + static path2number(path) { + try { + const numbers = path.split("."); + if (numbers.length > 0) { + return Number(numbers[numbers.length - 1]); + } + } + catch(e) { + // ignore + } + } +} + +module.exports = TreeNode; \ No newline at end of file diff --git a/EmberLib/constants.js b/EmberLib/constants.js new file mode 100755 index 0000000..06a63e8 --- /dev/null +++ b/EmberLib/constants.js @@ -0,0 +1,15 @@ +const COMMAND_SUBSCRIBE = 30; +const COMMAND_UNSUBSCRIBE = 31; +const COMMAND_GETDIRECTORY = 32; +const COMMAND_INVOKE = 33; + +module.exports = { + COMMAND_SUBSCRIBE, + COMMAND_UNSUBSCRIBE, + COMMAND_GETDIRECTORY, + COMMAND_INVOKE, + Subscribe: COMMAND_SUBSCRIBE, + Unsubscribe: COMMAND_UNSUBSCRIBE, + GetDirectory: COMMAND_GETDIRECTORY, + Invoke: COMMAND_INVOKE +}; \ No newline at end of file diff --git a/EmberLib/index.js b/EmberLib/index.js new file mode 100755 index 0000000..73789ac --- /dev/null +++ b/EmberLib/index.js @@ -0,0 +1,164 @@ +const {Subscribe,COMMAND_SUBSCRIBE,Unsubscribe,COMMAND_UNSUBSCRIBE, + GetDirectory,COMMAND_GETDIRECTORY,Invoke,COMMAND_INVOKE} = require("./constants"); +const BER = require('../ber.js'); + +const TreeNode = require("./TreeNode"); +const Command = require("./Command"); +const Function = require("./Function"); +const FunctionArgument = require("./FunctionArgument"); +const FunctionContent = require("./FunctionContent"); +const Invocation = require("./Invocation"); +const InvocationResult = require("./InvocationResult"); +const Label = require("./Label"); +const MatrixNode = require("./MatrixNode"); +const MatrixMode = require("./MatrixMode"); +const MatrixType = require("./MatrixType"); +const MatrixContents = require("./MatrixContents"); +const MatrixConnection = require("./MatrixConnection"); +const Matrixoperation = require("./MatrixOperation"); +const Node = require("./Node"); +const NodeContents = require("./NodeContents"); +const Parameter = require("./Parameter"); +const ParameterContents = require("./ParameterContents"); +const ParameterAccess = require("./ParameterAccess"); +const ParameterType = require("./ParameterType").ParameterType; +const QualifiedFunction = require("./QualifiedFunction"); +const QualifiedMatrix = require("./QualifiedMatrix"); +const QualifiedNode = require("./QualifiedNode"); +const QualifiedParameter = require("./QualifiedParameter"); + +const rootDecode = function(ber) { + const r = new TreeNode(); + let tag = undefined; + while(ber.remain > 0) { + if (DEBUG) { console.log("Reading root"); } + tag = ber.peek(); + if (tag === BER.APPLICATION(0)) { + ber = ber.getSequence(BER.APPLICATION(0)); + tag = ber.peek(); + if (DEBUG) { + console.log("Application 0 start"); + } + + if (tag === BER.APPLICATION(11)) { + if (DEBUG) { + console.log("Application 11 start"); + } + const seq = ber.getSequence(BER.APPLICATION(11)); + while (seq.remain > 0) { + try { + const rootReader = seq.getSequence(BER.CONTEXT(0)); + while (rootReader.remain > 0) { + r.addElement(childDecode(rootReader)); + } + } + catch (e) { + if (DEBUG) { + console.log("Decode ERROR", e.stack); + return r; + } + throw e; + } + } + } + else if (tag === BER.APPLICATION(23)) { // InvocationResult BER.APPLICATION(23) + return InvocationResult.decode(ber) + } + else { + // StreamCollection BER.APPLICATION(6) + // InvocationResult BER.APPLICATION(23) + throw new errors.UnimplementedEmberTypeError(tag); + } + } + else if (tag === BER.CONTEXT(0)) { + // continuation of previous message + try { + var rootReader = ber.getSequence(BER.CONTEXT(0)); + return Element.decode(rootReader) + } + catch (e) { + console.log(e.stack); + return r; + } + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return r; +} + +const childDecode = function(ber) { + const tag = ber.peek(); + if (tag == BER.APPLICATION(1)) { + if (DEBUG) { console.log("Parameter decode");} + return Parameter.decode(ber); + } else if(tag == BER.APPLICATION(3)) { + if (DEBUG) { console.log("Node decode");} + return Node.decode(ber); + } else if(tag == BER.APPLICATION(2)) { + if (DEBUG) { console.log("Command decode");} + return Command.decode(ber); + } else if(tag == BER.APPLICATION(9)) { + if (DEBUG) { console.log("QualifiedParameter decode");} + return QualifiedParameter.decode(ber); + } else if(tag == BER.APPLICATION(10)) { + if (DEBUG) { console.log("QualifiedNode decode");} + return QualifiedNode.decode(ber); + } else if(tag == BER.APPLICATION(13)) { + if (DEBUG) { console.log("MatrixNode decode");} + return MatrixNode.decode(ber); + } + else if(tag == BER.APPLICATION(17)) { + if (DEBUG) { console.log("QualifiedMatrix decode");} + return QualifiedMatrix.decode(ber); + } + else if(tag == BER.APPLICATION(19)) { + if (DEBUG) { console.log("Function decode");} + return Function.decode(ber); + } else if (tag == BER.APPLICATION(20)) { + if (DEBUG) { console.log("QualifiedFunction decode");} + return QualifiedFunction.decode(ber); + } + else if(tag == BER.APPLICATION(24)) { + // Template + throw new errors.UnimplementedEmberTypeError(tag); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } +} + +TreeNode.decode = childDecode; + +module.exports = { + Command, + childDecode: childDecode, + rootDecode: rootDecode, + Root: TreeNode, + Function, + FunctionArgument, + FunctionContent, + Invocation, + InvocationResult, + Label, + MatrixNode, + MatrixMode, + MatrixType, + MatrixContents, + MatrixConnection, + Matrixoperation, + Node, + NodeContents, + Parameter, + ParameterContents, + ParameterAccess, + ParameterType, + QualifiedFunction , + QualifiedMatrix, + QualifiedNode, + QualifiedParameter, + Subscribe,COMMAND_SUBSCRIBE, + Unsubscribe,COMMAND_UNSUBSCRIBE, + GetDirectory,COMMAND_GETDIRECTORY, + Invoke,COMMAND_INVOKE +} \ No newline at end of file diff --git a/EmberServer.js b/EmberServer.js new file mode 100755 index 0000000..0a7a7e7 --- /dev/null +++ b/EmberServer.js @@ -0,0 +1,827 @@ +const EventEmitter = require('events').EventEmitter; +const util = require('util'); +const S101Server = require('./client.js').S101Server; +const ember = require('./EmberLib'); + +function TreeServer(host, port, tree) { + TreeServer.super_.call(this); + var self = this; + self._debug = false; + + self.callback = undefined; + self.timeoutValue = 2000; + self.server = new S101Server(host, port); + self.tree = tree; + self.clients = new Set(); + self.subscribers = {}; + + self.server.on('listening', () => { + if (self._debug) { console.log("listening"); } + self.emit('listening'); + if (self.callback !== undefined) { + self.callback(); + self.callback = undefined; + } + }); + + self.server.on('connection', (client) => { + if (self._debug) { console.log("ember new connection from", client.remoteAddress()); } + self.clients.add(client); + client.on("emberTree", (root) => { + if (self._debug) { console.log("ember new request from", client.remoteAddress(), root); } + // Queue the action to make sure responses are sent in order. + client.addRequest(() => { + try { + let path = self.handleRoot(client, root); + self.emit("request", {client: client.remoteAddress(), root: root, path: path}); + } + catch(e) { + if (self._debug) { console.log(e.stack); } + self.emit("error", e); + } + }); + }); + client.on("disconnected", () => { + self.clients.delete(client); + self.emit('disconnect', client.remoteAddress()); + }); + client.on("error", error => { + self.emit('clientError', { remoteAddress: client.remoteAddress(), error }); + }); + self.emit('connection', client.remoteAddress()); + }); + + self.server.on('disconnected', () => { + self.emit('disconnected', client.remoteAddress()); + }); + + self.server.on("error", (e) => { + self.emit("error", e); + if (self.callback !== undefined) { + self.callback(e); + } + }); + +} + +util.inherits(TreeServer, EventEmitter); + + +TreeServer.prototype.listen = function() { + return new Promise((resolve, reject) => { + this.callback = (e) => { + if (e == null) { + return resolve(); + } + return reject(e); + }; + this.server.listen(); + }); +}; + +TreeServer.prototype.close = function () { + return new Promise((resolve, reject) => { + this.callback = (e) => { + if (e == null) { + return resolve(); + } + return reject(e); + }; + this.server.server.close(); + }); +}; + +TreeServer.prototype.handleRoot = function(client, root) { + if ((root == null) || (root.elements == null) || (root.elements.size < 1)) { + // ignore empty requests. + return; + } + + const node = root.getChildren()[0]; + client.request = node; + + if (node.path !== undefined) { + return this.handleQualifiedNode(client, node); + } + else if (node.isCommand()) { + // Command on root element + this.handleCommand(client, this.tree, node); + return "root"; + } + else { + return this.handleNode(client, node); + } +} + +TreeServer.prototype.handleError = function(client, node) { + if (client !== undefined) { + const res = node == null ? this.tree.getMinimal() : node; + client.sendBERNode(res); + } +} + + +TreeServer.prototype.handleQualifiedNode = function(client, node) { + const path = node.path; + // Find this element in our tree + const element = this.tree.getElementByPath(path); + + if (element == null) { + this.emit("error", new Error(`unknown element at path ${path}`)); + return this.handleError(client); + } + + if (node.hasChildren()) { + for(child of node.children) { + if (child.isCommand()) { + this.handleCommand(client, element, child); + } + break; + } + } + else { + if (node.isMatrix()) { + this.handleQualifiedMatrix(client, element, node); + } + else if (node.isParameter()) { + this.handleQualifiedParameter(client, element, node); + } + } + return path; +} + + +TreeServer.prototype.handleNode = function(client, node) { + // traverse the tree + let element = node; + let path = []; + while(element !== undefined) { + if (element.number == null) { + this.emit("error", "invalid request"); + return; + } + if (element.isCommand()) { + break; + } + path.push(element.number); + + let children = element.getChildren(); + if ((! children) || (children.length === 0)) { + break; + } + element = element.children[0]; + } + let cmd = element; + + if (cmd == null) { + this.emit("error", "invalid request"); + return this.handleError(client); + } + + element = this.tree.getElementByPath(path.join(".")); + + if (element == null) { + this.emit("error", new Error(`unknown element at path ${path}`)); + return this.handleError(client); + } + + if (cmd.isCommand()) { + this.handleCommand(client, element, cmd); + } + else if ((cmd.isCommand()) && (cmd.connections !== undefined)) { + this.handleMatrixConnections(client, element, cmd.connections); + } + else if ((cmd.isParameter()) && + (cmd.contents !== undefined) && (cmd.contents.value !== undefined)) { + if (this._debug) { console.log(`setValue for element at path ${path} with value ${cmd.contents.value}`); } + this.setValue(element, cmd.contents.value, client); + let res = this.getResponse(element); + client.sendBERNode(res) + this.updateSubscribers(element.getPath(), res, client); + } + else { + this.emit("error", new Error("invalid request format")); + if (this._debug) { console.log("invalid request format"); } + return this.handleError(client, element.getTreeBranch()); + } + return path; +} + +TreeServer.prototype.handleQualifiedMatrix = function(client, element, matrix) +{ + this.handleMatrixConnections(client, element, matrix.connections); +} + +TreeServer.prototype.handleQualifiedParameter = function(client, element, parameter) +{ + if (parameter.contents.value !== undefined) { + this.setValue(element, parameter.contents.value, client); + let res = this.getQualifiedResponse(element); + client.sendBERNode(res) + this.updateSubscribers(element.getPath(), res, client); + } +} + + +TreeServer.prototype.handleMatrixConnections = function(client, matrix, connections, response = true) { + var res,conResult; + var root; // ember message root + if (this._debug) { + console.log("Handling Matrix Connection"); + } + if (client != null && client.request.isQualified()) { + root = new ember.Root(); + res = new ember.QualifiedMatrix(matrix.getPath()); + //root.elements = [res]; // do not use addchild or the element will get removed from the tree. + root.addElement(res); + } + else { + res = new ember.MatrixNode(matrix.number); + root = matrix._parent.getTreeBranch(res); + } + res.connections = {}; + for(let id in connections) { + if (!connections.hasOwnProperty(id)) { + continue; + } + let connection = connections[id]; + conResult = new ember.MatrixConnection(connection.target); + let emitType; + res.connections[connection.target] = conResult; + + if (matrix.connections[connection.target].isLocked()) { + conResult.disposition = ember.MatrixDisposition.locked; + } + else if (matrix.contents.type !== ember.MatrixType.nToN && + connection.operation !== ember.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length === 1) { + if (matrix.contents.type === ember.MatrixType.oneToOne) { + // if the source is being used already, disconnect it. + const targets = matrix.getSourceConnections(connection.sources[0]); + if (targets.length === 1 && targets[0] !== connection.target) { + const disconnect = new ember.MatrixConnection(targets[0]); + disconnect.setSources([]); + disconnect.disposition = ember.MatrixDisposition.modified; + res.connections[targets[0]] = disconnect; + matrix.setSources(targets[0], []); + if (response) { + this.emit("matrix-disconnect", { + target: targets[0], + sources: connection.sources, + client: client == null ? null : client.remoteAddress() + }); + } + } + } + // if the target is connected already, disconnect it + if (matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length === 1) { + if (matrix.contents.type === ember.MatrixType.oneToN) { + const disconnectSource = this.getDisconnectSource(matrix, connection.target); + if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { + if (disconnectSource != null && disconnectSource != -1 && + disconnectSource != connection.sources[0]) { + connection.sources = [disconnectSource]; + } + else { + // do nothing + connection.operarion = ember.MatrixOperation.tally; + } + } + } + if (matrix.connections[connection.target].sources[0] !== connection.sources[0]) { + const source = matrix.connections[connection.target].sources[0]; + matrix.setSources(connection.target, []); + if (response) { + this.emit("matrix-disconnect", { + target: connection.target, + sources: [source], + client: client == null ? null : client.remoteAddress() + }); + } + } + else if (matrix.contents.type === ember.MatrixType.oneToOne) { + // let's change the request into a disconnect + connection.operation = ember.MatrixOperation.disconnect; + } + } + } + + if (connection.operation !== ember.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length > 0 && + matrix.canConnect(connection.target,connection.sources,connection.operation)) { + // Apply changes + if ((connection.operation == null) || + (connection.operation.value == ember.MatrixOperation.absolute)) { + matrix.setSources(connection.target, connection.sources); + emitType = "matrix-change"; + } + else if (connection.operation == ember.MatrixOperation.connect) { + matrix.connectSources(connection.target, connection.sources); + emitType = "matrix-connect"; + } + conResult.disposition = ember.MatrixDisposition.modified; + } + else if (connection.operation !== ember.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length === 0 && + matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length > 0) { + // let's disconnect + if (response) { + this.emit("matrix-disconnect", { + target: connection.target, + sources: matrix.connections[connection.target].sources, + client: client == null ? null : client.remoteAddress() + }); + } + matrix.setSources(connection.target, []); + conResult.disposition = ember.MatrixDisposition.modified; + } + else if (connection.operation === ember.MatrixOperation.disconnect && + matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length > 0) { + // Disconnect + if (matrix.contents.type === ember.MatrixType.oneToN) { + const disconnectSource = this.getDisconnectSource(matrix, connection.target); + if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { + if (disconnectSource != null && disconnectSource != -1 && + disconnectSource != connection.sources[0]) { + if (response) { + this.emit("matrix-disconnect", { + target: connection.target, + sources: matrix.connections[connection.target].sources, + client: client == null ? null : client.remoteAddress() + }); + } + matrix.setSources(connection.target, [disconnectSource]); + connection.operarion = ember.MatrixOperation.modified; + } + else { + // do nothing + connection.operarion = ember.MatrixOperation.tally; + } + } + } + else { + matrix.disconnectSources(connection.target, connection.sources); + conResult.disposition = ember.MatrixDisposition.modified; + emitType = "matrix-disconnect"; + } + } + else if (conResult.disposition !== ember.MatrixDisposition.locked){ + if (this._debug) { + console.log(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); + } + conResult.disposition = ember.MatrixDisposition.tally; + } + + // Send response or update subscribers. + conResult.sources = matrix.connections[connection.target].sources; + if (response) { + // We got a request so emit something. + this.emit(emitType, { + target: connection.target, + sources: connection.sources, + client: client == null ? null : client.remoteAddress() + }); + } + } + if (client != null) { + client.sendBERNode(root); + } + + if (conResult != null && conResult.disposition !== ember.MatrixDisposition.tally) { + if (this._debug) { + console.log("Updating subscribers for matrix change"); + } + this.updateSubscribers(matrix.getPath(), root, client); + } +} + +const validateMatrixOperation = function(matrix, target, sources) { + if (matrix == null) { + throw new Error(`matrix not found with path ${path}`); + } + if (matrix.contents == null) { + throw new Error(`invalid matrix at ${path} : no contents`); + } + if (matrix.contents.targetCount == null) { + throw new Error(`invalid matrix at ${path} : no targetCount`); + } + if ((target < 0) || (target >= matrix.contents.targetCount)) { + throw new Error(`target id ${target} out of range 0 - ${matrix.contents.targetCount}`); + } + if (sources.length == null) { + throw new Error("invalid sources format"); + } +} + +const doMatrixOperation = function(server, path, target, sources, operation) { + let matrix = server.tree.getElementByPath(path); + + validateMatrixOperation(matrix, target, sources); + + let connection = new ember.MatrixConnection(target); + connection.sources = sources; + connection.operation = operation; + server.handleMatrixConnections(undefined, matrix, [connection], false); +} + +TreeServer.prototype.matrixConnect = function(path, target, sources) { + doMatrixOperation(this, path, target, sources, ember.MatrixOperation.connect); +} + +TreeServer.prototype.matrixDisconnect = function(path, target, sources) { + doMatrixOperation(this, path, target, sources, ember.MatrixOperation.disconnect); +} + +TreeServer.prototype.matrixSet = function(path, target, sources) { + doMatrixOperation(this, path, target, sources, ember.MatrixOperation.absolute); +} + +TreeServer.prototype.handleQualifiedFunction = function(client, element, node) { + +} + + +TreeServer.prototype.handleCommand = function(client, element, cmd) { + switch(cmd.number) { + case ember.COMMAND_GETDIRECTORY: + this.handleGetDirectory(client, element); + break; + case ember.COMMAND_SUBSCRIBE: + this.handleSubscribe(client, element); + break; + case ember.COMMAND_UNSUBSCRIBE: + this.handleUnSubscribe(client, element); + break; + case ember.COMMAND_INVOKE: + this.handleInvoke(client, cmd.invocation, element); + break; + default: + this.emit("error", new Error(`invalid command ${cmd.number}`)); + break; + } +} + +TreeServer.prototype.getResponse = function(element) { + return element.getTreeBranch(undefined, function(node) { + node.update(element); + let children = element.getChildren(); + if (children != null) { + for (let i = 0; i < children.length; i++) { + node.addChild(children[i].getDuplicate()); + } + } + else if (this._debug) { + console.log("getResponse","no children"); + } + }); +} + +TreeServer.prototype.getQualifiedResponse = function(element) { + const res = new ember.Root(); + let dup; + if (element.isRoot() === false) { + dup = element.toQualified(); + } + let children = element.getChildren(); + if (children != null) { + for (let i = 0; i < children.length; i++) { + res.addChild(children[i].toQualified().getMinimalContent()); + } + } + else { + res.addChild(dup); + } + return res; +} + +TreeServer.prototype.handleInvoke = function(client, invocation, element) { + const result = new ember.InvocationResult(); + result.invocationId = invocation.id; + if (element == null || !element.isFunction()) { + result.setFailure(); + } + else { + try { + result.setResult(element.func(invocation.arguments)); + } + catch(e){ + this.emit("error", e); + result.setFailure(); + } + } + const res = new ember.Root(); + res.addResult(result); + client.sendBERNode(res); +} + +TreeServer.prototype.handleGetDirectory = function(client, element) { + if (client !== undefined) { + if ((element.isMatrix() || element.isParameter()) && + (!element.isStream())) { + // ember spec: parameter without streamIdentifier should + // report their value changes automatically. + this.subscribe(client, element); + } + else if (element.isNode()) { + const children = element.getChildren(); + if (children != null) { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if ((child.isMatrix() || child.isParameter()) && + (!child.isStream())) { + this.subscribe(client, child); + } + } + } + } + + const res = this.getQualifiedResponse(element); + if (this._debug) { + console.log("getDirectory response", res); + } + client.sendBERNode(res); + } +} + +TreeServer.prototype.handleSubscribe = function(client, element) { + if (this._debug) { + console.log("subscribe"); + } + this.subscribe(client, element); +} + +TreeServer.prototype.handleUnSubscribe = function(client, element) { + if (this._debug) { + console.log("unsubscribe"); + } + this.unsubscribe(client, element); +} + + +TreeServer.prototype.subscribe = function(client, element) { + const path = element.getPath(); + if (this.subscribers[path] == null) { + this.subscribers[path] = new Set(); + } + this.subscribers[path].add(client); +} + +TreeServer.prototype.unsubscribe = function(client, element) { + const path = element.getPath(); + if (this.subscribers[path] == null) { + return; + } + this.subscribers[path].delete(client); +} + +TreeServer.prototype.setValue = function(element, value, origin, key) { + return new Promise((resolve, reject) => { + // Change the element value if write access permitted. + if (element.contents == null) { + return resolve(); + } + if (element.isParameter()) { + if ((element.contents.access !== undefined) && + (element.contents.access.value > 1)) { + element.contents.value = value; + const res = this.getResponse(element); + this.updateSubscribers(element.getPath(),res, origin); + this.emit("value-change", element); + } + } + else if (element.isMatrix()) { + if ((key !== undefined) && (element.contents.hasOwnProperty(key))) { + element.contents[key] = value; + const res = this.getResponse(element); + this.updateSubscribers(element.getPath(),res, origin); + this.emit("value-change", element); + } + } + return resolve(); + }); +} + +TreeServer.prototype.replaceElement = function(element) { + let path = element.getPath(); + let parent = this.tree.getElementByPath(path); + if ((parent == null)||(parent._parent == null)) { + throw new Error(`Could not find element at path ${path}`); + } + parent = parent._parent; + let children = parent.getChildren(); + let newList = []; + for(let i = 0; i <= children.length; i++) { + if (children[i] && children[i].getPath() == path) { + element._parent = parent; // move it to new tree. + children[i] = element; + let res = this.getResponse(element); + this.updateSubscribers(path,res); + return; + } + } +} + +TreeServer.prototype.getDisconnectSource = function(matrix, targetID) { + if (matrix.defaultSources) { + return matrix.defaultSources[targetID].contents.value; + } + if (matrix.contents.labels == null || matrix.contents.labels.length == 0) { + return null; + } + const basePath = matrix.contents.labels[0].basePath; + const labels = this.tree.getElementByPath(basePath); + const number = labels.getNumber() + 1; + const parent = labels.getParent(); + const children = parent.getChildren(); + for(let child of children) { + if (child.getNumber() === number) { + matrix.defaultSources = child.getChildren(); + return matrix.defaultSources[targetID].contents.value; + } + } + return null; +} + +TreeServer.prototype.updateSubscribers = function(path, response, origin) { + if (this.subscribers[path] == null) { + return; + } + + for (let client of this.subscribers[path]) { + if (client === origin) { + continue; // already sent the response to origin + } + if (this.clients.has(client)) { + client.queueMessage(response); + } + else { + // clean up subscribers - client is gone + this.subscribers[path].delete(client); + } + } +} + +const parseMatrixContent = function(matrixContent, content) { + if (content.labels) { + matrixContent.labels = []; + for(let l = 0; l < content.labels.length; l++) { + if (typeof (content.labels[l]) === "object") { + matrixContent.labels.push( + new ember.Label( + content.labels[l].basePath, + content.labels[l].description + ) + ); + } + else { + // for backward compatibility... Remove in the future + matrixContent.labels.push( + new ember.Label(content.labels[l]) + ); + } + } + delete content.labels; + } + if (content.type != null) { + if (content.type == "oneToN") { + matrixContent.type = ember.MatrixType.oneToN; + } + else if (content.type == "oneToOne") { + matrixContent.type = ember.MatrixType.oneToOne; + } + else if (content.type == "nToN") { + matrixContent.type = ember.MatrixType.nToN; + matrixContent.maximumTotalConnects = content.maximumTotalConnects == null ? + Number(content.targetCount) * Number(content.sourceCount) : Number(content.maximumTotalConnects); + matrixContent.maximumConnectsPerTarget = content.maximumConnectsPerTarget == null ? + Number(content.sourceCount) : Number(content.maximumConnectsPerTarget); + } + else { + throw new Error(`Invalid matrix type ${content.type}`); + } + delete content.type; + } + if (content.mode != null) { + if (content.mode == "linear") { + matrixContent.mode = ember.MatrixMode.linear; + } + else if (content.mode == "nonLinear") { + matrixContent.mode = ember.MatrixMode.nonLinear; + } + else { + throw new Error(`Invalid matrix mode ${content.mode}`); + } + delete content.mode; + } +} + +const parseObj = function(parent, obj) { + for(let i = 0; i < obj.length; i++) { + let emberElement; + let content = obj[i]; + let number = content.number !== undefined ? content.number : i; + delete content.number; + if (content.value != null) { + emberElement = new ember.Parameter(number); + emberElement.contents = new ember.ParameterContents(content.value); + if (content.type) { + emberElement.contents.type = ember.ParameterType.get(content.type); + delete content.type; + } + else { + emberElement.contents.type = ember.ParameterType.string; + } + if (content.access) { + emberElement.contents.access = ember.ParameterAccess.get(content.access); + delete content.access; + } + else { + emberElement.contents.access = ember.ParameterAccess.read; + } + } + else if (content.func != null) { + emberElement = new ember.Function(number, content.func); + emberElement.contents = new ember.FunctionContent(); + if (content.arguments != null) { + for(let argument of content.arguments) { + emberElement.contents.arguments.push(new ember.FunctionArgument( + argument.type, + argument.value, + argument.name + )); + } + } + if (content.result != null) { + for(let argument of content.result) { + emberElement.contents.result.push(new ember.FunctionArgument( + argument.type, + argument.value, + argument.name + )); + } + } + delete content.result; + } + else if (content.targetCount != null) { + emberElement = new ember.MatrixNode(number); + emberElement.contents = new ember.MatrixContents(); + parseMatrixContent(emberElement.contents, content); + if (content.connections) { + emberElement.connections = {}; + for (let c in content.connections) { + if (! content.connections.hasOwnProperty(c)) { + continue; + } + const t = content.connections[c].target != null ? content.connections[c].target : 0; + emberElement.setSources(t, content.connections[c].sources); + } + delete content.connections; + } + else { + emberElement.connections = {}; + for (let t = 0; t < content.targetCount; t++) { + let connection = new ember.MatrixConnection(t); + emberElement.connections[t] = connection; + } + } + } + else { + emberElement = new ember.Node(number); + emberElement.contents = new ember.NodeContents(); + } + for(let id in content) { + if (emberElement.isFunction() && id === "arguments") { + // we did it already. + continue; + } + if ((id !== "children") && (content.hasOwnProperty(id))) { + emberElement.contents[id] = content[id]; + } + else { + parseObj(emberElement, content.children); + } + } + parent.addChild(emberElement); + } +} + +TreeServer.JSONtoTree = function(obj) { + const tree = new ember.Root(); + parseObj(tree, obj); + return tree; +} + + +TreeServer.prototype.toJSON = function() { + if (this.tree == null) { + return []; + } + const elements = this.tree.getChildren(); + + return elements.map(element => element.toJSON()); +}; + +module.exports = TreeServer; diff --git a/client.js b/client.js index 20bf7ad..1f297f3 100755 --- a/client.js +++ b/client.js @@ -3,7 +3,7 @@ const util = require('util'); const winston = require('winston'); const net = require('net'); const BER = require('./ber.js'); -const ember = require('./ember.js'); +const ember = require('./EmberLib'); const S101Codec = require('./s101.js'); @@ -46,7 +46,7 @@ function S101Client(socket, server) { self.emit('emberPacket', packet); var ber = new BER.Reader(packet); try { - var root = ember.Root.decode(ber); + var root = ember.rootDecode(ber); if (root !== undefined) { self.emit('emberTree', root); } @@ -199,7 +199,7 @@ S101Socket.prototype.connect = function (timeout = 2) { var ber = new BER.Reader(packet); try { - var root = ember.Root.decode(ber); + var root = ember.rootDecode(ber); if (root !== undefined) { self.emit('emberTree', root); } diff --git a/device.js b/device.js index ba32402..a8a85a5 100755 --- a/device.js +++ b/device.js @@ -1,7 +1,7 @@ const EventEmitter = require('events').EventEmitter; const util = require('util'); const S101Client = require('./client.js').S101Socket; -const ember = require('./ember.js'); +const ember = require('./EmberLib'); const BER = require('./ber.js'); const errors = require('./errors.js'); @@ -175,15 +175,16 @@ DeviceTree.prototype.getDirectory = function (qnode, callback = null) { reject(error); return; } - if (qnode instanceof ember.Root) { - if (qnode.elements == null || qnode.elements.length === 0) { + if (qnode.isRoot()) { + const elements = qnode.getChildren(); + if (elements == null || elements.length === 0) { if (self._debug) { console.log("getDirectory response", node); } return self.callback(new Error("Invalid qnode for getDirectory")); } - const nodeElements = node == null ? null : node.elements; + const nodeElements = node == null ? null : node.getChildren(); if (nodeElements != null && nodeElements.every(el => el._parent instanceof ember.Root)) { @@ -197,8 +198,14 @@ DeviceTree.prototype.getDirectory = function (qnode, callback = null) { else { return self.callback(new Error(`Invalid response for getDirectory ${requestedPath}`)); } - } else { - const nodeElements = node == null ? null : node.elements; + } + else if (node.getElementByPath(requestedPath) != null) { + self.clearTimeout(); // clear the timeout now. The resolve below may take a while. + self.finishRequest(); + return resolve(node); // make sure the info is treated before going to next request. + } + else { + const nodeElements = node == null ? null : node.getChildren(); if (nodeElements != null && ((qnode.isMatrix() && nodeElements.length === 1 && nodeElements[0].getPath() === requestedPath) || (!qnode.isMatrix() && nodeElements.every(el => isDirectSubPathOf(el.getPath(), requestedPath))))) { @@ -207,7 +214,7 @@ DeviceTree.prototype.getDirectory = function (qnode, callback = null) { } self.clearTimeout(); // clear the timeout now. The resolve below may take a while. self.finishRequest(); - resolve(node); // make sure the info is treated before going to next request. + return resolve(node); // make sure the info is treated before going to next request. } else if (self._debug) { console.log(node); @@ -395,12 +402,13 @@ DeviceTree.prototype.handleRoot = function (root) { } self.root.update(root); if (root.elements !== undefined) { - for (var i = 0; i < root.elements.length; i++) { - if (root.elements[i].isQualified()) { - this.handleQualifiedNode(this.root, root.elements[i]); + const elements = root.getChildren(); + for (var i = 0; i < elements.length; i++) { + if (elements[i].isQualified()) { + this.handleQualifiedNode(this.root, elements[i]); } else { - this.handleNode(this.root, root.elements[i]); + this.handleNode(this.root, elements[i]); } } } diff --git a/ember.js b/ember.js index 2201ae8..1ce667b 100755 --- a/ember.js +++ b/ember.js @@ -18,14 +18,20 @@ module.exports.DEBUG = function(d) { DEBUG = d; }; +/**************************************************************************** + * Elements + */ + + function Elements() { + Elements.super_.call(this); + } + /**************************************************************************** * Root ***************************************************************************/ function Root() { Root.super_.call(this); - - //Object.defineProperty(this, '_parent', {value: null, enumerable: false}); }; util.inherits(Root, TreeNode); @@ -49,8 +55,7 @@ Root.decode = function(ber) { if (DEBUG) { console.log("Application 11 start"); } - var seq = ber.getSequence(BER.APPLICATION(11)); - r.elements = []; + var seq = ber.getSequence(BER.APPLICATION(11)); while (seq.remain > 0) { try { var rootReader = seq.getSequence(BER.CONTEXT(0)); @@ -94,12 +99,16 @@ Root.decode = function(ber) { return r; } -Root.prototype.addElement = function(ele) { - ele._parent = this; - if(this.elements === undefined) { - this.elements = []; +function addElement(parent, element) { + element._parent = parent; + if(parent.elements == null) { + parent.elements = new Map(); } - this.elements.push(ele); + parent.elements.set(element.getNumber(), element); +} + +Root.prototype.addElement = function(ele) { + addElement(this, ele); } Root.prototype.addResult = function(result) { @@ -113,10 +122,11 @@ Root.prototype.addChild = function(child) { Root.prototype.encode = function(ber) { ber.startSequence(BER.APPLICATION(0)); if(this.elements != null) { + const elements = this.getChildren(); ber.startSequence(BER.APPLICATION(11)); - for(var i=0; i element.toJSON()); }; module.exports = TreeServer; diff --git a/test/Ember.test.js b/test/Ember.test.js index e1c6e23..abe772e 100755 --- a/test/Ember.test.js +++ b/test/Ember.test.js @@ -28,5 +28,6 @@ describe("Ember", () => { it("should handle errors in message", () => { var ber = new BER.Reader(errorBuffer); expect(() => ember.Root.decode(ber)).toThrow(errors.UnimplementedEmberTypeError); - }) + }); + }); From 40c3e5dea2ac8abad2058c40bdc002857f3902f6 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 6 Jan 2020 17:18:08 +0100 Subject: [PATCH 35/64] Finalized version 2.0.0 --- EmberClient/EmberClient.js | 730 ++++++++ EmberClient/index.js | 1 + EmberLib/Command.js | 12 +- EmberLib/Element.js | 38 + EmberLib/Function.js | 37 +- EmberLib/MatrixNode.js | 1 - EmberLib/Node.js | 36 +- EmberLib/Parameter.js | 29 +- EmberLib/QualifiedElement.js | 98 + EmberLib/QualifiedFunction.js | 76 +- EmberLib/QualifiedMatrix.js | 1 - EmberLib/QualifiedNode.js | 97 +- EmberLib/QualifiedParameter.js | 92 +- EmberLib/TreeNode.js | 54 +- EmberLib/index.js | 38 +- EmberServer.js | 827 -------- EmberServer/ElementHandlers.js | 186 ++ EmberServer/EmberServer.js | 376 ++++ EmberServer/JSONParser.js | 160 ++ EmberServer/MatrixHandlers.js | 223 +++ EmberServer/QualifiedHandlers.js | 75 + EmberServer/index.js | 1 + EmberSocket/S101Client.js | 87 + EmberSocket/S101Server.js | 53 + EmberSocket/S101Socket.js | 186 ++ EmberSocket/index.js | 7 + README.md | 95 +- client.js | 308 --- device.js | 657 ------- ember.js | 3021 ------------------------------ index.js | 12 +- server.js | 823 -------- test/utils.js | 2 +- 33 files changed, 2372 insertions(+), 6067 deletions(-) create mode 100755 EmberClient/EmberClient.js create mode 100755 EmberClient/index.js create mode 100755 EmberLib/Element.js create mode 100755 EmberLib/QualifiedElement.js delete mode 100755 EmberServer.js create mode 100755 EmberServer/ElementHandlers.js create mode 100755 EmberServer/EmberServer.js create mode 100755 EmberServer/JSONParser.js create mode 100755 EmberServer/MatrixHandlers.js create mode 100755 EmberServer/QualifiedHandlers.js create mode 100755 EmberServer/index.js create mode 100755 EmberSocket/S101Client.js create mode 100755 EmberSocket/S101Server.js create mode 100755 EmberSocket/S101Socket.js create mode 100755 EmberSocket/index.js delete mode 100755 client.js delete mode 100755 device.js delete mode 100755 ember.js delete mode 100755 server.js diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js new file mode 100755 index 0000000..e139bbe --- /dev/null +++ b/EmberClient/EmberClient.js @@ -0,0 +1,730 @@ +const EventEmitter = require('events').EventEmitter; +const S101Client = require('../EmberSocket').S101Socket; +const ember = require('../EmberLib'); +const BER = require('../ber.js'); +const errors = require('../errors.js'); + +const DEFAULT_PORT = 9000; +const DEFAULT_TIMEOUT = 3000; + +/** @typedef {{ + * node: TreeNode, + * func: function + * }} REQUEST*/ +class EmberClient extends EventEmitter { + /** + * + * @param {string} host + * @param {number} port + */ + constructor(host, port = DEFAULT_PORT) { + super(); + this._debug = false; + /** @type {REQUEST[]} */ + this._pendingRequests = []; + /** @type {REQUEST} */ + this._activeRequest = null; + this._timeout = null; + this._callback = undefined; + this._requestID = 0; + this._client = new S101Client(host, port); + this.timeoutValue = DEFAULT_TIMEOUT; + /** @type {Root} */ + this.root = new ember.Root(); + + this._client.on('connecting', () => { + this.emit('connecting'); + }); + + this._client.on('connected', () => { + this.emit('connected'); + if (this._callback !== undefined) { + this._callback(); + } + }); + + this._client.on('disconnected', () => { + this.emit('disconnected'); + }); + + this._client.on("error", e => { + if (this._callback !== undefined) { + this._callback(e); + } + this.emit("error", e); + }); + + this._client.on('emberTree', root => { + try { + if (root instanceof ember.InvocationResult) { + this.emit('invocationResult', root); + if (this._debug) { + console.log("Received InvocationResult", root); + } + } else { + this._handleRoot(root); + if (this._debug) { + console.log("Received root", root); + } + } + if (this._callback) { + this._callback(undefined, root); + } + } + catch(e) { + if (this._debug) { + console.log(e, root); + } + if (this._callback) { + this._callback(e); + } + } + }); + } + + _finishRequest() { + this._clearTimeout(); + this._activeRequest = null; + try { + this._makeRequest(); + } catch(e) { + if (this._debug) {console.log(e);} + if (this._callback != null) { + this._callback(e); + } + this.emit("error", e); + } + } + + _makeRequest() { + if (this._activeRequest == null && this._pendingRequests.length > 0) { + this._activeRequest = this._pendingRequests.shift(); + const req = `${ this._requestID++} - ${this._activeRequest.node.getPath()}`; + this._activeRequest.timeoutError = new errors.EmberTimeoutError(`Request ${req} timed out`) + + if (this._debug) { + console.log(`Making request ${req}`, Date.now()); + } + this._timeout = setTimeout(() => { + this._timeoutRequest(); + }, this.timeoutValue); + this._activeRequest.func(); + } + } + + _timeoutRequest(id) { + this._activeRequest.func(this._activeRequest.timeoutError); + } + + /** + * + * @param {function} req + */ + addRequest(req) { + this._pendingRequests.push(req); + this._makeRequest(); + }; + + _clearTimeout() { + if (this._timeout != null) { + clearTimeout(this._timeout); + this._timeout = null; + } + }; + + /** + * + * @param {TreeNode} parent + * @param {TreeNode} node + */ + _handleNode(parent, node) { + var n = parent.getElementByNumber(node.getNumber()); + if (n === null) { + parent.addChild(node); + n = node; + } else { + n.update(node); + } + + var children = node.getChildren(); + if (children !== null) { + for (var i = 0; i < children.length; i++) { + this._handleNode(n, children[i]); + } + } + else { + this.emit("value-change", node); + } + return; + } + + /** + * + * @param {TreeNode} parent + * @param {TreeNode} node + */ + _handleQualifiedNode(parent, node) { + var element = parent.getElementByPath(node.path); + if (element !== null) { + this.emit("value-change", node); + element.update(node); + } + else { + var path = node.path.split("."); + if (path.length === 1) { + this.root.addChild(node); + } + else { + // Let's try to get the parent + path.pop(); + parent = this.root.getElementByPath(path.join(".")); + if (parent === null) { + return; + } + parent.addChild(node); + parent.update(parent); + } + element = node; + } + + var children = node.getChildren(); + if (children !== null) { + for (var i = 0; i < children.length; i++) { + if (children[i].isQualified()) { + this._handleQualifiedNode(element, children[i]); + } + else { + this._handleNode(element, children[i]); + } + } + } + + return; + } + + /** + * + * @param {TreeNode} root + */ + _handleRoot (root) { + if (this._debug) { + console.log("handling root", JSON.stringify(root)); + } + this.root.update(root); + if (root.elements !== undefined) { + const elements = root.getChildren(); + for (var i = 0; i < elements.length; i++) { + if (elements[i].isQualified()) { + this._handleQualifiedNode(this.root, elements[i]); + } + else { + this._handleNode(this.root, elements[i]); + } + } + } + if (this._callback) { + this._callback(null, root); + } + } + + /** + * + * @param {number} timeout + */ + connect(timeout = 2) { + return new Promise((resolve, reject) => { + this._callback = e => { + this._callback = undefined; + if (e === undefined) { + return resolve(); + } + return reject(e); + }; + if ((this._client !== undefined) && (this._client.isConnected())) { + this._client.disconnect(); + } + this._client.connect(timeout); + }); + } + + disconnect() { + if (this._client != null) { + return this._client.disconnect(); + } + }; + + /** + * + * @param {TreeNode} qnode + * @param {function} callback=null + * @returns {Promise} + */ + expand(node, callback = null) { + if (node == null) { + return Promise.reject(new Error("Invalid null node")); + } + if (node.isParameter() || node.isMatrix() || node.isFunction()) { + return this.getDirectory(node); + } + return this.getDirectory(node, callback).then((res) => { + let children = node.getChildren(); + if ((res === undefined) || (children === undefined) || (children === null)) { + if (this._debug) { + console.log("No more children for ", node); + } + return; + } + let p = Promise.resolve(); + for (let child of children) { + if (child.isParameter()) { + // Parameter can only have a single child of type Command. + continue; + } + if (this._debug) { + console.log("Expanding child", child); + } + p = p.then(() => { + return this.expand(child).catch((e) => { + // We had an error on some expansion + // let's save it on the child itthis + child.error = e; + }); + }); + } + return p; + }); + } + + /** + * + * @param {TreeNode} qnode + * @param {function} callback=null + * @returns {Promise} + */ + getDirectory(qnode, callback = null) { + if (qnode == null) { + this.root.clear(); + qnode = this.root; + } + return new Promise((resolve, reject) => { + this.addRequest({node: qnode, func: error => { + if (error) { + this._finishRequest(); + reject(error); + return; + } + + this._callback = (error, node) => { + const requestedPath = qnode.getPath(); + if (node == null) { + if (this._debug) { + console.log(`received null response for ${requestedPath}`); + } + return; + } + if (error) { + if (this._debug) { + console.log("Received getDirectory error", error); + } + this._clearTimeout(); // clear the timeout now. The resolve below may take a while. + this._finishRequest(); + reject(error); + return; + } + if (qnode.isRoot()) { + const elements = qnode.getChildren(); + if (elements == null || elements.length === 0) { + if (this._debug) { + console.log("getDirectory response", node); + } + return this._callback(new Error("Invalid qnode for getDirectory")); + } + + const nodeElements = node == null ? null : node.getChildren(); + + if (nodeElements != null + && nodeElements.every(el => el._parent instanceof ember.Root)) { + if (this._debug) { + console.log("Received getDirectory response", node); + } + this._clearTimeout(); // clear the timeout now. The resolve below may take a while. + this._finishRequest(); + resolve(node); // make sure the info is treated before going to next request. + } + else { + return this._callback(new Error(`Invalid response for getDirectory ${requestedPath}`)); + } + } + else if (node.getElementByPath(requestedPath) != null) { + this._clearTimeout(); // clear the timeout now. The resolve below may take a while. + this._finishRequest(); + return resolve(node); // make sure the info is treated before going to next request. + } + else { + const nodeElements = node == null ? null : node.getChildren(); + if (nodeElements != null && + ((qnode.isMatrix() && nodeElements.length === 1 && nodeElements[0].getPath() === requestedPath) || + (!qnode.isMatrix() && nodeElements.every(el => isDirectSubPathOf(el.getPath(), requestedPath))))) { + if (this._debug) { + console.log("Received getDirectory response", node); + } + this._clearTimeout(); // clear the timeout now. The resolve below may take a while. + this._finishRequest(); + return resolve(node); // make sure the info is treated before going to next request. + } + else if (this._debug) { + console.log(node); + console.log(new Error(requestedPath)); + } + } + }; + + if (this._debug) { + console.log("Sending getDirectory", qnode); + } + this._client.sendBERNode(qnode.getDirectory(callback)); + }}); + }); + } + + /** + * + * @param {string} path ie: "path/to/destination" + * @param {function} callback=null + * @returns {Promise} + */ + getNodeByPath(path, callback = null) { + if (typeof path === 'string') { + path = path.split('/'); + } + var pathError = new Error(`Failed path discovery at ${path.slice(0, pos + 1).join("/")}`); + var pos = 0; + var lastMissingPos = -1; + var currentNode = this.root; + const getNext = () => { + return Promise.resolve() + .then(() => { + const children = currentNode.getChildren(); + const identifier = path[pos]; + if (children != null) { + for (let i = 0; i < children.length; i++) { + var node = children[i]; + if (node.contents != null && node.contents.identifier === identifier) { + // We have this part already. + pos++; + if (pos >= path.length) { + return node; + } + currentNode = node; + return getNext(); + } + } + } + // We do not have that node yet. + if (lastMissingPos === pos) { + throw pathError; + } + lastMissingPos = pos; + return this.getDirectory(currentNode, callback).then(() => getNext()); + }); + } + return getNext(); + } + + /** + * + * @param {string|number[]} path ie: 1.0.2 + * @param {function} callback=null + * @returns {Promise} + */ + getNodeByPathnum(path, callback = null) { + if (typeof path === 'string') { + path = path.split('.'); + } + var pathnumError = new Error(`Failed path discovery at ${path.slice(0, pos).join("/")}`); + var pos = 0; + var lastMissingPos = -1; + var currentNode = this.root; + const getNext = () => { + return Promise.resolve() + .then(() => { + const children = currentNode.getChildren(); + const number = Number(path[pos]); + if (children != null) { + for (let i = 0; i < children.length; i++) { + var node = children[i]; + if (node.getNumber() === number) { + // We have this part already. + pos++; + if (pos >= path.length) { + return node; + } + currentNode = node; + return getNext(); + } + } + } + // We do not have that node yet. + if (lastMissingPos === pos) { + throw pathnumError; + } + lastMissingPos = pos; + return this.getDirectory(currentNode, callback).then(() => getNext()); + }); + } + return getNext(); + } + + /** + * + * @param {TreeNode} fnNode + * @param {FunctionArgument[]} params + */ + invokeFunction(fnNode, params) { + return new Promise((resolve, reject) => { + this.addRequest({node: fnNode, func: (error) => { + if (error) { + reject(error); + this._finishRequest(); + return; + } + + const cb = (error, result) => { + this._clearTimeout(); + if (error) { + reject(error); + } + else { + if (this._debug) { + console.log("InvocationResult", result); + } + resolve(result); + } + // cleaning callback and making next request. + this._finishRequest(); + }; + + if (this._debug) { + console.log("Invocking function", fnNode); + } + this._callback = cb; + this._client.sendBERNode(fnNode.invoke(params)); + }}); + }) + } + + /** + * @returns {boolean} + */ + isConnected() { + return ((this._client !== undefined) && (this._client.isConnected())); + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + * @param {MatrixOperation} operation + */ + matrixOPeration(matrixNode, targetID, sources, operation = ember.MatrixOperation.connect) { + return new Promise((resolve, reject) => { + if (!Array.isArray(sources)) { + return reject(new Error("Sources should be an array")); + } + try { + matrixNode.validateConnection(targetID, sources); + } + catch(e) { + return reject(e); + } + const connections = {} + const targetConnection = new ember.MatrixConnection(targetID); + targetConnection.operation = operation; + targetConnection.setSources(sources); + connections[targetID] = targetConnection; + + this.addRequest({node: matrixNode, func: (error) => { + if (error) { + this._finishRequest(); + reject(error); + return; + } + + this._callback = (error, node) => { + const requestedPath = matrixNode.getPath(); + if (node == null) { + if (this._debug) { + console.log(`received null response for ${requestedPath}`); + } + return; + } + if (error) { + if (this._debug) { + console.log("Received getDirectory error", error); + } + this._clearTimeout(); // clear the timeout now. The resolve below may take a while. + this._finishRequest(); + reject(error); + return; + } + let matrix = null; + if (node != null) { + matrix = node.elements[0]; + } + if (matrix != null && matrix.isMatrix() && matrix.getPath() === requestedPath) { + this._clearTimeout(); // clear the timeout now. The resolve below may take a while. + this._finishRequest(); + resolve(matrix); + } + else { + if (this._debug) { + console.log(`unexpected node response during matrix connect ${requestedPath}`, + JSON.stringify(matrix.toJSON(), null, 4)); + } + } + } + this._client.sendBERNode(matrixNode.connect(connections)); + }}); + }); + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + */ + matrixConnect(matrixNode, targetID, sources) { + return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.connect) + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + */ + matrixDisconnect(matrixNode, targetID, sources) { + return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.disconnect) + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + */ + matrixSetConnection(matrixNode, targetID, sources) { + return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.absolute) + } + + /** + * + * @param {function} f + */ + saveTree(f) { + const writer = new BER.Writer(); + this.root.encode(writer); + f(writer.buffer); + } + + /** + * + * @param {TreeNode} node + * @param {string|number} value + * @returns {Promise} + */ + setValue(node, value) { + return new Promise((resolve, reject) => { + if ((!(node instanceof ember.Parameter)) && + (!(node instanceof ember.QualifiedParameter))) { + reject(new errors.EmberAccessError('not a property')); + } + else { + // if (this._debug) { console.log('setValue', node.getPath(), value); } + this.addRequest({node: node, func: error => { + if (error) { + this._finishRequest(); + reject(error); + return; + } + + let cb = (error, node) => { + this._clearTimeout(); + this._finishRequest(); + if (error) { + reject(error); + } + else { + resolve(node); + } + }; + + this._callback = cb; + if (this._debug) { + console.log('setValue sending ...', node.getPath(), value); + } + this._client.sendBERNode(node.setValue(value)); + }}); + } + }); + } + + /** + * + * @param {TreeNode} qnode + * @param {function} callback + */ + subscribe(qnode, callback) { + if ((qnode.isParameter() || qnode.isMatrix()) && qnode.isStream()) { + if (qnode == null) { + this.root.clear(); + qnode = this.root; + } + return new Promise((resolve, reject) => { + this.addRequest({node: qnode, func: (error) => { + if (this._debug) { + console.log("Sending subscribe", qnode); + } + this._client.sendBERNode(qnode.subscribe(callback)); + this._finishRequest(); + resolve(); + }}); + }); + } else { + node.addCallback(callback); + } + } + + /** + * + * @param {TreeNode} qnode + * @param {function} callback + */ + unsubscribe(qnode, callback) { + if (qnode.isParameter() && qnode.isStream()) { + if (qnode == null) { + this.root.clear(); + qnode = this.root; + } + return new Promise((resolve, reject) => { + this.addRequest({node: qnode, func: (error) => { + if (this._debug) { + console.log("Sending subscribe", qnode); + } + this._client.sendBERNode(qnode.unsubscribe(callback)); + this._finishRequest(); + resolve(); + }}); + }); + } + } +} + +function isDirectSubPathOf(path, parent) { + return path === parent || (path.lastIndexOf('.') === parent.length && path.startsWith(parent)); +} + +module.exports = EmberClient; + diff --git a/EmberClient/index.js b/EmberClient/index.js new file mode 100755 index 0000000..964a37d --- /dev/null +++ b/EmberClient/index.js @@ -0,0 +1 @@ +module.exports = require("./EmberClient"); \ No newline at end of file diff --git a/EmberLib/Command.js b/EmberLib/Command.js index 0ee5b77..5d76c2b 100755 --- a/EmberLib/Command.js +++ b/EmberLib/Command.js @@ -15,14 +15,20 @@ const FieldFlags = new Enum({ }); class Command { + /** + * + * @param {number} number + */ constructor(number) { - if(number !== undefined) - this.number = number; + this.number = number; if(number == COMMAND_GETDIRECTORY) { - this.fieldFlags = FieldFlags.all; + this.fieldFlags = FieldFlags.all; } } + /** + * @returns {boolean} + */ isCommand() { return true; } diff --git a/EmberLib/Element.js b/EmberLib/Element.js new file mode 100755 index 0000000..26bfa9d --- /dev/null +++ b/EmberLib/Element.js @@ -0,0 +1,38 @@ +"use strict"; +const TreeNode = require("./TreeNode"); +const BER = require('../ber.js'); + +class Element extends TreeNode { + /** + * + * @param {number} number + */ + constructor(number) { + super(); + this.number = number; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(this._seqID); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.number); + ber.endSequence(); // BER.CONTEXT(0) + + if(this.contents != null) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + this.encodeChildren(ber); + + ber.endSequence(); // BER.APPLICATION(3) + } +} + +module.exports = Element; \ No newline at end of file diff --git a/EmberLib/Function.js b/EmberLib/Function.js index ee53b7a..3ccb79b 100755 --- a/EmberLib/Function.js +++ b/EmberLib/Function.js @@ -8,42 +8,14 @@ class Function extends TreeNode { super(); this.number = number; this.func = func; - } - - isFunction() { - return true; + this._seqID = BER.APPLICATION(19); } /** - * - * @param {BER} ber + * @returns {boolean} */ - encode(ber) { - ber.startSequence(BER.APPLICATION(19)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.number); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i 0; } + /** + * @returns {boolean} + */ isCommand() { return false; } - + /** + * @returns {boolean} + */ isNode() { return false; } - + /** + * @returns {boolean} + */ isMatrix() { return false; } - + /** + * @returns {boolean} + */ isParameter() { return false; } - + /** + * @returns {boolean} + */ isFunction() { return false; } - + /** + * @returns {boolean} + */ isRoot() { return this._parent == null; } - + /** + * @returns {boolean} + */ isQualified() { return false; } - + /** + * @returns {boolean} + */ isStream() { return this.contents != null && this.contents.streamIdentifier != null; } - + /** + * @returns {TreeNode} + */ getMinimalContent() { let obj; if (this.isQualified()) { @@ -149,7 +178,9 @@ class TreeNode { } return obj; }; - + /** + * @returns {TreeNode} + */ getDuplicate() { const obj = this.getMinimal(); obj.update(this); @@ -388,7 +419,7 @@ class TreeNode { * */ toJSON() { - const res = {}; + const res = {nodeType: this.constructor.name}; const node = this; if (this.isRoot()) { const elements = this.getChildren(); @@ -400,6 +431,9 @@ class TreeNode { res.path = node.getPath(); if (node.contents) { for(let prop in node.contents) { + if (prop[0] == "_") { + continue; + } if (node.contents.hasOwnProperty(prop)) { const type = typeof node.contents[prop]; if ((type === "string") || (type === "number")) { diff --git a/EmberLib/index.js b/EmberLib/index.js index 73789ac..10ebba6 100755 --- a/EmberLib/index.js +++ b/EmberLib/index.js @@ -1,7 +1,7 @@ const {Subscribe,COMMAND_SUBSCRIBE,Unsubscribe,COMMAND_UNSUBSCRIBE, GetDirectory,COMMAND_GETDIRECTORY,Invoke,COMMAND_INVOKE} = require("./constants"); const BER = require('../ber.js'); - +const errors = require("../errors"); const TreeNode = require("./TreeNode"); const Command = require("./Command"); const Function = require("./Function"); @@ -31,19 +31,12 @@ const rootDecode = function(ber) { const r = new TreeNode(); let tag = undefined; while(ber.remain > 0) { - if (DEBUG) { console.log("Reading root"); } tag = ber.peek(); if (tag === BER.APPLICATION(0)) { ber = ber.getSequence(BER.APPLICATION(0)); tag = ber.peek(); - if (DEBUG) { - console.log("Application 0 start"); - } if (tag === BER.APPLICATION(11)) { - if (DEBUG) { - console.log("Application 11 start"); - } const seq = ber.getSequence(BER.APPLICATION(11)); while (seq.remain > 0) { try { @@ -53,10 +46,6 @@ const rootDecode = function(ber) { } } catch (e) { - if (DEBUG) { - console.log("Decode ERROR", e.stack); - return r; - } throw e; } } @@ -77,7 +66,6 @@ const rootDecode = function(ber) { return Element.decode(rootReader) } catch (e) { - console.log(e.stack); return r; } } @@ -91,36 +79,24 @@ const rootDecode = function(ber) { const childDecode = function(ber) { const tag = ber.peek(); if (tag == BER.APPLICATION(1)) { - if (DEBUG) { console.log("Parameter decode");} return Parameter.decode(ber); } else if(tag == BER.APPLICATION(3)) { - if (DEBUG) { console.log("Node decode");} return Node.decode(ber); } else if(tag == BER.APPLICATION(2)) { - if (DEBUG) { console.log("Command decode");} return Command.decode(ber); } else if(tag == BER.APPLICATION(9)) { - if (DEBUG) { console.log("QualifiedParameter decode");} return QualifiedParameter.decode(ber); } else if(tag == BER.APPLICATION(10)) { - if (DEBUG) { console.log("QualifiedNode decode");} return QualifiedNode.decode(ber); } else if(tag == BER.APPLICATION(13)) { - if (DEBUG) { console.log("MatrixNode decode");} return MatrixNode.decode(ber); - } - else if(tag == BER.APPLICATION(17)) { - if (DEBUG) { console.log("QualifiedMatrix decode");} + } else if(tag == BER.APPLICATION(17)) { return QualifiedMatrix.decode(ber); - } - else if(tag == BER.APPLICATION(19)) { - if (DEBUG) { console.log("Function decode");} + } else if(tag == BER.APPLICATION(19)) { return Function.decode(ber); } else if (tag == BER.APPLICATION(20)) { - if (DEBUG) { console.log("QualifiedFunction decode");} return QualifiedFunction.decode(ber); - } - else if(tag == BER.APPLICATION(24)) { + } else if(tag == BER.APPLICATION(24)) { // Template throw new errors.UnimplementedEmberTypeError(tag); } else { @@ -130,10 +106,16 @@ const childDecode = function(ber) { TreeNode.decode = childDecode; +const DecodeBuffer = function (packet) { + const ber = new BER.Reader(packet); + return TreeNode.decode(ber); +}; + module.exports = { Command, childDecode: childDecode, rootDecode: rootDecode, + DecodeBuffer, Root: TreeNode, Function, FunctionArgument, diff --git a/EmberServer.js b/EmberServer.js deleted file mode 100755 index 0a7a7e7..0000000 --- a/EmberServer.js +++ /dev/null @@ -1,827 +0,0 @@ -const EventEmitter = require('events').EventEmitter; -const util = require('util'); -const S101Server = require('./client.js').S101Server; -const ember = require('./EmberLib'); - -function TreeServer(host, port, tree) { - TreeServer.super_.call(this); - var self = this; - self._debug = false; - - self.callback = undefined; - self.timeoutValue = 2000; - self.server = new S101Server(host, port); - self.tree = tree; - self.clients = new Set(); - self.subscribers = {}; - - self.server.on('listening', () => { - if (self._debug) { console.log("listening"); } - self.emit('listening'); - if (self.callback !== undefined) { - self.callback(); - self.callback = undefined; - } - }); - - self.server.on('connection', (client) => { - if (self._debug) { console.log("ember new connection from", client.remoteAddress()); } - self.clients.add(client); - client.on("emberTree", (root) => { - if (self._debug) { console.log("ember new request from", client.remoteAddress(), root); } - // Queue the action to make sure responses are sent in order. - client.addRequest(() => { - try { - let path = self.handleRoot(client, root); - self.emit("request", {client: client.remoteAddress(), root: root, path: path}); - } - catch(e) { - if (self._debug) { console.log(e.stack); } - self.emit("error", e); - } - }); - }); - client.on("disconnected", () => { - self.clients.delete(client); - self.emit('disconnect', client.remoteAddress()); - }); - client.on("error", error => { - self.emit('clientError', { remoteAddress: client.remoteAddress(), error }); - }); - self.emit('connection', client.remoteAddress()); - }); - - self.server.on('disconnected', () => { - self.emit('disconnected', client.remoteAddress()); - }); - - self.server.on("error", (e) => { - self.emit("error", e); - if (self.callback !== undefined) { - self.callback(e); - } - }); - -} - -util.inherits(TreeServer, EventEmitter); - - -TreeServer.prototype.listen = function() { - return new Promise((resolve, reject) => { - this.callback = (e) => { - if (e == null) { - return resolve(); - } - return reject(e); - }; - this.server.listen(); - }); -}; - -TreeServer.prototype.close = function () { - return new Promise((resolve, reject) => { - this.callback = (e) => { - if (e == null) { - return resolve(); - } - return reject(e); - }; - this.server.server.close(); - }); -}; - -TreeServer.prototype.handleRoot = function(client, root) { - if ((root == null) || (root.elements == null) || (root.elements.size < 1)) { - // ignore empty requests. - return; - } - - const node = root.getChildren()[0]; - client.request = node; - - if (node.path !== undefined) { - return this.handleQualifiedNode(client, node); - } - else if (node.isCommand()) { - // Command on root element - this.handleCommand(client, this.tree, node); - return "root"; - } - else { - return this.handleNode(client, node); - } -} - -TreeServer.prototype.handleError = function(client, node) { - if (client !== undefined) { - const res = node == null ? this.tree.getMinimal() : node; - client.sendBERNode(res); - } -} - - -TreeServer.prototype.handleQualifiedNode = function(client, node) { - const path = node.path; - // Find this element in our tree - const element = this.tree.getElementByPath(path); - - if (element == null) { - this.emit("error", new Error(`unknown element at path ${path}`)); - return this.handleError(client); - } - - if (node.hasChildren()) { - for(child of node.children) { - if (child.isCommand()) { - this.handleCommand(client, element, child); - } - break; - } - } - else { - if (node.isMatrix()) { - this.handleQualifiedMatrix(client, element, node); - } - else if (node.isParameter()) { - this.handleQualifiedParameter(client, element, node); - } - } - return path; -} - - -TreeServer.prototype.handleNode = function(client, node) { - // traverse the tree - let element = node; - let path = []; - while(element !== undefined) { - if (element.number == null) { - this.emit("error", "invalid request"); - return; - } - if (element.isCommand()) { - break; - } - path.push(element.number); - - let children = element.getChildren(); - if ((! children) || (children.length === 0)) { - break; - } - element = element.children[0]; - } - let cmd = element; - - if (cmd == null) { - this.emit("error", "invalid request"); - return this.handleError(client); - } - - element = this.tree.getElementByPath(path.join(".")); - - if (element == null) { - this.emit("error", new Error(`unknown element at path ${path}`)); - return this.handleError(client); - } - - if (cmd.isCommand()) { - this.handleCommand(client, element, cmd); - } - else if ((cmd.isCommand()) && (cmd.connections !== undefined)) { - this.handleMatrixConnections(client, element, cmd.connections); - } - else if ((cmd.isParameter()) && - (cmd.contents !== undefined) && (cmd.contents.value !== undefined)) { - if (this._debug) { console.log(`setValue for element at path ${path} with value ${cmd.contents.value}`); } - this.setValue(element, cmd.contents.value, client); - let res = this.getResponse(element); - client.sendBERNode(res) - this.updateSubscribers(element.getPath(), res, client); - } - else { - this.emit("error", new Error("invalid request format")); - if (this._debug) { console.log("invalid request format"); } - return this.handleError(client, element.getTreeBranch()); - } - return path; -} - -TreeServer.prototype.handleQualifiedMatrix = function(client, element, matrix) -{ - this.handleMatrixConnections(client, element, matrix.connections); -} - -TreeServer.prototype.handleQualifiedParameter = function(client, element, parameter) -{ - if (parameter.contents.value !== undefined) { - this.setValue(element, parameter.contents.value, client); - let res = this.getQualifiedResponse(element); - client.sendBERNode(res) - this.updateSubscribers(element.getPath(), res, client); - } -} - - -TreeServer.prototype.handleMatrixConnections = function(client, matrix, connections, response = true) { - var res,conResult; - var root; // ember message root - if (this._debug) { - console.log("Handling Matrix Connection"); - } - if (client != null && client.request.isQualified()) { - root = new ember.Root(); - res = new ember.QualifiedMatrix(matrix.getPath()); - //root.elements = [res]; // do not use addchild or the element will get removed from the tree. - root.addElement(res); - } - else { - res = new ember.MatrixNode(matrix.number); - root = matrix._parent.getTreeBranch(res); - } - res.connections = {}; - for(let id in connections) { - if (!connections.hasOwnProperty(id)) { - continue; - } - let connection = connections[id]; - conResult = new ember.MatrixConnection(connection.target); - let emitType; - res.connections[connection.target] = conResult; - - if (matrix.connections[connection.target].isLocked()) { - conResult.disposition = ember.MatrixDisposition.locked; - } - else if (matrix.contents.type !== ember.MatrixType.nToN && - connection.operation !== ember.MatrixOperation.disconnect && - connection.sources != null && connection.sources.length === 1) { - if (matrix.contents.type === ember.MatrixType.oneToOne) { - // if the source is being used already, disconnect it. - const targets = matrix.getSourceConnections(connection.sources[0]); - if (targets.length === 1 && targets[0] !== connection.target) { - const disconnect = new ember.MatrixConnection(targets[0]); - disconnect.setSources([]); - disconnect.disposition = ember.MatrixDisposition.modified; - res.connections[targets[0]] = disconnect; - matrix.setSources(targets[0], []); - if (response) { - this.emit("matrix-disconnect", { - target: targets[0], - sources: connection.sources, - client: client == null ? null : client.remoteAddress() - }); - } - } - } - // if the target is connected already, disconnect it - if (matrix.connections[connection.target].sources != null && - matrix.connections[connection.target].sources.length === 1) { - if (matrix.contents.type === ember.MatrixType.oneToN) { - const disconnectSource = this.getDisconnectSource(matrix, connection.target); - if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { - if (disconnectSource != null && disconnectSource != -1 && - disconnectSource != connection.sources[0]) { - connection.sources = [disconnectSource]; - } - else { - // do nothing - connection.operarion = ember.MatrixOperation.tally; - } - } - } - if (matrix.connections[connection.target].sources[0] !== connection.sources[0]) { - const source = matrix.connections[connection.target].sources[0]; - matrix.setSources(connection.target, []); - if (response) { - this.emit("matrix-disconnect", { - target: connection.target, - sources: [source], - client: client == null ? null : client.remoteAddress() - }); - } - } - else if (matrix.contents.type === ember.MatrixType.oneToOne) { - // let's change the request into a disconnect - connection.operation = ember.MatrixOperation.disconnect; - } - } - } - - if (connection.operation !== ember.MatrixOperation.disconnect && - connection.sources != null && connection.sources.length > 0 && - matrix.canConnect(connection.target,connection.sources,connection.operation)) { - // Apply changes - if ((connection.operation == null) || - (connection.operation.value == ember.MatrixOperation.absolute)) { - matrix.setSources(connection.target, connection.sources); - emitType = "matrix-change"; - } - else if (connection.operation == ember.MatrixOperation.connect) { - matrix.connectSources(connection.target, connection.sources); - emitType = "matrix-connect"; - } - conResult.disposition = ember.MatrixDisposition.modified; - } - else if (connection.operation !== ember.MatrixOperation.disconnect && - connection.sources != null && connection.sources.length === 0 && - matrix.connections[connection.target].sources != null && - matrix.connections[connection.target].sources.length > 0) { - // let's disconnect - if (response) { - this.emit("matrix-disconnect", { - target: connection.target, - sources: matrix.connections[connection.target].sources, - client: client == null ? null : client.remoteAddress() - }); - } - matrix.setSources(connection.target, []); - conResult.disposition = ember.MatrixDisposition.modified; - } - else if (connection.operation === ember.MatrixOperation.disconnect && - matrix.connections[connection.target].sources != null && - matrix.connections[connection.target].sources.length > 0) { - // Disconnect - if (matrix.contents.type === ember.MatrixType.oneToN) { - const disconnectSource = this.getDisconnectSource(matrix, connection.target); - if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { - if (disconnectSource != null && disconnectSource != -1 && - disconnectSource != connection.sources[0]) { - if (response) { - this.emit("matrix-disconnect", { - target: connection.target, - sources: matrix.connections[connection.target].sources, - client: client == null ? null : client.remoteAddress() - }); - } - matrix.setSources(connection.target, [disconnectSource]); - connection.operarion = ember.MatrixOperation.modified; - } - else { - // do nothing - connection.operarion = ember.MatrixOperation.tally; - } - } - } - else { - matrix.disconnectSources(connection.target, connection.sources); - conResult.disposition = ember.MatrixDisposition.modified; - emitType = "matrix-disconnect"; - } - } - else if (conResult.disposition !== ember.MatrixDisposition.locked){ - if (this._debug) { - console.log(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); - } - conResult.disposition = ember.MatrixDisposition.tally; - } - - // Send response or update subscribers. - conResult.sources = matrix.connections[connection.target].sources; - if (response) { - // We got a request so emit something. - this.emit(emitType, { - target: connection.target, - sources: connection.sources, - client: client == null ? null : client.remoteAddress() - }); - } - } - if (client != null) { - client.sendBERNode(root); - } - - if (conResult != null && conResult.disposition !== ember.MatrixDisposition.tally) { - if (this._debug) { - console.log("Updating subscribers for matrix change"); - } - this.updateSubscribers(matrix.getPath(), root, client); - } -} - -const validateMatrixOperation = function(matrix, target, sources) { - if (matrix == null) { - throw new Error(`matrix not found with path ${path}`); - } - if (matrix.contents == null) { - throw new Error(`invalid matrix at ${path} : no contents`); - } - if (matrix.contents.targetCount == null) { - throw new Error(`invalid matrix at ${path} : no targetCount`); - } - if ((target < 0) || (target >= matrix.contents.targetCount)) { - throw new Error(`target id ${target} out of range 0 - ${matrix.contents.targetCount}`); - } - if (sources.length == null) { - throw new Error("invalid sources format"); - } -} - -const doMatrixOperation = function(server, path, target, sources, operation) { - let matrix = server.tree.getElementByPath(path); - - validateMatrixOperation(matrix, target, sources); - - let connection = new ember.MatrixConnection(target); - connection.sources = sources; - connection.operation = operation; - server.handleMatrixConnections(undefined, matrix, [connection], false); -} - -TreeServer.prototype.matrixConnect = function(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.connect); -} - -TreeServer.prototype.matrixDisconnect = function(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.disconnect); -} - -TreeServer.prototype.matrixSet = function(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.absolute); -} - -TreeServer.prototype.handleQualifiedFunction = function(client, element, node) { - -} - - -TreeServer.prototype.handleCommand = function(client, element, cmd) { - switch(cmd.number) { - case ember.COMMAND_GETDIRECTORY: - this.handleGetDirectory(client, element); - break; - case ember.COMMAND_SUBSCRIBE: - this.handleSubscribe(client, element); - break; - case ember.COMMAND_UNSUBSCRIBE: - this.handleUnSubscribe(client, element); - break; - case ember.COMMAND_INVOKE: - this.handleInvoke(client, cmd.invocation, element); - break; - default: - this.emit("error", new Error(`invalid command ${cmd.number}`)); - break; - } -} - -TreeServer.prototype.getResponse = function(element) { - return element.getTreeBranch(undefined, function(node) { - node.update(element); - let children = element.getChildren(); - if (children != null) { - for (let i = 0; i < children.length; i++) { - node.addChild(children[i].getDuplicate()); - } - } - else if (this._debug) { - console.log("getResponse","no children"); - } - }); -} - -TreeServer.prototype.getQualifiedResponse = function(element) { - const res = new ember.Root(); - let dup; - if (element.isRoot() === false) { - dup = element.toQualified(); - } - let children = element.getChildren(); - if (children != null) { - for (let i = 0; i < children.length; i++) { - res.addChild(children[i].toQualified().getMinimalContent()); - } - } - else { - res.addChild(dup); - } - return res; -} - -TreeServer.prototype.handleInvoke = function(client, invocation, element) { - const result = new ember.InvocationResult(); - result.invocationId = invocation.id; - if (element == null || !element.isFunction()) { - result.setFailure(); - } - else { - try { - result.setResult(element.func(invocation.arguments)); - } - catch(e){ - this.emit("error", e); - result.setFailure(); - } - } - const res = new ember.Root(); - res.addResult(result); - client.sendBERNode(res); -} - -TreeServer.prototype.handleGetDirectory = function(client, element) { - if (client !== undefined) { - if ((element.isMatrix() || element.isParameter()) && - (!element.isStream())) { - // ember spec: parameter without streamIdentifier should - // report their value changes automatically. - this.subscribe(client, element); - } - else if (element.isNode()) { - const children = element.getChildren(); - if (children != null) { - for (let i = 0; i < children.length; i++) { - const child = children[i]; - if ((child.isMatrix() || child.isParameter()) && - (!child.isStream())) { - this.subscribe(client, child); - } - } - } - } - - const res = this.getQualifiedResponse(element); - if (this._debug) { - console.log("getDirectory response", res); - } - client.sendBERNode(res); - } -} - -TreeServer.prototype.handleSubscribe = function(client, element) { - if (this._debug) { - console.log("subscribe"); - } - this.subscribe(client, element); -} - -TreeServer.prototype.handleUnSubscribe = function(client, element) { - if (this._debug) { - console.log("unsubscribe"); - } - this.unsubscribe(client, element); -} - - -TreeServer.prototype.subscribe = function(client, element) { - const path = element.getPath(); - if (this.subscribers[path] == null) { - this.subscribers[path] = new Set(); - } - this.subscribers[path].add(client); -} - -TreeServer.prototype.unsubscribe = function(client, element) { - const path = element.getPath(); - if (this.subscribers[path] == null) { - return; - } - this.subscribers[path].delete(client); -} - -TreeServer.prototype.setValue = function(element, value, origin, key) { - return new Promise((resolve, reject) => { - // Change the element value if write access permitted. - if (element.contents == null) { - return resolve(); - } - if (element.isParameter()) { - if ((element.contents.access !== undefined) && - (element.contents.access.value > 1)) { - element.contents.value = value; - const res = this.getResponse(element); - this.updateSubscribers(element.getPath(),res, origin); - this.emit("value-change", element); - } - } - else if (element.isMatrix()) { - if ((key !== undefined) && (element.contents.hasOwnProperty(key))) { - element.contents[key] = value; - const res = this.getResponse(element); - this.updateSubscribers(element.getPath(),res, origin); - this.emit("value-change", element); - } - } - return resolve(); - }); -} - -TreeServer.prototype.replaceElement = function(element) { - let path = element.getPath(); - let parent = this.tree.getElementByPath(path); - if ((parent == null)||(parent._parent == null)) { - throw new Error(`Could not find element at path ${path}`); - } - parent = parent._parent; - let children = parent.getChildren(); - let newList = []; - for(let i = 0; i <= children.length; i++) { - if (children[i] && children[i].getPath() == path) { - element._parent = parent; // move it to new tree. - children[i] = element; - let res = this.getResponse(element); - this.updateSubscribers(path,res); - return; - } - } -} - -TreeServer.prototype.getDisconnectSource = function(matrix, targetID) { - if (matrix.defaultSources) { - return matrix.defaultSources[targetID].contents.value; - } - if (matrix.contents.labels == null || matrix.contents.labels.length == 0) { - return null; - } - const basePath = matrix.contents.labels[0].basePath; - const labels = this.tree.getElementByPath(basePath); - const number = labels.getNumber() + 1; - const parent = labels.getParent(); - const children = parent.getChildren(); - for(let child of children) { - if (child.getNumber() === number) { - matrix.defaultSources = child.getChildren(); - return matrix.defaultSources[targetID].contents.value; - } - } - return null; -} - -TreeServer.prototype.updateSubscribers = function(path, response, origin) { - if (this.subscribers[path] == null) { - return; - } - - for (let client of this.subscribers[path]) { - if (client === origin) { - continue; // already sent the response to origin - } - if (this.clients.has(client)) { - client.queueMessage(response); - } - else { - // clean up subscribers - client is gone - this.subscribers[path].delete(client); - } - } -} - -const parseMatrixContent = function(matrixContent, content) { - if (content.labels) { - matrixContent.labels = []; - for(let l = 0; l < content.labels.length; l++) { - if (typeof (content.labels[l]) === "object") { - matrixContent.labels.push( - new ember.Label( - content.labels[l].basePath, - content.labels[l].description - ) - ); - } - else { - // for backward compatibility... Remove in the future - matrixContent.labels.push( - new ember.Label(content.labels[l]) - ); - } - } - delete content.labels; - } - if (content.type != null) { - if (content.type == "oneToN") { - matrixContent.type = ember.MatrixType.oneToN; - } - else if (content.type == "oneToOne") { - matrixContent.type = ember.MatrixType.oneToOne; - } - else if (content.type == "nToN") { - matrixContent.type = ember.MatrixType.nToN; - matrixContent.maximumTotalConnects = content.maximumTotalConnects == null ? - Number(content.targetCount) * Number(content.sourceCount) : Number(content.maximumTotalConnects); - matrixContent.maximumConnectsPerTarget = content.maximumConnectsPerTarget == null ? - Number(content.sourceCount) : Number(content.maximumConnectsPerTarget); - } - else { - throw new Error(`Invalid matrix type ${content.type}`); - } - delete content.type; - } - if (content.mode != null) { - if (content.mode == "linear") { - matrixContent.mode = ember.MatrixMode.linear; - } - else if (content.mode == "nonLinear") { - matrixContent.mode = ember.MatrixMode.nonLinear; - } - else { - throw new Error(`Invalid matrix mode ${content.mode}`); - } - delete content.mode; - } -} - -const parseObj = function(parent, obj) { - for(let i = 0; i < obj.length; i++) { - let emberElement; - let content = obj[i]; - let number = content.number !== undefined ? content.number : i; - delete content.number; - if (content.value != null) { - emberElement = new ember.Parameter(number); - emberElement.contents = new ember.ParameterContents(content.value); - if (content.type) { - emberElement.contents.type = ember.ParameterType.get(content.type); - delete content.type; - } - else { - emberElement.contents.type = ember.ParameterType.string; - } - if (content.access) { - emberElement.contents.access = ember.ParameterAccess.get(content.access); - delete content.access; - } - else { - emberElement.contents.access = ember.ParameterAccess.read; - } - } - else if (content.func != null) { - emberElement = new ember.Function(number, content.func); - emberElement.contents = new ember.FunctionContent(); - if (content.arguments != null) { - for(let argument of content.arguments) { - emberElement.contents.arguments.push(new ember.FunctionArgument( - argument.type, - argument.value, - argument.name - )); - } - } - if (content.result != null) { - for(let argument of content.result) { - emberElement.contents.result.push(new ember.FunctionArgument( - argument.type, - argument.value, - argument.name - )); - } - } - delete content.result; - } - else if (content.targetCount != null) { - emberElement = new ember.MatrixNode(number); - emberElement.contents = new ember.MatrixContents(); - parseMatrixContent(emberElement.contents, content); - if (content.connections) { - emberElement.connections = {}; - for (let c in content.connections) { - if (! content.connections.hasOwnProperty(c)) { - continue; - } - const t = content.connections[c].target != null ? content.connections[c].target : 0; - emberElement.setSources(t, content.connections[c].sources); - } - delete content.connections; - } - else { - emberElement.connections = {}; - for (let t = 0; t < content.targetCount; t++) { - let connection = new ember.MatrixConnection(t); - emberElement.connections[t] = connection; - } - } - } - else { - emberElement = new ember.Node(number); - emberElement.contents = new ember.NodeContents(); - } - for(let id in content) { - if (emberElement.isFunction() && id === "arguments") { - // we did it already. - continue; - } - if ((id !== "children") && (content.hasOwnProperty(id))) { - emberElement.contents[id] = content[id]; - } - else { - parseObj(emberElement, content.children); - } - } - parent.addChild(emberElement); - } -} - -TreeServer.JSONtoTree = function(obj) { - const tree = new ember.Root(); - parseObj(tree, obj); - return tree; -} - - -TreeServer.prototype.toJSON = function() { - if (this.tree == null) { - return []; - } - const elements = this.tree.getChildren(); - - return elements.map(element => element.toJSON()); -}; - -module.exports = TreeServer; diff --git a/EmberServer/ElementHandlers.js b/EmberServer/ElementHandlers.js new file mode 100755 index 0000000..c30f888 --- /dev/null +++ b/EmberServer/ElementHandlers.js @@ -0,0 +1,186 @@ +"use strict"; +const QualifiedHandlers = require("./QualifiedHandlers"); +const ember = require('../EmberLib'); + +class ElementHandlers extends QualifiedHandlers{ + /** + * + * @param {EmberServer} server + */ + constructor(server) { + super(server); + } + /** + * + * @param {S101Client} client + * @param {TreeNode} root + * @param {Command} cmd + */ + handleCommand(client, element, cmd) { + switch(cmd.number) { + case ember.COMMAND_GETDIRECTORY: + this.handleGetDirectory(client, element); + break; + case ember.COMMAND_SUBSCRIBE: + this.handleSubscribe(client, element); + break; + case ember.COMMAND_UNSUBSCRIBE: + this.handleUnSubscribe(client, element); + break; + case ember.COMMAND_INVOKE: + this.handleInvoke(client, cmd.invocation, element); + break; + default: + this.server.emit("error", new Error(`invalid command ${cmd.number}`)); + break; + } + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleGetDirectory(client, element) { + if (client !== undefined) { + if ((element.isMatrix() || element.isParameter()) && + (!element.isStream())) { + // ember spec: parameter without streamIdentifier should + // report their value changes automatically. + this.server.subscribe(client, element); + } + else if (element.isNode()) { + const children = element.getChildren(); + if (children != null) { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if ((child.isMatrix() || child.isParameter()) && + (!child.isStream())) { + this.server.subscribe(client, child); + } + } + } + } + + const res = this.server.getQualifiedResponse(element); + if (this.server._debug) { + console.log("getDirectory response", res); + } + client.sendBERNode(res); + } + } + + /** + * + * @param {S101Client} client + * @param {Invocation} invocation + * @param {TreeNode} element + */ + handleInvoke(client, invocation, element) { + const result = new ember.InvocationResult(); + result.invocationId = invocation.id; + if (element == null || !element.isFunction()) { + result.setFailure(); + } + else { + try { + result.setResult(element.func(invocation.arguments)); + } + catch(e){ + this.server.emit("error", e); + result.setFailure(); + } + } + const res = new ember.Root(); + res.addResult(result); + client.sendBERNode(res); + } + + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleNode(client, node) { + // traverse the tree + let element = node; + let path = []; + while(element !== undefined) { + if (element.number == null) { + this.server.emit("error", "invalid request"); + return; + } + if (element.isCommand()) { + break; + } + path.push(element.number); + + let children = element.getChildren(); + if ((! children) || (children.length === 0)) { + break; + } + element = element.children[0]; + } + let cmd = element; + + if (cmd == null) { + this.server.emit("error", "invalid request"); + return this.server.handleError(client); + } + + element = this.server.tree.getElementByPath(path.join(".")); + + if (element == null) { + this.server.emit("error", new Error(`unknown element at path ${path}`)); + return this.server.handleError(client); + } + + if (cmd.isCommand()) { + this.handleCommand(client, element, cmd); + } + else if ((cmd.isCommand()) && (cmd.connections !== undefined)) { + this.handleMatrixConnections(client, element, cmd.connections); + } + else if ((cmd.isParameter()) && + (cmd.contents !== undefined) && (cmd.contents.value !== undefined)) { + if (this.server._debug) { console.log(`setValue for element at path ${path} with value ${cmd.contents.value}`); } + this.setValue(element, cmd.contents.value, client); + let res = this.server.getResponse(element); + client.sendBERNode(res) + this.server.updateSubscribers(element.getPath(), res, client); + } + else { + this.server.emit("error", new Error("invalid request format")); + if (this.server._debug) { console.log("invalid request format"); } + return this.server.handleError(client, element.getTreeBranch()); + } + return path; + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleSubscribe(client, element) { + if (this.server._debug) { + console.log("subscribe"); + } + this.server.subscribe(client, element); + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleUnSubscribe(client, element) { + if (this.server._debug) { + console.log("unsubscribe"); + } + this.server.unsubscribe(client, element); + } +} + +module.exports = ElementHandlers; \ No newline at end of file diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js new file mode 100755 index 0000000..5407b46 --- /dev/null +++ b/EmberServer/EmberServer.js @@ -0,0 +1,376 @@ +const EventEmitter = require('events').EventEmitter; +const S101Server = require('../EmberSocket').S101Server; +const ember = require('../EmberLib'); +const JSONParser = require("./JSONParser"); +const ElementHandlers = require("./ElementHandlers"); + +class TreeServer extends EventEmitter{ + /** + * + * @param {string} host + * @param {number} port + * @param {TreeNode} tree + */ + constructor(host, port, tree) { + super(); + this._debug = false; + this.callback = undefined; + this.timeoutValue = 2000; + this.server = new S101Server(host, port); + this.tree = tree; + this.clients = new Set(); + this.subscribers = {}; + this._handlers = new ElementHandlers(this); + + this.server.on('listening', () => { + if (this._debug) { console.log("listening"); } + this.emit('listening'); + if (this.callback !== undefined) { + this.callback(); + this.callback = undefined; + } + }); + + this.server.on('connection', client => { + if (this._debug) { console.log("ember new connection from", client.remoteAddress()); } + this.clients.add(client); + client.on("emberTree", (root) => { + if (this._debug) { console.log("ember new request from", client.remoteAddress(), root); } + // Queue the action to make sure responses are sent in order. + client.addRequest(() => { + try { + let path = this.handleRoot(client, root); + this.emit("request", {client: client.remoteAddress(), root: root, path: path}); + } + catch(e) { + if (this._debug) { console.log(e.stack); } + this.emit("error", e); + } + }); + }); + client.on("disconnected", () => { + this.clients.delete(client); + this.emit('disconnect', client.remoteAddress()); + }); + client.on("error", error => { + this.emit('clientError', { remoteAddress: client.remoteAddress(), error }); + }); + this.emit('connection', client.remoteAddress()); + }); + + this.server.on('disconnected', () => { + this.emit('disconnected', client.remoteAddress()); + }); + + this.server.on("error", (e) => { + this.emit("error", e); + if (this.callback !== undefined) { + this.callback(e); + } + }); + } + + /** + * @returns {Promise} + */ + close() { + return new Promise((resolve, reject) => { + this.callback = (e) => { + if (e == null) { + return resolve(); + } + return reject(e); + }; + this.server.server.close(); + }); + }; + + /** + * + * @param {TreeNode} element + */ + getResponse(element) { + return element.getTreeBranch(undefined, node => { + node.update(element); + let children = element.getChildren(); + if (children != null) { + for (let i = 0; i < children.length; i++) { + node.addChild(children[i].getDuplicate()); + } + } + else if (this._debug) { + console.log("getResponse","no children"); + } + }); + } + + /** + * + * @param {TreeNode} element + */ + getQualifiedResponse(element) { + const res = new ember.Root(); + let dup; + if (element.isRoot() === false) { + dup = element.toQualified(); + } + let children = element.getChildren(); + if (children != null) { + for (let i = 0; i < children.length; i++) { + res.addChild(children[i].toQualified().getMinimalContent()); + } + } + else { + res.addChild(dup); + } + return res; + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleError(client, node) { + if (client !== undefined) { + const res = node == null ? this.tree.getMinimal() : node; + client.sendBERNode(res); + } + } + + /** + * + * @param {S101Socket} client + * @param {TreeNode} root + */ + handleRoot(client, root) { + if ((root == null) || (root.elements == null) || (root.elements.size < 1)) { + // ignore empty requests. + return; + } + + const node = root.getChildren()[0]; + client.request = node; + + if (node.path !== undefined) { + return this._handlers.handleQualifiedNode(client, node); + } + else if (node.isCommand()) { + // Command on root element + this._handlers.handleCommand(client, this.tree, node); + return "root"; + } + else { + return this._handlers.handleNode(client, node); + } + } + + /** + * @returns {Promise} + */ + listen() { + return new Promise((resolve, reject) => { + this.callback = (e) => { + if (e == null) { + return resolve(); + } + return reject(e); + }; + this.server.listen(); + }); + }; + + /** + * + * @param {string} path + * @param {number} target + * @param {number[]} sources + */ + matrixConnect(path, target, sources) { + doMatrixOperation(this, path, target, sources, ember.MatrixOperation.connect); + } + + /** + * + * @param {string} path + * @param {number} target + * @param {number[]} sources + */ + matrixDisconnect(path, target, sources) { + doMatrixOperation(this, path, target, sources, ember.MatrixOperation.disconnect); + } + + /** + * + * @param {string} path + * @param {number} target + * @param {number[]} sources + */ + matrixSet(path, target, sources) { + doMatrixOperation(this, path, target, sources, ember.MatrixOperation.absolute); + } + + /** + * + * @param {TreeNode} element + */ + replaceElement(element) { + let path = element.getPath(); + let parent = this.tree.getElementByPath(path); + if ((parent == null)||(parent._parent == null)) { + throw new Error(`Could not find element at path ${path}`); + } + parent = parent._parent; + let children = parent.getChildren(); + let newList = []; + for(let i = 0; i <= children.length; i++) { + if (children[i] && children[i].getPath() == path) { + element._parent = parent; // move it to new tree. + children[i] = element; + let res = this.getResponse(element); + this.updateSubscribers(path,res); + return; + } + } + } + + /** + * + * @param {TreeNode} element + * @param {string|number} value + * @param {S101Socket} origin + * @param {string} key + */ + setValue(element, value, origin, key) { + return new Promise((resolve, reject) => { + // Change the element value if write access permitted. + if (element.contents == null) { + return resolve(); + } + if (element.isParameter()) { + if ((element.contents.access !== undefined) && + (element.contents.access.value > 1)) { + element.contents.value = value; + const res = this.getResponse(element); + this.updateSubscribers(element.getPath(),res, origin); + this.emit("value-change", element); + } + } + else if (element.isMatrix()) { + if ((key !== undefined) && (element.contents.hasOwnProperty(key))) { + element.contents[key] = value; + const res = this.getResponse(element); + this.updateSubscribers(element.getPath(),res, origin); + this.emit("value-change", element); + } + } + return resolve(); + }); + } + + /** + * + * @param {S101Socket} client + * @param {TreeNode} root + */ + subscribe(client, element) { + const path = element.getPath(); + if (this.subscribers[path] == null) { + this.subscribers[path] = new Set(); + } + this.subscribers[path].add(client); + } + + /** + * @returns {Object} + */ + toJSON() { + if (this.tree == null) { + return []; + } + const elements = this.tree.getChildren(); + + return elements.map(element => element.toJSON()); + }; + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + unsubscribe(client, element) { + const path = element.getPath(); + if (this.subscribers[path] == null) { + return; + } + this.subscribers[path].delete(client); + } + + /** + * + * @param {string} path + * @param {TreeNode} response + * @param {S101Socket} origin + */ + updateSubscribers(path, response, origin) { + if (this.subscribers[path] == null) { + return; + } + + for (let client of this.subscribers[path]) { + if (client === origin) { + continue; // already sent the response to origin + } + if (this.clients.has(client)) { + client.queueMessage(response); + } + else { + // clean up subscribers - client is gone + this.subscribers[path].delete(client); + } + } + } + + /** + * + * @param {object} obj + * @returns {TreeNode} + */ + static JSONtoTree(obj) { + const tree = new ember.Root(); + JSONParser.parseObj(tree, obj); + return tree; + } +} + + +const validateMatrixOperation = function(matrix, target, sources) { + if (matrix == null) { + throw new Error(`matrix not found with path ${path}`); + } + if (matrix.contents == null) { + throw new Error(`invalid matrix at ${path} : no contents`); + } + if (matrix.contents.targetCount == null) { + throw new Error(`invalid matrix at ${path} : no targetCount`); + } + if ((target < 0) || (target >= matrix.contents.targetCount)) { + throw new Error(`target id ${target} out of range 0 - ${matrix.contents.targetCount}`); + } + if (sources.length == null) { + throw new Error("invalid sources format"); + } +} + +const doMatrixOperation = function(server, path, target, sources, operation) { + let matrix = server.tree.getElementByPath(path); + + validateMatrixOperation(matrix, target, sources); + + let connection = new ember.MatrixConnection(target); + connection.sources = sources; + connection.operation = operation; + server.handleMatrixConnections(undefined, matrix, [connection], false); +} + +module.exports = TreeServer; diff --git a/EmberServer/JSONParser.js b/EmberServer/JSONParser.js new file mode 100755 index 0000000..1908b11 --- /dev/null +++ b/EmberServer/JSONParser.js @@ -0,0 +1,160 @@ +"use strict"; +const ember = require('../EmberLib'); + +class JSONParser { + /** + * + * @param {MatrixContent} matrixContent + * @param {object} content + */ + static parseMatrixContent(matrixContent, content) { + if (content.labels) { + matrixContent.labels = []; + for(let l = 0; l < content.labels.length; l++) { + if (typeof (content.labels[l]) === "object") { + matrixContent.labels.push( + new ember.Label( + content.labels[l].basePath, + content.labels[l].description + ) + ); + } + else { + // for backward compatibility... Remove in the future + matrixContent.labels.push( + new ember.Label(content.labels[l]) + ); + } + } + delete content.labels; + } + if (content.type != null) { + if (content.type == "oneToN") { + matrixContent.type = ember.MatrixType.oneToN; + } + else if (content.type == "oneToOne") { + matrixContent.type = ember.MatrixType.oneToOne; + } + else if (content.type == "nToN") { + matrixContent.type = ember.MatrixType.nToN; + matrixContent.maximumTotalConnects = content.maximumTotalConnects == null ? + Number(content.targetCount) * Number(content.sourceCount) : Number(content.maximumTotalConnects); + matrixContent.maximumConnectsPerTarget = content.maximumConnectsPerTarget == null ? + Number(content.sourceCount) : Number(content.maximumConnectsPerTarget); + } + else { + throw new Error(`Invalid matrix type ${content.type}`); + } + delete content.type; + } + if (content.mode != null) { + if (content.mode == "linear") { + matrixContent.mode = ember.MatrixMode.linear; + } + else if (content.mode == "nonLinear") { + matrixContent.mode = ember.MatrixMode.nonLinear; + } + else { + throw new Error(`Invalid matrix mode ${content.mode}`); + } + delete content.mode; + } + } + + /** + * + * @param {TreeNode} parent + * @param {object} obj + */ + static parseObj(parent, obj) { + for(let i = 0; i < obj.length; i++) { + let emberElement; + let content = obj[i]; + let number = content.number !== undefined ? content.number : i; + delete content.number; + if (content.value != null) { + emberElement = new ember.Parameter(number); + emberElement.contents = new ember.ParameterContents(content.value); + if (content.type) { + emberElement.contents.type = ember.ParameterType.get(content.type); + delete content.type; + } + else { + emberElement.contents.type = ember.ParameterType.string; + } + if (content.access) { + emberElement.contents.access = ember.ParameterAccess.get(content.access); + delete content.access; + } + else { + emberElement.contents.access = ember.ParameterAccess.read; + } + } + else if (content.func != null) { + emberElement = new ember.Function(number, content.func); + emberElement.contents = new ember.FunctionContent(); + if (content.arguments != null) { + for(let argument of content.arguments) { + emberElement.contents.arguments.push(new ember.FunctionArgument( + argument.type, + argument.value, + argument.name + )); + } + } + if (content.result != null) { + for(let argument of content.result) { + emberElement.contents.result.push(new ember.FunctionArgument( + argument.type, + argument.value, + argument.name + )); + } + } + delete content.result; + } + else if (content.targetCount != null) { + emberElement = new ember.MatrixNode(number); + emberElement.contents = new ember.MatrixContents(); + this.parseMatrixContent(emberElement.contents, content); + if (content.connections) { + emberElement.connections = {}; + for (let c in content.connections) { + if (! content.connections.hasOwnProperty(c)) { + continue; + } + const t = content.connections[c].target != null ? content.connections[c].target : 0; + emberElement.setSources(t, content.connections[c].sources); + } + delete content.connections; + } + else { + emberElement.connections = {}; + for (let t = 0; t < content.targetCount; t++) { + let connection = new ember.MatrixConnection(t); + emberElement.connections[t] = connection; + } + } + } + else { + emberElement = new ember.Node(number); + emberElement.contents = new ember.NodeContents(); + } + for(let id in content) { + if (emberElement.isFunction() && id === "arguments") { + // we did it already. + continue; + } + if ((id !== "children") && (content.hasOwnProperty(id))) { + emberElement.contents[id] = content[id]; + } + else { + this.parseObj(emberElement, content.children); + } + } + parent.addChild(emberElement); + } + } +} + +module.exports = JSONParser; \ No newline at end of file diff --git a/EmberServer/MatrixHandlers.js b/EmberServer/MatrixHandlers.js new file mode 100755 index 0000000..58e7391 --- /dev/null +++ b/EmberServer/MatrixHandlers.js @@ -0,0 +1,223 @@ +"use strict"; +const ember = require('../EmberLib'); + +class MatrixHandlers { + /** + * + * @param {EmberServer} server + */ + constructor(server) { + this.server = server; + } + + /** + * + * @param {Matrix} matrix + * @param {number} targetID + */ + getDisconnectSource(matrix, targetID) { + if (matrix.defaultSources) { + return matrix.defaultSources[targetID].contents.value; + } + if (matrix.contents.labels == null || matrix.contents.labels.length == 0) { + return null; + } + const basePath = matrix.contents.labels[0].basePath; + const labels = this.server.tree.getElementByPath(basePath); + const number = labels.getNumber() + 1; + const parent = labels.getParent(); + const children = parent.getChildren(); + for(let child of children) { + if (child.getNumber() === number) { + matrix.defaultSources = child.getChildren(); + return matrix.defaultSources[targetID].contents.value; + } + } + return null; + } + + /** + * + * @param {S101Client} client + * @param {Matrix} matrix + * @param {Object} connections + * @param {boolean} response=true + */ + handleMatrixConnections(client, matrix, connections, response = true) { + var res,conResult; + var root; // ember message root + if (this.server._debug) { + console.log("Handling Matrix Connection"); + } + if (client != null && client.request.isQualified()) { + root = new ember.Root(); + res = new ember.QualifiedMatrix(matrix.getPath()); + //root.elements = [res]; // do not use addchild or the element will get removed from the tree. + root.addElement(res); + } + else { + res = new ember.MatrixNode(matrix.number); + root = matrix._parent.getTreeBranch(res); + } + res.connections = {}; + for(let id in connections) { + if (!connections.hasOwnProperty(id)) { + continue; + } + let connection = connections[id]; + conResult = new ember.MatrixConnection(connection.target); + let emitType; + res.connections[connection.target] = conResult; + + if (matrix.connections[connection.target].isLocked()) { + conResult.disposition = ember.MatrixDisposition.locked; + } + else if (matrix.contents.type !== ember.MatrixType.nToN && + connection.operation !== ember.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length === 1) { + if (matrix.contents.type === ember.MatrixType.oneToOne) { + // if the source is being used already, disconnect it. + const targets = matrix.getSourceConnections(connection.sources[0]); + if (targets.length === 1 && targets[0] !== connection.target) { + const disconnect = new ember.MatrixConnection(targets[0]); + disconnect.setSources([]); + disconnect.disposition = ember.MatrixDisposition.modified; + res.connections[targets[0]] = disconnect; + matrix.setSources(targets[0], []); + if (response) { + this.server.emit("matrix-disconnect", { + target: targets[0], + sources: connection.sources, + client: client == null ? null : client.remoteAddress() + }); + } + } + } + // if the target is connected already, disconnect it + if (matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length === 1) { + if (matrix.contents.type === ember.MatrixType.oneToN) { + const disconnectSource = this.getDisconnectSource(matrix, connection.target); + if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { + if (disconnectSource != null && disconnectSource != -1 && + disconnectSource != connection.sources[0]) { + connection.sources = [disconnectSource]; + } + else { + // do nothing + connection.operarion = ember.MatrixOperation.tally; + } + } + } + if (matrix.connections[connection.target].sources[0] !== connection.sources[0]) { + const source = matrix.connections[connection.target].sources[0]; + matrix.setSources(connection.target, []); + if (response) { + this.server.emit("matrix-disconnect", { + target: connection.target, + sources: [source], + client: client == null ? null : client.remoteAddress() + }); + } + } + else if (matrix.contents.type === ember.MatrixType.oneToOne) { + // let's change the request into a disconnect + connection.operation = ember.MatrixOperation.disconnect; + } + } + } + + if (connection.operation !== ember.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length > 0 && + matrix.canConnect(connection.target,connection.sources,connection.operation)) { + // Apply changes + if ((connection.operation == null) || + (connection.operation.value == ember.MatrixOperation.absolute)) { + matrix.setSources(connection.target, connection.sources); + emitType = "matrix-change"; + } + else if (connection.operation == ember.MatrixOperation.connect) { + matrix.connectSources(connection.target, connection.sources); + emitType = "matrix-connect"; + } + conResult.disposition = ember.MatrixDisposition.modified; + } + else if (connection.operation !== ember.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length === 0 && + matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length > 0) { + // let's disconnect + if (response) { + this.server.emit("matrix-disconnect", { + target: connection.target, + sources: matrix.connections[connection.target].sources, + client: client == null ? null : client.remoteAddress() + }); + } + matrix.setSources(connection.target, []); + conResult.disposition = ember.MatrixDisposition.modified; + } + else if (connection.operation === ember.MatrixOperation.disconnect && + matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length > 0) { + // Disconnect + if (matrix.contents.type === ember.MatrixType.oneToN) { + const disconnectSource = this.getDisconnectSource(matrix, connection.target); + if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { + if (disconnectSource != null && disconnectSource != -1 && + disconnectSource != connection.sources[0]) { + if (response) { + this.server.emit("matrix-disconnect", { + target: connection.target, + sources: matrix.connections[connection.target].sources, + client: client == null ? null : client.remoteAddress() + }); + } + matrix.setSources(connection.target, [disconnectSource]); + connection.operarion = ember.MatrixOperation.modified; + } + else { + // do nothing + connection.operarion = ember.MatrixOperation.tally; + } + } + } + else { + matrix.disconnectSources(connection.target, connection.sources); + conResult.disposition = ember.MatrixDisposition.modified; + emitType = "matrix-disconnect"; + } + } + else if (conResult.disposition !== ember.MatrixDisposition.locked){ + if (this.server._debug) { + console.log(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); + } + conResult.disposition = ember.MatrixDisposition.tally; + } + + // Send response or update subscribers. + conResult.sources = matrix.connections[connection.target].sources; + if (response) { + // We got a request so emit something. + this.server.emit(emitType, { + target: connection.target, + sources: connection.sources, + client: client == null ? null : client.remoteAddress() + }); + } + } + if (client != null) { + client.sendBERNode(root); + } + + if (conResult != null && conResult.disposition !== ember.MatrixDisposition.tally) { + if (this.server._debug) { + console.log("Updating subscribers for matrix change"); + } + this.server.updateSubscribers(matrix.getPath(), root, client); + } + } + +} + +module.exports = MatrixHandlers; \ No newline at end of file diff --git a/EmberServer/QualifiedHandlers.js b/EmberServer/QualifiedHandlers.js new file mode 100755 index 0000000..03915ff --- /dev/null +++ b/EmberServer/QualifiedHandlers.js @@ -0,0 +1,75 @@ +"use strict"; +const MatrixHandlers = require("./MatrixHandlers"); + +class QualifiedHandlers extends MatrixHandlers { + /** + * + * @param {EmberServer} server + */ + constructor(server) { + super(server); + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} element + * @param {QualifiedMatrix} matrix + */ + handleQualifiedMatrix(client, element, matrix) + { + this.handleMatrixConnections(client, element, matrix.connections); + } + + /** + * + * @param {S101Client} client + * @param {QualifiedNode} root + */ + handleQualifiedNode(client, node) { + const path = node.path; + // Find this element in our tree + const element = this.server.tree.getElementByPath(path); + + if (element == null) { + this.server.emit("error", new Error(`unknown element at path ${path}`)); + return this.server.handleError(client); + } + + if (node.hasChildren()) { + for(let child of node.children) { + if (child.isCommand()) { + this.handleCommand(client, element, child); + } + break; + } + } + else { + if (node.isMatrix()) { + this.handleQualifiedMatrix(client, element, node); + } + else if (node.isParameter()) { + this.handleQualifiedParameter(client, element, node); + } + } + return path; + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} element + * @param {QualifiedParameter} parameter + */ + handleQualifiedParameter(client, element, parameter) + { + if (parameter.contents.value !== undefined) { + this.server.setValue(element, parameter.contents.value, client); + let res = this.server.getQualifiedResponse(element); + client.sendBERNode(res) + this.server.updateSubscribers(element.getPath(), res, client); + } + } +} + +module.exports = QualifiedHandlers; diff --git a/EmberServer/index.js b/EmberServer/index.js new file mode 100755 index 0000000..17080bd --- /dev/null +++ b/EmberServer/index.js @@ -0,0 +1 @@ +module.exports = require("./EmberServer"); \ No newline at end of file diff --git a/EmberSocket/S101Client.js b/EmberSocket/S101Client.js new file mode 100755 index 0000000..b4c3f76 --- /dev/null +++ b/EmberSocket/S101Client.js @@ -0,0 +1,87 @@ +"use strict"; + +const S101Socket = require("./S101Socket"); +const S101Codec = require('../s101.js'); +const BER = require('../ber.js'); +const ember = require("../EmberLib"); + +class S101Client extends S101Socket { + constructor(socket, server) { + super() + this.request = null; + this.server = server; + this.socket = socket; + + this.pendingRequests = []; + this.activeRequest = null; + + this.status = "connected"; + + this.codec = new S101Codec(); + this.codec.on('keepaliveReq', () => { + this.sendKeepaliveResponse(); + }); + + this.codec.on('emberPacket', (packet) => { + this.emit('emberPacket', packet); + const ber = new BER.Reader(packet); + try { + const root = ember.rootDecode(ber); + if (root !== undefined) { + this.emit('emberTree', root); + } + } catch (e) { + this.emit("error", e); + } + }); + + if (socket != null) { + this.socket.on('data', (data) => { + this.codec.dataIn(data); + }); + + this.socket.on('close', () => { + this.emit('disconnected'); + this.status = "disconnected"; + this.socket = null; + }); + + this.socket.on('error', (e) => { + this.emit("error", e); + }); + } + } + + addRequest(cb) { + this.pendingRequests.push(cb); + this._makeRequest(); + } + + _makeRequest() { + if (this.activeRequest === null && this.pendingRequests.length > 0) { + this.activeRequest = this.pendingRequests.shift(); + this.activeRequest(); + this.activeRequest = null; + } + }; + + /** + * + * @param {TreeNode} node + */ + queueMessage(node) { + this.addRequest(() => this.sendBERNode(node)); + } + + /** + * @returns {string} - ie: "10.1.1.1:9000" + */ + remoteAddress() { + if (this.socket === undefined) { + return; + } + return `${this.socket.remoteAddress}:${this.socket.remotePort}` + } +} + +module.exports = S101Client; diff --git a/EmberSocket/S101Server.js b/EmberSocket/S101Server.js new file mode 100755 index 0000000..67dcb84 --- /dev/null +++ b/EmberSocket/S101Server.js @@ -0,0 +1,53 @@ +"use strict"; +const EventEmitter = require('events').EventEmitter; +const S101Client = require("./S101Client"); +const net = require('net'); + +class S101Server extends EventEmitter { + /** + * + * @param {string} address + * @param {number} port + */ + constructor(address, port) { + super(); + this.address = address; + this.port = Number(port); + this.server = null; + this.status = "disconnected"; + } + /** + * + * @param {Socket} socket + */ + addClient(socket) { + var client = new S101Client(socket, this); + this.emit("connection", client); + } + /** + * + */ + listen () { + if (this.status !== "disconnected") { + return; + } + + this.server = net.createServer((socket) => { + this.addClient(socket); + }); + + this.server.on("error", (e) => { + this.emit("error", e); + }); + + this.server.on("listening", () => { + this.emit("listening"); + this.status = "listening"; + }); + + this.server.listen(this.port, this.address); + } + +} + +module.exports = S101Server; \ No newline at end of file diff --git a/EmberSocket/S101Socket.js b/EmberSocket/S101Socket.js new file mode 100755 index 0000000..a38b51d --- /dev/null +++ b/EmberSocket/S101Socket.js @@ -0,0 +1,186 @@ +"use strict"; + +const EventEmitter = require('events').EventEmitter; +const net = require('net'); +const BER = require('../ber.js'); +const ember = require('../EmberLib'); +const S101Codec = require('../s101.js'); + + +class S101Socket extends EventEmitter{ + constructor(address, port) { + super(); + this.address = address; + this.port = port; + this.socket = null; + this.keepaliveInterval = 10; + this.codec = null; + this.status = "disconnected"; + } + + /** + * + * @param {number} timeout + */ + connect(timeout = 2) { + if (this.status !== "disconnected") { + return; + } + + this.emit('connecting'); + + this.codec = new S101Codec(); + + const connectTimeoutListener = () => { + this.socket.destroy(); + this.emit("error", new Error(`Could not connect to ${this.address}:${this.port} after a timeout of ${timeout} seconds`)); + }; + + this.socket = net.createConnection({ + port: this.port, + host: this.address, + timeout: 1000 * timeout + }, + () => { + // Disable connect timeout to hand-over to keepalive mechanism + this.socket.removeListener("timeout", connectTimeoutListener); + this.socket.setTimeout(0); + + this.keepaliveIntervalTimer = setInterval(() => { + try { + this.sendKeepaliveRequest(); + } catch (e) { + this.emit("error", e); + } + }, 1000 * this.keepaliveInterval); + + this.codec.on('keepaliveReq', () => { + this.sendKeepaliveResponse(); + }); + + this.codec.on('emberPacket', (packet) => { + this.emit('emberPacket', packet); + + const ber = new BER.Reader(packet); + try { + const root = ember.rootDecode(ber); + if (root !== undefined) { + this.emit('emberTree', root); + } + } catch (e) { + this.emit("error", e); + } + }); + + this.emit('connected'); + }) + .on('error', (e) => { + this.emit("error", e); + }) + .once("timeout", connectTimeoutListener) + .on('data', (data) => { + if (this.isConnected()) { + this.codec.dataIn(data); + } + }) + .on('close', this.handleClose) + .on("end", this.handleClose); + } + + /** + * + */ + disconnect() { + if (!this.isConnected()) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + this.socket.once('close', () => { + this.codec = null; + this.socket = null; + resolve(); + }); + this.socket.once('error', reject); + clearInterval(this.keepaliveIntervalTimer); + this.socket.end(); + this.status = "disconnected"; + } + ); + }; + + /** + * + */ + handleClose() { + this.socket = null; + clearInterval(this.keepaliveIntervalTimer); + this.status = "disconnected"; + this.emit('disconnected'); + }; + + /** + * @returns {boolean} + */ + isConnected() { + return ((this.socket !== null) && (this.socket !== undefined)); + } + + /** + * + * @param {Buffer} data + */ + sendBER(data) { + if (this.isConnected()) { + try { + const frames = this.codec.encodeBER(data); + for (let i = 0; i < frames.length; i++) { + this.socket.write(frames[i]); + } + } + catch(e){ + this.handleClose(); + } + } + } + + /** + * + */ + sendKeepaliveRequest() { + if (this.isConnected()) { + try { + this.socket.write(this.codec.keepAliveRequest()); + } + catch(e){ + this.handleClose(); + } + } + } + + /** + * + */ + sendKeepaliveResponse() { + if (this.isConnected()) { + try { + this.socket.write(this.codec.keepAliveResponse()); + } + catch(e){ + this.handleClose(); + } + } + } + + /** + * + * @param {TreeNode} node + */ + sendBERNode(node) { + if (!node) return; + const writer = new BER.Writer(); + node.encode(writer); + this.sendBER(writer.buffer); + } +} + +module.exports = S101Socket; \ No newline at end of file diff --git a/EmberSocket/index.js b/EmberSocket/index.js new file mode 100755 index 0000000..d755a5b --- /dev/null +++ b/EmberSocket/index.js @@ -0,0 +1,7 @@ +const S101Socket = require("./S101Socket"); +const S101Server = require("./S101Server"); +const S101Client = require("./S101Client"); + +module.exports = { + S101Client, S101Server, S101Socket +}; \ No newline at end of file diff --git a/README.md b/README.md index 5b3d203..332997f 100755 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # node-emberplus -This is an implementation of [Lawo's -Ember+](https://github.com/Lawo/ember-plus) control protocol for Node. One of -Node's great strengths is the ready availability of frameworks for various +This is version 2 of emberplus library. +An implementation of [Lawo's Ember+](https://github.com/Lawo/ember-plus) control protocol for Node. +One of Node's great strengths is the ready availability of frameworks for various communication protocols and user interfaces; this module allows those to be integrated with Ember+ somewhat more easily than the reference libember C++ implementation. @@ -22,48 +22,36 @@ Server has been added in version 1.6.0. ### Client Get Full tree: ```javascript -const DeviceTree = require('emberplus').DeviceTree; -var root; -var tree = new DeviceTree("10.9.8.7", 9000); -tree.on("error", e => { +const EmberClient = require('node-emberplus').EmberClient; +const client = new EmberClient("10.9.8.7", 9000); +client.on("error", e => { console.log(e); }); -tree.connect() - .then(() => tree.getDirectory()) - .then((r) => { - root = r ; - return tree.expand(r.elements[0]); +client.connect() + // Get Root info + .then(() => client.getDirectory()) + // Get a Specific Node + .then(() => client.getNodeByPathnum("0.0.2")) + .then(node => { + console.log(node); }) - .then(() => { - console.log("done"); + // Get a node by its path identifiers + .then(() => client.getNodeByPath("path/to/node")) + .then(node => { + console.log(node); }) + // Expand entire tree under node 0 + .then(() => client.expand(client.root.getElementByNumber(0))) .catch((e) => { console.log(e.stack); }); ``` -Get Specific Node: -```javascript -const DeviceTree = require('emberplus').DeviceTree; -const ember = require("emberplus").Ember; - -const client = new DeviceTree(HOST, PORT); -client.connect()) - .then(() => client.getDirectory()) - .then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) - .then(() => client.getNodeByPath("scoreMaster/router/labels/group 1")) - .then(() => client.getNodeByPathnum("0.2")) - .then(node => { - console.log(JSON.stringify(node.toJSON(), null, 4)); - }); -``` - Subsribe to changes ```javascript -const DeviceTree = require('emberplus').DeviceTree; -const ember = require("emberplus").Ember; +const {EmberClient, EmberLib} = require('node-emberplus'); -const client = new DeviceTree(HOST, PORT); +const client = new EmberClient(HOST, PORT); client.connect()) .then(() => client.getDirectory()) .then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) @@ -89,36 +77,29 @@ client.connect()) ### Invoking Function ```javascript -const DeviceTree = require('emberplus').DeviceTree; -const ember = require("emberplus").Ember; +const {EmberClient, EmberLib} = require('node-emberplus'); -const client = new DeviceTree(HOST, PORT); +const client = new EmberClient(HOST, PORT); client.connect()) .then(() => client.getDirectory()) .then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) - .then(() => client.expand(client.root.elements[0])) + .then(() => client.expand(client.root.getElementByNumber(0))) .then(() => { - console.log(JSON.stringify(client.root.elements[0].toJSON(), null, 4)); - let func; - if (TINYEMBER) { - func = client.root.elements[0].children[4].children[0]; - } - else { - func = client.root.elements[0].children[2]; - } - return client.invokeFunction(func, [ - new ember.FunctionArgument(ember.ParameterType.integer, 1), - new ember.FunctionArgument(ember.ParameterType.integer, 7) + console.log(JSON.stringify(client.root.getElementByNumber(0).toJSON(), null, 4)); + const funcNode = client.root.getElementByNumber(0).getElementByNumber(5).getElementByNumber(0); + return client.invokeFunction(funcNode, [ + new ember.FunctionArgument(EmberLib.ParameterType.integer, 1), + new ember.FunctionArgument(EmberLib.ParameterType.integer, 7) ]); }); ``` ### Matrix Connection ```javascript -const DeviceTree = require('emberplus').DeviceTree; -const ember = require("emberplus").Ember; +const {EmberClient, EmberLib} = require('node-emberplus'); + -const client = new DeviceTree(HOST, PORT); +const client = new EmberClient(HOST, PORT); client.connect() .then(() => client.getDirectory()) .then(() => client.getNodeByPathnum("0.1.0")) @@ -136,7 +117,7 @@ client.connect() ### Packet decoder ```javascript // Simple packet decoder -const Decoder = require('emberplus').Decoder; +const Decoder = require('node-emberplus').Decoder; const fs = require("fs"); fs.readFile("tree.ember", (e,data) => { @@ -147,8 +128,8 @@ fs.readFile("tree.ember", (e,data) => { ```javascript // Server -const TreeServer = require("emberplus").TreeServer; -const server = new TreeServer("127.0.0.1", 9000, root); +const EmberServer = require("node-emberplus").EmberServer; +const server = new EmberServer("127.0.0.1", 9000, root); server.on("error", e => { console.log("Server Error", e); }); @@ -169,8 +150,8 @@ server.listen().then(() => { console.log("listening"); }).catch((e) => { console ### Construct Tree ```javascript -const TreeServer = require("emberplus").TreeServer; -const {ParameterType, FunctionArgument} = require("emberplus").Ember; +const EmberServer = require("node-emberplus").EmberServer; +const {ParameterType, FunctionArgument} = require("node-emberplus").EmberLib; const targets = [ "tgt1", "tgt2", "tgt3" ]; const sources = [ "src1", "src2", "src3" ]; @@ -291,6 +272,6 @@ const jsonTree = [ ] } ]; -const root = TreeServer.JSONtoTree(jsonTree); +const root = EmberServer.JSONtoTree(jsonTree); ``` diff --git a/client.js b/client.js deleted file mode 100755 index 1f297f3..0000000 --- a/client.js +++ /dev/null @@ -1,308 +0,0 @@ -const EventEmitter = require('events').EventEmitter; -const util = require('util'); -const winston = require('winston'); -const net = require('net'); -const BER = require('./ber.js'); -const ember = require('./EmberLib'); - -const S101Codec = require('./s101.js'); - - -function S101Socket(address, port) { - var self = this; - S101Socket.super_.call(this); - - self.address = address; - self.port = port; - self.socket = null; - self.keepaliveInterval = 10; - self.codec = null; - self.status = "disconnected"; - -} - -util.inherits(S101Socket, EventEmitter); - - -function S101Client(socket, server) { - var self = this; - S101Client.super_.call(this); - - self.request = null; - self.server = server; - self.socket = socket; - - self.pendingRequests = []; - self.activeRequest = null; - - self.status = "connected"; - - self.codec = new S101Codec(); - self.codec.on('keepaliveReq', () => { - self.sendKeepaliveResponse(); - }); - - self.codec.on('emberPacket', (packet) => { - self.emit('emberPacket', packet); - var ber = new BER.Reader(packet); - try { - var root = ember.rootDecode(ber); - if (root !== undefined) { - self.emit('emberTree', root); - } - } catch (e) { - self.emit("error", e); - } - }); - - if (socket !== undefined) { - self.socket.on('data', (data) => { - self.codec.dataIn(data); - }); - - self.socket.on('close', () => { - self.emit('disconnected'); - self.status = "disconnected"; - self.socket = null; - }); - - self.socket.on('error', (e) => { - self.emit("error", e); - }); - } -} - -util.inherits(S101Client, S101Socket); - - -/********************************************** - * SERVER - **********************************************/ - -function S101Server(address, port) { - var self = this; - S101Server.super_.call(this); - - self.address = address; - self.port = port; - self.server = null; - self.status = "disconnected"; -} - -util.inherits(S101Server, EventEmitter); - -S101Server.prototype.listen = function () { - var self = this; - if (self.status !== "disconnected") { - return; - } - - self.server = net.createServer((socket) => { - self.addClient(socket); - }); - - self.server.on("error", (e) => { - self.emit("error", e); - }); - - self.server.on("listening", () => { - self.emit("listening"); - self.status = "listening"; - }); - - self.server.listen(self.port, self.address); -} - - -S101Server.prototype.addClient = function (socket) { - var client = new S101Client(socket, this); - this.emit("connection", client); -} - - -/***************************************************** - * Client - *****************************************************/ - -S101Client.prototype.remoteAddress = function () { - if (this.socket === undefined) { - return; - } - return `${this.socket.remoteAddress}:${this.socket.remotePort}` -} - -S101Client.prototype.queueMessage = function (node) { - const self = this; - this.addRequest(() => { - self.sendBERNode(node); - }); -} - -S101Client.prototype.makeRequest = function () { - if (this.activeRequest === null && this.pendingRequests.length > 0) { - this.activeRequest = this.pendingRequests.shift(); - this.activeRequest(); - this.activeRequest = null; - } -}; - -S101Client.prototype.addRequest = function (cb) { - this.pendingRequests.push(cb); - this.makeRequest(); -} - - -/***************************************************** - * Socket - *****************************************************/ - -S101Socket.prototype.connect = function (timeout = 2) { - var self = this; - if (self.status !== "disconnected") { - return; - } - - self.emit('connecting'); - - self.codec = new S101Codec(); - - const connectTimeoutListener = () => { - self.socket.destroy(); - self.emit("error", new Error(`Could not connect to ${self.address}:${self.port} after a timeout of ${timeout} seconds`)); - }; - - self.socket = net.createConnection({ - port: self.port, - host: self.address, - timeout: 1000 * timeout - }, - () => { - winston.debug('socket connected'); - // Disable connect timeout to hand-over to keepalive mechanism - self.socket.removeListener("timeout", connectTimeoutListener); - self.socket.setTimeout(0); - - self.keepaliveIntervalTimer = setInterval(() => { - try { - self.sendKeepaliveRequest(); - } catch (e) { - self.emit("error", e); - } - }, 1000 * self.keepaliveInterval); - - self.codec.on('keepaliveReq', () => { - self.sendKeepaliveResponse(); - }); - - self.codec.on('emberPacket', (packet) => { - self.emit('emberPacket', packet); - - var ber = new BER.Reader(packet); - try { - var root = ember.rootDecode(ber); - if (root !== undefined) { - self.emit('emberTree', root); - } - } catch (e) { - self.emit("error", e); - } - }); - - self.emit('connected'); - }) - .on('error', (e) => { - self.emit("error", e); - }) - .once("timeout", connectTimeoutListener) - .on('data', (data) => { - if (self.isConnected()) { - self.codec.dataIn(data); - } - }) - .on('close', self.handleClose) - .on("end", self.handleClose); -} - -S101Socket.prototype.handleClose = function() { - this.socket = null; - clearInterval(this.keepaliveIntervalTimer); - this.status = "disconnected"; - this.emit('disconnected'); -}; - -S101Socket.prototype.isConnected = function () { - return ((this.socket !== null) && (this.socket !== undefined)); -} - -S101Socket.prototype.disconnect = function () { - var self = this; - - if (!self.isConnected()) { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - self.socket.once('close', () => { - self.codec = null; - self.socket = null; - resolve(); - }); - self.socket.once('error', reject); - clearInterval(self.keepaliveIntervalTimer); - self.socket.end(); - self.status = "disconnected"; - } - ); -}; - -S101Socket.prototype.sendKeepaliveRequest = function () { - var self = this; - if (self.isConnected()) { - try { - self.socket.write(self.codec.keepAliveRequest()); - winston.debug('sent keepalive request'); - } - catch(e){ - self.handleClose(); - } - } -} - -S101Socket.prototype.sendKeepaliveResponse = function () { - var self = this; - if (self.isConnected()) { - try { - self.socket.write(self.codec.keepAliveResponse()); - } - catch(e){ - self.handleClose(); - } - winston.debug('sent keepalive response'); - } -} - -S101Socket.prototype.sendBER = function (data) { - var self = this; - if (self.isConnected()) { - try { - var frames = self.codec.encodeBER(data); - for (var i = 0; i < frames.length; i++) { - self.socket.write(frames[i]); - } - } - catch(e){ - self.handleClose(); - } - } -} - -S101Socket.prototype.sendBERNode = function (node) { - var self = this; - if (!node) return; - var writer = new BER.Writer(); - node.encode(writer); - self.sendBER(writer.buffer); -} - - -module.exports = { S101Socket, S101Server, S101Client }; - diff --git a/device.js b/device.js deleted file mode 100755 index a8a85a5..0000000 --- a/device.js +++ /dev/null @@ -1,657 +0,0 @@ -const EventEmitter = require('events').EventEmitter; -const util = require('util'); -const S101Client = require('./client.js').S101Socket; -const ember = require('./EmberLib'); -const BER = require('./ber.js'); -const errors = require('./errors.js'); - - -function DeviceTree(host, port = 9000) { - DeviceTree.super_.call(this); - var self = this; - self._debug = false; - self.timeoutValue = 3000; - self.client = new S101Client(host, port); - self.root = new ember.Root(); - self.pendingRequests = []; - self.activeRequest = null; - self.timeout = null; - self.callback = undefined; - self.requestID = 0; - - self.client.on('connecting', () => { - self.emit('connecting'); - }); - - self.client.on('connected', () => { - self.emit('connected'); - if (self.callback !== undefined) { - self.callback(); - } - }); - - self.client.on('disconnected', () => { - self.emit('disconnected'); - }); - - self.client.on("error", (e) => { - if (self.callback !== undefined) { - self.callback(e); - } - self.emit("error", e); - }); - - self.client.on('emberTree', (root) => { - try { - if (root instanceof ember.InvocationResult) { - self.emit('invocationResult', root); - if (self._debug) { - console.log("Received InvocationResult", root); - } - } else { - self.handleRoot(root); - if (self._debug) { - console.log("Received root", root); - } - } - if (self.callback) { - self.callback(undefined, root); - } - } - catch(e) { - if (self._debug) { - console.log(e, root); - } - if (self.callback) { - self.callback(e); - } - } - }); -} - -util.inherits(DeviceTree, EventEmitter); - - -var DecodeBuffer = function (packet) { - var ber = new BER.Reader(packet); - return ember.Root.decode(ber); -}; - -DeviceTree.prototype.saveTree = function (f) { - var writer = new BER.Writer(); - this.root.encode(writer); - f(writer.buffer); -}; - -DeviceTree.prototype.isConnected = function () { - return ((this.client !== undefined) && (this.client.isConnected())); -}; - -DeviceTree.prototype.connect = function (timeout = 2) { - return new Promise((resolve, reject) => { - this.callback = (e) => { - this.callback = undefined; - if (e === undefined) { - return resolve(); - } - return reject(e); - }; - if ((this.client !== undefined) && (this.client.isConnected())) { - this.client.disconnect(); - } - this.client.connect(timeout); - }); -}; - -DeviceTree.prototype.expand = function (node, callback = null) { - let self = this; - if (node == null) { - return Promise.reject(new Error("Invalid null node")); - } - if (node.isParameter() || node.isMatrix() || node.isFunction()) { - return self.getDirectory(node); - } - return self.getDirectory(node, callback).then((res) => { - let children = node.getChildren(); - if ((res === undefined) || (children === undefined) || (children === null)) { - if (self._debug) { - console.log("No more children for ", node); - } - return; - } - let p = Promise.resolve(); - for (let child of children) { - if (child.isParameter()) { - // Parameter can only have a single child of type Command. - continue; - } - if (self._debug) { - console.log("Expanding child", child); - } - p = p.then(() => { - return self.expand(child).catch((e) => { - // We had an error on some expansion - // let's save it on the child itself - child.error = e; - }); - }); - } - return p; - }); -}; - -function isDirectSubPathOf(path, parent) { - return path === parent || (path.lastIndexOf('.') === parent.length && path.startsWith(parent)); -} - -DeviceTree.prototype.getDirectory = function (qnode, callback = null) { - var self = this; - if (qnode == null) { - self.root.clear(); - qnode = self.root; - } - return new Promise((resolve, reject) => { - self.addRequest({node: qnode, func: (error) => { - if (error) { - self.finishRequest(); - reject(error); - return; - } - - self.callback = (error, node) => { - const requestedPath = qnode.getPath(); - if (node == null) { - if (self._debug) { - console.log(`received null response for ${requestedPath}`); - } - return; - } - if (error) { - if (self._debug) { - console.log("Received getDirectory error", error); - } - self.clearTimeout(); // clear the timeout now. The resolve below may take a while. - self.finishRequest(); - reject(error); - return; - } - if (qnode.isRoot()) { - const elements = qnode.getChildren(); - if (elements == null || elements.length === 0) { - if (self._debug) { - console.log("getDirectory response", node); - } - return self.callback(new Error("Invalid qnode for getDirectory")); - } - - const nodeElements = node == null ? null : node.getChildren(); - - if (nodeElements != null - && nodeElements.every(el => el._parent instanceof ember.Root)) { - if (self._debug) { - console.log("Received getDirectory response", node); - } - self.clearTimeout(); // clear the timeout now. The resolve below may take a while. - self.finishRequest(); - resolve(node); // make sure the info is treated before going to next request. - } - else { - return self.callback(new Error(`Invalid response for getDirectory ${requestedPath}`)); - } - } - else if (node.getElementByPath(requestedPath) != null) { - self.clearTimeout(); // clear the timeout now. The resolve below may take a while. - self.finishRequest(); - return resolve(node); // make sure the info is treated before going to next request. - } - else { - const nodeElements = node == null ? null : node.getChildren(); - if (nodeElements != null && - ((qnode.isMatrix() && nodeElements.length === 1 && nodeElements[0].getPath() === requestedPath) || - (!qnode.isMatrix() && nodeElements.every(el => isDirectSubPathOf(el.getPath(), requestedPath))))) { - if (self._debug) { - console.log("Received getDirectory response", node); - } - self.clearTimeout(); // clear the timeout now. The resolve below may take a while. - self.finishRequest(); - return resolve(node); // make sure the info is treated before going to next request. - } - else if (self._debug) { - console.log(node); - console.log(new Error(requestedPath)); - } - } - }; - - if (self._debug) { - console.log("Sending getDirectory", qnode); - } - self.client.sendBERNode(qnode.getDirectory(callback)); - }}); - }); -}; - -DeviceTree.prototype.matrixOPeration = function(matrixNode, targetID, sources, operation = ember.MatrixOperation.connect) { - return new Promise((resolve, reject) => { - if (!Array.isArray(sources)) { - return reject(new Error("Sources should be an array")); - } - try { - matrixNode.validateConnection(targetID, sources); - } - catch(e) { - return reject(e); - } - const connections = {} - const targetConnection = new ember.MatrixConnection(targetID); - targetConnection.operation = operation; - targetConnection.setSources(sources); - connections[targetID] = targetConnection; - - this.addRequest({node: matrixNode, func: (error) => { - if (error) { - this.finishRequest(); - reject(error); - return; - } - - this.callback = (error, node) => { - const requestedPath = matrixNode.getPath(); - if (node == null) { - if (this._debug) { - console.log(`received null response for ${requestedPath}`); - } - return; - } - if (error) { - if (this._debug) { - console.log("Received getDirectory error", error); - } - this.clearTimeout(); // clear the timeout now. The resolve below may take a while. - this.finishRequest(); - reject(error); - return; - } - let matrix = null; - if (node != null) { - matrix = node.elements[0]; - } - if (matrix != null && matrix.isMatrix() && matrix.getPath() === requestedPath) { - this.clearTimeout(); // clear the timeout now. The resolve below may take a while. - this.finishRequest(); - resolve(matrix); - } - else { - if (this._debug) { - console.log(`unexpected node response during matrix connect ${requestedPath}`, - JSON.stringify(matrix.toJSON(), null, 4)); - } - } - } - this.client.sendBERNode(matrixNode.connect(connections)); - }}); - }); -} - -DeviceTree.prototype.matrixConnect = function(matrixNode, targetID, sources) { - return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.connect) -} - -DeviceTree.prototype.matrixDisconnect = function(matrixNode, targetID, sources) { - return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.disconnect) -} - -DeviceTree.prototype.matrixSetConnection = function(matrixNode, targetID, sources) { - return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.absolute) -} - -DeviceTree.prototype.invokeFunction = function (fnNode, params) { - var self = this; - return new Promise((resolve, reject) => { - self.addRequest({node: fnNode, func: (error) => { - if (error) { - reject(error); - self.finishRequest(); - return; - } - - let cb = (error, result) => { - self.clearTimeout(); - if (error) { - reject(error); - } - else { - if (self._debug) { - console.log("InvocationResult", result); - } - resolve(result); - } - // cleaning callback and making next request. - self.finishRequest(); - }; - - if (self._debug) { - console.log("Invocking function", fnNode); - } - self.callback = cb; - self.client.sendBERNode(fnNode.invoke(params)); - }}); - }) -}; - -DeviceTree.prototype.disconnect = function () { - if (this.client !== undefined) { - return this.client.disconnect(); - } -}; - -DeviceTree.prototype.makeRequest = function () { - const self = this; - if (self.activeRequest === null && self.pendingRequests.length > 0) { - self.activeRequest = self.pendingRequests.shift(); - const req = `${ self.requestID++} - ${self.activeRequest.node.getPath()}`; - self.activeRequest.timeoutError = new errors.EmberTimeoutError(`Request ${req} timed out`) - - if (self._debug) { - console.log(`Making request ${req}`, Date.now()); - } - self.timeout = setTimeout(() => { - self.timeoutRequest(); - }, self.timeoutValue); - self.activeRequest.func(); - } -}; - -DeviceTree.prototype.addRequest = function (req) { - var self = this; - self.pendingRequests.push(req); - self.makeRequest(); -}; - -DeviceTree.prototype.clearTimeout = function () { - if (this.timeout != null) { - clearTimeout(this.timeout); - this.timeout = null; - } -}; - -DeviceTree.prototype.finishRequest = function () { - var self = this; - self.clearTimeout(); - self.activeRequest = null; - try { - self.makeRequest(); - } catch(e) { - if (self._debug) {console.log(e);} - if (self.callback != null) { - self.callback(e); - } - self.emit("error", e); - } -}; - -DeviceTree.prototype.timeoutRequest = function (id) { - this.activeRequest.func(this.activeRequest.timeoutError); -}; - -DeviceTree.prototype.handleRoot = function (root) { - var self = this; - - if (self._debug) { - console.log("handling root", JSON.stringify(root)); - } - self.root.update(root); - if (root.elements !== undefined) { - const elements = root.getChildren(); - for (var i = 0; i < elements.length; i++) { - if (elements[i].isQualified()) { - this.handleQualifiedNode(this.root, elements[i]); - } - else { - this.handleNode(this.root, elements[i]); - } - } - } - if (self.callback) { - self.callback(null, root); - } -}; - -DeviceTree.prototype.handleQualifiedNode = function (parent, node) { - var self = this; - var element = parent.getElementByPath(node.path); - if (element !== null) { - self.emit("value-change", node); - element.update(node); - } - else { - var path = node.path.split("."); - if (path.length === 1) { - this.root.addChild(node); - } - else { - // Let's try to get the parent - path.pop(); - parent = this.root.getElementByPath(path.join(".")); - if (parent === null) { - return; - } - parent.addChild(node); - parent.update(parent); - } - element = node; - } - - var children = node.getChildren(); - if (children !== null) { - for (var i = 0; i < children.length; i++) { - if (children[i].isQualified()) { - this.handleQualifiedNode(element, children[i]); - } - else { - this.handleNode(element, children[i]); - } - } - } - - return; -}; - -DeviceTree.prototype.handleNode = function (parent, node) { - var self = this; - var n = parent.getElementByNumber(node.getNumber()); - if (n === null) { - parent.addChild(node); - n = node; - } else { - n.update(node); - } - - var children = node.getChildren(); - if (children !== null) { - for (var i = 0; i < children.length; i++) { - this.handleNode(n, children[i]); - } - } - else { - self.emit("value-change", node); - } - return; -}; - -DeviceTree.prototype.getNodeByPathnum = function (path, callback = null) { - var self = this; - if (typeof path === 'string') { - path = path.split('.'); - } - var pathnumError = new Error(`Failed path discovery at ${path.slice(0, pos).join("/")}`); - var pos = 0; - var lastMissingPos = -1; - var currentNode = this.root; - const getNext = () => { - return Promise.resolve() - .then(() => { - const children = currentNode.getChildren(); - const number = Number(path[pos]); - if (children != null) { - for (let i = 0; i < children.length; i++) { - var node = children[i]; - if (node.getNumber() === number) { - // We have this part already. - pos++; - if (pos >= path.length) { - return node; - } - currentNode = node; - return getNext(); - } - } - } - // We do not have that node yet. - if (lastMissingPos === pos) { - throw pathnumError; - } - lastMissingPos = pos; - return this.getDirectory(currentNode, callback).then(() => getNext()); - }); - } - return getNext(); -}; - -DeviceTree.prototype.getNodeByPath = function (path, callback = null) { - var self = this; - if (typeof path === 'string') { - path = path.split('/'); - } - var pathError = new Error(`Failed path discovery at ${path.slice(0, pos + 1).join("/")}`); - var pos = 0; - var lastMissingPos = -1; - var currentNode = this.root; - const getNext = () => { - return Promise.resolve() - .then(() => { - const children = currentNode.getChildren(); - const identifier = path[pos]; - if (children != null) { - for (let i = 0; i < children.length; i++) { - var node = children[i]; - if (node.contents != null && node.contents.identifier === identifier) { - // We have this part already. - pos++; - if (pos >= path.length) { - return node; - } - currentNode = node; - return getNext(); - } - } - } - // We do not have that node yet. - if (lastMissingPos === pos) { - throw pathError; - } - lastMissingPos = pos; - return this.getDirectory(currentNode, callback).then(() => getNext()); - }); - } - return getNext(); -}; - -DeviceTree.prototype.subscribe = function (qnode, callback) { - if ((qnode.isParameter() || qnode.isMatrix()) && qnode.isStream()) { - var self = this; - if (qnode == null) { - self.root.clear(); - qnode = self.root; - } - return new Promise((resolve, reject) => { - self.addRequest({node: qnode, func: (error) => { - if (self._debug) { - console.log("Sending subscribe", qnode); - } - self.client.sendBERNode(qnode.subscribe(callback)); - self.finishRequest(); - resolve(); - }}); - }); - } else { - node.addCallback(callback); - } -}; - -DeviceTree.prototype.unsubscribe = function (qnode, callback) { - if (qnode.isParameter() && qnode.isStream()) { - var self = this; - if (qnode == null) { - self.root.clear(); - qnode = self.root; - } - return new Promise((resolve, reject) => { - self.addRequest({node: qnode, func: (error) => { - if (self._debug) { - console.log("Sending subscribe", qnode); - } - self.client.sendBERNode(qnode.unsubscribe(callback)); - self.finishRequest(); - resolve(); - }}); - }); - } -}; - -DeviceTree.prototype.setValue = function (node, value) { - var self = this; - return new Promise((resolve, reject) => { - if ((!(node instanceof ember.Parameter)) && - (!(node instanceof ember.QualifiedParameter))) { - reject(new errors.EmberAccessError('not a property')); - } - else { - // if (this._debug) { console.log('setValue', node.getPath(), value); } - self.addRequest({node: node, func: (error) => { - if (error) { - self.finishRequest(); - reject(error); - return; - } - - let cb = (error, node) => { - //console.log('setValue complete...', node.getPath(), value); - self.finishRequest(); - if (error) { - reject(error); - } - else { - resolve(node); - } - }; - - self.callback = cb; - if (this._debug) { - console.log('setValue sending ...', node.getPath(), value); - } - self.client.sendBERNode(node.setValue(value)); - }}); - } - }); -}; - -function TreePath(path) { - this.identifiers = []; - this.numbers = []; - - if (path !== undefined) { - for (var i = 0; i < path.length; i++) { - if (Number.isInteger(path[i])) { - this.numbers.push(path[i]); - this.identifiers.push(null); - } else { - this.identifiers.push(path[i]); - this.numbers.push(null); - } - } - } -} - - -module.exports = { DeviceTree, DecodeBuffer }; diff --git a/ember.js b/ember.js deleted file mode 100755 index 1ce667b..0000000 --- a/ember.js +++ /dev/null @@ -1,3021 +0,0 @@ -const BER = require('./ber.js'); -const errors = require('./errors.js'); -const util = require('util'); -const Enum = require('enum'); - -const COMMAND_SUBSCRIBE = 30; -const COMMAND_UNSUBSCRIBE = 31; -const COMMAND_GETDIRECTORY = 32; -const COMMAND_INVOKE = 33; -module.exports.Subscribe = COMMAND_SUBSCRIBE; -module.exports.Unsubscribe = COMMAND_UNSUBSCRIBE; -module.exports.GetDirectory = COMMAND_GETDIRECTORY; -module.exports.Invoke = COMMAND_INVOKE; - -DEBUG = false; - -module.exports.DEBUG = function(d) { - DEBUG = d; -}; - -/**************************************************************************** - * Elements - */ - - function Elements() { - Elements.super_.call(this); - } - -/**************************************************************************** - * Root - ***************************************************************************/ - -function Root() { - Root.super_.call(this); -}; - -util.inherits(Root, TreeNode); - - -Root.decode = function(ber) { - let r = new Root(); - let tag = undefined; - - while(ber.remain > 0) { - if (DEBUG) { console.log("Reading root"); } - tag = ber.peek(); - if (tag === BER.APPLICATION(0)) { - ber = ber.getSequence(BER.APPLICATION(0)); - tag = ber.peek(); - if (DEBUG) { - console.log("Application 0 start"); - } - - if (tag === BER.APPLICATION(11)) { - if (DEBUG) { - console.log("Application 11 start"); - } - var seq = ber.getSequence(BER.APPLICATION(11)); - while (seq.remain > 0) { - try { - var rootReader = seq.getSequence(BER.CONTEXT(0)); - while (rootReader.remain > 0) { - r.addElement(RootElement.decode(rootReader)); - } - } - catch (e) { - if (DEBUG) { - console.log("Decode ERROR", e.stack); - return r; - } - throw e; - } - } - } - else if (tag === BER.APPLICATION(23)) { // InvocationResult BER.APPLICATION(23) - return InvocationResult.decode(ber) - } - else { - // StreamCollection BER.APPLICATION(6) - // InvocationResult BER.APPLICATION(23) - throw new errors.UnimplementedEmberTypeError(tag); - } - } - else if (tag === BER.CONTEXT(0)) { - // continuation of previous message - try { - var rootReader = ber.getSequence(BER.CONTEXT(0)); - return RootElement.decode(rootReader) - } - catch (e) { - console.log(e.stack); - return r; - } - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return r; -} - -function addElement(parent, element) { - element._parent = parent; - if(parent.elements == null) { - parent.elements = new Map(); - } - parent.elements.set(element.getNumber(), element); -} - -Root.prototype.addElement = function(ele) { - addElement(this, ele); -} - -Root.prototype.addResult = function(result) { - this.result = result; -} - -Root.prototype.addChild = function(child) { - this.addElement(child); -} - -Root.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(0)); - if(this.elements != null) { - const elements = this.getChildren(); - ber.startSequence(BER.APPLICATION(11)); - for(var i=0; i < elements.length; i++) { - ber.startSequence(BER.CONTEXT(0)); - elements[i].encode(ber); - ber.endSequence(); // BER.CONTEXT(0) - } - ber.endSequence(); - } - if (this.result != null) { - this.result.encode(ber); - } - ber.endSequence(); // BER.APPLICATION(0) -} - -Root.prototype.clear = function() { - this.elements = undefined; -} - -Root.prototype.getPath = function() { - throw new Error("old lib"); -} - -Root.prototype.getChildren = function() { - if(this.elements != null) { - return [...this.elements.values()]; - } - return null; -} - -module.exports.Root = Root; - -/**************************************************************************** - * TreeNode (abstract) - ***************************************************************************/ - -function TreeNode() { - Object.defineProperty(this, '_parent', {value: null, enumerable: false, writable: true}); -} - -TreeNode.prototype.addChild = function(child) { - addElement(this, child); -} - -TreeNode.prototype.isCommand = function() { - return this instanceof Command; -} - -TreeNode.prototype.isNode = function() { - return ((this instanceof Node) || (this instanceof QualifiedNode)); -} - -TreeNode.prototype.isMatrix = function() { - return ((this instanceof MatrixNode) || (this instanceof QualifiedMatrix)); -} - -TreeNode.prototype.isParameter = function() { - return ((this instanceof Parameter) || (this instanceof QualifiedParameter)); -} - -TreeNode.prototype.isFunction = function() { - return ((this instanceof QualifiedFunction) || (this instanceof Function)); -} - -TreeNode.prototype.isRoot = function() { - return this._parent == null; -} - -TreeNode.prototype.isQualified = function() { - return ((this instanceof QualifiedParameter)|| - (this instanceof QualifiedNode) || - (this instanceof QualifiedMatrix) || - (this instanceof QualifiedFunction)); -} - -TreeNode.prototype.isStream = function() { - return this.contents != null && - this.contents.streamIdentifier != null; -} - -TreeNode.prototype.getMinimalContent = function() { - let obj; - if (this.isQualified()) { - obj = new this.constructor(this.path); - } - else { - obj = new this.constructor(this.number); - } - if (this.contents !== undefined) { - obj.contents= this.contents; - } - return obj; -}; - -TreeNode.prototype.getDuplicate = function() { - let obj = this.getMinimal(); - obj.update(this); - return obj; -} - -TreeNode.prototype.getMinimal = function() { - if (this.isQualified()) { - return new this.constructor(this.path); - } - else { - return new this.constructor(this.number); - } -} - -TreeNode.prototype.getTreeBranch = function(child, modifier) { - var m = this.getMinimal(); - if(child !== undefined) { - m.addChild(child); - } - - if(modifier !== undefined) { - modifier(m); - } - - if(this._parent === null) { - return m; - } - else { - var p = this._parent.getTreeBranch(m); - return p; - } -} - -TreeNode.prototype.getRoot = function() { - if(this._parent === null) { - return this; - } else { - return this._parent.getRoot(); - } -} - -TreeNode.prototype.getDirectory = function(callback) { - if (callback != null && this.contents != null && !this.isStream()) { - this.contents._subscribers.add(callback); - } - return this.getTreeBranch(new Command(COMMAND_GETDIRECTORY)); -} - -TreeNode.prototype.subscribe = function(callback) { - if (callback != null && this.isParameter() && this.isStream()) { - this.contents._subscribers.add(callback); - } - return this.getTreeBranch(new Command(COMMAND_SUBSCRIBE)); -} - -TreeNode.prototype.unsubscribe = function(callback) { - if (callback != null && this.isParameter() && this.isStream()) { - this.contents._subscribers.delete(callback); - } - return this.getTreeBranch(new Command(COMMAND_UNSUBSCRIBE)); -} - -TreeNode.prototype.getChildren = function() { - if(this.children !== undefined) { - return this.children; - } - return null; -} - -function path2number(path) { - try { - let numbers = path.split("."); - if (numbers.length > 0) { - return Number(numbers[numbers.length - 1]); - } - } - catch(e) { - // ignore - } -} - -TreeNode.prototype.getNumber = function() { - if (this.isQualified()) { - return path2number(this.getPath()); - } - else { - return this.number; - } -} - -_getElementByPath = function(children, pathArray, path) { - if ((children === null)||(children === undefined)||(pathArray.length < 1)) { - return null; - } - var number = pathArray[pathArray.length - 1]; - - if (children[i].getNumber() == number) { - if (path.length === 0) { - return children[i]; - } - pathArray.push(path.splice(0,1)); - return _getElementByPath(children[i].getChildren(), pathArray, path); - } - - return null; -} - -TreeNode.prototype.toJSON = function() { - let res = {}; - const node = this; - if (!this.isRoot()) { - res.number = node.getNumber(); - res.path = node.getPath(); - } - if (node.contents) { - for(let prop in node.contents) { - if (node.contents.hasOwnProperty(prop)) { - let type = typeof node.contents[prop]; - if ((type === "string") || (type === "number")) { - res[prop] = node.contents[prop]; - } - else if (node.contents[prop].value !== undefined) { - res[prop] = node.contents[prop].value; - } - else { - res[prop] = node.contents[prop]; - } - } - } - } - if (node.isMatrix()) { - if (node.targets) { - res.targets = node.targets.slice(0); - } - if (node.sources) { - res.sources = node.sources.slice(0); - } - if (node.connections) { - res.connections = {}; - for (let target in node.connections) { - if (node.connections.hasOwnProperty(target)) { - res.connections[target] = {target: target, sources: []}; - if (node.connections[target].sources) { - res.connections[target].sources = node.connections[target].sources.slice(0); - } - } - } - - } - } - let children = node.getChildren(); - if (children) { - res.children = []; - for(let child of children) { - res.children.push(child.toJSON()); - } - } - return res; -}; - -TreeNode.prototype.getParent = function() { - return this._parent; -} - -TreeNode.prototype.getElementByPath = function(path) { - var children = this.getChildren(); - if (children == null) { - return null; - } - - var myPath = this.getPath(); - if (path == myPath) { - return this; - } - var myPathArray = []; - if (this._parent) { - myPathArray = myPath.split("."); - } - path = path.split("."); - - if (path.length > myPathArray.length) { - pathArray = path.splice(0, myPath.length + 1); - for(var i = 0; i < pathArray.length - 1; i++) { - if (pathArray[i] != myPathArray[i]) { - return null; - } - } - } - else { - return null; - } - return _getElementByPath(children, pathArray, path); -} - -TreeNode.prototype.getElementByNumber = function(number) { - if (this.elements != null) { - return this.elements.get(number); - } - return null; -} - -TreeNode.prototype.getElementByIdentifier = function(identifier) { - var children = this.getChildren(); - if (children == null) return null; - for(var i = 0; i < children.length; i++) { - if(children[i].contents !== undefined && - children[i].contents.identifier == identifier) { - return children[i]; - } - } - return null; -} - -TreeNode.prototype.getElement = function(id) { - if(Number.isInteger(id)) { - return this.getElementByNumber(id); - } else { - return this.getElementByIdentifier(id); - } -} - -TreeNode.prototype.update = function(other) { - return; -} - -TreeNode.prototype.getNodeByPath = function(client, path, callback) { - var self=this; - - if(path.length == 0) { - callback(null, self); - return; - } - - var child = self.getElement(path[0]); - if(child !== null) { - child.getNodeByPath(client, path.slice(1), callback); - } else { - var cmd = self.getDirectory((error, node) => { - if(error) { - callback(error); - } - child = node.getElement(path[0]); - if(child === null) { - //let e = new Error('invalid path: "' + path[0] + '"'); - // console.log(e.message ,"Self:", self, "Node:", node); - //callback(e); - //DO NOT REJECT !!!! We could still be updating the tree. - return; - } else { - child.getNodeByPath(client, path.slice(1), callback); - } - }); - if(cmd !== null) { - client.sendBERNode(cmd); - } - } -} - -TreeNode.prototype.getPath = function() { - if (this.path !== undefined) { - return this.path; - } - if(this._parent == null) { - if(this.number === undefined) { - return ""; - } else { - return this.number.toString(); - } - } else { - var path = this._parent.getPath(); - if(path.length > 0) { - path = path + "."; - } - return path + this.number; - } -} - -/**************************************************************************** - * RootElement - ***************************************************************************/ - -function RootElement() {}; - -RootElement.decode = function(ber) { - return Element.decode(ber); - - // TODO: handle qualified types -} - -/**************************************************************************** - * Element - ***************************************************************************/ - -function Element() {}; - -Element.decode = function(ber) { - var tag = ber.peek(); - if(tag == BER.APPLICATION(1)) { - if (DEBUG) { console.log("Parameter decode");} - return Parameter.decode(ber); - } else if(tag == BER.APPLICATION(3)) { - if (DEBUG) { console.log("Node decode");} - return Node.decode(ber); - } else if(tag == BER.APPLICATION(2)) { - if (DEBUG) { console.log("Command decode");} - return Command.decode(ber); - } else if(tag == BER.APPLICATION(9)) { - if (DEBUG) { console.log("QualifiedParameter decode");} - return QualifiedParameter.decode(ber); - } else if(tag == BER.APPLICATION(10)) { - if (DEBUG) { console.log("QualifiedNode decode");} - return QualifiedNode.decode(ber); - } else if(tag == BER.APPLICATION(13)) { - if (DEBUG) { console.log("MatrixNode decode");} - return MatrixNode.decode(ber); - } - else if(tag == BER.APPLICATION(17)) { - if (DEBUG) { console.log("QualifiedMatrix decode");} - return QualifiedMatrix.decode(ber); - } - else if(tag == BER.APPLICATION(19)) { - if (DEBUG) { console.log("Function decode");} - return Function.decode(ber); - } else if (tag == BER.APPLICATION(20)) { - if (DEBUG) { console.log("QualifiedFunction decode");} - return QualifiedFunction.decode(ber); - } - else if(tag == BER.APPLICATION(24)) { - // Template - throw new errors.UnimplementedEmberTypeError(tag); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } -} - -/**************************************************************************** - * ElementCollection - ***************************************************************************/ - - - -/**************************************************************************** - * QualifiedNode - ***************************************************************************/ - -function QualifiedNode(path) { - QualifiedNode.super_.call(this); - if (path != undefined) { - this.path = path; - } -} - -util.inherits(QualifiedNode, TreeNode); - - -QualifiedNode.decode = function(ber) { - var qn = new QualifiedNode(); - ber = ber.getSequence(BER.APPLICATION(10)); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - qn.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID - } - else if(tag == BER.CONTEXT(1)) { - qn.contents = NodeContents.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - qn.children = []; - seq = seq.getSequence(BER.APPLICATION(4)); - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - qn.addChild(Element.decode(nodeSeq)); - } - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("QualifiedNode", qn); } - return qn; -} - -QualifiedNode.prototype.getMinimal = function(complete = false) { - const number = this.getNumber(); - const n = new Node(number); - if (complete && (this.contents != null)) { - n.contents = this.contents; - } - return n; -} - -QualifiedNode.prototype.update = function(other) { - QualifiedNode.super_.prototype.update.apply(this); - if((other !== undefined) && (other.contents !== undefined)) { - if (this.contents == null) { - this.contents = other.contents; - } - else { - for(var key in other.contents) { - if(other.contents.hasOwnProperty(key)) { - this.contents[key] = other.contents[key]; - } - } - } - } - return; -} - -function QualifiedNodeCommand(self, cmd) { - var r = new Root(); - var qn = new QualifiedNode(); - qn.path = self.path; - r.addElement(qn); - qn.addChild(new Command(cmd)); - return r; -} - -QualifiedNode.prototype.getDirectory = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - if (callback != null && this.contents != null && !this.isStream()) { - this.contents._subscribers.add(callback); - } - return QualifiedNodeCommand(this, COMMAND_GETDIRECTORY) -} - -QualifiedNode.prototype.subscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - if (callback != null && this.isStream()) { - this.contents._subscribers.add(callback); - } - return QualifiedNodeCommand(this, COMMAND_SUBSCRIBE) -} - -QualifiedNode.prototype.unsubscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - if (callback != null && this.isStream()) { - this.contents._subscribers.delete(callback); - } - return QualifiedNodeCommand(this, COMMAND_UNSUBSCRIBE) -} - -QualifiedNode.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(10)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - n.number = seq.readInt(); - } else if(tag == BER.CONTEXT(1)) { - n.contents = NodeContents.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - seq = seq.getSequence(BER.APPLICATION(4)); - n.children = []; - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - n.addChild(Element.decode(nodeSeq)); - } - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("Node", n); } - return n; -} - -Node.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(3)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.number); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i Number(i))); - - if (matrixNode.connections[targetID].isLocked()) { - return false; - } - if (type === MatrixType.oneToN && - matrixNode.contents.maximumConnectsPerTarget == null && - matrixNode.contents.maximumConnectsPerTarget == null) { - return sMap.size < 2; - } - else if (type === MatrixType.oneToN && sMap.size >= 2) { - return false; - } - else if (type === MatrixType.oneToOne) { - if (sMap.size > 1) { - return false; - } - const sourceConnections = matrixNode._connectedSources[sources[0]]; - return sourceConnections == null || sourceConnections.size === 0 || sourceConnections.has(targetID); - } - else { - // N to N - if (matrixNode.contents.maximumConnectsPerTarget != null && - newSources.length > matrixNode.contents.maximumConnectsPerTarget) { - return false; - } - if (matrixNode.contents.maximumTotalConnects != null) { - let count = matrixNode._numConnections; - if (oldSources) { - count -= oldSources.length; - } - if (newSources) { - count += newSources.length; - } - return count <= matrixNode.contents.maximumTotalConnects; - } - - } - return true; -} - -function validateConnection(matrixNode, targetID, sources) { - if (targetID < 0) { - throw new Error(`Invalid negative target index ${targetID}`); - } - for(let i = 0; i < sources.length; i++) { - if (sources[i] < 0) { - throw new Error(`Invalid negative source at index ${i}`); - } - } - if (matrixNode.contents.mode === MatrixMode.linear) { - if (targetID >= matrixNode.contents.targetCount) { - throw new Error(`targetID ${targetID} higher than max value ${matrixNode.contents.targetCount}`); - } - for(let i = 0; i < sources.length; i++) { - if (sources[i] >= matrixNode.contents.sourceCount) { - throw new Error(`Invalid source at index ${i}`); - } - } - } - else if ((matrixNode.targets == null) || (matrixNode.sources == null)) { - throw new Error("Non-Linear matrix should have targets and sources"); - } - else { - let found = false; - for(let i = 0; i < matrixNode.targets; i++) { - if (matrixNode.targets[i] === targetID) { - found = true; - break; - } - } - if (!found) { - throw new Error(`Unknown targetid ${targetID}`); - } - found = false; - for(let i = 0; i < sources.length; i++) { - for(let j = 0; i < matrixNode.sources; j++) { - if (matrixNode.sources[j] === sources[i]) { - found = true; - break; - } - } - if (!found) { - throw new Error(`Unknown source at index ${i}`); - } - } - } -} - - -MatrixNode.decode = function(ber) { - var m = new MatrixNode(); - ber = ber.getSequence(BER.APPLICATION(13)); - while (ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if (tag == BER.CONTEXT(0)) { - m.number = seq.readInt(); - } - else if (tag == BER.CONTEXT(1)) { - m.contents = MatrixContents.decode(seq); - - } else if (tag == BER.CONTEXT(2)) { - m.children = []; - seq = seq.getSequence(BER.APPLICATION(4)); - while (seq.remain > 0) { - var childSeq = seq.getSequence(BER.CONTEXT(0)); - m.addChild(Element.decode(childSeq)); - } - } else if (tag == BER.CONTEXT(3)) { - m.targets = decodeTargets(seq); - } else if (tag == BER.CONTEXT(4)) { - m.sources = decodeSources(seq); - } else if (tag == BER.CONTEXT(5)) { - m.connections = decodeConnections(seq); - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("MatrixNode", m); } - return m; -}; - -MatrixNode.prototype.validateConnection = function(targetID, sources) { - validateConnection(this, targetID, sources); -} - -MatrixNode.prototype.canConnect = function(targetID, sources, operation) { - return canConnect(this, targetID, sources, operation); -} - -MatrixNode.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(13)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.number); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i 0) { - MatrixNode.disconnectSources(matrix, targetID, currentSource) - } - MatrixNode.connectSources(matrix, targetID, sources); -} - -MatrixNode.connectSources = function(matrix, targetID, sources) { - const target = Number(targetID); - if (matrix.connections[target] == null) { - matrix.connections[target] = new MatrixConnection(target); - } - matrix.connections[target].connectSources(sources); - if (sources != null) { - for(let source of sources) { - if (matrix._connectedSources[source] == null) { - matrix._connectedSources[source] = new Set(); - } - if (!matrix._connectedSources[source].has(target)) { - matrix._connectedSources[source].add(target); - matrix._numConnections++; - } - } - } -} - -MatrixNode.disconnectSources = function(matrix, targetID, sources) { - const target = Number(targetID); - if (matrix.connections[target] == null) { - matrix.connections[target] = new MatrixConnection(target); - } - matrix.connections[target].disconnectSources(sources); - if (sources != null) { - for(let source of sources) { - if (matrix._connectedSources[source] == null) { - continue; - } - if (matrix._connectedSources[source].has(target)) { - matrix._connectedSources[source].delete(target); - matrix._numConnections--; - } - } - } -} - -MatrixNode.prototype.update = function(other) { - MatrixNode.super_.prototype.update.apply(this); - MatrixUpdate(this, other); - return; -} - -MatrixNode.prototype.toQualified = function() { - let qm = new QualifiedMatrix(this.getPath()); - qm.update(this); - return qm; -} - -MatrixNode.prototype.connect = function(connections) { - let r = this.getTreeBranch(); - let m = r.getElementByPath(this.getPath()); - m.connections = connections; - return r; -} - -util.inherits(MatrixNode, TreeNode); - -module.exports.MatrixNode = MatrixNode; - -function MatrixContents() { - this.type = MatrixType.oneToN; - this.mode = MatrixMode.linear; -} - -MatrixContents.decode = function(ber) { - var mc = new MatrixContents(); - - ber = ber.getSequence(BER.EMBER_SET); - - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - - if(tag == BER.CONTEXT(0)) { - mc.identifier = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - mc.description = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(2)) { - mc.type = MatrixType.get(seq.readInt()); - } else if(tag == BER.CONTEXT(3)) { - mc.mode = MatrixMode.get(seq.readInt()); - } else if(tag == BER.CONTEXT(4)) { - mc.targetCount = seq.readInt(); - } else if(tag == BER.CONTEXT(5)) { - mc.sourceCount = seq.readInt(); - } else if(tag == BER.CONTEXT(6)) { - mc.maximumTotalConnects = seq.readInt(); - } else if(tag == BER.CONTEXT(7)) { - mc.maximumConnectsPerTarget = seq.readInt(); - } else if(tag == BER.CONTEXT(8)) { - tag = seq.peek(); - if (tag === BER.EMBER_RELATIVE_OID) { - mc.parametersLocation = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID - } - else { - mc.parametersLocation = seq.readInt(); - } - } else if(tag == BER.CONTEXT(9)) { - mc.gainParameterNumber = seq.readInt(); - } else if(tag == BER.CONTEXT(10)) { - mc.labels = []; - seq = seq.getSequence(BER.EMBER_SEQUENCE); - while(seq.remain > 0) { - var lSeq = seq.getSequence(BER.CONTEXT(0)); - mc.labels.push(Label.decode(lSeq)); - } - } else if(tag == BER.CONTEXT(11)) { - mc.schemaIdentifiers = seq.readInt(); - } else if(tag == BER.CONTEXT(12)) { - mc.templateReference = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return mc; -}; - -MatrixContents.prototype.encode = function(ber) { - ber.startSequence(BER.EMBER_SET); - if (this.identifier !== undefined) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeString(this.identifier, BER.EMBER_STRING); - ber.endSequence(); - } - if (this.description !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeString(this.description, BER.EMBER_STRING); - ber.endSequence(); - } - if (this.type !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.writeInt(this.type.value); - ber.endSequence(); - } - if (this.mode !== undefined) { - ber.startSequence(BER.CONTEXT(3)); - ber.writeInt(this.mode.value); - ber.endSequence(); - } - if (this.targetCount !== undefined) { - ber.startSequence(BER.CONTEXT(4)); - ber.writeInt(this.targetCount); - ber.endSequence(); - } - if (this.sourceCount !== undefined) { - ber.startSequence(BER.CONTEXT(5)); - ber.writeInt(this.sourceCount); - ber.endSequence(); - } - if (this.maximumTotalConnects !== undefined) { - ber.startSequence(BER.CONTEXT(6)); - ber.writeInt(this.maximumTotalConnects); - ber.endSequence(); - } - if (this.maximumConnectsPerTarget !== undefined) { - ber.startSequence(BER.CONTEXT(7)); - ber.writeInt(this.maximumConnectsPerTarget); - ber.endSequence(); - } - if (this.parametersLocation !== undefined) { - ber.startSequence(BER.CONTEXT(8)); - let param = Number(this.parametersLocation) - if (isNaN(param)) { - ber.writeRelativeOID(this.parametersLocation, BER.EMBER_RELATIVE_OID); - } - else { - ber.writeInt(param); - } - ber.endSequence(); - } - if (this.gainParameterNumber !== undefined) { - ber.startSequence(BER.CONTEXT(9)); - ber.writeInt(this.gainParameterNumber); - ber.endSequence(); - } - if (this.labels !== undefined) { - ber.startSequence(BER.CONTEXT(10)); - ber.startSequence(BER.EMBER_SEQUENCE); - for(var i =0; i < this.labels.length; i++) { - ber.startSequence(BER.CONTEXT(0)); - this.labels[i].encode(ber); - ber.endSequence(); - } - ber.endSequence(); - ber.endSequence(); - } - if (this.schemaIdentifiers !== undefined) { - ber.startSequence(BER.CONTEXT(11)); - ber.writeInt(this.schemaIdentifiers, BER.EMBER_STRING); - ber.endSequence(); - } - if (this.templateReference !== undefined) { - ber.startSequence(BER.CONTEXT(12)); - ber.writeRelativeOID(this.templateReference, BER.EMBER_RELATIVE_OID); - ber.endSequence(); - } - ber.endSequence(); -} - -decodeTargets = function(ber) { - let targets = []; - - ber = ber.getSequence(BER.EMBER_SEQUENCE); - - - while(ber.remain > 0) { - var seq = ber.getSequence(BER.CONTEXT(0)); - seq = seq.getSequence(BER.APPLICATION(14)); - seq = seq.getSequence(BER.CONTEXT(0)); - targets.push(seq.readInt()); - } - - return targets; -} - -decodeSources = function(ber) { - let sources = []; - - ber = ber.getSequence(BER.EMBER_SEQUENCE); - - while(ber.remain > 0) { - var seq = ber.getSequence(BER.CONTEXT(0)); - seq = seq.getSequence(BER.APPLICATION(15)); - seq = seq.getSequence(BER.CONTEXT(0)); - sources.push(seq.readInt()); - } - - return sources; -}; - -decodeConnections = function(ber) { - let connections = {}; - - let seq = ber.getSequence(BER.EMBER_SEQUENCE); - while(seq.remain > 0) { - var conSeq = seq.getSequence(BER.CONTEXT(0)); - var con = MatrixConnection.decode(conSeq); - if (con.target !== undefined) { - connections[con.target] = (con); - } - } - return connections; -} - -module.exports.MatrixContents = MatrixContents; - - - -function MatrixConnection(target) { - if (target) { - target = Number(target); - if (isNaN(target)) { target = 0; } - this.target = target; - } - else { - this.target = 0; - } - this._locked = false; -} - -// ConnectionOperation ::= -// INTEGER { -// absolute (0), -- default. sources contains absolute information -// connect (1), -- nToN only. sources contains sources to add to connection -// disconnect (2) -- nToN only. sources contains sources to remove from -// connection -// } -var MatrixOperation = new Enum({ - absolute: 0, - connect: 1, - disconnect: 2 -}); - -// ConnectionDisposition ::= -// INTEGER { -// tally (0), -- default -// modified (1), -- sources contains new current state -// pending (2), -- sources contains future state -// locked (3) -- error: target locked. sources contains current state -// -- more tbd. -// } -var MatrixDisposition = new Enum({ - tally: 0, - modified: 1, - pending: 2, - locked: 3 -}); - -module.exports.MatrixOperation = MatrixOperation; -module.exports.MatrixDisposition = MatrixDisposition; - -MatrixConnection.prototype.setSources = function(sources) { - if (sources === undefined) { - delete this.sources; - return; - } - let s = new Set(sources.map(i => Number(i))); - this.sources = [...s].sort(); // sources should be an array -} - -MatrixConnection.prototype.connectSources = function(sources) { - if (sources === undefined) { - return; - } - let s = new Set(this.sources); - for(let item of sources) { - s.add(item); - } - this.sources = [...s].sort(); -} - -MatrixConnection.prototype.lock = function() { - this._locked = true; -} -MatrixConnection.prototype.unlock = function() { - this._locked = false; -} -MatrixConnection.prototype.isLocked = function() { - return this._locked; -} - -MatrixConnection.prototype.disconnectSources = function(sources) { - if (sources === undefined) { - return; - } - let s = new Set(this.sources); - for(let item of sources) { - s.delete(item); - } - this.sources = [...s].sort(); -} - -MatrixConnection.decode = function(ber) { - var c = new MatrixConnection(); - ber = ber.getSequence(BER.APPLICATION(16)); - while (ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if (tag == BER.CONTEXT(0)) { - c.target = seq.readInt(); - } - else if (tag == BER.CONTEXT(1)) { - const sources = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); - if (sources === "") { - c .sources = []; - } - else { - c.sources = sources.split(".").map(i => Number(i)); - } - } else if (tag == BER.CONTEXT(2)) { - c.operation = MatrixOperation.get(seq.readInt()); - - } else if (tag == BER.CONTEXT(3)) { - c.disposition = MatrixDisposition.get(seq.readInt()); - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return c; -} - -MatrixConnection.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(16)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.target); - ber.endSequence(); - - if ((this.sources !== undefined)&& (this.sources.length > 0)) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeRelativeOID(this.sources.join("."), BER.EMBER_RELATIVE_OID); - ber.endSequence(); - } - if (this.operation !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.writeInt(this.operation.value); - ber.endSequence(); - } - if (this.disposition !== undefined) { - ber.startSequence(BER.CONTEXT(3)); - ber.writeInt(this.disposition.value); - ber.endSequence(); - } - ber.endSequence(); -} - -module.exports.MatrixConnection = MatrixConnection; - -function Label(path, description) { - if (path) { - this.basePath = path; - } - if (description) { - this.description = description; - } -} - -Label.decode = function(ber) { - var l = new Label(); - - ber = ber.getSequence(BER.APPLICATION(18)); - - while (ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if (tag == BER.CONTEXT(0)) { - l.basePath = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); - } else if (tag == BER.CONTEXT(1)) { - l.description = seq.readString(BER.EMBER_STRING); - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return l; -}; - -Label.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(18)); - if (this.basePath != null) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.basePath, BER.EMBER_RELATIVE_OID); - ber.endSequence(); - } - if (this.description != null) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeString(this.description, BER.EMBER_STRING); - ber.endSequence(); - } - ber.endSequence(); -} - -module.exports.Label = Label; - - -function ParametersLocation() { -} - -ParametersLocation.decode = function(ber) { - var tag = ber.peek(); - ber = ber.getSequence(tag); - this.value = ber.readValue(); -} - -module.exports.ParametersLocation = ParametersLocation; - - - -var MatrixType = new Enum({ - oneToN: 0, - oneToOne: 1, - nToN: 2 -}); - - -module.exports.MatrixType = MatrixType; - - -var MatrixMode = new Enum({ - linear: 0, - nonLinear: 1 -}); - - -module.exports.MatrixMode = MatrixMode; - - -/**************************************************************************** - * QualifiedMatrix - ***************************************************************************/ - -function QualifiedMatrix(path) { - QualifiedMatrix.super_.call(this); - if (path != undefined) { - this.path = path; - } - this._connectedSources = {}; - this._numConnections = 0; -} - -util.inherits(QualifiedMatrix, TreeNode); - -QualifiedMatrix.prototype.getMinimal = function(complete = false) { - const number = this.getNumber(); - const m = new MatrixNode(number); - if (complete) { - if (this.contents != null) { - m.contents = this.contents; - } - if (this.targets != null) { - m.targets = this.targets; - } - if (this.sources != null) { - m.sources = this.sources; - } - if (this.connections != null) { - m.connections = this.connections; - } - } - return m; -} - -QualifiedMatrix.prototype.validateConnection = function(targetID, sources) { - validateConnection(this, targetID, sources); -} - -QualifiedMatrix.prototype.canConnect = function(targetID, sources, operation) { - return canConnect(this, targetID, sources, operation); -} - -QualifiedMatrix.prototype.setSources = function(targetID, sources) { - return MatrixNode.setSources(this, targetID, sources); -} -QualifiedMatrix.prototype.connectSources = function(targetID, sources) { - return MatrixNode.connectSources(this, targetID, sources); -} -QualifiedMatrix.prototype.disconnectSources = function(targetID, sources) { - return MatrixNode.disconnectSources(this, targetID, sources); -} -QualifiedMatrix.prototype.getSourceConnections = function(source) { - return MatrixNode.getSourceConnections(this, source); -} - -QualifiedMatrix.decode = function(ber) { - var qm = new QualifiedMatrix(); - ber = ber.getSequence(BER.APPLICATION(17)); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - qm.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID - } - else if(tag == BER.CONTEXT(1)) { - qm.contents = MatrixContents.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - qm.children = []; - seq = seq.getSequence(BER.APPLICATION(4)); - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - qm.addChild(Element.decode(nodeSeq)); - } - } else if (tag == BER.CONTEXT(3)) { - qm.targets = decodeTargets(seq); - } else if (tag == BER.CONTEXT(4)) { - qm.sources = decodeSources(seq); - } else if (tag == BER.CONTEXT(5)) { - qm.connections = {}; - seq = seq.getSequence(BER.EMBER_SEQUENCE); - while(seq.remain > 0) { - var conSeq = seq.getSequence(BER.CONTEXT(0)); - var con = MatrixConnection.decode(conSeq); - if (con.target !== undefined) { - qm.connections[con.target] = con; - } - } - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("QualifiedMatrix", qm); } - return qm; -} - -function MatrixUpdate(matrix, newMatrix) { - if (newMatrix != null) { - if (newMatrix.contents != null) { - if (matrix.contents == null) { - matrix.contents = newMatrix.contents; - } - else { - for (var key in newMatrix.contents) { - if (newMatrix.contents.hasOwnProperty(key)) { - matrix.contents[key] = newMatrix.contents[key]; - } - } - } - } - if (newMatrix.targets != null) { - matrix.targets = newMatrix.targets; - } - if (newMatrix.sources != null) { - matrix.sources = newMatrix.sources; - } - if (newMatrix.connections != null) { - if (matrix.connections == null) { - matrix.connections = {}; - } - for(let id in newMatrix.connections) { - if (newMatrix.connections.hasOwnProperty(id)) { - let connection = newMatrix.connections[id]; - if ((connection.target < matrix.contents.targetCount) && - (connection.target >= 0)) { - if (matrix.connections[connection.target] == null) { - matrix.connections[connection.target] = new MatrixConnection(connection.target); - } - matrix.connections[connection.target].setSources(connection.sources); - } - } - } - } - } -} - -QualifiedMatrix.prototype.update = function(other) { - QualifiedMatrix.super_.prototype.update.apply(this); - MatrixUpdate(this, other); - return; -} - -function QualifiedMatrixCommand(self, cmd) { - var r = new Root(); - var qn = new QualifiedMatrix(); - qn.path = self.path; - r.addElement(qn); - qn.addChild(new Command(cmd)); - return r; -} - -QualifiedMatrix.prototype.getDirectory = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - if (callback != null && !this.isStream()) { - this.contents._subscribers.add(callback); - } - return QualifiedMatrixCommand(this, COMMAND_GETDIRECTORY); -} - -QualifiedMatrix.prototype.subscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - if (callback != null && this.isStream()) { - this.contents._subscribers.add(callback); - } - return QualifiedMatrixCommand(this, COMMAND_SUBSCRIBE); -} - -QualifiedMatrix.prototype.unsubscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - if (callback != null && this.isStream()) { - this.contents._subscribers.delete(callback); - } - return QualifiedMatrixCommand(this, COMMAND_UNSUBSCRIBE); -} - -QualifiedMatrix.prototype.connect = function(connections) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - var r = new Root(); - var qn = new QualifiedMatrix(); - qn.path = this.path; - r.addElement(qn); - qn.connections = connections; - return r; -} - -QualifiedMatrix.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(17)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i 0) { - tag = ber.peek(); - var seq = ber.getSequence(tag); - if (tag === BER.CONTEXT(0)) { - tuple.type = ParameterType.get(seq.readInt()); - } - else if (tag === BER.CONTEXT(1)) { - tuple.name = seq.readString(BER.EMBER_STRING); - } - } - return tuple; -} - -module.exports.FunctionArgument = FunctionArgument; - -/**************************************************************************** - * FunctionContent - ***************************************************************************/ - -function FunctionContent() { - this.arguments = []; - this.result = []; -} - - -FunctionContent.decode = function(ber) { - var fc = new FunctionContent(); - ber = ber.getSequence(BER.EMBER_SET); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - fc.identifier = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - fc.description = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(2)) { - fc.arguments = []; - var dataSeq = seq.getSequence(BER.EMBER_SEQUENCE); - while(dataSeq.remain > 0) { - seq = dataSeq.getSequence(BER.CONTEXT(0)); - fc.arguments.push(FunctionArgument.decode(seq)); - } - } else if(tag == BER.CONTEXT(3)) { - fc.result = []; - while(seq.remain > 0) { - tag = seq.peek(); - var dataSeq = seq.getSequence(tag); - if (tag === BER.CONTEXT(0)) { - fc.result.push(FunctionArgument.decode(dataSeq)); - } - } - } else if(tag == BER.CONTEXT(4)) { - fc.templateReference = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return fc; -} - -FunctionContent.prototype.encode = function(ber) { - ber.startSequence(BER.EMBER_SET); - - if(this.identifier != null) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeString(this.identifier, BER.EMBER_STRING); - ber.endSequence(); // BER.CONTEXT(0) - } - - if(this.description != null) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeString(this.description, BER.EMBER_STRING); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.arguments != null) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.EMBER_SEQUENCE); - for(var i = 0; i < this.arguments.length; i++) { - ber.startSequence(BER.CONTEXT(0)); - this.arguments[i].encode(ber); - ber.endSequence(); - } - ber.endSequence(); - ber.endSequence(); // BER.CONTEXT(2) - } - - if(this.result != null) { - ber.startSequence(BER.CONTEXT(3)); - ber.startSequence(BER.EMBER_SEQUENCE); - for(var i = 0; i < this.result.length; i++) { - ber.startSequence(BER.CONTEXT(0)); - this.result[i].encode(ber); - ber.endSequence(); - } - ber.endSequence(); - ber.endSequence(); // BER.CONTEXT(3) - } - - ber.endSequence(); // BER.EMBER_SET -} - -module.exports.FunctionContent = FunctionContent; - -/**************************************************************************** - * QualifiedFunction - ***************************************************************************/ - -function QualifiedFunction(path, func) { - QualifiedFunction.super_.call(this); - if (path != undefined) { - this.path = path; - } - this.func = func; -} - -util.inherits(QualifiedFunction, TreeNode); - - -QualifiedFunction.decode = function(ber) { - var qf = new QualifiedFunction(); - ber = ber.getSequence(BER.APPLICATION(20)); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - qf.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID - } - else if(tag == BER.CONTEXT(1)) { - qf.contents = FunctionContent.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - qf.children = []; - seq = seq.getSequence(BER.APPLICATION(4)); - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - qf.addChild(Element.decode(nodeSeq)); - } - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return qf; -} - -QualifiedFunction.prototype.update = function(other) { - QualifiedFunction.super_.prototype.update.apply(this); - if ((other !== undefined) && (other.contents !== undefined)) { - if (this.contents == null) { - this.contents = other.contents; - } - else { - for (var key in other.contents) { - if (other.contents.hasOwnProperty(key)) { - this.contents[key] = other.contents[key]; - } - } - } - } - return; -} - -function QualifiedFunctionCommand(self, cmd) { - var r = new Root(); - var qf = new QualifiedFunction(); - qf.path = self.path; - r.addElement(qf); - qf.addChild(new Command(cmd)); - return r; -} - -QualifiedFunction.prototype.invoke = function(params) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - var QualifiedFunctionNode = QualifiedFunctionCommand(this, COMMAND_INVOKE); - var invocation = new Invocation() - invocation.arguments = params; - QualifiedFunctionNode.getElementByPath(this.getPath()).getNumber(COMMAND_INVOKE).invocation = invocation - return QualifiedFunctionNode -} - -QualifiedFunction.prototype.getDirectory = function() { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedFunctionCommand(this, COMMAND_GETDIRECTORY); -} - -QualifiedFunction.prototype.subscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedFunctionCommand(this, COMMAND_SUBSCRIBE); -} - -QualifiedFunction.prototype.unsubscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedFunctionCommand(this, COMMAND_UNSUBSCRIBE); -} - -QualifiedFunction.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(20)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - f.number = seq.readInt(); - } else if(tag == BER.CONTEXT(1)) { - f.contents = FunctionContent.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - seq = seq.getSequence(BER.APPLICATION(4)); - f.children = []; - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - f.addChild(Element.decode(nodeSeq)); - } - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("Function", f); } - return f; -} - -Function.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(19)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.number); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i { - m.addChild(new Command(COMMAND_INVOKE)) - }); -} - -Function.prototype.update = function(other) { - Function.super_.prototype.update.apply(this); - if ((other !== undefined) && (other.contents !== undefined)) { - if (this.contents == null) { - this.contents = other.contents; - } - else { - for (var key in other.contents) { - if (other.contents.hasOwnProperty(key)) { - this.contents[key] = other.contents[key]; - } - } - } - } - return; -} - -module.exports.Function = Function; - - -/**************************************************************************** - * NodeContents - ***************************************************************************/ - -function NodeContents() { - this.isOnline = true; - this._subscribers = new Set(); -}; - - - -NodeContents.decode = function(ber) { - var nc = new NodeContents(); - ber = ber.getSequence(BER.EMBER_SET); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - nc.identifier = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - nc.description = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(2)) { - nc.isRoot = seq.readBoolean(); - } else if(tag == BER.CONTEXT(3)) { - nc.isOnline = seq.readBoolean(); - } else if(tag == BER.CONTEXT(4)) { - nc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return nc; -} - -NodeContents.prototype.encode = function(ber) { - ber.startSequence(BER.EMBER_SET); - - if(this.identifier !== undefined) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeString(this.identifier, BER.EMBER_STRING); - ber.endSequence(); // BER.CONTEXT(0) - } - - if(this.description !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeString(this.description, BER.EMBER_STRING); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.isRoot !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.writeBoolean(this.isRoot); - ber.endSequence(); // BER.CONTEXT(2) - } - - if(this.isOnline !== undefined) { - ber.startSequence(BER.CONTEXT(3)); - ber.writeBoolean(this.isOnline); - ber.endSequence(); // BER.CONTEXT(3) - } - - if(this.schemaIdentifiers !== undefined) { - ber.startSequence(BER.CONTEXT(4)); - ber.writeString(this.schemaIdentifiers, BER.EMBER_STRING); - ber.endSequence(); // BER.CONTEXT(4) - } - - ber.endSequence(); // BER.EMBER_SET -} - -module.exports.NodeContents = NodeContents; - -/**************************************************************************** - * Command - ***************************************************************************/ - -function Command(number) { - if(number !== undefined) - this.number = number; - if(number == COMMAND_GETDIRECTORY) { - this.fieldFlags = FieldFlags.all; - } -} - -var FieldFlags = new Enum({ - sparse: -2, - all: -1, - default: 0, - identifier: 1, - description: 2, - tree: 3, - value: 4, - connections: 5 -}); - -Command.decode = function(ber) { - var c = new Command(); - ber = ber.getSequence(BER.APPLICATION(2)); - - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - c.number = seq.readInt(); - } - else if(tag == BER.CONTEXT(1)) { - c.fieldFlags = FieldFlags.get(seq.readInt()); - } - else if(tag == BER.CONTEXT(2)) { - c.invocation = Invocation.decode(seq); - } - else { - // TODO: options - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return c; -} - -Command.prototype.toJSON = function() { - return { - number: this.number, - fieldFlags: this.fieldFlags, - invocation: this.invocation == null ? null : this.invocation.toJSON() - }; -} - -Command.prototype.isCommand = function() { - throw new Error("old ember lib"); -} - -Command.prototype.getNumber = function() { - return this.number; -} - -Command.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(2)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.number); - ber.endSequence(); // BER.CONTEXT(0) - - if (this.number === COMMAND_GETDIRECTORY && this.fieldFlags) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeInt(this.fieldFlags.value); - ber.endSequence(); - } - - if (this.number === COMMAND_INVOKE && this.invocation) { - ber.startSequence(BER.CONTEXT(2)); - this.invocation.encode(ber); - ber.endSequence(); - } - // TODO: options - - ber.endSequence(); // BER.APPLICATION(2) -} - -module.exports.Command = Command; - -/**************************************************************************** - * Invocation - ***************************************************************************/ -function Invocation(id = null) { - this.id = id == null ? Invocation._id++ : id; - this.arguments = []; -} - -Invocation._id = 1 - -Invocation.decode = function(ber) { - let invocation = null; - ber = ber.getSequence(BER.APPLICATION(22)); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - const invocationId = seq.readInt(); - invocation = new Invocation(invocationId); - } - else if(tag == BER.CONTEXT(1)) { - if (invocation == null) { - throw new Error("Missing invocationID"); - } - invocation.arguments = []; - seq = seq.getSequence(BER.EMBER_SEQUENCE); - while(seq.remain > 0) { - const dataSeq = seq.getSequence(BER.CONTEXT(0)); - tag = dataSeq.peek(); - const val = dataSeq.readValue(); - invocation.arguments.push( - new FunctionArgument(ParameterTypefromBERTAG(tag), val) - ); - } - } - else { - // TODO: options - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return invocation; -} - - -Invocation.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(22)); - // ber.startSequence(BER.EMBER_SEQUENCE); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.id) - ber.endSequence(); - - ber.startSequence(BER.CONTEXT(1)); - ber.startSequence(BER.EMBER_SEQUENCE); - for(var i = 0; i < this.arguments.length; i++) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeValue( - this.arguments[i].value, - ParameterTypetoBERTAG(this.arguments[i].type - )); - ber.endSequence(); - } - ber.endSequence(); - ber.endSequence(); - - ber.endSequence(); // BER.APPLICATION(22) -} -/**************************************************************************** - * InvocationResult - ***************************************************************************/ - - function InvocationResult(invocationId = null) { - this.invocationId = invocationId; -} - -util.inherits(InvocationResult, TreeNode); -module.exports.InvocationResult = InvocationResult; - -InvocationResult.prototype.setFailure = function() { - this.success = false; -} - -InvocationResult.prototype.setSuccess = function() { - this.success = true; -} - -/** - * @param{} - */ -InvocationResult.prototype.setResult = function(result) { - if (!Array.isArray(result)) { - throw new Error("Invalid inovation result. Should be array"); - } - this.result = result; -} - -InvocationResult.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(23)); - if (this.invocationId != null) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.invocationId); - ber.endSequence(); - } - if (this.success != null) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeBoolean(this.success); - ber.endSequence(); - } - if (this.result != null && this.result.length) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.EMBER_SEQUENCE); - for (let i = 0; i < this.result.length; i++) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeValue(this.result[i].value, ParameterTypetoBERTAG(this.result[i].type)); - ber.endSequence(); - } - ber.endSequence(); - ber.endSequence(); - } - ber.endSequence(); // BER.APPLICATION(23)} -} - -InvocationResult.prototype.toQualified = function() { - return this; -} - -InvocationResult.decode = function(ber) { - let invocationResult = new InvocationResult(); - ber = ber.getSequence(BER.APPLICATION(23)); - while(ber.remain > 0) { - tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { // invocationId - invocationResult.invocationId = seq.readInt(); - } else if(tag == BER.CONTEXT(1)) { // success - invocationResult.success = seq.readBoolean() - }else if(tag == BER.CONTEXT(2)) { - invocationResult.result = []; - let res = seq.getSequence(BER.EMBER_SEQUENCE); - while(res.remain > 0) { - tag = res.peek(); - if (tag === BER.CONTEXT(0)) { - var resTag = res.getSequence(BER.CONTEXT(0)); - tag = resTag.peek(); - invocationResult.result.push( - new FunctionArgument( - ParameterTypefromBERTAG(tag), - resTag.readValue() - )); - } - } - continue - } else { - // TODO: options - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return invocationResult; -} - - - -/**************************************************************************** - * QualifiedParameter - ***************************************************************************/ - -function QualifiedParameter(path) { - QualifiedParameter.super_.call(this); - if(path !== undefined) - this.path = path; -} - -util.inherits(QualifiedParameter, TreeNode); -module.exports.QualifiedParameter = QualifiedParameter; - -QualifiedParameter.prototype.getMinimal = function(complete = false) { - const number = this.getNumber(); - const p = new Parameter(number); - if (complete) { - if (this.contents != null) { - p = this.contents; - } - } - return p; -} - - -QualifiedParameter.decode = function(ber) { - var qp = new QualifiedParameter(); - ber = ber.getSequence(BER.APPLICATION(9)); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - qp.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID - } - else if(tag == BER.CONTEXT(1)) { - qp.contents = ParameterContents.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - qp.children = []; - seq = seq.getSequence(BER.APPLICATION(4)); - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - qp.addChild(Element.decode(nodeSeq)); - } - } else { - return qp; - } - } - if (DEBUG) { console.log("QualifiedParameter", qp); } - return qp; -} - -QualifiedParameter.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(9)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - p.number = seq.readInt(); - - } else if(tag == BER.CONTEXT(1)) { - p.contents = ParameterContents.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - seq = seq.getSequence(BER.APPLICATION(4)); - p.children = []; - while(seq.remain > 0) { - var paramSeq = seq.getSequence(BER.CONTEXT(0)); - p.addChild(Element.decode(paramSeq)); - } - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("Parameter", p); } - return p; -} - -Parameter.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(1)); - - ber.writeIfDefined(this.number, ber.writeInt, 0); - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i { - m.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); - }); -} - -Parameter.prototype.toQualified = function() { - let qp = new QualifiedParameter(this.getPath()); - qp.update(this); - return qp; -} - -Parameter.prototype.update = function(other) { - Parameter.super_.prototype.update.apply(this); - if ((other !== undefined) && (other.contents !== undefined)) { - if (this.contents == null) { - this.contents = other.contents; - } - else { - for (var key in other.contents) { - if (key[0] === "_") { continue; } - if (other.contents.hasOwnProperty(key)) { - this.contents[key] = other.contents[key]; - } - } - for(let cb of this.contents._subscribers) { - cb(this); - } - } - } - return; -} - - -var ParameterAccess = new Enum({ - none: 0, - read: 1, - write: 2, - readWrite: 3 -}); - - -/* -BER VAlue -Value ::= - CHOICE { - integer Integer64, - real REAL, - string EmberString, - boolean BOOLEAN, - octets OCTET STRING, - null NULL - }*/ - -var ParameterType = new Enum({ - integer: 1, - real: 2, - string: 3, - boolean: 4, - trigger: 5, - enum: 6, - octets: 7 -}); - -function ParameterTypetoBERTAG(type) { - switch (type.value) { - case 1: return BER.EMBER_INTEGER; - case 2: return BER.EMBER_REAL; - case 3: return BER.EMBER_STRING; - case 4: return BER.EMBER_BOOLEAN; - case 7: return BER.EMBER_OCTETSTRING; - default: - throw new Error(`Unhandled ParameterType ${type}`); - } -} - -function ParameterTypefromBERTAG(tag) { - switch (tag) { - case BER.EMBER_INTEGER: return ParameterType.integer; - case BER.EMBER_REAL: return ParameterType.real; - case BER.EMBER_STRING: return ParameterType.string; - case BER.EMBER_BOOLEAN: return ParameterType.boolean; - case BER.EMBER_OCTETSTRING: return ParameterType.octets; - default: - throw new Error(`Unhandled BER TAB ${tag}`); - } -} - -module.exports.ParameterAccess = ParameterAccess; -module.exports.ParameterType = ParameterType; - -function ParameterContents(value, type) { - this._subscribers = new Set(); - if(value !== undefined) { - this.value = value; - } - if(type !== undefined) { - if((type = ParameterType.get(type)) !== undefined){ - this.type = type - } - } -}; - -module.exports.ParameterContents = ParameterContents; - -ParameterContents.decode = function(ber) { - var pc = new ParameterContents(); - ber = ber.getSequence(BER.EMBER_SET); - - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - pc.identifier = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - pc.description = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(2)) { - pc.value = seq.readValue(); - } else if(tag == BER.CONTEXT(3)) { - pc.minimum = seq.readValue(); - } else if(tag == BER.CONTEXT(4)) { - pc.maximum = seq.readValue(); - } else if(tag == BER.CONTEXT(5)) { - pc.access = ParameterAccess.get(seq.readInt()); - } else if(tag == BER.CONTEXT(6)) { - pc.format = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(7)) { - pc.enumeration = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(8)) { - pc.factor = seq.readInt(); - } else if(tag == BER.CONTEXT(9)) { - pc.isOnline = seq.readBoolean(); - } else if(tag == BER.CONTEXT(10)) { - pc.formula = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(11)) { - pc.step = seq.readInt(); - } else if(tag == BER.CONTEXT(12)) { - pc.default = seq.readValue(); - } else if(tag == BER.CONTEXT(13)) { - pc.type = ParameterType.get(seq.readInt()); - } else if(tag == BER.CONTEXT(14)) { - pc.streamIdentifier = seq.readInt(); - } else if(tag == BER.CONTEXT(15)) { - pc.enumMap = StringIntegerCollection.decode(seq); - } else if(tag == BER.CONTEXT(16)) { - pc.streamDescriptor = StreamDescription.decode(seq); - } else if(tag == BER.CONTEXT(17)) { - pc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); - } else if (tag == null) { - break; - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return pc; -} - -ParameterContents.prototype.encode = function(ber) { - ber.startSequence(BER.EMBER_SET); - - ber.writeIfDefined(this.identifier, ber.writeString, 0, BER.EMBER_STRING); - ber.writeIfDefined(this.description, ber.writeString, 1, BER.EMBER_STRING); - ber.writeIfDefined(this.value, ber.writeValue, 2); - ber.writeIfDefined(this.minimum, ber.writeValue, 3); - ber.writeIfDefined(this.maximum, ber.writeValue, 4); - ber.writeIfDefinedEnum(this.access, ParameterAccess, ber.writeInt, 5); - ber.writeIfDefined(this.format, ber.writeString, 6, BER.EMBER_STRING); - ber.writeIfDefined(this.enumeration, ber.writeString, 7, BER.EMBER_STRING); - ber.writeIfDefined(this.factor, ber.writeInt, 8); - ber.writeIfDefined(this.isOnline, ber.writeBoolean, 9); - ber.writeIfDefined(this.formula, ber.writeString, 10, BER.EMBER_STRING); - ber.writeIfDefined(this.step, ber.writeInt, 11); - ber.writeIfDefined(this.default, ber.writeValue, 12); - ber.writeIfDefinedEnum(this.type, ParameterType, ber.writeInt, 13); - ber.writeIfDefined(this.streamIdentifier, ber.writeInt, 14); - - if(this.emumMap !== undefined) { - ber.startSequence(BER.CONTEXT(15)); - StringIntegerCollection.encode(ber, this.enumMap); - ber.endSequence(); - } - - if(this.streamDescriptor !== undefined) { - ber.startSequence(BER.CONTEXT(16)); - this.streamDescriptor.encode(ber); - ber.endSequence(); - } - - ber.writeIfDefined(this.schemaIdentifiers, ber.writeString, 17, BER.EMBER_STRING); - - ber.endSequence(); -} - -/**************************************************************************** - * StringIntegerCollection - ***************************************************************************/ - -// This is untested, VPB doesn't seem to use this that I've seen so far - -function StringIntegerCollection() {}; - -StringIntegerCollection.decode = function(ber) { - var enumMap = {}; - ber = ber.getSequence(BER.APPLICATION(8)); - while(ber.remain > 0) { - var seq = ber.getSequence(BER.CONTEXT(0)); - seq = seq.getSequence(BER.APPLICATION(7)); - var entryString, entryInteger; - while(seq.remain > 0) { - var tag = seq.peek(); - var dataSeq = seq.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - entryString = dataSeq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - entryInteger = dataSeq.readInt(); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - enumMap[entryString] = entryInteger; - } - - return new Enum(enumMap); -} - -StringIntegerCollection.encode = function(ber, e) { - ber.startSequence(BER.APPLICATION(8)); - ber.startSequence(BER.CONTEXT(0)); - e.enums.forEach((item) => { - ber.startSequence(BER.APPLICATION(7)); - ber.startSequence(BER.CONTEXT(0)); - ber.writeString(item.key, BER.EMBER_STRING); - ber.endSequence(); - ber.startSequence(BER.CONTEXT(1)); - ber.writeInt(item.value); - ber.endSequence(); - ber.endSequence(); - }); - ber.endSequence(); - ber.endSequence(); -} - -/**************************************************************************** - * StreamDescription - ***************************************************************************/ - -var StreamFormat = new Enum({ - unsignedInt8: 0, - unsignedInt16BigEndian: 2, - unsignedInt16LittleEndian: 3, - unsignedInt32BigEndian: 4, - unsignedInt32LittleEndian: 5, - unsignedInt64BigEndian: 6, - unsignedInt64LittleENdian: 7, - signedInt8: 8, - signedInt16BigEndian: 10, - signedInt16LittleEndian: 11, - signedInt32BigEndian: 12, - signedInt32LittleEndian: 13, - signedInt64BigEndian: 14, - signedInt64LittleEndian: 15, - ieeeFloat32BigEndian: 20, - ieeeFloat32LittleEndian: 21, - ieeeFloat64BigEndian: 22, - ieeeFloat64LittleEndian: 23 -}); - -function StreamDescription() {}; - -StreamDescription.decode = function(ber) { - var sd = new StreamDescription(); - ber = ber.getSequence(BER.APPLICATION(12)); - - while(ber.remain > 0) { - var tag = ber.peek(); - var seq =ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - sd.format = StreamFormat.get(seq.readInt()); - } else if(tag == BER.CONTEXT(1)) { - sd.offset = seq.readInt(); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return sd; -} - -StreamDescription.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(12)); - - ber.writeIfDefinedEnum(this.format, StreamFormat, ber.writeInt, 0); - ber.writeIfDefined(this.offset, ber.writeInt, 1); - - ber.endSequence(); -} - - diff --git a/index.js b/index.js index 924f192..7b60331 100755 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ -const DeviceTree = require('./device.js').DeviceTree; -const Decoder = require('./device.js').DecodeBuffer; -const Ember = require("./ember.js"); +const EmberClient = require('./EmberClient'); +const EmberLib = require("./EmberLib"); +const Decoder = EmberLib.DecodeBuffer; const S101 = require("./s101"); -const TreeServer = require("./server"); -const {S101Client} = require("./client"); -module.exports = {DeviceTree, Decoder, Ember, TreeServer, S101, S101Client}; +const EmberServer = require("./EmberServer"); +const {S101Client} = require("./EmberSocket"); +module.exports = {EmberClient, Decoder, EmberLib, EmberServer, S101, S101Client}; diff --git a/server.js b/server.js deleted file mode 100755 index 9210a69..0000000 --- a/server.js +++ /dev/null @@ -1,823 +0,0 @@ -const EventEmitter = require('events').EventEmitter; -const util = require('util'); -const S101Server = require('./client.js').S101Server; -const ember = require('./ember.js'); - -function TreeServer(host, port, tree) { - TreeServer.super_.call(this); - var self = this; - self._debug = false; - - self.callback = undefined; - self.timeoutValue = 2000; - self.server = new S101Server(host, port); - self.tree = tree; - self.clients = new Set(); - self.subscribers = {}; - - self.server.on('listening', () => { - if (self._debug) { console.log("listening"); } - self.emit('listening'); - if (self.callback !== undefined) { - self.callback(); - self.callback = undefined; - } - }); - - self.server.on('connection', (client) => { - if (self._debug) { console.log("ember new connection from", client.remoteAddress()); } - self.clients.add(client); - client.on("emberTree", (root) => { - if (self._debug) { console.log("ember new request from", client.remoteAddress(), root); } - // Queue the action to make sure responses are sent in order. - client.addRequest(() => { - try { - let path = self.handleRoot(client, root); - self.emit("request", {client: client.remoteAddress(), root: root, path: path}); - } - catch(e) { - if (self._debug) { console.log(e.stack); } - self.emit("error", e); - } - }); - }); - client.on("disconnected", () => { - self.clients.delete(client); - self.emit('disconnect', client.remoteAddress()); - }); - client.on("error", error => { - self.emit('clientError', { remoteAddress: client.remoteAddress(), error }); - }); - self.emit('connection', client.remoteAddress()); - }); - - self.server.on('disconnected', () => { - self.emit('disconnected', client.remoteAddress()); - }); - - self.server.on("error", (e) => { - self.emit("error", e); - if (self.callback !== undefined) { - self.callback(e); - } - }); - -} - -util.inherits(TreeServer, EventEmitter); - - -TreeServer.prototype.listen = function() { - return new Promise((resolve, reject) => { - this.callback = (e) => { - if (e == null) { - return resolve(); - } - return reject(e); - }; - this.server.listen(); - }); -}; - -TreeServer.prototype.close = function () { - return new Promise((resolve, reject) => { - this.callback = (e) => { - if (e == null) { - return resolve(); - } - return reject(e); - }; - this.server.server.close(); - }); -}; - -TreeServer.prototype.handleRoot = function(client, root) { - if ((root == null) || (root.elements == null) || (root.elements.size < 1)) { - // ignore empty requests. - return; - } - - const node = root.getChildren()[0]; - client.request = node; - - if (node.path !== undefined) { - return this.handleQualifiedNode(client, node); - } - else if (node instanceof ember.Command) { - // Command on root element - this.handleCommand(client, this.tree, node); - return "root"; - } - else { - return this.handleNode(client, node); - } -} - -TreeServer.prototype.handleError = function(client, node) { - if (client !== undefined) { - let res = node == null ? this.tree._root.getMinimal() : node; - client.sendBERNode(res); - } -} - - -TreeServer.prototype.handleQualifiedNode = function(client, node) { - const path = node.path; - // Find this element in our tree - const element = this.tree.getElementByPath(path); - - if (element == null) { - this.emit("error", new Error(`unknown element at path ${path}`)); - return this.handleError(client); - } - - if ((node.children != null) && (node.children.length === 1) && - (node.children[0] instanceof ember.Command)) { - this.handleCommand(client, element, node.children[0]); - } - else { - if (node instanceof ember.QualifiedMatrix) { - this.handleQualifiedMatrix(client, element, node); - } - else if (node instanceof ember.QualifiedParameter) { - this.handleQualifiedParameter(client, element, node); - } - } - return path; -} - - -TreeServer.prototype.handleNode = function(client, node) { - // traverse the tree - let element = node; - let path = []; - while(element !== undefined) { - if (element.number == null) { - this.emit("error", "invalid request"); - return; - } - if (element instanceof ember.Command) { - break; - } - path.push(element.number); - - let children = element.getChildren(); - if ((! children) || (children.length === 0)) { - break; - } - element = element.children[0]; - } - let cmd = element; - - if (cmd == null) { - this.emit("error", "invalid request"); - return this.handleError(client); - } - - element = this.tree.getElementByPath(path.join(".")); - - if (element == null) { - this.emit("error", new Error(`unknown element at path ${path}`)); - return this.handleError(client); - } - - if (cmd instanceof ember.Command) { - this.handleCommand(client, element, cmd); - } - else if ((cmd instanceof ember.MatrixNode) && (cmd.connections !== undefined)) { - this.handleMatrixConnections(client, element, cmd.connections); - } - else if ((cmd instanceof ember.Parameter) && - (cmd.contents !== undefined) && (cmd.contents.value !== undefined)) { - if (this._debug) { console.log(`setValue for element at path ${path} with value ${cmd.contents.value}`); } - this.setValue(element, cmd.contents.value, client); - let res = this.getResponse(element); - client.sendBERNode(res) - this.updateSubscribers(element.getPath(), res, client); - } - else { - this.emit("error", new Error("invalid request format")); - if (this._debug) { console.log("invalid request format"); } - return this.handleError(client, element.getTreeBranch()); - } - return path; -} - -TreeServer.prototype.handleQualifiedMatrix = function(client, element, matrix) -{ - this.handleMatrixConnections(client, element, matrix.connections); -} - -TreeServer.prototype.handleQualifiedParameter = function(client, element, parameter) -{ - if (parameter.contents.value !== undefined) { - this.setValue(element, parameter.contents.value, client); - let res = this.getQualifiedResponse(element); - client.sendBERNode(res) - this.updateSubscribers(element.getPath(), res, client); - } -} - - -TreeServer.prototype.handleMatrixConnections = function(client, matrix, connections, response = true) { - var res,conResult; - var root; // ember message root - if (this._debug) { - console.log("Handling Matrix Connection"); - } - if (client != null && client.request.isQualified()) { - root = new ember.Root(); - res = new ember.QualifiedMatrix(matrix.getPath()); - //root.elements = [res]; // do not use addchild or the element will get removed from the tree. - root.addElement(res); - } - else { - res = new ember.MatrixNode(matrix.number); - root = matrix._parent.getTreeBranch(res); - } - res.connections = {}; - for(let id in connections) { - if (!connections.hasOwnProperty(id)) { - continue; - } - let connection = connections[id]; - conResult = new ember.MatrixConnection(connection.target); - let emitType; - res.connections[connection.target] = conResult; - - if (matrix.connections[connection.target].isLocked()) { - conResult.disposition = ember.MatrixDisposition.locked; - } - else if (matrix.contents.type !== ember.MatrixType.nToN && - connection.operation !== ember.MatrixOperation.disconnect && - connection.sources != null && connection.sources.length === 1) { - if (matrix.contents.type === ember.MatrixType.oneToOne) { - // if the source is being used already, disconnect it. - const targets = matrix.getSourceConnections(connection.sources[0]); - if (targets.length === 1 && targets[0] !== connection.target) { - const disconnect = new ember.MatrixConnection(targets[0]); - disconnect.setSources([]); - disconnect.disposition = ember.MatrixDisposition.modified; - res.connections[targets[0]] = disconnect; - matrix.setSources(targets[0], []); - if (response) { - this.emit("matrix-disconnect", { - target: targets[0], - sources: connection.sources, - client: client == null ? null : client.remoteAddress() - }); - } - } - } - // if the target is connected already, disconnect it - if (matrix.connections[connection.target].sources != null && - matrix.connections[connection.target].sources.length === 1) { - if (matrix.contents.type === ember.MatrixType.oneToN) { - const disconnectSource = this.getDisconnectSource(matrix, connection.target); - if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { - if (disconnectSource != null && disconnectSource != -1 && - disconnectSource != connection.sources[0]) { - connection.sources = [disconnectSource]; - } - else { - // do nothing - connection.operarion = ember.MatrixOperation.tally; - } - } - } - if (matrix.connections[connection.target].sources[0] !== connection.sources[0]) { - const source = matrix.connections[connection.target].sources[0]; - matrix.setSources(connection.target, []); - if (response) { - this.emit("matrix-disconnect", { - target: connection.target, - sources: [source], - client: client == null ? null : client.remoteAddress() - }); - } - } - else if (matrix.contents.type === ember.MatrixType.oneToOne) { - // let's change the request into a disconnect - connection.operation = ember.MatrixOperation.disconnect; - } - } - } - - if (connection.operation !== ember.MatrixOperation.disconnect && - connection.sources != null && connection.sources.length > 0 && - matrix.canConnect(connection.target,connection.sources,connection.operation)) { - // Apply changes - if ((connection.operation == null) || - (connection.operation.value == ember.MatrixOperation.absolute)) { - matrix.setSources(connection.target, connection.sources); - emitType = "matrix-change"; - } - else if (connection.operation == ember.MatrixOperation.connect) { - matrix.connectSources(connection.target, connection.sources); - emitType = "matrix-connect"; - } - conResult.disposition = ember.MatrixDisposition.modified; - } - else if (connection.operation !== ember.MatrixOperation.disconnect && - connection.sources != null && connection.sources.length === 0 && - matrix.connections[connection.target].sources != null && - matrix.connections[connection.target].sources.length > 0) { - // let's disconnect - if (response) { - this.emit("matrix-disconnect", { - target: connection.target, - sources: matrix.connections[connection.target].sources, - client: client == null ? null : client.remoteAddress() - }); - } - matrix.setSources(connection.target, []); - conResult.disposition = ember.MatrixDisposition.modified; - } - else if (connection.operation === ember.MatrixOperation.disconnect && - matrix.connections[connection.target].sources != null && - matrix.connections[connection.target].sources.length > 0) { - // Disconnect - if (matrix.contents.type === ember.MatrixType.oneToN) { - const disconnectSource = this.getDisconnectSource(matrix, connection.target); - if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { - if (disconnectSource != null && disconnectSource != -1 && - disconnectSource != connection.sources[0]) { - if (response) { - this.emit("matrix-disconnect", { - target: connection.target, - sources: matrix.connections[connection.target].sources, - client: client == null ? null : client.remoteAddress() - }); - } - matrix.setSources(connection.target, [disconnectSource]); - connection.operarion = ember.MatrixOperation.modified; - } - else { - // do nothing - connection.operarion = ember.MatrixOperation.tally; - } - } - } - else { - matrix.disconnectSources(connection.target, connection.sources); - conResult.disposition = ember.MatrixDisposition.modified; - emitType = "matrix-disconnect"; - } - } - else if (conResult.disposition !== ember.MatrixDisposition.locked){ - if (this._debug) { - console.log(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); - } - conResult.disposition = ember.MatrixDisposition.tally; - } - - // Send response or update subscribers. - conResult.sources = matrix.connections[connection.target].sources; - if (response) { - // We got a request so emit something. - this.emit(emitType, { - target: connection.target, - sources: connection.sources, - client: client == null ? null : client.remoteAddress() - }); - } - } - if (client != null) { - client.sendBERNode(root); - } - - if (conResult != null && conResult.disposition !== ember.MatrixDisposition.tally) { - if (this._debug) { - console.log("Updating subscribers for matrix change"); - } - this.updateSubscribers(matrix.getPath(), root, client); - } -} - -const validateMatrixOperation = function(matrix, target, sources) { - if (matrix == null) { - throw new Error(`matrix not found with path ${path}`); - } - if (matrix.contents == null) { - throw new Error(`invalid matrix at ${path} : no contents`); - } - if (matrix.contents.targetCount == null) { - throw new Error(`invalid matrix at ${path} : no targetCount`); - } - if ((target < 0) || (target >= matrix.contents.targetCount)) { - throw new Error(`target id ${target} out of range 0 - ${matrix.contents.targetCount}`); - } - if (sources.length == null) { - throw new Error("invalid sources format"); - } -} - -const doMatrixOperation = function(server, path, target, sources, operation) { - let matrix = server.tree.getElementByPath(path); - - validateMatrixOperation(matrix, target, sources); - - let connection = new ember.MatrixConnection(target); - connection.sources = sources; - connection.operation = operation; - server.handleMatrixConnections(undefined, matrix, [connection], false); -} - -TreeServer.prototype.matrixConnect = function(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.connect); -} - -TreeServer.prototype.matrixDisconnect = function(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.disconnect); -} - -TreeServer.prototype.matrixSet = function(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.absolute); -} - -TreeServer.prototype.handleQualifiedFunction = function(client, element, node) { - -} - - -TreeServer.prototype.handleCommand = function(client, element, cmd) { - switch(cmd.number) { - case ember.GetDirectory: - this.handleGetDirectory(client, element); - break; - case ember.Subscribe: - this.handleSubscribe(client, element); - break; - case ember.Unsubscribe: - this.handleUnSubscribe(client, element); - break; - case ember.Invoke: - this.handleInvoke(client, cmd.invocation, element); - break; - default: - this.emit("error", new Error(`invalid command ${cmd.number}`)); - break; - } -} - -TreeServer.prototype.getResponse = function(element) { - return element.getTreeBranch(undefined, function(node) { - node.update(element); - let children = element.getChildren(); - if (children != null) { - for (let i = 0; i < children.length; i++) { - node.addChild(children[i].getDuplicate()); - } - } - else if (this._debug) { - console.log("getResponse","no children"); - } - }); -} - -TreeServer.prototype.getQualifiedResponse = function(element) { - const res = new ember.Root(); - let dup; - if (element.isRoot() === false) { - dup = element.toQualified(); - } - let children = element.getChildren(); - if (children != null) { - for (let i = 0; i < children.length; i++) { - res.addChild(children[i].toQualified().getMinimalContent()); - } - } - else { - res.addChild(dup); - } - return res; -} - -TreeServer.prototype.handleInvoke = function(client, invocation, element) { - const result = new ember.InvocationResult(); - result.invocationId = invocation.id; - if (element == null || !element.isFunction()) { - result.setFailure(); - } - else { - try { - result.setResult(element.func(invocation.arguments)); - } - catch(e){ - this.emit("error", e); - result.setFailure(); - } - } - const res = new ember.Root(); - res.addResult(result); - client.sendBERNode(res); -} - -TreeServer.prototype.handleGetDirectory = function(client, element) { - if (client !== undefined) { - if ((element.isMatrix() || element.isParameter()) && - (!element.isStream())) { - // ember spec: parameter without streamIdentifier should - // report their value changes automatically. - this.subscribe(client, element); - } - else if (element.isNode()) { - const children = element.getChildren(); - if (children != null) { - for (let i = 0; i < children.length; i++) { - const child = children[i]; - if ((child.isMatrix() || child.isParameter()) && - (!child.isStream())) { - this.subscribe(client, child); - } - } - } - } - - const res = this.getQualifiedResponse(element); - if (this._debug) { - console.log("getDirectory response", res); - } - client.sendBERNode(res); - } -} - -TreeServer.prototype.handleSubscribe = function(client, element) { - if (this._debug) { - console.log("subscribe"); - } - this.subscribe(client, element); -} - -TreeServer.prototype.handleUnSubscribe = function(client, element) { - if (this._debug) { - console.log("unsubscribe"); - } - this.unsubscribe(client, element); -} - - -TreeServer.prototype.subscribe = function(client, element) { - const path = element.getPath(); - if (this.subscribers[path] == null) { - this.subscribers[path] = new Set(); - } - this.subscribers[path].add(client); -} - -TreeServer.prototype.unsubscribe = function(client, element) { - const path = element.getPath(); - if (this.subscribers[path] == null) { - return; - } - this.subscribers[path].delete(client); -} - -TreeServer.prototype.setValue = function(element, value, origin, key) { - return new Promise((resolve, reject) => { - // Change the element value if write access permitted. - if (element.contents == null) { - return resolve(); - } - if (element.isParameter()) { - if ((element.contents.access !== undefined) && - (element.contents.access.value > 1)) { - element.contents.value = value; - const res = this.getResponse(element); - this.updateSubscribers(element.getPath(),res, origin); - this.emit("value-change", element); - } - } - else if (element.isMatrix()) { - if ((key !== undefined) && (element.contents.hasOwnProperty(key))) { - element.contents[key] = value; - const res = this.getResponse(element); - this.updateSubscribers(element.getPath(),res, origin); - this.emit("value-change", element); - } - } - return resolve(); - }); -} - -TreeServer.prototype.replaceElement = function(element) { - let path = element.getPath(); - let parent = this.tree.getElementByPath(path); - if ((parent == null)||(parent._parent == null)) { - throw new Error(`Could not find element at path ${path}`); - } - parent = parent._parent; - let children = parent.getChildren(); - let newList = []; - for(let i = 0; i <= children.length; i++) { - if (children[i] && children[i].getPath() == path) { - element._parent = parent; // move it to new tree. - children[i] = element; - let res = this.getResponse(element); - this.updateSubscribers(path,res); - return; - } - } -} - -TreeServer.prototype.getDisconnectSource = function(matrix, targetID) { - if (matrix.defaultSources) { - return matrix.defaultSources[targetID].contents.value; - } - if (matrix.contents.labels == null || matrix.contents.labels.length == 0) { - return null; - } - const basePath = matrix.contents.labels[0].basePath; - const labels = this.tree.getElementByPath(basePath); - const number = labels.getNumber() + 1; - const parent = labels.getParent(); - const children = parent.getChildren(); - for(let child of children) { - if (child.getNumber() === number) { - matrix.defaultSources = child.getChildren(); - return matrix.defaultSources[targetID].contents.value; - } - } - return null; -} - -TreeServer.prototype.updateSubscribers = function(path, response, origin) { - if (this.subscribers[path] == null) { - return; - } - - for (let client of this.subscribers[path]) { - if (client === origin) { - continue; // already sent the response to origin - } - if (this.clients.has(client)) { - client.queueMessage(response); - } - else { - // clean up subscribers - client is gone - this.subscribers[path].delete(client); - } - } -} - -const parseMatrixContent = function(matrixContent, content) { - if (content.labels) { - matrixContent.labels = []; - for(let l = 0; l < content.labels.length; l++) { - if (typeof (content.labels[l]) === "object") { - matrixContent.labels.push( - new ember.Label( - content.labels[l].basePath, - content.labels[l].description - ) - ); - } - else { - // for backward compatibility... Remove in the future - matrixContent.labels.push( - new ember.Label(content.labels[l]) - ); - } - } - delete content.labels; - } - if (content.type != null) { - if (content.type == "oneToN") { - matrixContent.type = ember.MatrixType.oneToN; - } - else if (content.type == "oneToOne") { - matrixContent.type = ember.MatrixType.oneToOne; - } - else if (content.type == "nToN") { - matrixContent.type = ember.MatrixType.nToN; - matrixContent.maximumTotalConnects = content.maximumTotalConnects == null ? - Number(content.targetCount) * Number(content.sourceCount) : Number(content.maximumTotalConnects); - matrixContent.maximumConnectsPerTarget = content.maximumConnectsPerTarget == null ? - Number(content.sourceCount) : Number(content.maximumConnectsPerTarget); - } - else { - throw new Error(`Invalid matrix type ${content.type}`); - } - delete content.type; - } - if (content.mode != null) { - if (content.mode == "linear") { - matrixContent.mode = ember.MatrixMode.linear; - } - else if (content.mode == "nonLinear") { - matrixContent.mode = ember.MatrixMode.nonLinear; - } - else { - throw new Error(`Invalid matrix mode ${content.mode}`); - } - delete content.mode; - } -} - -const parseObj = function(parent, obj) { - for(let i = 0; i < obj.length; i++) { - let emberElement; - let content = obj[i]; - let number = content.number !== undefined ? content.number : i; - delete content.number; - if (content.value != null) { - emberElement = new ember.Parameter(number); - emberElement.contents = new ember.ParameterContents(content.value); - if (content.type) { - emberElement.contents.type = ember.ParameterType.get(content.type); - delete content.type; - } - else { - emberElement.contents.type = ember.ParameterType.string; - } - if (content.access) { - emberElement.contents.access = ember.ParameterAccess.get(content.access); - delete content.access; - } - else { - emberElement.contents.access = ember.ParameterAccess.read; - } - } - else if (content.func != null) { - emberElement = new ember.Function(number, content.func); - emberElement.contents = new ember.FunctionContent(); - if (content.arguments != null) { - for(let argument of content.arguments) { - emberElement.contents.arguments.push(new ember.FunctionArgument( - argument.type, - argument.value, - argument.name - )); - } - } - if (content.result != null) { - for(let argument of content.result) { - emberElement.contents.result.push(new ember.FunctionArgument( - argument.type, - argument.value, - argument.name - )); - } - } - delete content.result; - } - else if (content.targetCount != null) { - emberElement = new ember.MatrixNode(number); - emberElement.contents = new ember.MatrixContents(); - parseMatrixContent(emberElement.contents, content); - if (content.connections) { - emberElement.connections = {}; - for (let c in content.connections) { - if (! content.connections.hasOwnProperty(c)) { - continue; - } - const t = content.connections[c].target != null ? content.connections[c].target : 0; - emberElement.setSources(t, content.connections[c].sources); - } - delete content.connections; - } - else { - emberElement.connections = {}; - for (let t = 0; t < content.targetCount; t++) { - let connection = new ember.MatrixConnection(t); - emberElement.connections[t] = connection; - } - } - } - else { - emberElement = new ember.Node(number); - emberElement.contents = new ember.NodeContents(); - } - for(let id in content) { - if (emberElement.isFunction() && id === "arguments") { - // we did it already. - continue; - } - if ((id !== "children") && (content.hasOwnProperty(id))) { - emberElement.contents[id] = content[id]; - } - else { - parseObj(emberElement, content.children); - } - } - parent.addChild(emberElement); - } -} - -TreeServer.JSONtoTree = function(obj) { - let tree = new ember.Root(); - parseObj(tree, obj); - return tree; -} - - -TreeServer.prototype.toJSON = function() { - if (this.tree == null) { - return []; - } - const elements = this.tree.getChildren(); - - return elements.map(element => element.toJSON()); -}; - -module.exports = TreeServer; diff --git a/test/utils.js b/test/utils.js index 5393953..7fb6f65 100755 --- a/test/utils.js +++ b/test/utils.js @@ -1,4 +1,4 @@ -const {ParameterType, FunctionArgument} = require("../ember"); +const {ParameterType, FunctionArgument} = require("../EmberLib"); const init = function(_src,_tgt) { From a6fb9ca03db06bca5b22acb5e80769f80fcd7ffa Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Tue, 7 Jan 2020 10:57:34 +0100 Subject: [PATCH 36/64] version 2 final fixes and good to go --- EmberClient/EmberClient.js | 4 +- EmberLib/Command.js | 7 ++- EmberLib/Invocation.js | 15 +++-- EmberLib/InvocationResult.js | 6 +- EmberLib/Label.js | 1 + EmberLib/Matrix.js | 6 +- EmberLib/MatrixConnection.js | 3 + EmberLib/MatrixContents.js | 2 + EmberLib/MatrixOperation.js | 1 + EmberLib/ParameterContents.js | 9 ++- EmberLib/QualifiedElement.js | 12 +++- EmberLib/QualifiedFunction.js | 17 +++--- EmberLib/QualifiedMatrix.js | 6 +- EmberLib/QualifiedParameter.js | 2 +- EmberLib/TreeNode.js | 5 +- EmberLib/index.js | 10 +++- test/DeviceTree.test.js | 22 ++++---- test/Ember.test.js | 4 +- test/Server.test.js | 100 +++++++++++++++++---------------- 19 files changed, 129 insertions(+), 103 deletions(-) diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index e139bbe..4ab22c2 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -570,7 +570,7 @@ class EmberClient extends EventEmitter { } let matrix = null; if (node != null) { - matrix = node.elements[0]; + matrix = node.getElementByPath(requestedPath); } if (matrix != null && matrix.isMatrix() && matrix.getPath() === requestedPath) { this._clearTimeout(); // clear the timeout now. The resolve below may take a while. @@ -580,7 +580,7 @@ class EmberClient extends EventEmitter { else { if (this._debug) { console.log(`unexpected node response during matrix connect ${requestedPath}`, - JSON.stringify(matrix.toJSON(), null, 4)); + matrix == null ? null : JSON.stringify(matrix.toJSON(), null, 4)); } } } diff --git a/EmberLib/Command.js b/EmberLib/Command.js index 5d76c2b..c3247b0 100755 --- a/EmberLib/Command.js +++ b/EmberLib/Command.js @@ -2,6 +2,7 @@ const Enum = require('enum'); const {COMMAND_GETDIRECTORY, COMMAND_INVOKE} = require("./constants"); const BER = require('../ber.js'); +const Invocation = require("./Invocation"); const FieldFlags = new Enum({ sparse: -2, @@ -84,12 +85,12 @@ class Command { * @returns {Command} */ static decode(ber) { - var c = new Command(); + const c = new Command(); ber = ber.getSequence(BER.APPLICATION(2)); while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); + let tag = ber.peek(); + let seq = ber.getSequence(tag); if(tag == BER.CONTEXT(0)) { c.number = seq.readInt(); } diff --git a/EmberLib/Invocation.js b/EmberLib/Invocation.js index b7bf4f9..e12130b 100755 --- a/EmberLib/Invocation.js +++ b/EmberLib/Invocation.js @@ -1,6 +1,7 @@ "use strict"; const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); const BER = require('../ber.js'); +const FunctionArgument = require("./FunctionArgument"); let _id = 1; class Invocation { @@ -43,19 +44,17 @@ class Invocation { * @returns {Invocation} */ static decode(ber) { - const invocation = null; + let invocation = null; ber = ber.getSequence(BER.APPLICATION(22)); while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); + let tag = ber.peek(); + let seq = ber.getSequence(tag); if(tag == BER.CONTEXT(0)) { - const invocationId = seq.readInt(); - invocation = new Invocation(invocationId); + // Create the invocation with the id received otherwise we will + // increment the internal id counter. + invocation = new Invocation(seq.readInt()); } else if(tag == BER.CONTEXT(1)) { - if (invocation == null) { - throw new Error("Missing invocationID"); - } invocation.arguments = []; seq = seq.getSequence(BER.EMBER_SEQUENCE); while(seq.remain > 0) { diff --git a/EmberLib/InvocationResult.js b/EmberLib/InvocationResult.js index 6fae4b5..5e9505d 100755 --- a/EmberLib/InvocationResult.js +++ b/EmberLib/InvocationResult.js @@ -1,6 +1,8 @@ "use strict"; const BER = require('../ber.js'); +const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); +const FunctionArgument = require("./FunctionArgument"); class InvocationResult { @@ -80,8 +82,8 @@ class InvocationResult { const invocationResult = new InvocationResult(); ber = ber.getSequence(BER.APPLICATION(23)); while(ber.remain > 0) { - tag = ber.peek(); - var seq = ber.getSequence(tag); + let tag = ber.peek(); + let seq = ber.getSequence(tag); if(tag == BER.CONTEXT(0)) { // invocationId invocationResult.invocationId = seq.readInt(); } else if(tag == BER.CONTEXT(1)) { // success diff --git a/EmberLib/Label.js b/EmberLib/Label.js index d8fecde..069e84d 100755 --- a/EmberLib/Label.js +++ b/EmberLib/Label.js @@ -1,4 +1,5 @@ "use strict"; +const BER = require('../ber.js'); class Label { constructor(path, description) { diff --git a/EmberLib/Matrix.js b/EmberLib/Matrix.js index e697b74..c7b9b79 100755 --- a/EmberLib/Matrix.js +++ b/EmberLib/Matrix.js @@ -2,6 +2,10 @@ const MatrixConnection = require("./MatrixConnection"); const TreeNode = require("./TreeNode"); const BER = require('../ber.js'); +const MatrixMode = require("./MatrixMode"); +const MatrixOperation = require("./MatrixOperation"); +const MatrixType = require("./MatrixType"); +const MatrixNode = require("./MatrixNode"); class Matrix extends TreeNode { @@ -373,7 +377,7 @@ class Matrix extends TreeNode const currentSource = matrix.connections[targetID] == null || matrix.connections[targetID].sources == null ? [] : matrix.connections[targetID].sources; if (currentSource.length > 0) { - MatrixNode.disconnectSources(matrix, targetID, currentSource) + this.disconnectSources(matrix, targetID, currentSource) } Matrix.connectSources(matrix, targetID, sources); } diff --git a/EmberLib/MatrixConnection.js b/EmberLib/MatrixConnection.js index 5bd90c1..4e78e45 100755 --- a/EmberLib/MatrixConnection.js +++ b/EmberLib/MatrixConnection.js @@ -1,4 +1,7 @@ "use strict"; +const BER = require('../ber.js'); +const MatrixOperation = require("./MatrixOperation"); +const MatrixDisposition = require("./MatrixDisposition"); class MatrixConnection { /** diff --git a/EmberLib/MatrixContents.js b/EmberLib/MatrixContents.js index 7c11647..1e8df10 100755 --- a/EmberLib/MatrixContents.js +++ b/EmberLib/MatrixContents.js @@ -2,6 +2,8 @@ const MatrixType = require("./MatrixType"); const MatrixMode = require("./MatrixMode"); +const BER = require('../ber.js'); +const Label = require("./Label"); class MatrixContents { constructor(type = MatrixType.oneToN, mode = MatrixMode.linear) { diff --git a/EmberLib/MatrixOperation.js b/EmberLib/MatrixOperation.js index c3a4879..f211a0b 100755 --- a/EmberLib/MatrixOperation.js +++ b/EmberLib/MatrixOperation.js @@ -7,6 +7,7 @@ const Enum = require('enum'); // disconnect (2) -- nToN only. sources contains sources to remove from // connection // } + const MatrixOperation = new Enum({ absolute: 0, connect: 1, diff --git a/EmberLib/ParameterContents.js b/EmberLib/ParameterContents.js index f068e46..b213299 100755 --- a/EmberLib/ParameterContents.js +++ b/EmberLib/ParameterContents.js @@ -2,6 +2,7 @@ const {ParameterType} = require("./ParameterType"); const ParameterAccess = require("./ParameterAccess"); +const StringIntegerCollection = require("./StringIntegerCollection"); const BER = require('../ber.js'); @@ -41,10 +42,8 @@ class ParameterContents { ber.writeIfDefinedEnum(this.type, ParameterType, ber.writeInt, 13); ber.writeIfDefined(this.streamIdentifier, ber.writeInt, 14); - if(this.emumMap !== undefined) { - ber.startSequence(BER.CONTEXT(15)); - StringIntegerCollection.encode(ber, this.enumMap); - ber.endSequence(); + if(this.stringIntegerCollection !== undefined) { + this.stringIntegerCollection.encode(ber); } if(this.streamDescriptor !== undefined) { @@ -101,7 +100,7 @@ class ParameterContents { } else if(tag == BER.CONTEXT(14)) { pc.streamIdentifier = seq.readInt(); } else if(tag == BER.CONTEXT(15)) { - pc.enumMap = StringIntegerCollection.decode(seq); + pc.stringIntegerCollection = StringIntegerCollection.decode(seq); } else if(tag == BER.CONTEXT(16)) { pc.streamDescriptor = StreamDescription.decode(seq); } else if(tag == BER.CONTEXT(17)) { diff --git a/EmberLib/QualifiedElement.js b/EmberLib/QualifiedElement.js index b0ef471..59ab929 100755 --- a/EmberLib/QualifiedElement.js +++ b/EmberLib/QualifiedElement.js @@ -46,15 +46,21 @@ class QualifiedElement extends TreeNode { /** * - * @param {number} cmd + * @param {number} cmd + * @param {string} key + * @param {string} value * @returns {TreeNode} */ - getCommand(cmd) { + getCommand(cmd, key, value) { const r = this.getNewTree(); const qn = new this.constructor(); qn.path = this.getPath(); r.addElement(qn); - qn.addChild(new Command(cmd)); + const command = new Command(cmd); + if (key != null) { + command[key] = value; + } + qn.addChild(command); return r; } diff --git a/EmberLib/QualifiedFunction.js b/EmberLib/QualifiedFunction.js index 04dbc42..37cab00 100755 --- a/EmberLib/QualifiedFunction.js +++ b/EmberLib/QualifiedFunction.js @@ -2,9 +2,9 @@ const QualifiedElement = require("./QualifiedElement"); const FunctionContent = require("./FunctionContent"); -const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); +const {COMMAND_GETDIRECTORY, COMMAND_INVOKE} = require("./constants"); const BER = require('../ber.js'); -const Command = require("./Command"); +const Invocation = require("./Invocation"); class QualifiedFunction extends QualifiedElement { /** @@ -37,15 +37,12 @@ class QualifiedFunction extends QualifiedElement { * * @param {*} params */ - invoke(params) { - if (this.path == null) { - throw new Error("Invalid path"); - } - var QualifiedFunctionNode = this.getCommand(COMMAND_INVOKE); - var invocation = new Invocation() + invoke(params) { + const invocation = new Invocation() invocation.arguments = params; - QualifiedFunctionNode.getElementByPath(this.getPath()).getNumber(COMMAND_INVOKE).invocation = invocation - return QualifiedFunctionNode; + const qualifiedFunctionNode = this.getCommand(COMMAND_INVOKE, "invocation", invocation); + //qualifiedFunctionNode.getElementByNumber(this.getNumber()).getElementByNumber(COMMAND_INVOKE).invocation = invocation + return qualifiedFunctionNode; } diff --git a/EmberLib/QualifiedMatrix.js b/EmberLib/QualifiedMatrix.js index 86314f6..538e76c 100755 --- a/EmberLib/QualifiedMatrix.js +++ b/EmberLib/QualifiedMatrix.js @@ -4,6 +4,8 @@ const Matrix = require("./Matrix"); const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); const BER = require('../ber.js'); const Command = require("./Command"); +const MatrixContents = require("./MatrixContents"); +const MatrixConnection = require("./MatrixConnection"); class QualifiedMatrix extends Matrix { /** @@ -24,7 +26,7 @@ class QualifiedMatrix extends Matrix { * @returns {Root} */ connect(connections) { - const r = new Root(); + const r = this.getNewTree(); const qn = new QualifiedMatrix(); qn.path = this.path; r.addElement(qn); @@ -63,7 +65,7 @@ class QualifiedMatrix extends Matrix { * @returns {TreeNode} */ getCommand(cmd) { - const r = new TreeNode(); + const r = this.getNewTree(); const qn = new QualifiedMatrix(); qn.path = this.getPath(); r.addElement(qn); diff --git a/EmberLib/QualifiedParameter.js b/EmberLib/QualifiedParameter.js index 2e3e3d0..85a748f 100755 --- a/EmberLib/QualifiedParameter.js +++ b/EmberLib/QualifiedParameter.js @@ -46,7 +46,7 @@ class QualifiedParameter extends QualifiedElement { * @returns {TreeNode} */ setValue(value) { - let r = new TreeNode(); + let r = this.getNewTree(); let qp = new QualifiedParameter(this.path); r.addElement(qp); qp.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); diff --git a/EmberLib/TreeNode.js b/EmberLib/TreeNode.js index d09cdf4..c07849a 100755 --- a/EmberLib/TreeNode.js +++ b/EmberLib/TreeNode.js @@ -313,7 +313,10 @@ class TreeNode { node = node.getElementByNumber(number); if (node == null) { return null; - } + } + if (node.isQualified() && node.path == path) { + return node; + } myPathArray.push(number); } return node; diff --git a/EmberLib/index.js b/EmberLib/index.js index 10ebba6..1767ac5 100755 --- a/EmberLib/index.js +++ b/EmberLib/index.js @@ -15,7 +15,8 @@ const MatrixMode = require("./MatrixMode"); const MatrixType = require("./MatrixType"); const MatrixContents = require("./MatrixContents"); const MatrixConnection = require("./MatrixConnection"); -const Matrixoperation = require("./MatrixOperation"); +const MatrixOperation = require("./MatrixOperation"); +const MatrixDisposition = require("./MatrixDisposition"); const Node = require("./Node"); const NodeContents = require("./NodeContents"); const Parameter = require("./Parameter"); @@ -26,6 +27,7 @@ const QualifiedFunction = require("./QualifiedFunction"); const QualifiedMatrix = require("./QualifiedMatrix"); const QualifiedNode = require("./QualifiedNode"); const QualifiedParameter = require("./QualifiedParameter"); +const StringIntegerCollection = require("./StringIntegerCollection"); const rootDecode = function(ber) { const r = new TreeNode(); @@ -108,7 +110,7 @@ TreeNode.decode = childDecode; const DecodeBuffer = function (packet) { const ber = new BER.Reader(packet); - return TreeNode.decode(ber); + return rootDecode(ber); }; module.exports = { @@ -128,7 +130,8 @@ module.exports = { MatrixType, MatrixContents, MatrixConnection, - Matrixoperation, + MatrixDisposition, + MatrixOperation, Node, NodeContents, Parameter, @@ -139,6 +142,7 @@ module.exports = { QualifiedMatrix, QualifiedNode, QualifiedParameter, + StringIntegerCollection, Subscribe,COMMAND_SUBSCRIBE, Unsubscribe,COMMAND_UNSUBSCRIBE, GetDirectory,COMMAND_GETDIRECTORY, diff --git a/test/DeviceTree.test.js b/test/DeviceTree.test.js index 20c3995..c072418 100755 --- a/test/DeviceTree.test.js +++ b/test/DeviceTree.test.js @@ -1,16 +1,16 @@ const fs = require("fs"); const sinon = require("sinon"); -const Decoder = require('../').Decoder; -const DeviceTree = require("../").DeviceTree; -const TreeServer = require("../").TreeServer; +const Decoder = require('../EmberLib').DecodeBuffer; +const EmberClient = require("../EmberClient"); +const EmberServer = require("../EmberServer"); const LOCALHOST = "127.0.0.1"; const UNKNOWN_HOST = "192.168.99.99"; const PORT = 9008; -describe("DeviceTree", () => { +describe("EmberClient", () => { describe("With server", () => { - /** @type {TreeServer} */ + /** @type {EmberServer} */ let server; beforeAll(() => { return Promise.resolve() @@ -23,7 +23,7 @@ describe("DeviceTree", () => { }); })) .then(root => { - server = new TreeServer(LOCALHOST, PORT, root); + server = new EmberServer(LOCALHOST, PORT, root); return server.listen(); }); }); @@ -31,7 +31,7 @@ describe("DeviceTree", () => { it("should gracefully connect and disconnect", () => { return Promise.resolve() .then(() => { - let tree = new DeviceTree(LOCALHOST, PORT); + let tree = new EmberClient(LOCALHOST, PORT); return Promise.resolve() .then(() => tree.connect()) .then(() => tree.getDirectory()) @@ -45,7 +45,7 @@ describe("DeviceTree", () => { it("should not disconnect after 5 seconds of inactivity", () => { return Promise.resolve() .then(() => { - let tree = new DeviceTree(LOCALHOST, PORT); + let tree = new EmberClient(LOCALHOST, PORT); tree.on("error", error => { throw error; @@ -59,7 +59,7 @@ describe("DeviceTree", () => { }, 7000); it("timeout should be taken into account when connecting to unknown host", () => { - let tree = new DeviceTree(UNKNOWN_HOST, PORT); + let tree = new EmberClient(UNKNOWN_HOST, PORT); tree.on("error", () => { }); const expectedTimeoutInSec = 2; @@ -78,11 +78,11 @@ describe("DeviceTree", () => { it("should gracefully connect and getDirectory", () => { - let tree = new DeviceTree(LOCALHOST, PORT); + let tree = new EmberClient(LOCALHOST, PORT); tree.on("error", e => { console.log(e); }) - let stub = sinon.stub(tree.client, "sendBER"); + let stub = sinon.stub(tree._client, "sendBER"); tree._debug = true; server._debug = true; stub.onFirstCall().returns(); diff --git a/test/Ember.test.js b/test/Ember.test.js index abe772e..f9ea636 100755 --- a/test/Ember.test.js +++ b/test/Ember.test.js @@ -1,8 +1,8 @@ const expect = require("expect"); -const { S101Client } = require("../index"); +const { S101Client } = require("../EmberSocket"); const s101Buffer = Buffer.from("fe000e0001c001021f026082008d6b820089a0176a15a0050d03010201a10c310aa0080c066c6162656c73a01b6a19a0050d03010202a110310ea00c0c0a706172616d6574657273a051714fa0050d03010203a1463144a0080c066d6174726978a403020104a503020104aa183016a0147212a0050d03010201a1090c075072696d617279a203020102a303020101a8050d03010202a903020101f24cff", "hex"); const errorBuffer = Buffer.from("76fe000e0001c001021f026082008d6b820089a0176a15a0050d03010201a10c310aa0080c066c6162656c73a01b6a19a0050d03010202a110310ea00c0c0a706172616d6574657273a051714fa0050d03010203a1463144a0080c066d6174726978a403020104a503020104aa183016a0147212a0050d03010201a1090c075072696d617279a203020102a303020101a8050d03010202a903020101f24cff", "hex"); -const ember = require("../ember"); +const ember = require("../EmberLib"); const BER = require('../ber.js'); const errors = require('../errors.js'); diff --git a/test/Server.test.js b/test/Server.test.js index 898ef55..4c9ac92 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -1,8 +1,9 @@ const expect = require("expect"); -const TreeServer = require("../server"); -const DeviceTree = require("../").DeviceTree; -const ember = require("../ember"); +const EmberServer = require("../EmberServer"); +const EmberClient = require("../EmberClient"); +const ember = require("../EmberLib"); const {jsonRoot} = require("./utils"); +const MatrixHandlers = require("../EmberServer/MatrixHandlers"); const LOCALHOST = "127.0.0.1"; const PORT = 9009; @@ -20,13 +21,13 @@ describe("server", function() { jsonTree = jsonRoot(); }); it("should generate an ember tree from json", function() { - const root = TreeServer.JSONtoTree(jsonTree); + const root = EmberServer.JSONtoTree(jsonTree); expect(root).toBeDefined(); expect(root.elements).toBeDefined(); - expect(root.elements.length).toBe(1); - console.log("root", root.elements[0].contents); - expect(root.elements[0].contents.identifier).toBe("scoreMaster"); - expect(root.elements[0].children.length).toBe(jsonTree[0].children.length); + expect(root.elements.size).toBe(1); + console.log("root", root.getElementByNumber(0).contents); + expect(root.getElementByNumber(0).contents.identifier).toBe("scoreMaster"); + expect(root.getElementByNumber(0).elements.size).toBe(jsonTree[0].children.length); }); }); @@ -34,8 +35,8 @@ describe("server", function() { let server,client; beforeAll(function() { jsonTree = jsonRoot(); - const root = TreeServer.JSONtoTree(jsonTree); - server = new TreeServer(LOCALHOST, PORT, root); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); server.on("error", e => { console.log(e); }); @@ -51,7 +52,7 @@ describe("server", function() { return server.close(); }); it("should receive and decode the full tree", function () { - client = new DeviceTree(LOCALHOST, PORT); + client = new EmberClient(LOCALHOST, PORT); //client._debug = true; return Promise.resolve() .then(() => client.connect()) @@ -62,53 +63,53 @@ describe("server", function() { .then(() => { expect(client.root).toBeDefined(); expect(client.root.elements).toBeDefined(); - expect(client.root.elements.length).toBe(1); - expect(client.root.elements[0].contents.identifier).toBe("scoreMaster"); - return client.getDirectory(client.root.elements[0]); + expect(client.root.elements.size).toBe(1); + expect(client.root.getElementByNumber(0).contents.identifier).toBe("scoreMaster"); + return client.getDirectory(client.root.getElementByNumber(0)); }) .then(() => { - expect(client.root.elements[0].children.length).toBe(jsonTree[0].children.length); - return client.getDirectory(client.root.elements[0].children[0]); + expect(client.root.getElementByNumber(0).elements.size).toBe(jsonTree[0].children.length); + return client.getDirectory(client.root.getElementByNumber(0).getElementByNumber(0)); }) .then(() => { - expect(client.root.elements[0].children[0].children.length).toBe(4); - expect(client.root.elements[0].children[0].children[3].contents.identifier).toBe("author"); - // Issue #33 TreeServer.handleGetDirectory does not subscribe to child parameters + expect(client.root.getElementByNumber(0).getElementByNumber(0).elements.size).toBe(4); + expect(client.root.getElementByNumber(0).getElementByNumber(0).getElementByNumber(3).contents.identifier).toBe("author"); + // Issue #33 EmberServer.handleGetDirectory does not subscribe to child parameters expect(server.subscribers["0.0.0"]).toBeDefined(); // Keepalive return client.disconnect(); }); }); it("should be able to modify a parameter", () => { - client = new DeviceTree(LOCALHOST, PORT); + client = new EmberClient(LOCALHOST, PORT); //client._debug = true; return Promise.resolve() .then(() => client.connect()) .then(() => { return client.getDirectory(); }) - .then(() => client.expand(client.root.elements[0])) + .then(() => client.expand(client.root.getElementByNumber(0))) .then(() => { - expect(server.tree.elements[0].children[0].children[1].contents.value).not.toBe("gdnet"); - return client.setValue(client.root.elements[0].children[0].children[1], "gdnet"); + expect(server.tree.getElementByNumber(0).getElementByNumber(0).getElementByNumber(1).contents.value).not.toBe("gdnet"); + return client.setValue(client.root.getElementByNumber(0).getElementByNumber(0).getElementByNumber(1), "gdnet"); }) .then(() => { - expect(server.tree.elements[0].children[0].children[1].contents.value).toBe("gdnet"); + expect(server.tree.getElementByNumber(0).getElementByNumber(0).getElementByNumber(1).contents.value).toBe("gdnet"); return client.disconnect(); }); }); it("should be able to call a function with parameters", () => { - client = new DeviceTree(LOCALHOST, PORT); + client = new EmberClient(LOCALHOST, PORT); //client._debug = true; return Promise.resolve() .then(() => client.connect()) .then(() => { return client.getDirectory(); }) - .then(() => client.expand(client.root.elements[0])) + .then(() => client.expand(client.root.getElementByNumber(0))) .then(() => { - const func = client.root.elements[0].children[2]; + const func = client.root.getElementByNumber(0).getElementByNumber(2); return client.invokeFunction(func, [ new ember.FunctionArgument(ember.ParameterType.integer, 1), new ember.FunctionArgument(ember.ParameterType.integer, 7) @@ -126,7 +127,7 @@ describe("server", function() { it("should be able to get child with tree.getNodeByPath", function() { //server._debug = true; - client = new DeviceTree(LOCALHOST, PORT); + client = new EmberClient(LOCALHOST, PORT); //client._debug = true; //client._debug = true; return Promise.resolve() @@ -147,7 +148,7 @@ describe("server", function() { }); it("should throw an error if getNodeByPath for unknown path", function() { //server._debug = true; - client = new DeviceTree(LOCALHOST, PORT); + client = new EmberClient(LOCALHOST, PORT); return Promise.resolve() .then(() => client.connect()) .then(() => { @@ -166,7 +167,7 @@ describe("server", function() { }); }); it("should be able to make a matrix connection", () => { - client = new DeviceTree(LOCALHOST, PORT); + client = new EmberClient(LOCALHOST, PORT); //client._debug = true; return Promise.resolve() .then(() => client.connect()) @@ -195,8 +196,8 @@ describe("server", function() { let server; beforeEach(function() { jsonTree = jsonRoot(); - const root = TreeServer.JSONtoTree(jsonTree); - server = new TreeServer(LOCALHOST, PORT, root); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); }); it("should verify if connection allowed in 1-to-N", function() { let disconnectCount = 0; @@ -204,7 +205,7 @@ describe("server", function() { disconnectCount++; } server.on("matrix-disconnect", handleDisconnect.bind(this)); - const matrix = server.tree.elements[0].children[1].children[0]; + const matrix = server.tree.getElementByNumber(0).getElementByNumber(1).getElementByNumber(0); let connection = new ember.MatrixConnection(0); connection.setSources([1]); connection.operation = ember.MatrixOperation.connect; @@ -217,14 +218,15 @@ describe("server", function() { res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); // We can't connect. But server will disconnect existing source and connect new one. - server.handleMatrixConnections(null, matrix, {0: connection}); + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); expect(matrix.connections[0].sources[0]).toBe(1); expect(disconnectCount).toBe(1); // But if connecting same source and dest this is a disconnect. But not possible in 1toN. // instead connect with defaultSource or do nothing - server.getDisconnectSource(matrix, 0); + const matrixHandlers = new MatrixHandlers(server); + matrixHandlers.getDisconnectSource(matrix, 0); matrix.defaultSources[0].contents.value = 222; - server.handleMatrixConnections(null, matrix, {0: connection}); + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); expect(disconnectCount).toBe(2); expect(matrix.connections[0].sources[0]).toBe(222); matrix.setSources(0, [0]); @@ -235,7 +237,7 @@ describe("server", function() { expect(res).toBeTruthy(); }); it("should verify if connection allowed in 1-to-1", function() { - const matrix = server.tree.elements[0].children[1].children[0]; + const matrix = server.tree.getElementByNumber(0).getElementByNumber(1).getElementByNumber(0); let disconnectCount = 0; const handleDisconnect = info => { disconnectCount++; @@ -251,11 +253,11 @@ describe("server", function() { res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeFalsy(); // We can't connect but in 1-on-1 server should disconnect existing source and connect new one. - server.handleMatrixConnections(null, matrix, {0: connection}); + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); expect(matrix.connections[0].sources[0]).toBe(1); expect(disconnectCount).toBe(1); // But if connecting same source and dest. just disconnect and do not reconnect. - server.handleMatrixConnections(null, matrix, {0: connection}); + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); expect(disconnectCount).toBe(2); connection.operation = ember.MatrixOperation.absolute; res = matrix.canConnect(connection.target,connection.sources,connection.operation); @@ -267,7 +269,7 @@ describe("server", function() { server.off("matrix-disconnect", handleDisconnect); }); it("should disconnect if trying to connect same source and target in 1-to-1", function() { - const matrix = server.tree.elements[0].children[1].children[0]; + const matrix = server.tree.getElementByNumber(0).getElementByNumber(1).getElementByNumber(0); let disconnectCount = 0; const handleDisconnect = info => { disconnectCount++; @@ -278,12 +280,12 @@ describe("server", function() { const connection = new ember.MatrixConnection(0); connection.setSources([1]); connection.operation = ember.MatrixOperation.connect; - server.handleMatrixConnections(null, matrix, {0: connection}); + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); expect(matrix.connections[0].sources.length).toBe(0); expect(disconnectCount).toBe(1); }); it("should be able to lock a connection", function() { - const matrix = server.tree.elements[0].children[1].children[0]; + const matrix = server.tree.getElementByNumber(0).getElementByNumber(1).getElementByNumber(0); let disconnectCount = 0; const handleDisconnect = info => { disconnectCount++; @@ -295,13 +297,13 @@ describe("server", function() { const connection = new ember.MatrixConnection(0); connection.setSources([0]); connection.operation = ember.MatrixOperation.connect; - server.handleMatrixConnections(null, matrix, {0: connection}); + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); expect(matrix.connections[0].sources.length).toBe(1); expect(matrix.connections[0].sources[0]).toBe(1); expect(disconnectCount).toBe(0); }); it("should verify if connection allowed in N-to-N", function() { - const matrix = server.tree.elements[0].children[1].children[0]; + const matrix = server.tree.getElementByNumber(0).getElementByNumber(1).getElementByNumber(0); matrix.contents.type = ember.MatrixType.nToN; matrix.contents.maximumTotalConnects = 2; matrix.setSources(0, [0,1]); @@ -362,7 +364,7 @@ describe("server", function() { //server._debug = true; return server.listen() .then(() => { - client = new DeviceTree(LOCALHOST, PORT); + client = new EmberClient(LOCALHOST, PORT); return Promise.resolve() }) .then(() => client.connect()) @@ -391,8 +393,8 @@ describe("server", function() { let server; beforeAll(function() { jsonTree = jsonRoot(); - const root = TreeServer.JSONtoTree(jsonTree); - server = new TreeServer(LOCALHOST, PORT, root); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); server.on("error", e => { console.log(e); }); @@ -405,13 +407,13 @@ describe("server", function() { return server.close(); }); it("should not auto subscribe stream parameter", function() { - const parameter = server.tree.elements[0].children[0].children[2]; + const parameter = server.tree.getElementByNumber(0).getElementByNumber(0).getElementByNumber(2); console.log(parameter); expect(parameter.isStream()).toBeTruthy(); expect(server.subscribers["0.0.2"]).not.toBeDefined(); }); it("should be able subscribe to parameter changes", function() { - const client = new DeviceTree(LOCALHOST, PORT); + const client = new EmberClient(LOCALHOST, PORT); const cb = () => { return "updated"; } From 080c7087f368679901482824bbc1a746c5611d9f Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Tue, 7 Jan 2020 14:32:19 +0100 Subject: [PATCH 37/64] Fixing missing EmberLib file. Added server events --- EmberClient/EmberClient.js | 19 ++++- EmberLib/StringIntegerCollection.js | 72 ++++++++++++++++ EmberLib/constants.js | 10 ++- EmberLib/index.js | 3 +- EmberServer/ElementHandlers.js | 27 +++--- EmberServer/EmberServer.js | 15 ++-- EmberServer/MatrixHandlers.js | 1 + README.md | 19 +++-- package.json | 2 +- test/Server.test.js | 128 ++++++++++++++++++++++------ 10 files changed, 241 insertions(+), 55 deletions(-) create mode 100755 EmberLib/StringIntegerCollection.js diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index 4ab22c2..ead404f 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -388,7 +388,22 @@ class EmberClient extends EventEmitter { } /** - * + * @deprecated + * @param {string} path ie: "path/to/destination" + * @param {function} callback=null + * @returns {Promise} + */ + getElementByPath(path, callback=null) { + if (path.indexOf("/") >= 0) { + return this.getNodeByPath(path, callback); + } + else { + return this.getNodeByPathnum(path, callback); + } + } + + /** + * @deprecated * @param {string} path ie: "path/to/destination" * @param {function} callback=null * @returns {Promise} @@ -432,7 +447,7 @@ class EmberClient extends EventEmitter { } /** - * + * @deprecated * @param {string|number[]} path ie: 1.0.2 * @param {function} callback=null * @returns {Promise} diff --git a/EmberLib/StringIntegerCollection.js b/EmberLib/StringIntegerCollection.js new file mode 100755 index 0000000..5bd00ea --- /dev/null +++ b/EmberLib/StringIntegerCollection.js @@ -0,0 +1,72 @@ +"use strict"; +const Element = require("./Element"); +const BER = require('../ber.js'); + +class StringIntegerCollection extends Element { + constructor() { + super(); + this._seqID = BER.APPLICATION(8); + this._collection = new Map(); + } + + addEntry(key, value) { + this._collection.set(key, value); + } + + get(key) { + return this._collection.get(key); + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.CONTEXT(15)); + ber.startSequence(BER.APPLICATION(8)); + ber.startSequence(BER.CONTEXT(0)); + for(let [key,value] of this._collection) { + ber.startSequence(BER.APPLICATION(7)); + ber.startSequence(BER.CONTEXT(0)); + ber.writeString(key, BER.EMBER_STRING); + ber.endSequence(); + ber.startSequence(BER.CONTEXT(1)); + ber.writeInt(value); + ber.endSequence(); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); + ber.endSequence(); + } + + /** + * + * @param {BER} ber + */ + static decode(ber) { + const sc = new StringIntegerCollection(); + ber = ber.getSequence(BER.APPLICATION(8)); + while(ber.remain > 0) { + var seq = ber.getSequence(BER.CONTEXT(0)); + seq = seq.getSequence(BER.APPLICATION(7)); + var entryString, entryInteger; + while(seq.remain > 0) { + var tag = seq.peek(); + var dataSeq = seq.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + entryString = dataSeq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(1)) { + entryInteger = dataSeq.readInt(); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + + sc.addEntry(entryString,entryInteger); + } + return sc; + } +} + +module.exports = StringIntegerCollection; \ No newline at end of file diff --git a/EmberLib/constants.js b/EmberLib/constants.js index 06a63e8..e49d071 100755 --- a/EmberLib/constants.js +++ b/EmberLib/constants.js @@ -3,6 +3,13 @@ const COMMAND_UNSUBSCRIBE = 31; const COMMAND_GETDIRECTORY = 32; const COMMAND_INVOKE = 33; +const COMMAND_STRINGS = { + [COMMAND_SUBSCRIBE]: "subscribe", + [COMMAND_UNSUBSCRIBE]: "unsubscribe", + [COMMAND_GETDIRECTORY]: "getdirectory", + [COMMAND_INVOKE]: "invoke" +}; + module.exports = { COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE, @@ -11,5 +18,6 @@ module.exports = { Subscribe: COMMAND_SUBSCRIBE, Unsubscribe: COMMAND_UNSUBSCRIBE, GetDirectory: COMMAND_GETDIRECTORY, - Invoke: COMMAND_INVOKE + Invoke: COMMAND_INVOKE, + COMMAND_STRINGS }; \ No newline at end of file diff --git a/EmberLib/index.js b/EmberLib/index.js index 1767ac5..d3a9695 100755 --- a/EmberLib/index.js +++ b/EmberLib/index.js @@ -1,5 +1,5 @@ const {Subscribe,COMMAND_SUBSCRIBE,Unsubscribe,COMMAND_UNSUBSCRIBE, - GetDirectory,COMMAND_GETDIRECTORY,Invoke,COMMAND_INVOKE} = require("./constants"); + GetDirectory,COMMAND_GETDIRECTORY,Invoke,COMMAND_INVOKE, COMMAND_STRINGS} = require("./constants"); const BER = require('../ber.js'); const errors = require("../errors"); const TreeNode = require("./TreeNode"); @@ -115,6 +115,7 @@ const DecodeBuffer = function (packet) { module.exports = { Command, + COMMAND_STRINGS, childDecode: childDecode, rootDecode: rootDecode, DecodeBuffer, diff --git a/EmberServer/ElementHandlers.js b/EmberServer/ElementHandlers.js index c30f888..8cbfcce 100755 --- a/EmberServer/ElementHandlers.js +++ b/EmberServer/ElementHandlers.js @@ -1,6 +1,6 @@ "use strict"; const QualifiedHandlers = require("./QualifiedHandlers"); -const ember = require('../EmberLib'); +const EmberLib = require('../EmberLib'); class ElementHandlers extends QualifiedHandlers{ /** @@ -17,23 +17,30 @@ class ElementHandlers extends QualifiedHandlers{ * @param {Command} cmd */ handleCommand(client, element, cmd) { + switch(cmd.number) { - case ember.COMMAND_GETDIRECTORY: + case EmberLib.COMMAND_GETDIRECTORY: this.handleGetDirectory(client, element); break; - case ember.COMMAND_SUBSCRIBE: + case EmberLib.COMMAND_SUBSCRIBE: this.handleSubscribe(client, element); break; - case ember.COMMAND_UNSUBSCRIBE: + case EmberLib.COMMAND_UNSUBSCRIBE: this.handleUnSubscribe(client, element); break; - case ember.COMMAND_INVOKE: + case EmberLib.COMMAND_INVOKE: this.handleInvoke(client, cmd.invocation, element); break; default: this.server.emit("error", new Error(`invalid command ${cmd.number}`)); - break; + return; + } + let identifier = "root" + if (!element.isRoot()) { + const node = this.server.tree.getElementByPath(element.getPath()); + identifier = node == null || node.contents == null || node.contents.identifier == null ? "unknown" : node.contents.identifier; } + this.server.emit("event", `${EmberLib.COMMAND_STRINGS[cmd.number]} to ${identifier}(path: ${element.getPath()})`); } /** @@ -77,7 +84,7 @@ class ElementHandlers extends QualifiedHandlers{ * @param {TreeNode} element */ handleInvoke(client, invocation, element) { - const result = new ember.InvocationResult(); + const result = new EmberLib.InvocationResult(); result.invocationId = invocation.id; if (element == null || !element.isFunction()) { result.setFailure(); @@ -91,7 +98,7 @@ class ElementHandlers extends QualifiedHandlers{ result.setFailure(); } } - const res = new ember.Root(); + const res = new EmberLib.Root(); res.addResult(result); client.sendBERNode(res); } @@ -145,8 +152,8 @@ class ElementHandlers extends QualifiedHandlers{ else if ((cmd.isParameter()) && (cmd.contents !== undefined) && (cmd.contents.value !== undefined)) { if (this.server._debug) { console.log(`setValue for element at path ${path} with value ${cmd.contents.value}`); } - this.setValue(element, cmd.contents.value, client); - let res = this.server.getResponse(element); + this.server.setValue(element, cmd.contents.value, client); + const res = this.server.getResponse(element); client.sendBERNode(res) this.server.updateSubscribers(element.getPath(), res, client); } diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js index 5407b46..6b248a4 100755 --- a/EmberServer/EmberServer.js +++ b/EmberServer/EmberServer.js @@ -247,22 +247,21 @@ class TreeServer extends EventEmitter{ if (element.contents == null) { return resolve(); } - if (element.isParameter()) { - if ((element.contents.access !== undefined) && + if (element.isParameter() || element.isMatrix()) { + if (element.isParameter() && + (element.contents.access !== undefined) && (element.contents.access.value > 1)) { element.contents.value = value; const res = this.getResponse(element); - this.updateSubscribers(element.getPath(),res, origin); - this.emit("value-change", element); + this.updateSubscribers(element.getPath(),res, origin); } - } - else if (element.isMatrix()) { - if ((key !== undefined) && (element.contents.hasOwnProperty(key))) { + else if ((key !== undefined) && (element.contents.hasOwnProperty(key))) { element.contents[key] = value; const res = this.getResponse(element); this.updateSubscribers(element.getPath(),res, origin); - this.emit("value-change", element); } + this.emit("value-change", element); + this.emit("event", `set value for ${element.contents.identifier}(${element.getPath()})` ); } return resolve(); }); diff --git a/EmberServer/MatrixHandlers.js b/EmberServer/MatrixHandlers.js index 58e7391..28dc3fd 100755 --- a/EmberServer/MatrixHandlers.js +++ b/EmberServer/MatrixHandlers.js @@ -65,6 +65,7 @@ class MatrixHandlers { continue; } let connection = connections[id]; + this.server.emit("event", `Matrix connection to ${matrix.contents.identifier}(path: ${matrix.getPath()}) target ${id} connections: ${connection.sources.toString()}`); conResult = new ember.MatrixConnection(connection.target); let emitType; res.connections[connection.target] = conResult; diff --git a/README.md b/README.md index 332997f..39c937b 100755 --- a/README.md +++ b/README.md @@ -31,12 +31,12 @@ client.connect() // Get Root info .then(() => client.getDirectory()) // Get a Specific Node - .then(() => client.getNodeByPathnum("0.0.2")) + .then(() => client.getElementByPath("0.0.2")) .then(node => { console.log(node); }) // Get a node by its path identifiers - .then(() => client.getNodeByPath("path/to/node")) + .then(() => client.getElementByPath("path/to/node")) .then(node => { console.log(node); }) @@ -55,23 +55,23 @@ const client = new EmberClient(HOST, PORT); client.connect()) .then(() => client.getDirectory()) .then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) - .then(() => client.getNodeByPath("scoreMaster/router/labels/group 1")) + .then(() => client.getElementByPath("scoreMaster/router/labels/group 1")) .then(node => { // For streams, use subscribe return client.subscribe(node, update => { console.log(udpate); }); }) - .then(() => client.getNodeByPathnum("0.2")) + .then(() => client.getElementByPath("0.2")) .then(node => { // For non-streams a getDirectory will automatically subscribe for update return client.getDirectory(node, update => { console.log(udpate); }); }) - // You can also provide a callback to the getNodeNyPath + // You can also provide a callback to the getElementByPath // Be carefull that subscription will be done for all elements in the path - .then(() => client.getNodeByPathnum("0.3", update => {console.log(update);})) + .then(() => client.getElementByPath("0.3", update => {console.log(update);})) ; ``` @@ -102,14 +102,14 @@ const {EmberClient, EmberLib} = require('node-emberplus'); const client = new EmberClient(HOST, PORT); client.connect() .then(() => client.getDirectory()) - .then(() => client.getNodeByPathnum("0.1.0")) + .then(() => client.getElementByPath("0.1.0")) .then(matrix => { console.log("Connecting source 1 to target 0); return client.matrixConnect(matrix, 0, [1]); }) .then(() => client.matrixDisconnect(matrix, 0, [1])) .then(() => client.matrixSetConnection(matrix, 0, [0,1])) - .then(matrix => client.getNodeByPathnum(matrix.getPath())) + .then(matrix => client.getElementByPath(matrix.getPath())) .then(() => client.disconnect()); ``` @@ -145,6 +145,9 @@ server.on("matrix-connect", info => { server.on("matrix-change", info => { console.log(`Client ${info.client} changed ${info.target} and ${info.sources}`); } +server.on("event", txt => { + console.log("event: " + txt); +}) server.listen().then(() => { console.log("listening"); }).catch((e) => { console.log(e.stack); }); ``` diff --git a/package.json b/package.json index bce4fa1..7f634e0 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.0.0", + "version": "2.1.0", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/test/Server.test.js b/test/Server.test.js index 4c9ac92..d11a682 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -69,11 +69,11 @@ describe("server", function() { }) .then(() => { expect(client.root.getElementByNumber(0).elements.size).toBe(jsonTree[0].children.length); - return client.getDirectory(client.root.getElementByNumber(0).getElementByNumber(0)); + return client.getDirectory(client.root.getElementByPath("0.0")); }) .then(() => { - expect(client.root.getElementByNumber(0).getElementByNumber(0).elements.size).toBe(4); - expect(client.root.getElementByNumber(0).getElementByNumber(0).getElementByNumber(3).contents.identifier).toBe("author"); + expect(client.root.getElementByPath("0.0").elements.size).toBe(4); + expect(client.root.getElementByPath("0.0.3").contents.identifier).toBe("author"); // Issue #33 EmberServer.handleGetDirectory does not subscribe to child parameters expect(server.subscribers["0.0.0"]).toBeDefined(); // Keepalive @@ -90,11 +90,11 @@ describe("server", function() { }) .then(() => client.expand(client.root.getElementByNumber(0))) .then(() => { - expect(server.tree.getElementByNumber(0).getElementByNumber(0).getElementByNumber(1).contents.value).not.toBe("gdnet"); - return client.setValue(client.root.getElementByNumber(0).getElementByNumber(0).getElementByNumber(1), "gdnet"); + expect(server.tree.getElementByPath("0.0.1").contents.value).not.toBe("gdnet"); + return client.setValue(client.root.getElementByPath("0.0.1"), "gdnet"); }) .then(() => { - expect(server.tree.getElementByNumber(0).getElementByNumber(0).getElementByNumber(1).contents.value).toBe("gdnet"); + expect(server.tree.getElementByPath("0.0.1").contents.value).toBe("gdnet"); return client.disconnect(); }); }); @@ -109,7 +109,7 @@ describe("server", function() { }) .then(() => client.expand(client.root.getElementByNumber(0))) .then(() => { - const func = client.root.getElementByNumber(0).getElementByNumber(2); + const func = client.root.getElementByPath("0.2"); return client.invokeFunction(func, [ new ember.FunctionArgument(ember.ParameterType.integer, 1), new ember.FunctionArgument(ember.ParameterType.integer, 7) @@ -146,6 +146,27 @@ describe("server", function() { return client.disconnect(); }); }); + it("should be able to get child with getElementByPath", function() { + //server._debug = true; + client = new EmberClient(LOCALHOST, PORT); + //client._debug = true; + //client._debug = true; + return Promise.resolve() + .then(() => client.connect()) + .then(() => { + console.log("client connected"); + return client.getDirectory(); + }) + .then(() => client.getElementByPath("scoreMaster/identity/product")) + .then(child => { + console.log(child); + return client.getElementByPath("scoreMaster/router/labels/group 1"); + }) + .then(child => { + console.log("router/labels", child); + return client.disconnect(); + }); + }); it("should throw an error if getNodeByPath for unknown path", function() { //server._debug = true; client = new EmberClient(LOCALHOST, PORT); @@ -168,27 +189,86 @@ describe("server", function() { }); it("should be able to make a matrix connection", () => { client = new EmberClient(LOCALHOST, PORT); - //client._debug = true; return Promise.resolve() .then(() => client.connect()) .then(() => { return client.getDirectory(); }) - .then(() => client.getNodeByPathnum("0.1.0")) - .then(matrix => { - console.log(matrix); - client._debug = true; - server._debug = true; - return client.matrixConnect(matrix, 0, [1]); + .then(() => client.getElementByPath("0.1.0")) + .then(matrix => client.matrixConnect(matrix, 0, [1])) + .then(matrix => { + return client.getElementByPath(matrix.getPath()); }) - .then(matrix => client.getNodeByPathnum(matrix.getPath())) .then(matrix => { console.log(matrix); expect(matrix.connections['0'].sources).toBeDefined(); expect(matrix.connections['0'].sources.length).toBe(1); - expect(matrix.connections['0'].sources[0]).toBe(1); - return client.disconnect(); - }); + expect(matrix.connections['0'].sources[0]).toBe(1); + }) + .then(() => client.disconnect()); + }); + it("should generate events on command and matrix connection", () => { + client = new EmberClient(LOCALHOST, PORT); + let count = 0; + let receivedEvent = null; + const eventHandler = event => { + count++; + receivedEvent = event; + } + return Promise.resolve() + .then(() => client.connect()) + .then(() => { + count = 0; + server.on("event", eventHandler); + return client.getDirectory(); + }) + .then(() => { + expect(count).toBe(1); + expect(receivedEvent).toMatch(/getdirectory to root/); + return client.getElementByPath("0.1.0"); + }) + .then(matrix => { + count = 0; + return client.matrixConnect(matrix, 0, [1]); + }) + .then(() => { + expect(count).toBe(1); + expect(receivedEvent).toMatch(/Matrix connection to matrix/); + }) + .then(() => { + count = 0; + const func = client.root.getElementByPath("0.2"); + return client.invokeFunction(func, [ + new ember.FunctionArgument(ember.ParameterType.integer, 1), + new ember.FunctionArgument(ember.ParameterType.integer, 7) + ]); + }) + .then(() => { + expect(count).toBe(1); + expect(receivedEvent).toMatch(/invoke to /); + }) + .then(() => client.getNodeByPathnum("0.0.2")) + .then(parameter => { + server._subscribe = server.subscribe; + let _resolve; + const p = new Promise((resolve, reject) => { + _resolve = resolve; + }); + server.subscribe = (c,e) => { + server._subscribe(c,e); + _resolve(); + }; + count = 0; + return client.subscribe(parameter).then(() => (p)) + }) + .then(() => { + expect(count).toBe(1); + expect(receivedEvent).toMatch(/subscribe to version/); + }) + .then(() => { + server.off("event", eventHandler); + }) + .then(() => client.disconnect()); }); }); describe("Matrix Connect", function() { @@ -205,7 +285,7 @@ describe("server", function() { disconnectCount++; } server.on("matrix-disconnect", handleDisconnect.bind(this)); - const matrix = server.tree.getElementByNumber(0).getElementByNumber(1).getElementByNumber(0); + const matrix = server.tree.getElementByPath("0.1.0"); let connection = new ember.MatrixConnection(0); connection.setSources([1]); connection.operation = ember.MatrixOperation.connect; @@ -237,7 +317,7 @@ describe("server", function() { expect(res).toBeTruthy(); }); it("should verify if connection allowed in 1-to-1", function() { - const matrix = server.tree.getElementByNumber(0).getElementByNumber(1).getElementByNumber(0); + const matrix = server.tree.getElementByPath("0.1.0"); let disconnectCount = 0; const handleDisconnect = info => { disconnectCount++; @@ -269,7 +349,7 @@ describe("server", function() { server.off("matrix-disconnect", handleDisconnect); }); it("should disconnect if trying to connect same source and target in 1-to-1", function() { - const matrix = server.tree.getElementByNumber(0).getElementByNumber(1).getElementByNumber(0); + const matrix = server.tree.getElementByPath("0.1.0"); let disconnectCount = 0; const handleDisconnect = info => { disconnectCount++; @@ -285,7 +365,7 @@ describe("server", function() { expect(disconnectCount).toBe(1); }); it("should be able to lock a connection", function() { - const matrix = server.tree.getElementByNumber(0).getElementByNumber(1).getElementByNumber(0); + const matrix = server.tree.getElementByPath("0.1.0"); let disconnectCount = 0; const handleDisconnect = info => { disconnectCount++; @@ -303,7 +383,7 @@ describe("server", function() { expect(disconnectCount).toBe(0); }); it("should verify if connection allowed in N-to-N", function() { - const matrix = server.tree.getElementByNumber(0).getElementByNumber(1).getElementByNumber(0); + const matrix = server.tree.getElementByPath("0.1.0"); matrix.contents.type = ember.MatrixType.nToN; matrix.contents.maximumTotalConnects = 2; matrix.setSources(0, [0,1]); @@ -407,7 +487,7 @@ describe("server", function() { return server.close(); }); it("should not auto subscribe stream parameter", function() { - const parameter = server.tree.getElementByNumber(0).getElementByNumber(0).getElementByNumber(2); + const parameter = server.tree.getElementByPath("0.0.2"); console.log(parameter); expect(parameter.isStream()).toBeTruthy(); expect(server.subscribers["0.0.2"]).not.toBeDefined(); From 8be7a6217f9da806a30c0e77d781be165061d74a Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Tue, 7 Jan 2020 14:51:33 +0100 Subject: [PATCH 38/64] Change server event format to include source ip:port --- EmberServer/ElementHandlers.js | 3 ++- EmberServer/EmberServer.js | 3 ++- EmberServer/MatrixHandlers.js | 3 ++- package.json | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/EmberServer/ElementHandlers.js b/EmberServer/ElementHandlers.js index 8cbfcce..db51289 100755 --- a/EmberServer/ElementHandlers.js +++ b/EmberServer/ElementHandlers.js @@ -40,7 +40,8 @@ class ElementHandlers extends QualifiedHandlers{ const node = this.server.tree.getElementByPath(element.getPath()); identifier = node == null || node.contents == null || node.contents.identifier == null ? "unknown" : node.contents.identifier; } - this.server.emit("event", `${EmberLib.COMMAND_STRINGS[cmd.number]} to ${identifier}(path: ${element.getPath()})`); + const src = client == null ? "local" : `${client.socket.remoteAddress}:${client.socket.remotePort}`; + this.server.emit("event", `${EmberLib.COMMAND_STRINGS[cmd.number]} to ${identifier}(path: ${element.getPath()}) from ${src}`); } /** diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js index 6b248a4..5a5e33c 100755 --- a/EmberServer/EmberServer.js +++ b/EmberServer/EmberServer.js @@ -260,8 +260,9 @@ class TreeServer extends EventEmitter{ const res = this.getResponse(element); this.updateSubscribers(element.getPath(),res, origin); } + const src = origin == null ? "local" : `${origin.socket.remoteAddress}:${origin.socket.remotePort}`; this.emit("value-change", element); - this.emit("event", `set value for ${element.contents.identifier}(${element.getPath()})` ); + this.emit("event", `set value for ${element.contents.identifier}(${element.getPath()}) from ${src}` ); } return resolve(); }); diff --git a/EmberServer/MatrixHandlers.js b/EmberServer/MatrixHandlers.js index 28dc3fd..6ce6bd1 100755 --- a/EmberServer/MatrixHandlers.js +++ b/EmberServer/MatrixHandlers.js @@ -65,7 +65,8 @@ class MatrixHandlers { continue; } let connection = connections[id]; - this.server.emit("event", `Matrix connection to ${matrix.contents.identifier}(path: ${matrix.getPath()}) target ${id} connections: ${connection.sources.toString()}`); + const src = client == null ? "local" : `${client.socket.remoteAddress}:${client.socket.remotePort}`; + this.server.emit("event", `Matrix connection to ${matrix.contents.identifier}(path: ${matrix.getPath()}) target ${id} connections: ${connection.sources.toString()} from ${src}`); conResult = new ember.MatrixConnection(connection.target); let emitType; res.connections[connection.target] = conResult; diff --git a/package.json b/package.json index 7f634e0..ef0dd57 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.1.0", + "version": "2.2.0", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { From 1106a8b1ee4c24cdc74093b0e8e332b5ac4c8d62 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Tue, 7 Jan 2020 14:54:57 +0100 Subject: [PATCH 39/64] Fix setValue event to be inline with the other events --- EmberServer/EmberServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js index 5a5e33c..f9d8bc9 100755 --- a/EmberServer/EmberServer.js +++ b/EmberServer/EmberServer.js @@ -262,7 +262,7 @@ class TreeServer extends EventEmitter{ } const src = origin == null ? "local" : `${origin.socket.remoteAddress}:${origin.socket.remotePort}`; this.emit("value-change", element); - this.emit("event", `set value for ${element.contents.identifier}(${element.getPath()}) from ${src}` ); + this.emit("event", `set value for ${element.contents.identifier}(path: ${element.getPath()}) from ${src}` ); } return resolve(); }); From 1939824c33d177ae869318a631806dce22e28c37 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Wed, 8 Jan 2020 11:02:06 +0100 Subject: [PATCH 40/64] Fixed eslint errors --- EmberClient/EmberClient.js | 147 +++++++++++++--------------- EmberLib/Command.js | 1 + EmberLib/Elements.js | 8 -- EmberLib/Function.js | 4 + EmberLib/FunctionContent.js | 3 +- EmberLib/Invocation.js | 1 + EmberLib/InvocationResult.js | 1 + EmberLib/Label.js | 1 + EmberLib/Matrix.js | 6 +- EmberLib/MatrixConnection.js | 1 + EmberLib/MatrixContents.js | 3 +- EmberLib/MatrixNode.js | 1 + EmberLib/Node.js | 3 +- EmberLib/NodeContents.js | 1 + EmberLib/Parameter.js | 1 + EmberLib/ParameterContents.js | 3 +- EmberLib/QualifiedFunction.js | 3 +- EmberLib/QualifiedMatrix.js | 5 +- EmberLib/QualifiedNode.js | 4 +- EmberLib/QualifiedParameter.js | 6 +- EmberLib/StringIntegerCollection.js | 1 + EmberLib/TreeNode.js | 21 +--- EmberLib/index.js | 6 +- EmberServer/ElementHandlers.js | 19 ++-- EmberServer/EmberServer.js | 60 +++++++++--- EmberServer/MatrixHandlers.js | 16 +-- EmberSocket/S101Client.js | 2 +- EmberSocket/S101Socket.js | 4 +- package.json | 2 +- 29 files changed, 171 insertions(+), 163 deletions(-) delete mode 100755 EmberLib/Elements.js diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index ead404f..8420c6e 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -3,6 +3,7 @@ const S101Client = require('../EmberSocket').S101Socket; const ember = require('../EmberLib'); const BER = require('../ber.js'); const errors = require('../errors.js'); +const {Logger, LogLevel} = require("../Logger"); const DEFAULT_PORT = 9000; const DEFAULT_TIMEOUT = 3000; @@ -31,6 +32,14 @@ class EmberClient extends EventEmitter { this.timeoutValue = DEFAULT_TIMEOUT; /** @type {Root} */ this.root = new ember.Root(); + this.logger = new Logger(); + this.logLevel = LogLevel.INFO; + this._loggers = { + debug: (...args) => this._log(LogLevel.DEBUG, ...args), + error: (...args) => this._log(LogLevel.ERROR, ...args), + info: (...args) => this._log(LogLevel.INFO, ...args), + warn: (...args) => this._log(LogLevel.WARN, ...args) + }; this._client.on('connecting', () => { this.emit('connecting'); @@ -58,23 +67,17 @@ class EmberClient extends EventEmitter { try { if (root instanceof ember.InvocationResult) { this.emit('invocationResult', root); - if (this._debug) { - console.log("Received InvocationResult", root); - } + this.log.debug("Received InvocationResult", root); } else { this._handleRoot(root); - if (this._debug) { - console.log("Received root", root); - } + this.log.debug("Received root", root); } if (this._callback) { this._callback(undefined, root); } } catch(e) { - if (this._debug) { - console.log(e, root); - } + this.log.debug(e, root); if (this._callback) { this._callback(e); } @@ -82,13 +85,33 @@ class EmberClient extends EventEmitter { }); } + /** + * + * @param {Array} params + * @private + */ + _log(...params) { + if ((params.length > 1) && (Number(params[0]) <= this.logLevel)) { + const msg = params.slice(1); + this.logger[Logger.LogLevel[params[0]]](`[${Logger.LogLevel[params[0]]}]:`, ...msg); + } + } + + /** + * + * @returns {{debug: (function(...[*]): void), error: (function(...[*]): void), info: (function(...[*]): void), warn: (function(...[*]): void)}|*} + */ + get log() { + return this._loggers; + } + _finishRequest() { this._clearTimeout(); this._activeRequest = null; try { this._makeRequest(); } catch(e) { - if (this._debug) {console.log(e);} + this.log.debug(e); if (this._callback != null) { this._callback(e); } @@ -102,9 +125,7 @@ class EmberClient extends EventEmitter { const req = `${ this._requestID++} - ${this._activeRequest.node.getPath()}`; this._activeRequest.timeoutError = new errors.EmberTimeoutError(`Request ${req} timed out`) - if (this._debug) { - console.log(`Making request ${req}`, Date.now()); - } + this.log.debug(`Making request ${req}`, Date.now()); this._timeout = setTimeout(() => { this._timeoutRequest(); }, this.timeoutValue); @@ -112,7 +133,7 @@ class EmberClient extends EventEmitter { } } - _timeoutRequest(id) { + _timeoutRequest() { this._activeRequest.func(this._activeRequest.timeoutError); } @@ -123,14 +144,14 @@ class EmberClient extends EventEmitter { addRequest(req) { this._pendingRequests.push(req); this._makeRequest(); - }; + } _clearTimeout() { if (this._timeout != null) { clearTimeout(this._timeout); this._timeout = null; } - }; + } /** * @@ -207,9 +228,7 @@ class EmberClient extends EventEmitter { * @param {TreeNode} root */ _handleRoot (root) { - if (this._debug) { - console.log("handling root", JSON.stringify(root)); - } + this.log.debug("handling root", JSON.stringify(root)); this.root.update(root); if (root.elements !== undefined) { const elements = root.getChildren(); @@ -247,11 +266,14 @@ class EmberClient extends EventEmitter { }); } + /** + * + */ disconnect() { if (this._client != null) { return this._client.disconnect(); } - }; + } /** * @@ -269,9 +291,7 @@ class EmberClient extends EventEmitter { return this.getDirectory(node, callback).then((res) => { let children = node.getChildren(); if ((res === undefined) || (children === undefined) || (children === null)) { - if (this._debug) { - console.log("No more children for ", node); - } + this.log.debug("No more children for ", node); return; } let p = Promise.resolve(); @@ -280,9 +300,7 @@ class EmberClient extends EventEmitter { // Parameter can only have a single child of type Command. continue; } - if (this._debug) { - console.log("Expanding child", child); - } + this.log.debug("Expanding child", child); p = p.then(() => { return this.expand(child).catch((e) => { // We had an error on some expansion @@ -317,15 +335,11 @@ class EmberClient extends EventEmitter { this._callback = (error, node) => { const requestedPath = qnode.getPath(); if (node == null) { - if (this._debug) { - console.log(`received null response for ${requestedPath}`); - } + this.log.debug(`received null response for ${requestedPath}`); return; } if (error) { - if (this._debug) { - console.log("Received getDirectory error", error); - } + this.log.debug("Received getDirectory error", error); this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); reject(error); @@ -334,9 +348,7 @@ class EmberClient extends EventEmitter { if (qnode.isRoot()) { const elements = qnode.getChildren(); if (elements == null || elements.length === 0) { - if (this._debug) { - console.log("getDirectory response", node); - } + this.log.debug("getDirectory response", node); return this._callback(new Error("Invalid qnode for getDirectory")); } @@ -344,9 +356,7 @@ class EmberClient extends EventEmitter { if (nodeElements != null && nodeElements.every(el => el._parent instanceof ember.Root)) { - if (this._debug) { - console.log("Received getDirectory response", node); - } + this.log.debug("Received getDirectory response", node); this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); resolve(node); // make sure the info is treated before going to next request. @@ -365,23 +375,18 @@ class EmberClient extends EventEmitter { if (nodeElements != null && ((qnode.isMatrix() && nodeElements.length === 1 && nodeElements[0].getPath() === requestedPath) || (!qnode.isMatrix() && nodeElements.every(el => isDirectSubPathOf(el.getPath(), requestedPath))))) { - if (this._debug) { - console.log("Received getDirectory response", node); - } + this.log.debug("Received getDirectory response", node); this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); return resolve(node); // make sure the info is treated before going to next request. } - else if (this._debug) { - console.log(node); - console.log(new Error(requestedPath)); + else { + this.log.debug(node); + this.log.debug(new Error(requestedPath)); } } }; - - if (this._debug) { - console.log("Sending getDirectory", qnode); - } + this.log.debug("Sending getDirectory", qnode); this._client.sendBERNode(qnode.getDirectory(callback)); }}); }); @@ -503,25 +508,19 @@ class EmberClient extends EventEmitter { this._finishRequest(); return; } - const cb = (error, result) => { this._clearTimeout(); if (error) { reject(error); } else { - if (this._debug) { - console.log("InvocationResult", result); - } + this.log.debug("InvocationResult", result); resolve(result); } // cleaning callback and making next request. this._finishRequest(); }; - - if (this._debug) { - console.log("Invocking function", fnNode); - } + this.log.debug("Invocking function", fnNode); this._callback = cb; this._client.sendBERNode(fnNode.invoke(params)); }}); @@ -569,15 +568,11 @@ class EmberClient extends EventEmitter { this._callback = (error, node) => { const requestedPath = matrixNode.getPath(); if (node == null) { - if (this._debug) { - console.log(`received null response for ${requestedPath}`); - } + this.log.debug(`received null response for ${requestedPath}`); return; } if (error) { - if (this._debug) { - console.log("Received getDirectory error", error); - } + this.log.debug("Received getDirectory error", error); this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); reject(error); @@ -593,10 +588,8 @@ class EmberClient extends EventEmitter { resolve(matrix); } else { - if (this._debug) { - console.log(`unexpected node response during matrix connect ${requestedPath}`, + this.log.debug(`unexpected node response during matrix connect ${requestedPath}`, matrix == null ? null : JSON.stringify(matrix.toJSON(), null, 4)); - } } } this._client.sendBERNode(matrixNode.connect(connections)); @@ -677,9 +670,7 @@ class EmberClient extends EventEmitter { }; this._callback = cb; - if (this._debug) { - console.log('setValue sending ...', node.getPath(), value); - } + this.log.debug('setValue sending ...', node.getPath(), value); this._client.sendBERNode(node.setValue(value)); }}); } @@ -698,17 +689,18 @@ class EmberClient extends EventEmitter { qnode = this.root; } return new Promise((resolve, reject) => { - this.addRequest({node: qnode, func: (error) => { - if (this._debug) { - console.log("Sending subscribe", qnode); - } + this.addRequest({node: qnode, func: error => { + if (error != null) { + return reject(error); + } + this.log.debug("Sending subscribe", qnode); this._client.sendBERNode(qnode.subscribe(callback)); this._finishRequest(); resolve(); }}); }); } else { - node.addCallback(callback); + qnode.addCallback(callback); } } @@ -724,10 +716,11 @@ class EmberClient extends EventEmitter { qnode = this.root; } return new Promise((resolve, reject) => { - this.addRequest({node: qnode, func: (error) => { - if (this._debug) { - console.log("Sending subscribe", qnode); - } + this.addRequest({node: qnode, func: (error) => { + if (error != null) { + return reject(error); + } + this.log.debug("Sending subscribe", qnode); this._client.sendBERNode(qnode.unsubscribe(callback)); this._finishRequest(); resolve(); diff --git a/EmberLib/Command.js b/EmberLib/Command.js index c3247b0..3ff9d6a 100755 --- a/EmberLib/Command.js +++ b/EmberLib/Command.js @@ -3,6 +3,7 @@ const Enum = require('enum'); const {COMMAND_GETDIRECTORY, COMMAND_INVOKE} = require("./constants"); const BER = require('../ber.js'); const Invocation = require("./Invocation"); +const errors = require("../errors"); const FieldFlags = new Enum({ sparse: -2, diff --git a/EmberLib/Elements.js b/EmberLib/Elements.js deleted file mode 100755 index 1826ae5..0000000 --- a/EmberLib/Elements.js +++ /dev/null @@ -1,8 +0,0 @@ -"use stricts"; - -class Elements { - constructor() { - this._elements = new Map(); - } - -} \ No newline at end of file diff --git a/EmberLib/Function.js b/EmberLib/Function.js index 3ccb79b..ddc4aa4 100755 --- a/EmberLib/Function.js +++ b/EmberLib/Function.js @@ -2,6 +2,10 @@ const TreeNode = require("./TreeNode"); const QualifiedFunction = require("./QualifiedFunction"); const BER = require('../ber.js'); +const Command = require("./Command"); +const {COMMAND_INVOKE} = require("./constants"); +const FunctionContent = require("./FunctionContent"); +const errors = require("../errors"); class Function extends TreeNode { constructor(number, func) { diff --git a/EmberLib/FunctionContent.js b/EmberLib/FunctionContent.js index aab06ee..f5e040b 100755 --- a/EmberLib/FunctionContent.js +++ b/EmberLib/FunctionContent.js @@ -1,6 +1,7 @@ "use strict"; const BER = require('../ber.js'); const FunctionArgument = require("./FunctionArgument"); +const errors = require("../errors"); class FunctionContent { constructor() { @@ -42,7 +43,7 @@ class FunctionContent { if(this.result != null) { ber.startSequence(BER.CONTEXT(3)); ber.startSequence(BER.EMBER_SEQUENCE); - for(var i = 0; i < this.result.length; i++) { + for(let i = 0; i < this.result.length; i++) { ber.startSequence(BER.CONTEXT(0)); this.result[i].encode(ber); ber.endSequence(); diff --git a/EmberLib/Invocation.js b/EmberLib/Invocation.js index e12130b..b114e0a 100755 --- a/EmberLib/Invocation.js +++ b/EmberLib/Invocation.js @@ -2,6 +2,7 @@ const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); const BER = require('../ber.js'); const FunctionArgument = require("./FunctionArgument"); +const errors = require("../errors"); let _id = 1; class Invocation { diff --git a/EmberLib/InvocationResult.js b/EmberLib/InvocationResult.js index 5e9505d..901c60d 100755 --- a/EmberLib/InvocationResult.js +++ b/EmberLib/InvocationResult.js @@ -3,6 +3,7 @@ const BER = require('../ber.js'); const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); const FunctionArgument = require("./FunctionArgument"); +const errors = require("../errors"); class InvocationResult { diff --git a/EmberLib/Label.js b/EmberLib/Label.js index 069e84d..3c6d648 100755 --- a/EmberLib/Label.js +++ b/EmberLib/Label.js @@ -1,5 +1,6 @@ "use strict"; const BER = require('../ber.js'); +const errors = require("../errors"); class Label { constructor(path, description) { diff --git a/EmberLib/Matrix.js b/EmberLib/Matrix.js index c7b9b79..e0c7980 100755 --- a/EmberLib/Matrix.js +++ b/EmberLib/Matrix.js @@ -5,7 +5,6 @@ const BER = require('../ber.js'); const MatrixMode = require("./MatrixMode"); const MatrixOperation = require("./MatrixOperation"); const MatrixType = require("./MatrixType"); -const MatrixNode = require("./MatrixNode"); class Matrix extends TreeNode { @@ -176,8 +175,7 @@ class Matrix extends TreeNode */ static canConnect(matrixNode, targetID, sources, operation) { const type = matrixNode.contents.type == null ? MatrixType.oneToN : matrixNode.contents.type; - const mode = matrixNode.contents.mode == null ? MatrixConnection.linear : matrixNode.contents.mode; - const connection = matrixNode.connections[targetID];; + const connection = matrixNode.connections[targetID]; const oldSources = connection == null || connection.sources == null ? [] : connection.sources.slice(); const newSources = operation === MatrixOperation.absolute ? sources : oldSources.concat(sources); const sMap = new Set(newSources.map(i => Number(i))); @@ -278,7 +276,7 @@ class Matrix extends TreeNode sources.push(seq.readInt()); } return sources; - }; + } /** * diff --git a/EmberLib/MatrixConnection.js b/EmberLib/MatrixConnection.js index 4e78e45..51a817a 100755 --- a/EmberLib/MatrixConnection.js +++ b/EmberLib/MatrixConnection.js @@ -2,6 +2,7 @@ const BER = require('../ber.js'); const MatrixOperation = require("./MatrixOperation"); const MatrixDisposition = require("./MatrixDisposition"); +const errors = require("../errors"); class MatrixConnection { /** diff --git a/EmberLib/MatrixContents.js b/EmberLib/MatrixContents.js index 1e8df10..1f1d1a5 100755 --- a/EmberLib/MatrixContents.js +++ b/EmberLib/MatrixContents.js @@ -4,6 +4,7 @@ const MatrixType = require("./MatrixType"); const MatrixMode = require("./MatrixMode"); const BER = require('../ber.js'); const Label = require("./Label"); +const errors = require("../errors"); class MatrixContents { constructor(type = MatrixType.oneToN, mode = MatrixMode.linear) { @@ -147,7 +148,7 @@ class MatrixContents { } } return mc; - }; + } } module.exports = MatrixContents; \ No newline at end of file diff --git a/EmberLib/MatrixNode.js b/EmberLib/MatrixNode.js index 8db2f84..5f309e2 100755 --- a/EmberLib/MatrixNode.js +++ b/EmberLib/MatrixNode.js @@ -4,6 +4,7 @@ const Matrix = require("./Matrix"); const MatrixContents = require("./MatrixContents"); const QualifiedMatrix = require("./QualifiedMatrix"); const BER = require('../ber.js'); +const errors = require("../errors"); class MatrixNode extends Matrix { constructor(number = undefined) { diff --git a/EmberLib/Node.js b/EmberLib/Node.js index 68fe845..1617b28 100755 --- a/EmberLib/Node.js +++ b/EmberLib/Node.js @@ -4,6 +4,7 @@ const Element = require("./Element"); const QualifiedNode = require("./QualifiedNode"); const NodeContents = require("./NodeContents"); const BER = require('../ber.js'); +const errors = require("../errors"); class Node extends Element { /** @@ -65,6 +66,6 @@ class Node extends Element { } return n; } -}; +} module.exports = Node; diff --git a/EmberLib/NodeContents.js b/EmberLib/NodeContents.js index a34fc48..9260ed9 100755 --- a/EmberLib/NodeContents.js +++ b/EmberLib/NodeContents.js @@ -1,5 +1,6 @@ "use strict"; const BER = require('../ber.js'); +const errors = require("../errors"); class NodeContents{ constructor() { diff --git a/EmberLib/Parameter.js b/EmberLib/Parameter.js index f89d3f6..7357754 100755 --- a/EmberLib/Parameter.js +++ b/EmberLib/Parameter.js @@ -4,6 +4,7 @@ const Element = require("./Element"); const QualifiedParameter = require("./QualifiedParameter"); const BER = require('../ber.js'); const ParameterContents = require("./ParameterContents"); +const errors = require("../errors"); class Parameter extends Element { /** diff --git a/EmberLib/ParameterContents.js b/EmberLib/ParameterContents.js index b213299..7e79bbd 100755 --- a/EmberLib/ParameterContents.js +++ b/EmberLib/ParameterContents.js @@ -3,8 +3,9 @@ const {ParameterType} = require("./ParameterType"); const ParameterAccess = require("./ParameterAccess"); const StringIntegerCollection = require("./StringIntegerCollection"); - +const StreamDescription = require("./StreamDescription"); const BER = require('../ber.js'); +const errors = require("../errors"); class ParameterContents { constructor(value, type) { diff --git a/EmberLib/QualifiedFunction.js b/EmberLib/QualifiedFunction.js index 37cab00..96966ab 100755 --- a/EmberLib/QualifiedFunction.js +++ b/EmberLib/QualifiedFunction.js @@ -5,6 +5,7 @@ const FunctionContent = require("./FunctionContent"); const {COMMAND_GETDIRECTORY, COMMAND_INVOKE} = require("./constants"); const BER = require('../ber.js'); const Invocation = require("./Invocation"); +const errors = require("../errors"); class QualifiedFunction extends QualifiedElement { /** @@ -22,7 +23,7 @@ class QualifiedFunction extends QualifiedElement { * * @returns {TreeNode} */ - getDirectory(callback) { + getDirectory() { return this.getCommand(COMMAND_GETDIRECTORY); } diff --git a/EmberLib/QualifiedMatrix.js b/EmberLib/QualifiedMatrix.js index 538e76c..7c15ed6 100755 --- a/EmberLib/QualifiedMatrix.js +++ b/EmberLib/QualifiedMatrix.js @@ -6,6 +6,7 @@ const BER = require('../ber.js'); const Command = require("./Command"); const MatrixContents = require("./MatrixContents"); const MatrixConnection = require("./MatrixConnection"); +const errors = require("../errors"); class QualifiedMatrix extends Matrix { /** @@ -137,9 +138,9 @@ class QualifiedMatrix extends Matrix { } else if(tag == BER.CONTEXT(2)) { qm.decodeChildren(seq); } else if (tag == BER.CONTEXT(3)) { - qm.targets = decodeTargets(seq); + qm.targets = Matrix.decodeTargets(seq); } else if (tag == BER.CONTEXT(4)) { - qm.sources = decodeSources(seq); + qm.sources = Matrix.decodeSources(seq); } else if (tag == BER.CONTEXT(5)) { qm.connections = {}; seq = seq.getSequence(BER.EMBER_SEQUENCE); diff --git a/EmberLib/QualifiedNode.js b/EmberLib/QualifiedNode.js index 0776cff..7dce7a5 100755 --- a/EmberLib/QualifiedNode.js +++ b/EmberLib/QualifiedNode.js @@ -2,8 +2,8 @@ const QualifiedElement = require("./QualifiedElement"); const BER = require('../ber.js'); const NodeContents = require("./NodeContents"); -const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); -const Command = require("./Command"); +const Node = require("./Node"); +const errors = require("../errors"); class QualifiedNode extends QualifiedElement { constructor (path) { diff --git a/EmberLib/QualifiedParameter.js b/EmberLib/QualifiedParameter.js index 85a748f..717c8d7 100755 --- a/EmberLib/QualifiedParameter.js +++ b/EmberLib/QualifiedParameter.js @@ -1,11 +1,9 @@ "use strict"; const QualifiedElement = require("./QualifiedElement"); -const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); const ParameterContents = require("./ParameterContents"); const BER = require('../ber.js'); -const Command = require("./Command"); - +const Parameter = require("./Parameter"); class QualifiedParameter extends QualifiedElement { /** @@ -34,7 +32,7 @@ class QualifiedParameter extends QualifiedElement { const p = new Parameter(number); if (complete) { if (this.contents != null) { - p = this.contents; + p.contents = this.contents; } } return p; diff --git a/EmberLib/StringIntegerCollection.js b/EmberLib/StringIntegerCollection.js index 5bd00ea..a341145 100755 --- a/EmberLib/StringIntegerCollection.js +++ b/EmberLib/StringIntegerCollection.js @@ -1,6 +1,7 @@ "use strict"; const Element = require("./Element"); const BER = require('../ber.js'); +const errors = require("../errors"); class StringIntegerCollection extends Element { constructor() { diff --git a/EmberLib/TreeNode.js b/EmberLib/TreeNode.js index c07849a..94caf34 100755 --- a/EmberLib/TreeNode.js +++ b/EmberLib/TreeNode.js @@ -177,7 +177,7 @@ class TreeNode { obj.contents= this.contents; } return obj; - }; + } /** * @returns {TreeNode} */ @@ -271,13 +271,6 @@ class TreeNode { getParent() { return this._parent; } - - /** - * @returns {string} - */ - getPath() { - return ""; - } /** * @@ -370,11 +363,11 @@ class TreeNode { return; } - const child = this.getElement(path[0]); + let child = this.getElement(path[0]); if(child !== null) { child.getNodeByPath(client, path.slice(1), callback); } else { - var cmd = self.getDirectory((error, node) => { + const cmd = this.getDirectory((error, node) => { if(error) { callback(error); } @@ -479,7 +472,7 @@ class TreeNode { } } return res; - }; + } /** * @@ -498,11 +491,7 @@ class TreeNode { } } } - return; - } - - static decode(ber) { - + return; } /** diff --git a/EmberLib/index.js b/EmberLib/index.js index d3a9695..92bf00c 100755 --- a/EmberLib/index.js +++ b/EmberLib/index.js @@ -28,6 +28,8 @@ const QualifiedMatrix = require("./QualifiedMatrix"); const QualifiedNode = require("./QualifiedNode"); const QualifiedParameter = require("./QualifiedParameter"); const StringIntegerCollection = require("./StringIntegerCollection"); +const StreamFormat = require("./StreamFormat"); +const StreamDescription = require("./StreamDescription"); const rootDecode = function(ber) { const r = new TreeNode(); @@ -65,7 +67,7 @@ const rootDecode = function(ber) { // continuation of previous message try { var rootReader = ber.getSequence(BER.CONTEXT(0)); - return Element.decode(rootReader) + return childDecode(rootReader) } catch (e) { return r; @@ -143,6 +145,8 @@ module.exports = { QualifiedMatrix, QualifiedNode, QualifiedParameter, + StreamFormat, + StreamDescription, StringIntegerCollection, Subscribe,COMMAND_SUBSCRIBE, Unsubscribe,COMMAND_UNSUBSCRIBE, diff --git a/EmberServer/ElementHandlers.js b/EmberServer/ElementHandlers.js index db51289..c8d7079 100755 --- a/EmberServer/ElementHandlers.js +++ b/EmberServer/ElementHandlers.js @@ -16,8 +16,7 @@ class ElementHandlers extends QualifiedHandlers{ * @param {TreeNode} root * @param {Command} cmd */ - handleCommand(client, element, cmd) { - + handleCommand(client, element, cmd) { switch(cmd.number) { case EmberLib.COMMAND_GETDIRECTORY: this.handleGetDirectory(client, element); @@ -71,9 +70,7 @@ class ElementHandlers extends QualifiedHandlers{ } const res = this.server.getQualifiedResponse(element); - if (this.server._debug) { - console.log("getDirectory response", res); - } + this.server.log.debug("getDirectory response", res); client.sendBERNode(res); } } @@ -152,7 +149,7 @@ class ElementHandlers extends QualifiedHandlers{ } else if ((cmd.isParameter()) && (cmd.contents !== undefined) && (cmd.contents.value !== undefined)) { - if (this.server._debug) { console.log(`setValue for element at path ${path} with value ${cmd.contents.value}`); } + this.server.log.debug(`setValue for element at path ${path} with value ${cmd.contents.value}`); this.server.setValue(element, cmd.contents.value, client); const res = this.server.getResponse(element); client.sendBERNode(res) @@ -160,7 +157,7 @@ class ElementHandlers extends QualifiedHandlers{ } else { this.server.emit("error", new Error("invalid request format")); - if (this.server._debug) { console.log("invalid request format"); } + this.server.log.debug("invalid request format"); return this.server.handleError(client, element.getTreeBranch()); } return path; @@ -172,9 +169,7 @@ class ElementHandlers extends QualifiedHandlers{ * @param {TreeNode} root */ handleSubscribe(client, element) { - if (this.server._debug) { - console.log("subscribe"); - } + this.server.log.debug("subscribe"); this.server.subscribe(client, element); } @@ -184,9 +179,7 @@ class ElementHandlers extends QualifiedHandlers{ * @param {TreeNode} root */ handleUnSubscribe(client, element) { - if (this.server._debug) { - console.log("unsubscribe"); - } + this.server.log.debug("unsubscribe"); this.server.unsubscribe(client, element); } } diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js index f9d8bc9..516e142 100755 --- a/EmberServer/EmberServer.js +++ b/EmberServer/EmberServer.js @@ -3,6 +3,7 @@ const S101Server = require('../EmberSocket').S101Server; const ember = require('../EmberLib'); const JSONParser = require("./JSONParser"); const ElementHandlers = require("./ElementHandlers"); +const {Logger, LogLevel} = require("../Logger"); class TreeServer extends EventEmitter{ /** @@ -21,9 +22,17 @@ class TreeServer extends EventEmitter{ this.clients = new Set(); this.subscribers = {}; this._handlers = new ElementHandlers(this); + this.logger = new Logger(); + this.logLevel = LogLevel.INFO; + this._loggers = { + debug: (...args) => this._log(LogLevel.DEBUG, ...args), + error: (...args) => this._log(LogLevel.ERROR, ...args), + info: (...args) => this._log(LogLevel.INFO, ...args), + warn: (...args) => this._log(LogLevel.WARN, ...args) + }; this.server.on('listening', () => { - if (this._debug) { console.log("listening"); } + this.log.debug("listening"); this.emit('listening'); if (this.callback !== undefined) { this.callback(); @@ -32,10 +41,10 @@ class TreeServer extends EventEmitter{ }); this.server.on('connection', client => { - if (this._debug) { console.log("ember new connection from", client.remoteAddress()); } + this.log.debug("ember new connection from", client.remoteAddress()); this.clients.add(client); client.on("emberTree", (root) => { - if (this._debug) { console.log("ember new request from", client.remoteAddress(), root); } + this.log.debug("ember new request from", client.remoteAddress(), root); // Queue the action to make sure responses are sent in order. client.addRequest(() => { try { @@ -43,7 +52,7 @@ class TreeServer extends EventEmitter{ this.emit("request", {client: client.remoteAddress(), root: root, path: path}); } catch(e) { - if (this._debug) { console.log(e.stack); } + this.log.debug(e.stack) this.emit("error", e); } }); @@ -59,7 +68,7 @@ class TreeServer extends EventEmitter{ }); this.server.on('disconnected', () => { - this.emit('disconnected', client.remoteAddress()); + this.emit('disconnected'); }); this.server.on("error", (e) => { @@ -70,6 +79,26 @@ class TreeServer extends EventEmitter{ }); } + /** + * + * @param {Array} params + * @private + */ + _log(...params) { + if ((params.length > 1) && (Number(params[0]) <= this.logLevel)) { + const msg = params.slice(1); + this.logger[Logger.LogLevel[params[0]]](`[${Logger.LogLevel[params[0]]}]:`, ...msg); + } + } + + /** + * + * @returns {{debug: (function(...[*]): void), error: (function(...[*]): void), info: (function(...[*]): void), warn: (function(...[*]): void)}|*} + */ + get log() { + return this._loggers; + } + /** * @returns {Promise} */ @@ -83,7 +112,7 @@ class TreeServer extends EventEmitter{ }; this.server.server.close(); }); - }; + } /** * @@ -98,8 +127,8 @@ class TreeServer extends EventEmitter{ node.addChild(children[i].getDuplicate()); } } - else if (this._debug) { - console.log("getResponse","no children"); + else { + this.log.debug("getResponse","no children"); } }); } @@ -178,7 +207,7 @@ class TreeServer extends EventEmitter{ }; this.server.listen(); }); - }; + } /** * @@ -221,8 +250,7 @@ class TreeServer extends EventEmitter{ throw new Error(`Could not find element at path ${path}`); } parent = parent._parent; - let children = parent.getChildren(); - let newList = []; + const children = parent.getChildren(); for(let i = 0; i <= children.length; i++) { if (children[i] && children[i].getPath() == path) { element._parent = parent; // move it to new tree. @@ -242,7 +270,7 @@ class TreeServer extends EventEmitter{ * @param {string} key */ setValue(element, value, origin, key) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { // Change the element value if write access permitted. if (element.contents == null) { return resolve(); @@ -291,7 +319,7 @@ class TreeServer extends EventEmitter{ const elements = this.tree.getChildren(); return elements.map(element => element.toJSON()); - }; + } /** * @@ -346,13 +374,13 @@ class TreeServer extends EventEmitter{ const validateMatrixOperation = function(matrix, target, sources) { if (matrix == null) { - throw new Error(`matrix not found with path ${path}`); + throw new Error(`matrix not found`); } if (matrix.contents == null) { - throw new Error(`invalid matrix at ${path} : no contents`); + throw new Error(`invalid matrix at ${matrix.getPath()} : no contents`); } if (matrix.contents.targetCount == null) { - throw new Error(`invalid matrix at ${path} : no targetCount`); + throw new Error(`invalid matrix at ${matrix.getPath()} : no targetCount`); } if ((target < 0) || (target >= matrix.contents.targetCount)) { throw new Error(`target id ${target} out of range 0 - ${matrix.contents.targetCount}`); diff --git a/EmberServer/MatrixHandlers.js b/EmberServer/MatrixHandlers.js index 6ce6bd1..d96e254 100755 --- a/EmberServer/MatrixHandlers.js +++ b/EmberServer/MatrixHandlers.js @@ -44,11 +44,9 @@ class MatrixHandlers { * @param {boolean} response=true */ handleMatrixConnections(client, matrix, connections, response = true) { - var res,conResult; - var root; // ember message root - if (this.server._debug) { - console.log("Handling Matrix Connection"); - } + let res,conResult; + let root; // ember message root + this.server.log.debug("Handling Matrix Connection"); if (client != null && client.request.isQualified()) { root = new ember.Root(); res = new ember.QualifiedMatrix(matrix.getPath()); @@ -191,9 +189,7 @@ class MatrixHandlers { } } else if (conResult.disposition !== ember.MatrixDisposition.locked){ - if (this.server._debug) { - console.log(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); - } + this.server.log.debug(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); conResult.disposition = ember.MatrixDisposition.tally; } @@ -213,9 +209,7 @@ class MatrixHandlers { } if (conResult != null && conResult.disposition !== ember.MatrixDisposition.tally) { - if (this.server._debug) { - console.log("Updating subscribers for matrix change"); - } + this.server.log.debug("Updating subscribers for matrix change"); this.server.updateSubscribers(matrix.getPath(), root, client); } } diff --git a/EmberSocket/S101Client.js b/EmberSocket/S101Client.js index b4c3f76..5e69b25 100755 --- a/EmberSocket/S101Client.js +++ b/EmberSocket/S101Client.js @@ -63,7 +63,7 @@ class S101Client extends S101Socket { this.activeRequest(); this.activeRequest = null; } - }; + } /** * diff --git a/EmberSocket/S101Socket.js b/EmberSocket/S101Socket.js index a38b51d..7a2da2c 100755 --- a/EmberSocket/S101Socket.js +++ b/EmberSocket/S101Socket.js @@ -106,7 +106,7 @@ class S101Socket extends EventEmitter{ this.status = "disconnected"; } ); - }; + } /** * @@ -116,7 +116,7 @@ class S101Socket extends EventEmitter{ clearInterval(this.keepaliveIntervalTimer); this.status = "disconnected"; this.emit('disconnected'); - }; + } /** * @returns {boolean} diff --git a/package.json b/package.json index ef0dd57..5a931cb 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.2.0", + "version": "2.2.1", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { From b9f7d2cad4dff068e197105047187f96e1f5a1fa Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sat, 11 Jan 2020 21:19:21 +0100 Subject: [PATCH 41/64] Adding Streams. Fixing bugs. Event/Errors using static value --- EmberClient/EmberClient.js | 104 ++++++----------- EmberLib/Function.js | 4 +- EmberLib/FunctionContent.js | 4 +- EmberLib/InvocationResult.js | 6 +- EmberLib/Matrix.js | 21 ++-- EmberLib/MatrixConnection.js | 6 +- EmberLib/MatrixContents.js | 26 ++--- EmberLib/MatrixNode.js | 2 +- EmberLib/Node.js | 2 + EmberLib/NodeContents.js | 19 ++- EmberLib/Parameter.js | 2 +- EmberLib/ParameterContents.js | 10 +- EmberLib/ParameterType.js | 5 +- EmberLib/QualifiedMatrix.js | 11 +- EmberLib/QualifiedParameter.js | 2 +- EmberLib/StreamDescription.js | 47 ++++++++ EmberLib/StreamFormat.js | 25 ++++ EmberLib/TreeNode.js | 17 ++- EmberServer/ElementHandlers.js | 52 +++++---- EmberServer/EmberServer.js | 76 +++++------- EmberServer/JSONParser.js | 7 +- EmberServer/MatrixHandlers.js | 12 +- EmberServer/QualifiedHandlers.js | 5 +- EmberServer/ServerEvents.js | 123 +++++++++++++++++++ EmberServer/index.js | 5 +- EmberSocket/S101Client.js | 13 ++- EmberSocket/S101Server.js | 2 +- EmberSocket/S101Socket.js | 4 +- ber.js | 8 +- errors.js | 195 +++++++++++++++++++++++++------ index.js | 4 +- package.json | 4 +- test/DeviceTree.test.js | 2 +- test/Ember.test.js | 125 +++++++++++++++++--- test/Server.test.js | 19 +-- 35 files changed, 672 insertions(+), 297 deletions(-) create mode 100755 EmberLib/StreamDescription.js create mode 100755 EmberLib/StreamFormat.js create mode 100755 EmberServer/ServerEvents.js diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index 8420c6e..8ac03f5 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -1,9 +1,9 @@ const EventEmitter = require('events').EventEmitter; -const S101Client = require('../EmberSocket').S101Socket; +const S101Socket = require('../EmberSocket').S101Socket; const ember = require('../EmberLib'); const BER = require('../ber.js'); const errors = require('../errors.js'); -const {Logger, LogLevel} = require("../Logger"); +const winston = require("winston"); const DEFAULT_PORT = 9000; const DEFAULT_TIMEOUT = 3000; @@ -28,18 +28,10 @@ class EmberClient extends EventEmitter { this._timeout = null; this._callback = undefined; this._requestID = 0; - this._client = new S101Client(host, port); + this._client = new S101Socket(host, port); this.timeoutValue = DEFAULT_TIMEOUT; /** @type {Root} */ this.root = new ember.Root(); - this.logger = new Logger(); - this.logLevel = LogLevel.INFO; - this._loggers = { - debug: (...args) => this._log(LogLevel.DEBUG, ...args), - error: (...args) => this._log(LogLevel.ERROR, ...args), - info: (...args) => this._log(LogLevel.INFO, ...args), - warn: (...args) => this._log(LogLevel.WARN, ...args) - }; this._client.on('connecting', () => { this.emit('connecting'); @@ -47,7 +39,7 @@ class EmberClient extends EventEmitter { this._client.on('connected', () => { this.emit('connected'); - if (this._callback !== undefined) { + if (this._callback != null) { this._callback(); } }); @@ -57,7 +49,7 @@ class EmberClient extends EventEmitter { }); this._client.on("error", e => { - if (this._callback !== undefined) { + if (this._callback != null) { this._callback(e); } this.emit("error", e); @@ -67,17 +59,17 @@ class EmberClient extends EventEmitter { try { if (root instanceof ember.InvocationResult) { this.emit('invocationResult', root); - this.log.debug("Received InvocationResult", root); + winston.debug("Received InvocationResult", root); } else { this._handleRoot(root); - this.log.debug("Received root", root); + winston.debug("Received root", root); } if (this._callback) { this._callback(undefined, root); } } catch(e) { - this.log.debug(e, root); + winston.debug(e, root); if (this._callback) { this._callback(e); } @@ -85,33 +77,13 @@ class EmberClient extends EventEmitter { }); } - /** - * - * @param {Array} params - * @private - */ - _log(...params) { - if ((params.length > 1) && (Number(params[0]) <= this.logLevel)) { - const msg = params.slice(1); - this.logger[Logger.LogLevel[params[0]]](`[${Logger.LogLevel[params[0]]}]:`, ...msg); - } - } - - /** - * - * @returns {{debug: (function(...[*]): void), error: (function(...[*]): void), info: (function(...[*]): void), warn: (function(...[*]): void)}|*} - */ - get log() { - return this._loggers; - } - _finishRequest() { this._clearTimeout(); this._activeRequest = null; try { this._makeRequest(); } catch(e) { - this.log.debug(e); + winston.debug(e); if (this._callback != null) { this._callback(e); } @@ -125,7 +97,7 @@ class EmberClient extends EventEmitter { const req = `${ this._requestID++} - ${this._activeRequest.node.getPath()}`; this._activeRequest.timeoutError = new errors.EmberTimeoutError(`Request ${req} timed out`) - this.log.debug(`Making request ${req}`, Date.now()); + winston.debug(`Making request ${req}`, Date.now()); this._timeout = setTimeout(() => { this._timeoutRequest(); }, this.timeoutValue); @@ -228,9 +200,9 @@ class EmberClient extends EventEmitter { * @param {TreeNode} root */ _handleRoot (root) { - this.log.debug("handling root", JSON.stringify(root)); + winston.debug("handling root", JSON.stringify(root)); this.root.update(root); - if (root.elements !== undefined) { + if (root.elements != null) { const elements = root.getChildren(); for (var i = 0; i < elements.length; i++) { if (elements[i].isQualified()) { @@ -259,7 +231,7 @@ class EmberClient extends EventEmitter { } return reject(e); }; - if ((this._client !== undefined) && (this._client.isConnected())) { + if ((this._client != null) && (this._client.isConnected())) { this._client.disconnect(); } this._client.connect(timeout); @@ -283,7 +255,7 @@ class EmberClient extends EventEmitter { */ expand(node, callback = null) { if (node == null) { - return Promise.reject(new Error("Invalid null node")); + return Promise.reject(new errors.InvalidEmberNode("Invalid null node")); } if (node.isParameter() || node.isMatrix() || node.isFunction()) { return this.getDirectory(node); @@ -291,7 +263,7 @@ class EmberClient extends EventEmitter { return this.getDirectory(node, callback).then((res) => { let children = node.getChildren(); if ((res === undefined) || (children === undefined) || (children === null)) { - this.log.debug("No more children for ", node); + winston.debug("No more children for ", node); return; } let p = Promise.resolve(); @@ -300,7 +272,7 @@ class EmberClient extends EventEmitter { // Parameter can only have a single child of type Command. continue; } - this.log.debug("Expanding child", child); + winston.debug("Expanding child", child); p = p.then(() => { return this.expand(child).catch((e) => { // We had an error on some expansion @@ -335,11 +307,11 @@ class EmberClient extends EventEmitter { this._callback = (error, node) => { const requestedPath = qnode.getPath(); if (node == null) { - this.log.debug(`received null response for ${requestedPath}`); + winston.debug(`received null response for ${requestedPath}`); return; } if (error) { - this.log.debug("Received getDirectory error", error); + winston.debug("Received getDirectory error", error); this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); reject(error); @@ -348,21 +320,21 @@ class EmberClient extends EventEmitter { if (qnode.isRoot()) { const elements = qnode.getChildren(); if (elements == null || elements.length === 0) { - this.log.debug("getDirectory response", node); - return this._callback(new Error("Invalid qnode for getDirectory")); + winston.debug("getDirectory response", node); + return this._callback(new errors.InvalidEmberNode()); } const nodeElements = node == null ? null : node.getChildren(); if (nodeElements != null && nodeElements.every(el => el._parent instanceof ember.Root)) { - this.log.debug("Received getDirectory response", node); + winston.debug("Received getDirectory response", node); this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); resolve(node); // make sure the info is treated before going to next request. } else { - return this._callback(new Error(`Invalid response for getDirectory ${requestedPath}`)); + return this._callback(new errors.InvalidEmberResponse(`getDirectory ${requestedPath}`)); } } else if (node.getElementByPath(requestedPath) != null) { @@ -375,18 +347,18 @@ class EmberClient extends EventEmitter { if (nodeElements != null && ((qnode.isMatrix() && nodeElements.length === 1 && nodeElements[0].getPath() === requestedPath) || (!qnode.isMatrix() && nodeElements.every(el => isDirectSubPathOf(el.getPath(), requestedPath))))) { - this.log.debug("Received getDirectory response", node); + winston.debug("Received getDirectory response", node); this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); return resolve(node); // make sure the info is treated before going to next request. } else { - this.log.debug(node); - this.log.debug(new Error(requestedPath)); + winston.debug(node); + winston.debug(new Error(requestedPath)); } } }; - this.log.debug("Sending getDirectory", qnode); + winston.debug("Sending getDirectory", qnode); this._client.sendBERNode(qnode.getDirectory(callback)); }}); }); @@ -417,7 +389,7 @@ class EmberClient extends EventEmitter { if (typeof path === 'string') { path = path.split('/'); } - var pathError = new Error(`Failed path discovery at ${path.slice(0, pos + 1).join("/")}`); + var pathError = new errors.PathDiscoveryFailure(path.slice(0, pos + 1).join("/")); var pos = 0; var lastMissingPos = -1; var currentNode = this.root; @@ -461,7 +433,7 @@ class EmberClient extends EventEmitter { if (typeof path === 'string') { path = path.split('.'); } - var pathnumError = new Error(`Failed path discovery at ${path.slice(0, pos).join("/")}`); + var pathnumError = new errors.PathDiscoveryFailure(path.slice(0, pos).join("/")); var pos = 0; var lastMissingPos = -1; var currentNode = this.root; @@ -514,13 +486,13 @@ class EmberClient extends EventEmitter { reject(error); } else { - this.log.debug("InvocationResult", result); + winston.debug("InvocationResult", result); resolve(result); } // cleaning callback and making next request. this._finishRequest(); }; - this.log.debug("Invocking function", fnNode); + winston.debug("Invocking function", fnNode); this._callback = cb; this._client.sendBERNode(fnNode.invoke(params)); }}); @@ -531,7 +503,7 @@ class EmberClient extends EventEmitter { * @returns {boolean} */ isConnected() { - return ((this._client !== undefined) && (this._client.isConnected())); + return ((this._client != null) && (this._client.isConnected())); } /** @@ -544,7 +516,7 @@ class EmberClient extends EventEmitter { matrixOPeration(matrixNode, targetID, sources, operation = ember.MatrixOperation.connect) { return new Promise((resolve, reject) => { if (!Array.isArray(sources)) { - return reject(new Error("Sources should be an array")); + return reject(new errors.InvalidSourcesFormat()); } try { matrixNode.validateConnection(targetID, sources); @@ -568,11 +540,11 @@ class EmberClient extends EventEmitter { this._callback = (error, node) => { const requestedPath = matrixNode.getPath(); if (node == null) { - this.log.debug(`received null response for ${requestedPath}`); + winston.debug(`received null response for ${requestedPath}`); return; } if (error) { - this.log.debug("Received getDirectory error", error); + winston.debug("Received getDirectory error", error); this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); reject(error); @@ -588,7 +560,7 @@ class EmberClient extends EventEmitter { resolve(matrix); } else { - this.log.debug(`unexpected node response during matrix connect ${requestedPath}`, + winston.debug(`unexpected node response during matrix connect ${requestedPath}`, matrix == null ? null : JSON.stringify(matrix.toJSON(), null, 4)); } } @@ -670,7 +642,7 @@ class EmberClient extends EventEmitter { }; this._callback = cb; - this.log.debug('setValue sending ...', node.getPath(), value); + winston.debug('setValue sending ...', node.getPath(), value); this._client.sendBERNode(node.setValue(value)); }}); } @@ -693,7 +665,7 @@ class EmberClient extends EventEmitter { if (error != null) { return reject(error); } - this.log.debug("Sending subscribe", qnode); + winston.debug("Sending subscribe", qnode); this._client.sendBERNode(qnode.subscribe(callback)); this._finishRequest(); resolve(); @@ -720,7 +692,7 @@ class EmberClient extends EventEmitter { if (error != null) { return reject(error); } - this.log.debug("Sending subscribe", qnode); + winston.debug("Sending subscribe", qnode); this._client.sendBERNode(qnode.unsubscribe(callback)); this._finishRequest(); resolve(); diff --git a/EmberLib/Function.js b/EmberLib/Function.js index ddc4aa4..0c54eab 100755 --- a/EmberLib/Function.js +++ b/EmberLib/Function.js @@ -1,5 +1,5 @@ "use strict"; -const TreeNode = require("./TreeNode"); +const Element = require("./Element"); const QualifiedFunction = require("./QualifiedFunction"); const BER = require('../ber.js'); const Command = require("./Command"); @@ -7,7 +7,7 @@ const {COMMAND_INVOKE} = require("./constants"); const FunctionContent = require("./FunctionContent"); const errors = require("../errors"); -class Function extends TreeNode { +class Function extends Element { constructor(number, func) { super(); this.number = number; diff --git a/EmberLib/FunctionContent.js b/EmberLib/FunctionContent.js index f5e040b..dacab9e 100755 --- a/EmberLib/FunctionContent.js +++ b/EmberLib/FunctionContent.js @@ -4,9 +4,11 @@ const FunctionArgument = require("./FunctionArgument"); const errors = require("../errors"); class FunctionContent { - constructor() { + constructor(identifier=null, description=null) { this.arguments = []; this.result = []; + this.identifier = identifier; + this.description = description; } /** diff --git a/EmberLib/InvocationResult.js b/EmberLib/InvocationResult.js index 901c60d..bfd9f00 100755 --- a/EmberLib/InvocationResult.js +++ b/EmberLib/InvocationResult.js @@ -3,7 +3,7 @@ const BER = require('../ber.js'); const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); const FunctionArgument = require("./FunctionArgument"); -const errors = require("../errors"); +const Errors = require("../errors"); class InvocationResult { @@ -65,7 +65,7 @@ class InvocationResult { */ setResult(result) { if (!Array.isArray(result)) { - throw new Error("Invalid inovation result. Should be array"); + throw new Errors.InvalidResultFormat(); } this.result = result; } @@ -107,7 +107,7 @@ class InvocationResult { continue } else { // TODO: options - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } diff --git a/EmberLib/Matrix.js b/EmberLib/Matrix.js index e0c7980..a8e3cdf 100755 --- a/EmberLib/Matrix.js +++ b/EmberLib/Matrix.js @@ -5,6 +5,7 @@ const BER = require('../ber.js'); const MatrixMode = require("./MatrixMode"); const MatrixOperation = require("./MatrixOperation"); const MatrixType = require("./MatrixType"); +const Errors = require("../errors"); class Matrix extends TreeNode { @@ -64,7 +65,7 @@ class Matrix extends TreeNode * @param {BER} ber */ encodeConnections(ber) { - if (this.connections !== undefined) { + if (this.connections != null) { ber.startSequence(BER.CONTEXT(5)); ber.startSequence(BER.EMBER_SEQUENCE); @@ -109,7 +110,7 @@ class Matrix extends TreeNode * @param {BER} ber */ encodeTargets(ber) { - if (this.targets !== undefined) { + if (this.targets != null) { ber.startSequence(BER.CONTEXT(3)); ber.startSequence(BER.EMBER_SEQUENCE); @@ -289,7 +290,7 @@ class Matrix extends TreeNode while(seq.remain > 0) { var conSeq = seq.getSequence(BER.CONTEXT(0)); var con = MatrixConnection.decode(conSeq); - if (con.target !== undefined) { + if (con.target != null) { connections[con.target] = (con); } } @@ -388,25 +389,25 @@ class Matrix extends TreeNode */ static validateConnection(matrixNode, targetID, sources) { if (targetID < 0) { - throw new Error(`Invalid negative target index ${targetID}`); + throw new Errors.InvalidEmberNode(matrixNode.getPath(), `Invalid negative target index ${targetID}`); } for(let i = 0; i < sources.length; i++) { if (sources[i] < 0) { - throw new Error(`Invalid negative source at index ${i}`); + throw new Errors.InvalidEmberNode(matrixNode.getPath(),`Invalid negative source at index ${i}`); } } if (matrixNode.contents.mode === MatrixMode.linear) { if (targetID >= matrixNode.contents.targetCount) { - throw new Error(`targetID ${targetID} higher than max value ${matrixNode.contents.targetCount}`); + throw Errors.InvalidEmberNode(matrixNode.getPath(),`targetID ${targetID} higher than max value ${matrixNode.contents.targetCount}`); } for(let i = 0; i < sources.length; i++) { if (sources[i] >= matrixNode.contents.sourceCount) { - throw new Error(`Invalid source at index ${i}`); + throw new Errors.InvalidEmberNode(matrixNode.getPath(),`Invalid source at index ${i}`); } } } else if ((matrixNode.targets == null) || (matrixNode.sources == null)) { - throw new Error("Non-Linear matrix should have targets and sources"); + throw new Errors.InvalidEmberNode(matrixNode.getPath(),"Non-Linear matrix should have targets and sources"); } else { let found = false; @@ -417,7 +418,7 @@ class Matrix extends TreeNode } } if (!found) { - throw new Error(`Unknown targetid ${targetID}`); + throw new Errors.InvalidEmberNode(matrixNode.getPath(),`Unknown targetid ${targetID}`); } found = false; for(let i = 0; i < sources.length; i++) { @@ -428,7 +429,7 @@ class Matrix extends TreeNode } } if (!found) { - throw new Error(`Unknown source at index ${i}`); + throw new Errors.InvalidEmberNode(matrixNode.getPath(),`Unknown source at index ${i}`); } } } diff --git a/EmberLib/MatrixConnection.js b/EmberLib/MatrixConnection.js index 51a817a..693eee2 100755 --- a/EmberLib/MatrixConnection.js +++ b/EmberLib/MatrixConnection.js @@ -62,17 +62,17 @@ class MatrixConnection { ber.writeInt(this.target); ber.endSequence(); - if ((this.sources !== undefined)&& (this.sources.length > 0)) { + if ((this.sources != null)&& (this.sources.length > 0)) { ber.startSequence(BER.CONTEXT(1)); ber.writeRelativeOID(this.sources.join("."), BER.EMBER_RELATIVE_OID); ber.endSequence(); } - if (this.operation !== undefined) { + if (this.operation != null) { ber.startSequence(BER.CONTEXT(2)); ber.writeInt(this.operation.value); ber.endSequence(); } - if (this.disposition !== undefined) { + if (this.disposition != null) { ber.startSequence(BER.CONTEXT(3)); ber.writeInt(this.disposition.value); ber.endSequence(); diff --git a/EmberLib/MatrixContents.js b/EmberLib/MatrixContents.js index 1f1d1a5..e75643d 100755 --- a/EmberLib/MatrixContents.js +++ b/EmberLib/MatrixContents.js @@ -18,47 +18,47 @@ class MatrixContents { */ encode(ber) { ber.startSequence(BER.EMBER_SET); - if (this.identifier !== undefined) { + if (this.identifier != null) { ber.startSequence(BER.CONTEXT(0)); ber.writeString(this.identifier, BER.EMBER_STRING); ber.endSequence(); } - if (this.description !== undefined) { + if (this.description != null) { ber.startSequence(BER.CONTEXT(1)); ber.writeString(this.description, BER.EMBER_STRING); ber.endSequence(); } - if (this.type !== undefined) { + if (this.type != null) { ber.startSequence(BER.CONTEXT(2)); ber.writeInt(this.type.value); ber.endSequence(); } - if (this.mode !== undefined) { + if (this.mode != null) { ber.startSequence(BER.CONTEXT(3)); ber.writeInt(this.mode.value); ber.endSequence(); } - if (this.targetCount !== undefined) { + if (this.targetCount != null) { ber.startSequence(BER.CONTEXT(4)); ber.writeInt(this.targetCount); ber.endSequence(); } - if (this.sourceCount !== undefined) { + if (this.sourceCount != null) { ber.startSequence(BER.CONTEXT(5)); ber.writeInt(this.sourceCount); ber.endSequence(); } - if (this.maximumTotalConnects !== undefined) { + if (this.maximumTotalConnects != null) { ber.startSequence(BER.CONTEXT(6)); ber.writeInt(this.maximumTotalConnects); ber.endSequence(); } - if (this.maximumConnectsPerTarget !== undefined) { + if (this.maximumConnectsPerTarget != null) { ber.startSequence(BER.CONTEXT(7)); ber.writeInt(this.maximumConnectsPerTarget); ber.endSequence(); } - if (this.parametersLocation !== undefined) { + if (this.parametersLocation != null) { ber.startSequence(BER.CONTEXT(8)); let param = Number(this.parametersLocation) if (isNaN(param)) { @@ -69,12 +69,12 @@ class MatrixContents { } ber.endSequence(); } - if (this.gainParameterNumber !== undefined) { + if (this.gainParameterNumber != null) { ber.startSequence(BER.CONTEXT(9)); ber.writeInt(this.gainParameterNumber); ber.endSequence(); } - if (this.labels !== undefined) { + if (this.labels != null) { ber.startSequence(BER.CONTEXT(10)); ber.startSequence(BER.EMBER_SEQUENCE); for(var i =0; i < this.labels.length; i++) { @@ -85,12 +85,12 @@ class MatrixContents { ber.endSequence(); ber.endSequence(); } - if (this.schemaIdentifiers !== undefined) { + if (this.schemaIdentifiers != null) { ber.startSequence(BER.CONTEXT(11)); ber.writeInt(this.schemaIdentifiers, BER.EMBER_STRING); ber.endSequence(); } - if (this.templateReference !== undefined) { + if (this.templateReference != null) { ber.startSequence(BER.CONTEXT(12)); ber.writeRelativeOID(this.templateReference, BER.EMBER_RELATIVE_OID); ber.endSequence(); diff --git a/EmberLib/MatrixNode.js b/EmberLib/MatrixNode.js index 5f309e2..0c86a76 100755 --- a/EmberLib/MatrixNode.js +++ b/EmberLib/MatrixNode.js @@ -23,7 +23,7 @@ class MatrixNode extends Matrix { ber.writeInt(this.number); ber.endSequence(); // BER.CONTEXT(0) - if(this.contents !== undefined) { + if(this.contents != null) { ber.startSequence(BER.CONTEXT(1)); this.contents.encode(ber); ber.endSequence(); // BER.CONTEXT(1) diff --git a/EmberLib/Node.js b/EmberLib/Node.js index 1617b28..6af1c6a 100755 --- a/EmberLib/Node.js +++ b/EmberLib/Node.js @@ -14,6 +14,8 @@ class Node extends Element { constructor(number) { super(number); this._seqID = BER.APPLICATION(3); + /** @type {NodeContents} */ + this.contents = null; } /** diff --git a/EmberLib/NodeContents.js b/EmberLib/NodeContents.js index 9260ed9..68aeede 100755 --- a/EmberLib/NodeContents.js +++ b/EmberLib/NodeContents.js @@ -3,8 +3,15 @@ const BER = require('../ber.js'); const errors = require("../errors"); class NodeContents{ - constructor() { + /** + * + * @param {string} identifier + * @param {string} description + */ + constructor(identifier=null, description=null) { this.isOnline = true; + this.identifier = identifier; + this.description = description; this._subscribers = new Set(); } @@ -15,31 +22,31 @@ class NodeContents{ encode(ber) { ber.startSequence(BER.EMBER_SET); - if(this.identifier !== undefined) { + if(this.identifier != null) { ber.startSequence(BER.CONTEXT(0)); ber.writeString(this.identifier, BER.EMBER_STRING); ber.endSequence(); // BER.CONTEXT(0) } - if(this.description !== undefined) { + if(this.description != null) { ber.startSequence(BER.CONTEXT(1)); ber.writeString(this.description, BER.EMBER_STRING); ber.endSequence(); // BER.CONTEXT(1) } - if(this.isRoot !== undefined) { + if(this.isRoot != null) { ber.startSequence(BER.CONTEXT(2)); ber.writeBoolean(this.isRoot); ber.endSequence(); // BER.CONTEXT(2) } - if(this.isOnline !== undefined) { + if(this.isOnline != null) { ber.startSequence(BER.CONTEXT(3)); ber.writeBoolean(this.isOnline); ber.endSequence(); // BER.CONTEXT(3) } - if(this.schemaIdentifiers !== undefined) { + if(this.schemaIdentifiers != null) { ber.startSequence(BER.CONTEXT(4)); ber.writeString(this.schemaIdentifiers, BER.EMBER_STRING); ber.endSequence(); // BER.CONTEXT(4) diff --git a/EmberLib/Parameter.js b/EmberLib/Parameter.js index 7357754..bad3eaf 100755 --- a/EmberLib/Parameter.js +++ b/EmberLib/Parameter.js @@ -49,7 +49,7 @@ class Parameter extends Element { * @param {Parameter} other */ update(other) { - if ((other !== undefined) && (other.contents !== undefined)) { + if ((other != null) && (other.contents != null)) { if (this.contents == null) { this.contents = other.contents; } diff --git a/EmberLib/ParameterContents.js b/EmberLib/ParameterContents.js index 7e79bbd..93a5345 100755 --- a/EmberLib/ParameterContents.js +++ b/EmberLib/ParameterContents.js @@ -10,11 +10,11 @@ const errors = require("../errors"); class ParameterContents { constructor(value, type) { this._subscribers = new Set(); - if(value !== undefined) { + if(value != null) { this.value = value; } - if(type !== undefined) { - if((type = ParameterType.get(type)) !== undefined){ + if(type != null) { + if((type = ParameterType.get(type)) != null){ this.type = type } } @@ -43,11 +43,11 @@ class ParameterContents { ber.writeIfDefinedEnum(this.type, ParameterType, ber.writeInt, 13); ber.writeIfDefined(this.streamIdentifier, ber.writeInt, 14); - if(this.stringIntegerCollection !== undefined) { + if(this.stringIntegerCollection != null) { this.stringIntegerCollection.encode(ber); } - if(this.streamDescriptor !== undefined) { + if(this.streamDescriptor != null) { ber.startSequence(BER.CONTEXT(16)); this.streamDescriptor.encode(ber); ber.endSequence(); diff --git a/EmberLib/ParameterType.js b/EmberLib/ParameterType.js index 1611237..6b9fb04 100755 --- a/EmberLib/ParameterType.js +++ b/EmberLib/ParameterType.js @@ -1,5 +1,6 @@ const Enum = require('enum'); const BER = require('../ber.js'); +const Errors = require("../errors"); function ParameterTypetoBERTAG(type) { switch (type.value) { @@ -9,7 +10,7 @@ function ParameterTypetoBERTAG(type) { case 4: return BER.EMBER_BOOLEAN; case 7: return BER.EMBER_OCTETSTRING; default: - throw new Error(`Unhandled ParameterType ${type}`); + throw new Errors.InvalidBERFormat(`Unhandled ParameterType ${type}`); } } @@ -21,7 +22,7 @@ function ParameterTypefromBERTAG(tag) { case BER.EMBER_BOOLEAN: return ParameterType.boolean; case BER.EMBER_OCTETSTRING: return ParameterType.octets; default: - throw new Error(`Unhandled BER TAB ${tag}`); + throw new Errors.InvalidBERFormat(`Unhandled BER TAB ${tag}`); } } diff --git a/EmberLib/QualifiedMatrix.js b/EmberLib/QualifiedMatrix.js index 7c15ed6..a746846 100755 --- a/EmberLib/QualifiedMatrix.js +++ b/EmberLib/QualifiedMatrix.js @@ -80,9 +80,6 @@ class QualifiedMatrix extends Matrix { * @returns {TreeNode} */ getDirectory(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } if (callback != null && !this.isStream()) { this.contents._subscribers.add(callback); } @@ -95,9 +92,6 @@ class QualifiedMatrix extends Matrix { * @returns {TreeNode} */ subscribe(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } if (callback != null && this.isStream()) { this.contents._subscribers.add(callback); } @@ -110,9 +104,6 @@ class QualifiedMatrix extends Matrix { * @returns {TreeNode} */ unsubscribe(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } if (callback != null && this.isStream()) { this.contents._subscribers.delete(callback); } @@ -147,7 +138,7 @@ class QualifiedMatrix extends Matrix { while(seq.remain > 0) { let conSeq = seq.getSequence(BER.CONTEXT(0)); let con = MatrixConnection.decode(conSeq); - if (con.target !== undefined) { + if (con.target != null) { qm.connections[con.target] = con; } } diff --git a/EmberLib/QualifiedParameter.js b/EmberLib/QualifiedParameter.js index 717c8d7..aec7990 100755 --- a/EmberLib/QualifiedParameter.js +++ b/EmberLib/QualifiedParameter.js @@ -56,7 +56,7 @@ class QualifiedParameter extends QualifiedElement { * @param {QualifiedParameter} other */ update(other) { - if ((other !== undefined) && (other.contents !== undefined)) { + if ((other != null) && (other.contents != null)) { if (this.contents == null) { this.contents = other.contents; } diff --git a/EmberLib/StreamDescription.js b/EmberLib/StreamDescription.js new file mode 100755 index 0000000..08087b3 --- /dev/null +++ b/EmberLib/StreamDescription.js @@ -0,0 +1,47 @@ +"use strict"; +const Element = require("./Element"); +const BER = require('../ber.js'); +const StreamFormat = require("./StreamFormat"); +const errors = require("../errors"); + +class StreamDescription extends Element{ + /** + * + */ + constructor() { + super(); + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(12)); + + ber.writeIfDefinedEnum(this.format, StreamFormat, ber.writeInt, 0); + ber.writeIfDefined(this.offset, ber.writeInt, 1); + + ber.endSequence(); + } + + static decode(ber) { + const sd = new StreamDescription(); + ber = ber.getSequence(BER.APPLICATION(12)); + + while(ber.remain > 0) { + var tag = ber.peek(); + var seq =ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + sd.format = StreamFormat.get(seq.readInt()); + } else if(tag == BER.CONTEXT(1)) { + sd.offset = seq.readInt(); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return sd; + } +} + +module.exports = StreamDescription; \ No newline at end of file diff --git a/EmberLib/StreamFormat.js b/EmberLib/StreamFormat.js new file mode 100755 index 0000000..ed3435d --- /dev/null +++ b/EmberLib/StreamFormat.js @@ -0,0 +1,25 @@ +"use strict"; +const Enum = require('enum'); + +const StreamFormat = new Enum({ + unsignedInt8: 0, + unsignedInt16BigEndian: 2, + unsignedInt16LittleEndian: 3, + unsignedInt32BigEndian: 4, + unsignedInt32LittleEndian: 5, + unsignedInt64BigEndian: 6, + unsignedInt64LittleENdian: 7, + signedInt8: 8, + signedInt16BigEndian: 10, + signedInt16LittleEndian: 11, + signedInt32BigEndian: 12, + signedInt32LittleEndian: 13, + signedInt64BigEndian: 14, + signedInt64LittleEndian: 15, + ieeeFloat32BigEndian: 20, + ieeeFloat32LittleEndian: 21, + ieeeFloat64BigEndian: 22, + ieeeFloat64LittleEndian: 23 +}); + +module.exports = StreamFormat; \ No newline at end of file diff --git a/EmberLib/TreeNode.js b/EmberLib/TreeNode.js index 94caf34..508da87 100755 --- a/EmberLib/TreeNode.js +++ b/EmberLib/TreeNode.js @@ -173,7 +173,7 @@ class TreeNode { else { obj = new this.constructor(this.number); } - if (this.contents !== undefined) { + if (this.contents != null) { obj.contents= this.contents; } return obj; @@ -198,11 +198,11 @@ class TreeNode { getTreeBranch(child, modifier) { const m = this.getMinimal(); - if(child !== undefined) { + if(child != null) { m.addChild(child); } - if(modifier !== undefined) { + if(modifier != null) { modifier(m); } @@ -336,7 +336,7 @@ class TreeNode { const children = this.getChildren(); if (children == null) return null; for(var i = 0; i < children.length; i++) { - if(children[i].contents !== undefined && + if(children[i].contents != null && children[i].contents.identifier == identifier) { return children[i]; } @@ -373,10 +373,7 @@ class TreeNode { } child = node.getElement(path[0]); if(child === null) { - //let e = new Error('invalid path: "' + path[0] + '"'); - // console.log(e.message ,"Self:", self, "Node:", node); - //callback(e); - //DO NOT REJECT !!!! We could still be updating the tree. + //DO NOT REJECT !!!! We could still be updating the tree. return; } else { child.getNodeByPath(client, path.slice(1), callback); @@ -427,7 +424,7 @@ class TreeNode { res.path = node.getPath(); if (node.contents) { for(let prop in node.contents) { - if (prop[0] == "_") { + if (prop[0] == "_" || node.contents[prop] == null) { continue; } if (node.contents.hasOwnProperty(prop)) { @@ -435,7 +432,7 @@ class TreeNode { if ((type === "string") || (type === "number")) { res[prop] = node.contents[prop]; } - else if (node.contents[prop].value !== undefined) { + else if (node.contents[prop].value != null) { res[prop] = node.contents[prop].value; } else { diff --git a/EmberServer/ElementHandlers.js b/EmberServer/ElementHandlers.js index c8d7079..a318500 100755 --- a/EmberServer/ElementHandlers.js +++ b/EmberServer/ElementHandlers.js @@ -1,6 +1,9 @@ "use strict"; const QualifiedHandlers = require("./QualifiedHandlers"); const EmberLib = require('../EmberLib'); +const ServerEvents = require("./ServerEvents"); +const Errors = require("../errors"); +const winston = require("winston"); class ElementHandlers extends QualifiedHandlers{ /** @@ -16,31 +19,34 @@ class ElementHandlers extends QualifiedHandlers{ * @param {TreeNode} root * @param {Command} cmd */ - handleCommand(client, element, cmd) { + handleCommand(client, element, cmd) { + let identifier = "root" + if (!element.isRoot()) { + const node = this.server.tree.getElementByPath(element.getPath()); + identifier = node == null || node.contents == null || node.contents.identifier == null ? "unknown" : node.contents.identifier; + } + const src = client == null ? "local" : `${client.socket.remoteAddress}:${client.socket.remotePort}`; switch(cmd.number) { case EmberLib.COMMAND_GETDIRECTORY: + this.server.emit("event", ServerEvents.GETDIRECTORY(identifier, element.getPath(), src)); this.handleGetDirectory(client, element); break; case EmberLib.COMMAND_SUBSCRIBE: + this.server.emit("event", ServerEvents.SUBSCRIBE(identifier, element.getPath(), src)); this.handleSubscribe(client, element); break; case EmberLib.COMMAND_UNSUBSCRIBE: + this.server.emit("event", ServerEvents.UNSUBSCRIBE(identifier, element.getPath(), src)); this.handleUnSubscribe(client, element); break; case EmberLib.COMMAND_INVOKE: + this.server.emit("event", ServerEvents.INVOKE(identifier, element.getPath(), src)); this.handleInvoke(client, cmd.invocation, element); break; default: - this.server.emit("error", new Error(`invalid command ${cmd.number}`)); + this.server.emit("error", new Errors.InvalidCommand(cmd.number)); return; - } - let identifier = "root" - if (!element.isRoot()) { - const node = this.server.tree.getElementByPath(element.getPath()); - identifier = node == null || node.contents == null || node.contents.identifier == null ? "unknown" : node.contents.identifier; - } - const src = client == null ? "local" : `${client.socket.remoteAddress}:${client.socket.remotePort}`; - this.server.emit("event", `${EmberLib.COMMAND_STRINGS[cmd.number]} to ${identifier}(path: ${element.getPath()}) from ${src}`); + } } /** @@ -49,7 +55,7 @@ class ElementHandlers extends QualifiedHandlers{ * @param {TreeNode} root */ handleGetDirectory(client, element) { - if (client !== undefined) { + if (client != null) { if ((element.isMatrix() || element.isParameter()) && (!element.isStream())) { // ember spec: parameter without streamIdentifier should @@ -70,7 +76,7 @@ class ElementHandlers extends QualifiedHandlers{ } const res = this.server.getQualifiedResponse(element); - this.server.log.debug("getDirectory response", res); + winston.debug("getDirectory response", res); client.sendBERNode(res); } } @@ -111,9 +117,9 @@ class ElementHandlers extends QualifiedHandlers{ // traverse the tree let element = node; let path = []; - while(element !== undefined) { + while(element != null) { if (element.number == null) { - this.server.emit("error", "invalid request"); + this.server.emit("error", new Errors.MissingElementNumber()); return; } if (element.isCommand()) { @@ -130,34 +136,34 @@ class ElementHandlers extends QualifiedHandlers{ let cmd = element; if (cmd == null) { - this.server.emit("error", "invalid request"); + this.server.emit("error", new Errors.InvalidRequest()); return this.server.handleError(client); } element = this.server.tree.getElementByPath(path.join(".")); if (element == null) { - this.server.emit("error", new Error(`unknown element at path ${path}`)); + this.server.emit("error", new Errors.UnknownElement(path.join("."))); return this.server.handleError(client); } if (cmd.isCommand()) { this.handleCommand(client, element, cmd); } - else if ((cmd.isCommand()) && (cmd.connections !== undefined)) { + else if ((cmd.isCommand()) && (cmd.connections != null)) { this.handleMatrixConnections(client, element, cmd.connections); } else if ((cmd.isParameter()) && - (cmd.contents !== undefined) && (cmd.contents.value !== undefined)) { - this.server.log.debug(`setValue for element at path ${path} with value ${cmd.contents.value}`); + (cmd.contents != null) && (cmd.contents.value != null)) { + winston.debug(`setValue for element at path ${path} with value ${cmd.contents.value}`); this.server.setValue(element, cmd.contents.value, client); const res = this.server.getResponse(element); client.sendBERNode(res) this.server.updateSubscribers(element.getPath(), res, client); } else { - this.server.emit("error", new Error("invalid request format")); - this.server.log.debug("invalid request format"); + this.server.emit("error", new Errors.InvalidRequesrFormat(path.join("."))); + winston.debug("invalid request format"); return this.server.handleError(client, element.getTreeBranch()); } return path; @@ -169,7 +175,7 @@ class ElementHandlers extends QualifiedHandlers{ * @param {TreeNode} root */ handleSubscribe(client, element) { - this.server.log.debug("subscribe"); + winston.debug("subscribe"); this.server.subscribe(client, element); } @@ -179,7 +185,7 @@ class ElementHandlers extends QualifiedHandlers{ * @param {TreeNode} root */ handleUnSubscribe(client, element) { - this.server.log.debug("unsubscribe"); + winston.debug("unsubscribe"); this.server.unsubscribe(client, element); } } diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js index 516e142..9fe7c73 100755 --- a/EmberServer/EmberServer.js +++ b/EmberServer/EmberServer.js @@ -3,7 +3,9 @@ const S101Server = require('../EmberSocket').S101Server; const ember = require('../EmberLib'); const JSONParser = require("./JSONParser"); const ElementHandlers = require("./ElementHandlers"); -const {Logger, LogLevel} = require("../Logger"); +const ServerEvents = require("./ServerEvents"); +const winston = require("winston"); +const Errors = require("../errors"); class TreeServer extends EventEmitter{ /** @@ -22,37 +24,29 @@ class TreeServer extends EventEmitter{ this.clients = new Set(); this.subscribers = {}; this._handlers = new ElementHandlers(this); - this.logger = new Logger(); - this.logLevel = LogLevel.INFO; - this._loggers = { - debug: (...args) => this._log(LogLevel.DEBUG, ...args), - error: (...args) => this._log(LogLevel.ERROR, ...args), - info: (...args) => this._log(LogLevel.INFO, ...args), - warn: (...args) => this._log(LogLevel.WARN, ...args) - }; this.server.on('listening', () => { - this.log.debug("listening"); + winston.debug("listening"); this.emit('listening'); - if (this.callback !== undefined) { + if (this.callback != null) { this.callback(); this.callback = undefined; } }); this.server.on('connection', client => { - this.log.debug("ember new connection from", client.remoteAddress()); + winston.debug("ember new connection from", client.remoteAddress()); this.clients.add(client); client.on("emberTree", (root) => { - this.log.debug("ember new request from", client.remoteAddress(), root); + winston.debug("ember new request from", client.remoteAddress(), root); // Queue the action to make sure responses are sent in order. client.addRequest(() => { try { - let path = this.handleRoot(client, root); + const path = this.handleRoot(client, root); this.emit("request", {client: client.remoteAddress(), root: root, path: path}); } catch(e) { - this.log.debug(e.stack) + winston.debug(e.stack) this.emit("error", e); } }); @@ -68,37 +62,18 @@ class TreeServer extends EventEmitter{ }); this.server.on('disconnected', () => { + this.clients.clear(); this.emit('disconnected'); }); this.server.on("error", (e) => { this.emit("error", e); - if (this.callback !== undefined) { + if (this.callback != null) { this.callback(e); } }); } - /** - * - * @param {Array} params - * @private - */ - _log(...params) { - if ((params.length > 1) && (Number(params[0]) <= this.logLevel)) { - const msg = params.slice(1); - this.logger[Logger.LogLevel[params[0]]](`[${Logger.LogLevel[params[0]]}]:`, ...msg); - } - } - - /** - * - * @returns {{debug: (function(...[*]): void), error: (function(...[*]): void), info: (function(...[*]): void), warn: (function(...[*]): void)}|*} - */ - get log() { - return this._loggers; - } - /** * @returns {Promise} */ @@ -111,6 +86,7 @@ class TreeServer extends EventEmitter{ return reject(e); }; this.server.server.close(); + this.clients.clear(); }); } @@ -121,14 +97,14 @@ class TreeServer extends EventEmitter{ getResponse(element) { return element.getTreeBranch(undefined, node => { node.update(element); - let children = element.getChildren(); + const children = element.getChildren(); if (children != null) { for (let i = 0; i < children.length; i++) { node.addChild(children[i].getDuplicate()); } } else { - this.log.debug("getResponse","no children"); + winston.debug("getResponse","no children"); } }); } @@ -143,7 +119,7 @@ class TreeServer extends EventEmitter{ if (element.isRoot() === false) { dup = element.toQualified(); } - let children = element.getChildren(); + const children = element.getChildren(); if (children != null) { for (let i = 0; i < children.length; i++) { res.addChild(children[i].toQualified().getMinimalContent()); @@ -161,7 +137,7 @@ class TreeServer extends EventEmitter{ * @param {TreeNode} root */ handleError(client, node) { - if (client !== undefined) { + if (client != null) { const res = node == null ? this.tree.getMinimal() : node; client.sendBERNode(res); } @@ -181,7 +157,7 @@ class TreeServer extends EventEmitter{ const node = root.getChildren()[0]; client.request = node; - if (node.path !== undefined) { + if (node.path != null) { return this._handlers.handleQualifiedNode(client, node); } else if (node.isCommand()) { @@ -247,7 +223,7 @@ class TreeServer extends EventEmitter{ let path = element.getPath(); let parent = this.tree.getElementByPath(path); if ((parent == null)||(parent._parent == null)) { - throw new Error(`Could not find element at path ${path}`); + throw new Errors.UnknownElement(path); } parent = parent._parent; const children = parent.getChildren(); @@ -277,20 +253,20 @@ class TreeServer extends EventEmitter{ } if (element.isParameter() || element.isMatrix()) { if (element.isParameter() && - (element.contents.access !== undefined) && + (element.contents.access != null) && (element.contents.access.value > 1)) { element.contents.value = value; const res = this.getResponse(element); this.updateSubscribers(element.getPath(),res, origin); } - else if ((key !== undefined) && (element.contents.hasOwnProperty(key))) { + else if ((key != null) && (element.contents.hasOwnProperty(key))) { element.contents[key] = value; const res = this.getResponse(element); this.updateSubscribers(element.getPath(),res, origin); } const src = origin == null ? "local" : `${origin.socket.remoteAddress}:${origin.socket.remotePort}`; this.emit("value-change", element); - this.emit("event", `set value for ${element.contents.identifier}(path: ${element.getPath()}) from ${src}` ); + this.emit("event", ServerEvents.SETVALUE(element.contents.identifier,element.getPath(),src)); } return resolve(); }); @@ -374,19 +350,19 @@ class TreeServer extends EventEmitter{ const validateMatrixOperation = function(matrix, target, sources) { if (matrix == null) { - throw new Error(`matrix not found`); + throw new Errors.UnknownElement(`matrix not found`); } if (matrix.contents == null) { - throw new Error(`invalid matrix at ${matrix.getPath()} : no contents`); + throw new Errors.MissingElementContents(matrix.getPath()); } if (matrix.contents.targetCount == null) { - throw new Error(`invalid matrix at ${matrix.getPath()} : no targetCount`); + throw new Errors.InvalidEmberNode(matrix.getPath(), "no targetCount"); } if ((target < 0) || (target >= matrix.contents.targetCount)) { - throw new Error(`target id ${target} out of range 0 - ${matrix.contents.targetCount}`); + throw new Errors.InvalidEmberNode(matrix.getPath(), `target id ${target} out of range 0 - ${matrix.contents.targetCount}`); } if (sources.length == null) { - throw new Error("invalid sources format"); + throw new Errors.InvalidSourcesFormat(); } } diff --git a/EmberServer/JSONParser.js b/EmberServer/JSONParser.js index 1908b11..9fcd617 100755 --- a/EmberServer/JSONParser.js +++ b/EmberServer/JSONParser.js @@ -1,5 +1,6 @@ "use strict"; const ember = require('../EmberLib'); +const Errors = require("../errors"); class JSONParser { /** @@ -43,7 +44,7 @@ class JSONParser { Number(content.sourceCount) : Number(content.maximumConnectsPerTarget); } else { - throw new Error(`Invalid matrix type ${content.type}`); + throw new Errors.InvalidEmberNode("", `Invalid matrix type ${content.type}`); } delete content.type; } @@ -55,7 +56,7 @@ class JSONParser { matrixContent.mode = ember.MatrixMode.nonLinear; } else { - throw new Error(`Invalid matrix mode ${content.mode}`); + throw new Errors.InvalidEmberNode("",`Invalid matrix mode ${content.mode}`); } delete content.mode; } @@ -70,7 +71,7 @@ class JSONParser { for(let i = 0; i < obj.length; i++) { let emberElement; let content = obj[i]; - let number = content.number !== undefined ? content.number : i; + let number = content.number != null ? content.number : i; delete content.number; if (content.value != null) { emberElement = new ember.Parameter(number); diff --git a/EmberServer/MatrixHandlers.js b/EmberServer/MatrixHandlers.js index d96e254..1e47d8e 100755 --- a/EmberServer/MatrixHandlers.js +++ b/EmberServer/MatrixHandlers.js @@ -1,5 +1,7 @@ "use strict"; const ember = require('../EmberLib'); +const ServerEvents = require("./ServerEvents"); +const winston = require("winston"); class MatrixHandlers { /** @@ -46,7 +48,7 @@ class MatrixHandlers { handleMatrixConnections(client, matrix, connections, response = true) { let res,conResult; let root; // ember message root - this.server.log.debug("Handling Matrix Connection"); + winston.debug("Handling Matrix Connection"); if (client != null && client.request.isQualified()) { root = new ember.Root(); res = new ember.QualifiedMatrix(matrix.getPath()); @@ -64,7 +66,9 @@ class MatrixHandlers { } let connection = connections[id]; const src = client == null ? "local" : `${client.socket.remoteAddress}:${client.socket.remotePort}`; - this.server.emit("event", `Matrix connection to ${matrix.contents.identifier}(path: ${matrix.getPath()}) target ${id} connections: ${connection.sources.toString()} from ${src}`); + this.server.emit("event", ServerEvents.MATRIX_CONNECTION( + matrix.contents.identifier,matrix.getPath(),src,id,connection.sources + )); conResult = new ember.MatrixConnection(connection.target); let emitType; res.connections[connection.target] = conResult; @@ -189,7 +193,7 @@ class MatrixHandlers { } } else if (conResult.disposition !== ember.MatrixDisposition.locked){ - this.server.log.debug(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); + winston.debug(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); conResult.disposition = ember.MatrixDisposition.tally; } @@ -209,7 +213,7 @@ class MatrixHandlers { } if (conResult != null && conResult.disposition !== ember.MatrixDisposition.tally) { - this.server.log.debug("Updating subscribers for matrix change"); + winston.debug("Updating subscribers for matrix change"); this.server.updateSubscribers(matrix.getPath(), root, client); } } diff --git a/EmberServer/QualifiedHandlers.js b/EmberServer/QualifiedHandlers.js index 03915ff..1589087 100755 --- a/EmberServer/QualifiedHandlers.js +++ b/EmberServer/QualifiedHandlers.js @@ -1,5 +1,6 @@ "use strict"; const MatrixHandlers = require("./MatrixHandlers"); +const Errors = require("../errors"); class QualifiedHandlers extends MatrixHandlers { /** @@ -32,7 +33,7 @@ class QualifiedHandlers extends MatrixHandlers { const element = this.server.tree.getElementByPath(path); if (element == null) { - this.server.emit("error", new Error(`unknown element at path ${path}`)); + this.server.emit("error", new Errors.UnknownElement(path)); return this.server.handleError(client); } @@ -63,7 +64,7 @@ class QualifiedHandlers extends MatrixHandlers { */ handleQualifiedParameter(client, element, parameter) { - if (parameter.contents.value !== undefined) { + if (parameter.contents.value != null) { this.server.setValue(element, parameter.contents.value, client); let res = this.server.getQualifiedResponse(element); client.sendBERNode(res) diff --git a/EmberServer/ServerEvents.js b/EmberServer/ServerEvents.js new file mode 100755 index 0000000..6e95910 --- /dev/null +++ b/EmberServer/ServerEvents.js @@ -0,0 +1,123 @@ +"use strict"; + +const Enum = require('enum'); + +const Types = new Enum({ + UNKNOWN: 0, + SETVALUE: 1, + GETDIRECTORY: 2, + SUBSCRIBE: 3, + UNSUBSCRIBE: 4, + INVOKE: 5, + MATRIX_CONNECTION: 6 +}); + +class ServerEvents { + /** + * + * @param {string} txt + * @param {number} type=0 + */ + constructor(txt, type=Types.UNKNOWN) { + /** @type {string} */ + this._txt = txt; + /** @type {number} */ + this._type = type; + this._timestamp = Date.now(); + } + + /** + * @returns {number} + */ + get type() { + return this._type; + } + + /** + * @returns {number} + */ + get timestamp() { + return this._timestamp; + } + + /** + * @returns {string} + */ + toString() { + return this._txt; + } + + /** + * @returns {Enum} + */ + static get Types() { + return Types; + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + */ + static SETVALUE(identifier,path, src) { + return new ServerEvents(`set value for ${identifier}(path: ${path}) from ${src}`, Types.SETVALUE); + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + */ + static GETDIRECTORY(identifier,path, src) { + return new ServerEvents(`getdirectory to ${identifier}(path: ${path}) from ${src}`, Types.GETDIRECTORY); + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + */ + static SUBSCRIBE(identifier,path, src) { + return new ServerEvents(`subscribe to ${identifier}(path: ${path}) from ${src}`, Types.SUBSCRIBE); + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + */ + static UNSUBSCRIBE(identifier,path, src) { + return new ServerEvents(`unsubscribe to ${identifier}(path: ${path}) from ${src}`, Types.UNSUBSCRIBE); + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + */ + static INVOKE(identifier,path, src) { + return new ServerEvents(`invoke to ${identifier}(path: ${path}) from ${src}`, Types.INVOKE); + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + * @param {number} target + * @param {number[]} sources + */ + static MATRIX_CONNECTION(identifier, path, src, target, sources) { + return new ServerEvents( + `Matrix connection to ${identifier}(path: ${path}) target ${target} connections: ${sources.toString()} from ${src}`, + Types.MATRIX_CONNECTION + ); + } +} + +module.exports = ServerEvents; \ No newline at end of file diff --git a/EmberServer/index.js b/EmberServer/index.js index 17080bd..2a6ba5c 100755 --- a/EmberServer/index.js +++ b/EmberServer/index.js @@ -1 +1,4 @@ -module.exports = require("./EmberServer"); \ No newline at end of file +module.exports = { + EmberServer: require("./EmberServer"), + ServerEvents: require("./ServerEvents") +}; \ No newline at end of file diff --git a/EmberSocket/S101Client.js b/EmberSocket/S101Client.js index 5e69b25..d87ca87 100755 --- a/EmberSocket/S101Client.js +++ b/EmberSocket/S101Client.js @@ -6,10 +6,17 @@ const BER = require('../ber.js'); const ember = require("../EmberLib"); class S101Client extends S101Socket { + /** + * + * @param {Socket} socket + * @param {S101Server} server + */ constructor(socket, server) { super() this.request = null; + /** @type {S101Server} */ this.server = server; + /** @type {Socket} */ this.socket = socket; this.pendingRequests = []; @@ -27,7 +34,7 @@ class S101Client extends S101Socket { const ber = new BER.Reader(packet); try { const root = ember.rootDecode(ber); - if (root !== undefined) { + if (root != null) { this.emit('emberTree', root); } } catch (e) { @@ -52,6 +59,10 @@ class S101Client extends S101Socket { } } + /** + * + * @param {function} cb + */ addRequest(cb) { this.pendingRequests.push(cb); this._makeRequest(); diff --git a/EmberSocket/S101Server.js b/EmberSocket/S101Server.js index 67dcb84..c712292 100755 --- a/EmberSocket/S101Server.js +++ b/EmberSocket/S101Server.js @@ -21,7 +21,7 @@ class S101Server extends EventEmitter { * @param {Socket} socket */ addClient(socket) { - var client = new S101Client(socket, this); + const client = new S101Client(socket, this); this.emit("connection", client); } /** diff --git a/EmberSocket/S101Socket.js b/EmberSocket/S101Socket.js index 7a2da2c..dfab372 100755 --- a/EmberSocket/S101Socket.js +++ b/EmberSocket/S101Socket.js @@ -64,7 +64,7 @@ class S101Socket extends EventEmitter{ const ber = new BER.Reader(packet); try { const root = ember.rootDecode(ber); - if (root !== undefined) { + if (root != null) { this.emit('emberTree', root); } } catch (e) { @@ -122,7 +122,7 @@ class S101Socket extends EventEmitter{ * @returns {boolean} */ isConnected() { - return ((this.socket !== null) && (this.socket !== undefined)); + return ((this.socket !== null) && (this.socket != null)); } /** diff --git a/ber.js b/ber.js index 8518c39..3a64b08 100755 --- a/ber.js +++ b/ber.js @@ -108,7 +108,7 @@ ExtendedReader.prototype.readValue = function() { ExtendedReader.prototype.readReal = function(tag) { - if(tag !== undefined) { + if(tag != null) { tag = UNIVERSAL(9); } @@ -313,7 +313,7 @@ ExtendedWriter.prototype.writeValue = function(value, tag) { } ExtendedWriter.prototype.writeIfDefined = function(property, writer, outer, inner) { - if(property !== undefined) { + if(property != null) { this.startSequence(CONTEXT(outer)); writer.call(this, property, inner); this.endSequence(); @@ -321,9 +321,9 @@ ExtendedWriter.prototype.writeIfDefined = function(property, writer, outer, inne } ExtendedWriter.prototype.writeIfDefinedEnum = function(property, type, writer, outer, inner) { - if(property !== undefined) { + if(property != null) { this.startSequence(CONTEXT(outer)); - if(property.value !== undefined) { + if(property.value != null) { writer.call(this, property.value, inner); } else { writer.call(this, type.get(property), inner); diff --git a/errors.js b/errors.js index 9aad982..dd37c02 100755 --- a/errors.js +++ b/errors.js @@ -1,59 +1,178 @@ -const util = require('util'); /**************************************************************************** * UnimplementedEmberType error ***************************************************************************/ -function UnimplementedEmberTypeError(tag) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - var identifier = (tag & 0xC0) >> 6; - var value = (tag & 0x1F).toString(); - var tagStr = tag.toString(); - if(identifier == 0) { - tagStr = "[UNIVERSAL " + value + "]"; - } else if(identifier == 1) { - tagStr = "[APPLICATION " + value + "]"; - } else if(identifier == 2) { - tagStr = "[CONTEXT " + value + "]"; - } else { - tagStr = "[PRIVATE " + value + "]"; - } - this.message = "Unimplemented EmBER type " + tagStr; -} - -util.inherits(UnimplementedEmberTypeError, Error); +class UnimplementedEmberTypeError extends Error { + constructor(tag) { + super(); + this.name = this.constructor.name; + var identifier = (tag & 0xC0) >> 6; + var value = (tag & 0x1F).toString(); + var tagStr = tag.toString(); + if(identifier == 0) { + tagStr = "[UNIVERSAL " + value + "]"; + } else if(identifier == 1) { + tagStr = "[APPLICATION " + value + "]"; + } else if(identifier == 2) { + tagStr = "[CONTEXT " + value + "]"; + } else { + tagStr = "[PRIVATE " + value + "]"; + } + this.message = "Unimplemented EmBER type " + tagStr; + } +} + module.exports.UnimplementedEmberTypeError = UnimplementedEmberTypeError; -function ASN1Error(message) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.message = message; +class ASN1Error extends Error { + constructor(message) { + super(message); + } } -util.inherits(ASN1Error, Error); module.exports.ASN1Error = ASN1Error; -function EmberAccessError(message) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - if(this.message !== undefined) { - this.message = message; - } else { - this.message("Parameter access error"); +class EmberAccessError extends Error { + constructor(message) { + super(message); } } -util.inherits(EmberAccessError, Error); module.exports.EmberAccessError = EmberAccessError; -function EmberTimeoutError(message) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.message = message; +class EmberTimeoutError extends Error { + constructor(message) { + super(message); + } } -util.inherits(EmberTimeoutError, Error); module.exports.EmberTimeoutError = EmberTimeoutError; +class InvalidCommand extends Error { + /** + * + * @param {number} number + */ + constructor(number) { + super(`Invalid command ${number}`); + } +} + +module.exports.InvalidCommand = InvalidCommand; + +class MissingElementNumber extends Error { + constructor() { + super("Missing element number"); + } +} + +module.exports.MissingElementNumber = MissingElementNumber; + +class MissingElementContents extends Error { + /** + * + * @param {string} path + */ + constructor(path) { + super(`Missing element contents at ${path}`); + } +} + +module.exports.MissingElementContents = MissingElementContents; + +class UnknownElement extends Error { + /** + * + * @param {string} path + */ + constructor(path) { + super(`No element at path ${path}`); + } +} + +module.exports.UnknownElement = UnknownElement; + +class InvalidRequest extends Error { + constructor() { + super("Invalid Request"); + } +} + +module.exports.InvalidRequest = InvalidRequest; + +class InvalidRequestFormat extends Error { + /** + * + * @param {string} path + */ + constructor(path) { + super(`Invalid Request Format with path ${path}`); + } +} + +module.exports.InvalidRequestFormat = InvalidRequestFormat; + +class InvalidEmberNode extends Error { + /** + * + * @param {string} path + * @param {string} info + */ + constructor(path="unknown", info="") { + super(`Invalid Ember Node at ${path}: ${info}`); + } +} +module.exports.InvalidEmberNode = InvalidEmberNode; + +class InvalidEmberResponse extends Error { + /** + * + * @param {string} req + */ + constructor(req) { + super(`Invalid Ember Response to ${req}`); + } +} +module.exports.InvalidEmberResponse = InvalidEmberResponse; + +class PathDiscoveryFailure extends Error { + /** + * + * @param {string} path + */ + constructor(path) { + super(`Failed path discovery at ${path}`); + } +} +module.exports.PathDiscoveryFailure = PathDiscoveryFailure; + +class InvalidSourcesFormat extends Error { + constructor() { + super("Sources should be an array"); + } +} +module.exports.InvalidSourcesFormat = InvalidSourcesFormat; + +class InvalidBERFormat extends Error { + /** + * + * @param {string} info + */ + constructor(info="") { + super(`Invalid BER format: ${info}`); + } +} +module.exports.InvalidBERFormat = InvalidBERFormat; + +class InvalidResultFormat extends Error { + /** + * + * @param {string} info + */ + constructor(info="") { + super(`Invalid Result format: ${info}`); + } +} +module.exports.InvalidResultFormat = InvalidResultFormat; \ No newline at end of file diff --git a/index.js b/index.js index 7b60331..1de9a28 100755 --- a/index.js +++ b/index.js @@ -2,6 +2,6 @@ const EmberClient = require('./EmberClient'); const EmberLib = require("./EmberLib"); const Decoder = EmberLib.DecodeBuffer; const S101 = require("./s101"); -const EmberServer = require("./EmberServer"); +const {EmberServer,ServerEvents} = require("./EmberServer"); const {S101Client} = require("./EmberSocket"); -module.exports = {EmberClient, Decoder, EmberLib, EmberServer, S101, S101Client}; +module.exports = {EmberClient, Decoder, EmberLib, EmberServer,ServerEvents, S101, S101Client}; diff --git a/package.json b/package.json index 5a931cb..bb05bd1 100755 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "node-emberplus", - "version": "2.2.1", + "version": "2.3.0", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { - "test": "jest test", + "test": "jest test --coverage", "eslint": "eslint ./", "start": "node server.js" }, diff --git a/test/DeviceTree.test.js b/test/DeviceTree.test.js index c072418..4f9e879 100755 --- a/test/DeviceTree.test.js +++ b/test/DeviceTree.test.js @@ -2,7 +2,7 @@ const fs = require("fs"); const sinon = require("sinon"); const Decoder = require('../EmberLib').DecodeBuffer; const EmberClient = require("../EmberClient"); -const EmberServer = require("../EmberServer"); +const {EmberServer} = require("../EmberServer"); const LOCALHOST = "127.0.0.1"; const UNKNOWN_HOST = "192.168.99.99"; diff --git a/test/Ember.test.js b/test/Ember.test.js index f9ea636..a333af1 100755 --- a/test/Ember.test.js +++ b/test/Ember.test.js @@ -5,29 +5,122 @@ const errorBuffer = Buffer.from("76fe000e0001c001021f026082008d6b820089a0176a15a const ember = require("../EmberLib"); const BER = require('../ber.js'); const errors = require('../errors.js'); +const EmberLib = require("../EmberLib"); +const identifier = "node_identifier"; +const description = "node_description"; describe("Ember", () => { - let client; + describe("generic", () => { + let client; - beforeAll(() => { - client = new S101Client(); - }); + beforeAll(() => { + client = new S101Client(); + }); + + it("should parse S101 message without error", (done) => { + client.on("emberPacket", () => { + done(); + }); + client.on("error", e => { + // eslint-disable-next-line no-console + console.log(e); + expect(e).toBeUndefined(); + done(); + }); + client.codec.dataIn(s101Buffer); + }); - it("should parse S101 message without error", (done) => { - client.on("emberPacket", () => { - done(); + it("should handle errors in message", () => { + var ber = new BER.Reader(errorBuffer); + expect(() => ember.Root.decode(ber)).toThrow(errors.UnimplementedEmberTypeError); + }); + }); + describe("Command", () => { + it("should throw error if unknown context found", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.APPLICATION(2)); + writer.startSequence(BER.CONTEXT(0)); + writer.writeInt(EmberLib.COMMAND_GETDIRECTORY); + writer.endSequence(); // BER.CONTEXT(0) + writer.startSequence(BER.CONTEXT(1)); + writer.writeInt(0); + writer.endSequence(); + writer.startSequence(BER.CONTEXT(3)); + writer.writeInt(0); + writer.endSequence(); + writer.endSequence(); // BER.APPLICATION(2) + try { + EmberLib.Command.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e).not.toBe("Should not succeed"); + } }); - client.on("error", e => { - console.log(e); - expect(e).toBeUndefined(); - done(); + it("should have a toJSON", () => { + const command = new EmberLib.Command(EmberLib.COMMAND_GETDIRECTORY); + const jsonCommand = command.toJSON(); + expect(jsonCommand.number).toBe(EmberLib.COMMAND_GETDIRECTORY); }); - client.codec.dataIn(s101Buffer); }); + describe("Node", () => { + it("should have an encoder", () => { + const node = new EmberLib.Node(0); + const identifier = "node_identifier"; + const description = "node_description"; + node.contents = new EmberLib.NodeContents(identifier, description); + const root = new EmberLib.Node(0); + root.addChild(node); + const writer = new BER.Writer(); + root.encode(writer); + expect(writer.buffer.size).not.toBe(0); - it("should handle errors in message", () => { - var ber = new BER.Reader(errorBuffer); - expect(() => ember.Root.decode(ber)).toThrow(errors.UnimplementedEmberTypeError); + }); + it("should have a decoder", () => { + const node = new EmberLib.Node(0); + node.contents = new EmberLib.NodeContents(identifier, description); + const writer = new BER.Writer(); + node.encode(writer); + const n = EmberLib.Node.decode(new BER.Reader(writer.buffer)); + expect(n.number).toBe(node.number); + expect(n.contents.identifier).toBe(identifier); + expect(n.contents.description).toBe(description); + }); + }); + describe("Function", () => { + let func; + beforeAll(() => { + func = new EmberLib.Function(0, args => { + const res = new EmberLib.FunctionArgument(); + res.type = EmberLib.ParameterType.integer; + res.value = args[0].value + args[1].value; + return [res]; + }); + }) + it("should return true when calling isFunction", () => { + expect(func.isFunction()).toBeTruthy(); + }); + it("should have an invoke function", () => { + const invoke = func.invoke(); + const children = invoke.getChildren(); + expect(children.length).toBe(1); + expect(children[0].isCommand()).toBeTruthy(); + }); + it("should have a decoder", () => { + func.contents = new EmberLib.FunctionContent(identifier, description); + func.contents.arguments = [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer,null, "arg1"), + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer,null, "arg2") + ]; + func.contents.result = [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer,null, "result") + ]; + const writer = new BER.Writer(); + func.encode(writer); + const f = EmberLib.Function.decode(new BER.Reader(writer.buffer)); + expect(f.number).toBe(func.number); + expect(f.contents.identifier).toBe(identifier); + expect(f.contents.description).toBe(description); + }); }); - }); diff --git a/test/Server.test.js b/test/Server.test.js index d11a682..85a898a 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -1,5 +1,5 @@ const expect = require("expect"); -const EmberServer = require("../EmberServer"); +const {EmberServer, ServerEvents} = require("../EmberServer"); const EmberClient = require("../EmberClient"); const ember = require("../EmberLib"); const {jsonRoot} = require("./utils"); @@ -8,12 +8,6 @@ const MatrixHandlers = require("../EmberServer/MatrixHandlers"); const LOCALHOST = "127.0.0.1"; const PORT = 9009; -const wait = function(t) { - return new Promise(resolve => { - setTimeout(resolve, t); - }); -} - describe("server", function() { describe("JSONtoTree", function() { let jsonTree; @@ -182,7 +176,6 @@ describe("server", function() { throw new Error("Should not succeed"); }) .catch(e => { - console.log(e); expect(e.message).toMatch(/Failed path discovery/); return client.disconnect(); }); @@ -224,7 +217,7 @@ describe("server", function() { }) .then(() => { expect(count).toBe(1); - expect(receivedEvent).toMatch(/getdirectory to root/); + expect(receivedEvent.type).toBe(ServerEvents.Types.GETDIRECTORY); return client.getElementByPath("0.1.0"); }) .then(matrix => { @@ -233,7 +226,7 @@ describe("server", function() { }) .then(() => { expect(count).toBe(1); - expect(receivedEvent).toMatch(/Matrix connection to matrix/); + expect(receivedEvent.type).toBe(ServerEvents.Types.MATRIX_CONNECTION); }) .then(() => { count = 0; @@ -245,7 +238,7 @@ describe("server", function() { }) .then(() => { expect(count).toBe(1); - expect(receivedEvent).toMatch(/invoke to /); + expect(receivedEvent.type).toBe(ServerEvents.Types.INVOKE); }) .then(() => client.getNodeByPathnum("0.0.2")) .then(parameter => { @@ -263,7 +256,7 @@ describe("server", function() { }) .then(() => { expect(count).toBe(1); - expect(receivedEvent).toMatch(/subscribe to version/); + expect(receivedEvent.type).toBe(ServerEvents.Types.SUBSCRIBE); }) .then(() => { server.off("event", eventHandler); @@ -281,7 +274,7 @@ describe("server", function() { }); it("should verify if connection allowed in 1-to-N", function() { let disconnectCount = 0; - const handleDisconnect = info => { + const handleDisconnect = () => { disconnectCount++; } server.on("matrix-disconnect", handleDisconnect.bind(this)); From d05a8bd146a39dad92d3afb155ddbc71e6f4b6a1 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sat, 11 Jan 2020 21:46:50 +0100 Subject: [PATCH 42/64] Added code coverage for Parameter --- EmberLib/ParameterContents.js | 5 ++++ test/Ember.test.js | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/EmberLib/ParameterContents.js b/EmberLib/ParameterContents.js index 93a5345..54548c6 100755 --- a/EmberLib/ParameterContents.js +++ b/EmberLib/ParameterContents.js @@ -8,6 +8,11 @@ const BER = require('../ber.js'); const errors = require("../errors"); class ParameterContents { + /** + * + * @param {string|number} value + * @param {string} type + */ constructor(value, type) { this._subscribers = new Set(); if(value != null) { diff --git a/test/Ember.test.js b/test/Ember.test.js index a333af1..4bb9f81 100755 --- a/test/Ember.test.js +++ b/test/Ember.test.js @@ -69,6 +69,9 @@ describe("Ember", () => { const identifier = "node_identifier"; const description = "node_description"; node.contents = new EmberLib.NodeContents(identifier, description); + node.contents.isRoot = true; + node.contents.isOnline = true; + node.contents.schemaIdentifiers = "schema1"; const root = new EmberLib.Node(0); root.addChild(node); const writer = new BER.Writer(); @@ -79,6 +82,9 @@ describe("Ember", () => { it("should have a decoder", () => { const node = new EmberLib.Node(0); node.contents = new EmberLib.NodeContents(identifier, description); + node.contents.isRoot = true; + node.contents.isOnline = true; + node.contents.schemaIdentifiers = "schema1"; const writer = new BER.Writer(); node.encode(writer); const n = EmberLib.Node.decode(new BER.Reader(writer.buffer)); @@ -123,4 +129,48 @@ describe("Ember", () => { expect(f.contents.description).toBe(description); }); }); + describe("Parameter", () => { + it("should have an update function", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE = 1; + let count = 0; + parameter.contents = new EmberLib.ParameterContents(VALUE, "integer"); + parameter.contents._subscribers.add(() => {count++;}); + const newParameter = new EmberLib.Parameter(0); + const NEW_VALUE = VALUE + 1; + newParameter.contents = new EmberLib.ParameterContents(NEW_VALUE, "integer"); + parameter.update(newParameter); + expect(count).toBe(1); + expect(parameter.contents.value).toBe(NEW_VALUE); + }); + it("should have setValue function", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE = 1; + parameter.contents = new EmberLib.ParameterContents(VALUE, "integer"); + const NEW_VALUE = VALUE + 1; + const setVal = parameter.setValue(NEW_VALUE); + expect(setVal.contents.value).toBe(NEW_VALUE); + }); + it("should have decoder function", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE = 1; + parameter.contents = new EmberLib.ParameterContents(VALUE, "integer"); + parameter.contents.minimum = 0; + parameter.contents.maximum = 100; + parameter.contents.access = EmberLib.ParameterAccess.readWrite; + parameter.contents.format = "db"; + parameter.contents.factor = 10; + parameter.contents.isOnline = true; + parameter.contents.formula = "x10"; + parameter.contents.step = 2; + parameter.contents.default = 0; + parameter.contents.type = EmberLib.ParameterType.integer; + const node = new EmberLib.Node(0); + parameter.addChild(node); + const writer = new BER.Writer(); + parameter.encode(writer); + const newParameter = EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); + expect(newParameter.getChildren().length).toBe(1); + }); + }); }); From b129794ad539c22fe35b004a1a1b607f2dc51688 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sun, 12 Jan 2020 10:51:10 +0100 Subject: [PATCH 43/64] Matrix code coverage. Bug fixing --- EmberLib/Matrix.js | 29 +++-- EmberLib/index.js | 2 + ber.js | 6 - errors.js | 14 ++- test/Ember.test.js | 265 ++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 296 insertions(+), 20 deletions(-) diff --git a/EmberLib/Matrix.js b/EmberLib/Matrix.js index a8e3cdf..df2c2ea 100755 --- a/EmberLib/Matrix.js +++ b/EmberLib/Matrix.js @@ -13,6 +13,9 @@ class Matrix extends TreeNode super(); this._connectedSources = {}; this._numConnections = 0; + this.targets = null; + this.sources = null; + this.connections = {}; } isMatrix() { @@ -175,17 +178,23 @@ class Matrix extends TreeNode * @returns {boolean} */ static canConnect(matrixNode, targetID, sources, operation) { + if (matrixNode.connections == null) { + matrixNode.connections = {}; + } + if (matrixNode.connections[targetID] == null) { + matrixNode.connections[targetID] = new MatrixConnection(targetID); + } const type = matrixNode.contents.type == null ? MatrixType.oneToN : matrixNode.contents.type; const connection = matrixNode.connections[targetID]; const oldSources = connection == null || connection.sources == null ? [] : connection.sources.slice(); const newSources = operation === MatrixOperation.absolute ? sources : oldSources.concat(sources); const sMap = new Set(newSources.map(i => Number(i))); - + if (matrixNode.connections[targetID].isLocked()) { return false; } if (type === MatrixType.oneToN && - matrixNode.contents.maximumConnectsPerTarget == null && + matrixNode.contents.maximumTotalConnects == null && matrixNode.contents.maximumConnectsPerTarget == null) { return sMap.size < 2; } @@ -389,20 +398,20 @@ class Matrix extends TreeNode */ static validateConnection(matrixNode, targetID, sources) { if (targetID < 0) { - throw new Errors.InvalidEmberNode(matrixNode.getPath(), `Invalid negative target index ${targetID}`); + throw new Errors.InvalidMatrixSignal(targetID, "target"); } for(let i = 0; i < sources.length; i++) { if (sources[i] < 0) { - throw new Errors.InvalidEmberNode(matrixNode.getPath(),`Invalid negative source at index ${i}`); + throw new Errors.InvalidMatrixSignal(sources[i], `Source at index ${i}`); } } if (matrixNode.contents.mode === MatrixMode.linear) { if (targetID >= matrixNode.contents.targetCount) { - throw Errors.InvalidEmberNode(matrixNode.getPath(),`targetID ${targetID} higher than max value ${matrixNode.contents.targetCount}`); + throw new Errors.InvalidMatrixSignal(targetID, `Target higher than max value ${matrixNode.contents.targetCount}`); } for(let i = 0; i < sources.length; i++) { if (sources[i] >= matrixNode.contents.sourceCount) { - throw new Errors.InvalidEmberNode(matrixNode.getPath(),`Invalid source at index ${i}`); + throw new Errors.InvalidMatrixSignal(sources[i],`Source at index ${i} higher than max ${matrixNode.contents.sourceCount}`); } } } @@ -411,25 +420,25 @@ class Matrix extends TreeNode } else { let found = false; - for(let i = 0; i < matrixNode.targets; i++) { + for(let i = 0; i < matrixNode.targets.length; i++) { if (matrixNode.targets[i] === targetID) { found = true; break; } } if (!found) { - throw new Errors.InvalidEmberNode(matrixNode.getPath(),`Unknown targetid ${targetID}`); + throw new Errors.InvalidMatrixSignal(targetID, "Not part of existing targets"); } found = false; for(let i = 0; i < sources.length; i++) { - for(let j = 0; i < matrixNode.sources; j++) { + for(let j = 0; j < matrixNode.sources.length; j++) { if (matrixNode.sources[j] === sources[i]) { found = true; break; } } if (!found) { - throw new Errors.InvalidEmberNode(matrixNode.getPath(),`Unknown source at index ${i}`); + throw new Errors.InvalidMatrixSignal(sources[i],`Unknown source at index ${i}`); } } } diff --git a/EmberLib/index.js b/EmberLib/index.js index 92bf00c..6da638d 100755 --- a/EmberLib/index.js +++ b/EmberLib/index.js @@ -10,6 +10,7 @@ const FunctionContent = require("./FunctionContent"); const Invocation = require("./Invocation"); const InvocationResult = require("./InvocationResult"); const Label = require("./Label"); +const Matrix = require("./Matrix"); const MatrixNode = require("./MatrixNode"); const MatrixMode = require("./MatrixMode"); const MatrixType = require("./MatrixType"); @@ -128,6 +129,7 @@ module.exports = { Invocation, InvocationResult, Label, + Matrix, MatrixNode, MatrixMode, MatrixType, diff --git a/ber.js b/ber.js index 3a64b08..03d22d9 100755 --- a/ber.js +++ b/ber.js @@ -74,12 +74,6 @@ function ExtendedReader(data) { util.inherits(ExtendedReader, BER.Reader); module.exports.Reader = ExtendedReader; - -readBlock = function(ber) { - -} - - ExtendedReader.prototype.getSequence = function(tag) { var buf = this.readString(tag, true); return new ExtendedReader(buf); diff --git a/errors.js b/errors.js index dd37c02..e89d463 100755 --- a/errors.js +++ b/errors.js @@ -175,4 +175,16 @@ class InvalidResultFormat extends Error { super(`Invalid Result format: ${info}`); } } -module.exports.InvalidResultFormat = InvalidResultFormat; \ No newline at end of file +module.exports.InvalidResultFormat = InvalidResultFormat; + +class InvalidMatrixSignal extends Error { + /** + * + * @param {number} value + * @param {string} info + */ + constructor(value, info) { + super(`Invalid Matrix Signal ${value}: ${info}`); + } +} +module.exports.InvalidMatrixSignal = InvalidMatrixSignal; \ No newline at end of file diff --git a/test/Ember.test.js b/test/Ember.test.js index 4bb9f81..80c7789 100755 --- a/test/Ember.test.js +++ b/test/Ember.test.js @@ -4,7 +4,7 @@ const s101Buffer = Buffer.from("fe000e0001c001021f026082008d6b820089a0176a15a005 const errorBuffer = Buffer.from("76fe000e0001c001021f026082008d6b820089a0176a15a0050d03010201a10c310aa0080c066c6162656c73a01b6a19a0050d03010202a110310ea00c0c0a706172616d6574657273a051714fa0050d03010203a1463144a0080c066d6174726978a403020104a503020104aa183016a0147212a0050d03010201a1090c075072696d617279a203020102a303020101a8050d03010202a903020101f24cff", "hex"); const ember = require("../EmberLib"); const BER = require('../ber.js'); -const errors = require('../errors.js'); +const Errors = require('../errors.js'); const EmberLib = require("../EmberLib"); const identifier = "node_identifier"; @@ -30,9 +30,9 @@ describe("Ember", () => { client.codec.dataIn(s101Buffer); }); - it("should handle errors in message", () => { + it("should handle Errors in message", () => { var ber = new BER.Reader(errorBuffer); - expect(() => ember.Root.decode(ber)).toThrow(errors.UnimplementedEmberTypeError); + expect(() => ember.Root.decode(ber)).toThrow(Errors.UnimplementedEmberTypeError); }); }); describe("Command", () => { @@ -173,4 +173,263 @@ describe("Ember", () => { expect(newParameter.getChildren().length).toBe(1); }); }); + describe("Matrix", () => { + describe("validateConnection", () => { + let matrixNode; + const TARGETCOUNT = 5; + const SOURCECOUNT = 5; + beforeAll(() => { + matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.contents.targetCount = TARGETCOUNT; + matrixNode.contents.sourceCount = SOURCECOUNT; + }); + it("should through an error if target is negative", () => { + try { + EmberLib.Matrix.validateConnection(matrixNode, -1, []); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should through an error if source is negative", () => { + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [-1]); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should through an error if target higher than max target", () => { + try { + EmberLib.Matrix.validateConnection(matrixNode, TARGETCOUNT, [0]); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should through an error if target higher than max target", () => { + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [SOURCECOUNT]); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should through an error if non-Linear Matrix without targets", () => { + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [0]); + throw new Error("Should not succeed"); + } + catch(e) { + matrixNode.contents.mode = EmberLib.MatrixMode.linear; + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); + it("should through an error if non-Linear Matrix without sources", () => { + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + matrixNode.targets = [0, 3]; + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [0]); + throw new Error("Should not succeed"); + } + catch(e) { + matrixNode.contents.mode = EmberLib.MatrixMode.linear; + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); + it("should through an error if non-Linear Matrix and not valid target", () => { + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + matrixNode.targets = [0, 3]; + matrixNode.sources = [0, 3]; + try { + EmberLib.Matrix.validateConnection(matrixNode, 1, [0]); + throw new Error("Should not succeed"); + } + catch(e) { + matrixNode.contents.mode = EmberLib.MatrixMode.linear; + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should through an error if non-Linear Matrix and not valid source", () => { + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + matrixNode.targets = [0, 3]; + matrixNode.sources = [0, 3]; + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [1]); + throw new Error("Should not succeed"); + } + catch(e) { + matrixNode.contents.mode = EmberLib.MatrixMode.linear; + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should not through an error on valid non-linear connect", () => { + let error = null; + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + matrixNode.targets = [0, 3]; + matrixNode.sources = [0, 3]; + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [0]); + } + catch(e) { + error = e; + } + expect(error == null).toBeTruthy(); + }); + }); + describe("MatrixUpdate", () => { + let matrixNode; + const TARGETCOUNT = 5; + const SOURCECOUNT = 5; + beforeAll(() => { + matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.contents.targetCount = TARGETCOUNT; + matrixNode.contents.sourceCount = SOURCECOUNT; + }); + it("should not through an error on valid non-linear connect", () => { + let error = null; + const newMatrixNode = new EmberLib.MatrixNode(0); + newMatrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + newMatrixNode.targets = [0, 3]; + newMatrixNode.sources = [0, 3]; + newMatrixNode.contents.identifier = "matrix"; + newMatrixNode.contents.description = "matrix"; + + + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + matrixNode.connections = null; + try { + EmberLib.Matrix.MatrixUpdate(matrixNode, newMatrixNode); + } + catch(e) { + error = e; + } + expect(error == null).toBeTruthy(); + expect(matrixNode.targets).toBeDefined(); + expect(matrixNode.targets.length).toBe(newMatrixNode.targets.length); + expect(matrixNode.sources.length).toBe(newMatrixNode.sources.length); + }); + }); + describe("disconnectSources", () => { + let matrixNode; + const TARGETCOUNT = 5; + const SOURCECOUNT = 5; + beforeAll(() => { + matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.contents.targetCount = TARGETCOUNT; + matrixNode.contents.sourceCount = SOURCECOUNT; + }); + it("should generate the connection structure if not existent", () => { + EmberLib.Matrix.disconnectSources(matrixNode, 0, [1]); + }); + }); + describe("decodeConnections", () => { + let matrixNode; + const TARGETCOUNT = 5; + const SOURCECOUNT = 5; + beforeAll(() => { + matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.contents.targetCount = TARGETCOUNT; + matrixNode.contents.sourceCount = SOURCECOUNT; + }); + it("should generate the connection structure if not existent", () => { + const SOURCEID = 0; + EmberLib.Matrix.connectSources(matrixNode, 0, [SOURCEID]); + const writer = new BER.Writer(); + matrixNode.encodeConnections(writer); + const ber = new BER.Reader(writer.buffer); + const seq = ber.getSequence(BER.CONTEXT(5)); + const connections = EmberLib.Matrix.decodeConnections(seq); + expect(connections[0].sources).toBeDefined(); + expect(connections[0].sources.length).toBe(1); + expect(connections[0].sources[0]).toBe(SOURCEID); + }); + }); + describe("canConnect", () => { + let matrixNode; + const TARGETCOUNT = 5; + const SOURCECOUNT = 5; + beforeAll(() => { + matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.contents.targetCount = TARGETCOUNT; + matrixNode.contents.sourceCount = SOURCECOUNT; + }); + it("should return false if more than 1 source in 1toN", () => { + matrixNode.connections = null; + matrixNode.contents.maximumTotalConnects = 1; + const res = EmberLib.Matrix.canConnect(matrixNode, 0, [0,3]); + expect(res).toBeFalsy(); + }); + }); + describe("Matrix Non-Linear", () => { + it("should have encoder / decoder", () => { + const matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.targets = [0,3]; + matrixNode.sources = [1,2]; + const writer = new BER.Writer(); + matrixNode.encode(writer); + const newMatrixNode = EmberLib.Matrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.targets).toBeDefined(); + }); + it("should have connect function", () => { + const root = new EmberLib.Root(); + const matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.targets = [0,3]; + matrixNode.sources = [1,2]; + root.addChild(matrixNode); + const connect = matrixNode.connect({0: new EmberLib.MatrixConnection(0)}); + expect(connect).toBeDefined(); + }); + }); + }); }); From 3ee90868f12e4819563f8396b3876f1d508bfa89 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 13 Jan 2020 18:36:40 +0100 Subject: [PATCH 44/64] Adding test code coverage. EmberLib 100% upto Matrix --- EmberClient/EmberClient.js | 6 +- EmberLib/Command.js | 10 +- EmberLib/Function.js | 15 +- EmberLib/FunctionArgument.js | 35 ++- EmberLib/FunctionContent.js | 20 +- EmberLib/Invocation.js | 56 +++- EmberLib/InvocationResult.js | 16 +- EmberLib/Label.js | 22 +- EmberLib/Matrix.js | 5 +- EmberLib/MatrixContents.js | 4 +- EmberLib/MatrixNode.js | 9 +- EmberLib/Node.js | 25 +- EmberLib/Parameter.js | 39 +-- EmberLib/QualifiedElement.js | 24 -- EmberLib/QualifiedFunction.js | 13 +- EmberLib/QualifiedMatrix.js | 11 +- EmberLib/QualifiedNode.js | 11 +- EmberLib/QualifiedParameter.js | 11 +- EmberLib/StreamDescription.js | 32 ++- EmberLib/StringIntegerCollection.js | 77 +++--- EmberLib/TreeNode.js | 78 ++++-- EmberLib/index.js | 2 + ber.js | 10 + embrionix.ember | Bin 41743 -> 0 bytes test/DeviceTree.test.js | 2 +- test/Ember.test.js | 409 +++++++++++++++++++++++++++- test/Server.test.js | 67 ++--- 27 files changed, 771 insertions(+), 238 deletions(-) delete mode 100755 embrionix.ember diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index 8ac03f5..f2d2d43 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -683,10 +683,6 @@ class EmberClient extends EventEmitter { */ unsubscribe(qnode, callback) { if (qnode.isParameter() && qnode.isStream()) { - if (qnode == null) { - this.root.clear(); - qnode = this.root; - } return new Promise((resolve, reject) => { this.addRequest({node: qnode, func: (error) => { if (error != null) { @@ -698,7 +694,7 @@ class EmberClient extends EventEmitter { resolve(); }}); }); - } + } } } diff --git a/EmberLib/Command.js b/EmberLib/Command.js index 3ff9d6a..f91b78b 100755 --- a/EmberLib/Command.js +++ b/EmberLib/Command.js @@ -40,7 +40,7 @@ class Command { * @param {BER} ber */ encode(ber) { - ber.startSequence(BER.APPLICATION(2)); + ber.startSequence(Command.BERID); ber.startSequence(BER.CONTEXT(0)); ber.writeInt(this.number); @@ -87,7 +87,7 @@ class Command { */ static decode(ber) { const c = new Command(); - ber = ber.getSequence(BER.APPLICATION(2)); + ber = ber.getSequence(Command.BERID); while(ber.remain > 0) { let tag = ber.peek(); @@ -110,6 +110,12 @@ class Command { return c; } + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(2); + } } module.exports = Command; \ No newline at end of file diff --git a/EmberLib/Function.js b/EmberLib/Function.js index 0c54eab..4d3913c 100755 --- a/EmberLib/Function.js +++ b/EmberLib/Function.js @@ -5,14 +5,14 @@ const BER = require('../ber.js'); const Command = require("./Command"); const {COMMAND_INVOKE} = require("./constants"); const FunctionContent = require("./FunctionContent"); -const errors = require("../errors"); +const Errors = require("../errors"); class Function extends Element { constructor(number, func) { super(); this.number = number; this.func = func; - this._seqID = BER.APPLICATION(19); + this._seqID = Function.BERID; } /** @@ -48,7 +48,7 @@ class Function extends Element { */ static decode(ber) { const f = new Function(); - ber = ber.getSequence(BER.APPLICATION(19)); + ber = ber.getSequence(Function.BERID); while(ber.remain > 0) { let tag = ber.peek(); @@ -60,11 +60,18 @@ class Function extends Element { } else if(tag == BER.CONTEXT(2)) { f.decodeChildren(seq); } else { - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } return f; } + + /** + * + */ + static get BERID() { + return BER.APPLICATION(19); + } } module.exports = Function; \ No newline at end of file diff --git a/EmberLib/FunctionArgument.js b/EmberLib/FunctionArgument.js index 216e402..38a7d3d 100755 --- a/EmberLib/FunctionArgument.js +++ b/EmberLib/FunctionArgument.js @@ -1,6 +1,7 @@ "use strict"; const BER = require('../ber.js'); const {ParameterType} = require("./ParameterType"); +const Errors = require("../errors"); /* TupleDescription ::= @@ -40,12 +41,13 @@ class FunctionArgument { * @param {BER} ber */ encode(ber) { - ber.startSequence(BER.APPLICATION(21)); - if (this.type != null) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.type.value); - ber.endSequence(); + ber.startSequence(FunctionArgument.BERID); + if (this.type == null) { + throw new Errors.InvalidEmberNode("", "FunctionArgument requires a type") } + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.type.value); + ber.endSequence(); if (this.name != null) { ber.startSequence(BER.CONTEXT(1)); ber.writeString(this.name, BER.EMBER_STRING); @@ -54,6 +56,17 @@ class FunctionArgument { ber.endSequence(); } + /** + * + */ + toJSON() { + return { + type: this.type, + name: this.name, + value: this.value + }; + } + /** * * @param {BER} ber @@ -61,7 +74,7 @@ class FunctionArgument { */ static decode(ber) { const tuple = new FunctionArgument(); - ber = ber.getSequence(BER.APPLICATION(21)); + ber = ber.getSequence(FunctionArgument.BERID); while(ber.remain > 0) { let tag = ber.peek(); let seq = ber.getSequence(tag); @@ -71,9 +84,19 @@ class FunctionArgument { else if (tag === BER.CONTEXT(1)) { tuple.name = seq.readString(BER.EMBER_STRING); } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } } return tuple; } + + /** + * + */ + static get BERID() { + return BER.APPLICATION(21); + } } module.exports = FunctionArgument; \ No newline at end of file diff --git a/EmberLib/FunctionContent.js b/EmberLib/FunctionContent.js index dacab9e..37d434e 100755 --- a/EmberLib/FunctionContent.js +++ b/EmberLib/FunctionContent.js @@ -42,15 +42,20 @@ class FunctionContent { ber.endSequence(); // BER.CONTEXT(2) } - if(this.result != null) { + if(this.result != null && this.result.length > 0) { ber.startSequence(BER.CONTEXT(3)); - ber.startSequence(BER.EMBER_SEQUENCE); - for(let i = 0; i < this.result.length; i++) { + for(let i = 0; i < this.result.length; i++) { ber.startSequence(BER.CONTEXT(0)); + /** @type {FunctionArgument} */ this.result[i].encode(ber); ber.endSequence(); - } - ber.endSequence(); + } + ber.endSequence(); // BER.CONTEXT(3) + } + + if(this.templateReference != null) { + ber.startSequence(BER.CONTEXT(4)); + ber.writeRelativeOID(this.templateReference, BER.EMBER_RELATIVE_OID); ber.endSequence(); // BER.CONTEXT(3) } @@ -83,10 +88,13 @@ class FunctionContent { fc.result = []; while(seq.remain > 0) { tag = seq.peek(); - let dataSeq = seq.getSequence(tag); + const dataSeq = seq.getSequence(tag); if (tag === BER.CONTEXT(0)) { fc.result.push(FunctionArgument.decode(dataSeq)); } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } } } else if(tag == BER.CONTEXT(4)) { fc.templateReference = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); diff --git a/EmberLib/Invocation.js b/EmberLib/Invocation.js index b114e0a..580a68c 100755 --- a/EmberLib/Invocation.js +++ b/EmberLib/Invocation.js @@ -6,9 +6,14 @@ const errors = require("../errors"); let _id = 1; class Invocation { - constructor(id = null) { - this.id = id == null ? _id++ : id; - this.arguments = []; + /** + * + * @param {number} id + * @param {FunctionArgument[]} args + */ + constructor(id = null, args = []) { + this.id = id; + this.arguments = args; } /** @@ -16,13 +21,12 @@ class Invocation { * @param {BER} ber */ encode(ber) { - ber.startSequence(BER.APPLICATION(22)); - // ber.startSequence(BER.EMBER_SEQUENCE); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.id) - ber.endSequence(); - + ber.startSequence(Invocation.BERID); + if (this.id != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.id) + ber.endSequence(); + } ber.startSequence(BER.CONTEXT(1)); ber.startSequence(BER.EMBER_SEQUENCE); for(var i = 0; i < this.arguments.length; i++) { @@ -39,21 +43,29 @@ class Invocation { ber.endSequence(); // BER.APPLICATION(22) } + /** + * + */ + toJSON() { + return { + id: this.id, + arguments: this.arguments == null ? null : this.arguments.map(a => a.toJSON()), + } + } + /** * * @param {BER} ber * @returns {Invocation} */ static decode(ber) { - let invocation = null; - ber = ber.getSequence(BER.APPLICATION(22)); + const invocation = new Invocation(); + ber = ber.getSequence(Invocation.BERID); while(ber.remain > 0) { let tag = ber.peek(); let seq = ber.getSequence(tag); if(tag == BER.CONTEXT(0)) { - // Create the invocation with the id received otherwise we will - // increment the internal id counter. - invocation = new Invocation(seq.readInt()); + invocation.id = seq.readInt(); } else if(tag == BER.CONTEXT(1)) { invocation.arguments = []; @@ -74,6 +86,20 @@ class Invocation { } return invocation; } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(22); + } + + /** + * @returns {number} + */ + static newInvocationID() { + return _id++; + } } module.exports = Invocation; \ No newline at end of file diff --git a/EmberLib/InvocationResult.js b/EmberLib/InvocationResult.js index bfd9f00..f26ea41 100755 --- a/EmberLib/InvocationResult.js +++ b/EmberLib/InvocationResult.js @@ -20,7 +20,7 @@ class InvocationResult { * @param {BER} ber */ encode(ber) { - ber.startSequence(BER.APPLICATION(23)); + ber.startSequence(InvocationResult.BERID); if (this.invocationId != null) { ber.startSequence(BER.CONTEXT(0)); ber.writeInt(this.invocationId); @@ -81,7 +81,7 @@ class InvocationResult { */ static decode(ber) { const invocationResult = new InvocationResult(); - ber = ber.getSequence(BER.APPLICATION(23)); + ber = ber.getSequence(InvocationResult.BERID); while(ber.remain > 0) { let tag = ber.peek(); let seq = ber.getSequence(tag); @@ -103,16 +103,26 @@ class InvocationResult { resTag.readValue() )); } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } } continue } else { - // TODO: options throw new Errors.UnimplementedEmberTypeError(tag); } } return invocationResult; } + + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(23); + } } module.exports = InvocationResult; \ No newline at end of file diff --git a/EmberLib/Label.js b/EmberLib/Label.js index 3c6d648..280203e 100755 --- a/EmberLib/Label.js +++ b/EmberLib/Label.js @@ -1,6 +1,6 @@ "use strict"; const BER = require('../ber.js'); -const errors = require("../errors"); +const Errors = require("../errors"); class Label { constructor(path, description) { @@ -18,16 +18,18 @@ class Label { */ encode(ber) { ber.startSequence(BER.APPLICATION(18)); - if (this.basePath != null) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.basePath, BER.EMBER_RELATIVE_OID); - ber.endSequence(); + if (this.basePath == null) { + throw new Errors.InvalidEmberNode("", "Missing label base path"); } - if (this.description != null) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeString(this.description, BER.EMBER_STRING); - ber.endSequence(); + ber.startSequence(BER.CONTEXT(0)); + ber.writeRelativeOID(this.basePath, BER.EMBER_RELATIVE_OID); + ber.endSequence(); + if (this.description == null) { + throw new Errors.InvalidEmberNode("", "Missing label description"); } + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.description, BER.EMBER_STRING); + ber.endSequence(); ber.endSequence(); } @@ -50,7 +52,7 @@ class Label { l.description = seq.readString(BER.EMBER_STRING); } else { - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } return l; diff --git a/EmberLib/Matrix.js b/EmberLib/Matrix.js index df2c2ea..c8e4d95 100755 --- a/EmberLib/Matrix.js +++ b/EmberLib/Matrix.js @@ -224,9 +224,8 @@ class Matrix extends TreeNode } return count <= matrixNode.contents.maximumTotalConnects; } - - } - return true; + return true; + } } /** diff --git a/EmberLib/MatrixContents.js b/EmberLib/MatrixContents.js index e75643d..b7f75af 100755 --- a/EmberLib/MatrixContents.js +++ b/EmberLib/MatrixContents.js @@ -87,7 +87,7 @@ class MatrixContents { } if (this.schemaIdentifiers != null) { ber.startSequence(BER.CONTEXT(11)); - ber.writeInt(this.schemaIdentifiers, BER.EMBER_STRING); + ber.writeString(this.schemaIdentifiers, BER.EMBER_STRING); ber.endSequence(); } if (this.templateReference != null) { @@ -139,7 +139,7 @@ class MatrixContents { mc.labels.push(Label.decode(lSeq)); } } else if(tag == BER.CONTEXT(11)) { - mc.schemaIdentifiers = seq.readInt(); + mc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); } else if(tag == BER.CONTEXT(12)) { mc.templateReference = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); } diff --git a/EmberLib/MatrixNode.js b/EmberLib/MatrixNode.js index 0c86a76..b368f15 100755 --- a/EmberLib/MatrixNode.js +++ b/EmberLib/MatrixNode.js @@ -80,7 +80,7 @@ class MatrixNode extends Matrix { */ static decode(ber) { const m = new MatrixNode(); - ber = ber.getSequence(BER.APPLICATION(13)); + ber = ber.getSequence(MatrixNode.BERID); while (ber.remain > 0) { let tag = ber.peek(); let seq = ber.getSequence(tag); @@ -105,6 +105,13 @@ class MatrixNode extends Matrix { } return m; } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(13); + } } module.exports = MatrixNode; \ No newline at end of file diff --git a/EmberLib/Node.js b/EmberLib/Node.js index 6af1c6a..a42f001 100755 --- a/EmberLib/Node.js +++ b/EmberLib/Node.js @@ -4,7 +4,7 @@ const Element = require("./Element"); const QualifiedNode = require("./QualifiedNode"); const NodeContents = require("./NodeContents"); const BER = require('../ber.js'); -const errors = require("../errors"); +const Errors = require("../errors"); class Node extends Element { /** @@ -13,7 +13,7 @@ class Node extends Element { */ constructor(number) { super(number); - this._seqID = BER.APPLICATION(3); + this._seqID = Node.BERID; /** @type {NodeContents} */ this.contents = null; } @@ -25,16 +25,6 @@ class Node extends Element { return true; } - /** - * - * @param {function} callback - */ - subscribe(callback) { - if (callback != null && this.isStream()) { - this.contents._subscribers.add(callback); - } - } - /** * @returns {QualifiedNode} */ @@ -51,7 +41,7 @@ class Node extends Element { */ static decode(ber) { const n = new Node(); - ber = ber.getSequence(BER.APPLICATION(3)); + ber = ber.getSequence(Node.BERID); while(ber.remain > 0) { let tag = ber.peek(); @@ -63,11 +53,18 @@ class Node extends Element { } else if(tag == BER.CONTEXT(2)) { n.decodeChildren(seq); } else { - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } return n; } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(3); + } } module.exports = Node; diff --git a/EmberLib/Parameter.js b/EmberLib/Parameter.js index bad3eaf..c0ba322 100755 --- a/EmberLib/Parameter.js +++ b/EmberLib/Parameter.js @@ -4,7 +4,8 @@ const Element = require("./Element"); const QualifiedParameter = require("./QualifiedParameter"); const BER = require('../ber.js'); const ParameterContents = require("./ParameterContents"); -const errors = require("../errors"); +const Errors = require("../errors"); +const {COMMAND_SUBSCRIBE} = require("./Command"); class Parameter extends Element { /** @@ -14,7 +15,7 @@ class Parameter extends Element { constructor(number) { super(); this.number = number; - this._seqID = BER.APPLICATION(1); + this._seqID = Parameter.BERID; } /** @@ -43,30 +44,6 @@ class Parameter extends Element { qp.update(this); return qp; } - - /** - * - * @param {Parameter} other - */ - update(other) { - if ((other != null) && (other.contents != null)) { - if (this.contents == null) { - this.contents = other.contents; - } - else { - for (var key in other.contents) { - if (key[0] === "_") { continue; } - if (other.contents.hasOwnProperty(key)) { - this.contents[key] = other.contents[key]; - } - } - for(let cb of this.contents._subscribers) { - cb(this); - } - } - } - return; - } /** * @@ -75,7 +52,7 @@ class Parameter extends Element { */ static decode(ber) { const p = new Parameter(); - ber = ber.getSequence(BER.APPLICATION(1)); + ber = ber.getSequence(Parameter.BERID); while(ber.remain > 0) { let tag = ber.peek(); @@ -88,12 +65,18 @@ class Parameter extends Element { } else if(tag == BER.CONTEXT(2)) { p.decodeChildren(seq); } else { - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } return p; } + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(1); + } } module.exports = Parameter; \ No newline at end of file diff --git a/EmberLib/QualifiedElement.js b/EmberLib/QualifiedElement.js index 59ab929..f7a78c3 100755 --- a/EmberLib/QualifiedElement.js +++ b/EmberLib/QualifiedElement.js @@ -75,30 +75,6 @@ class QualifiedElement extends TreeNode { } return this.getCommand(COMMAND_GETDIRECTORY); } - - /** - * - * @param {function} callback - * @returns {TreeNode} - */ - subscribe(callback) { - if (callback != null && this.isStream()) { - this.contents._subscribers.add(callback); - } - return this.getCommand(COMMAND_SUBSCRIBE); - } - - /** - * - * @param {function} callback - * @returns {TreeNode} - */ - unsubscribe(callback) { - if (callback != null && this.isStream()) { - this.contents._subscribers.delete(callback); - } - return this.getCommand(COMMAND_UNSUBSCRIBE); - } } module.exports = QualifiedElement; \ No newline at end of file diff --git a/EmberLib/QualifiedFunction.js b/EmberLib/QualifiedFunction.js index 96966ab..b34ca15 100755 --- a/EmberLib/QualifiedFunction.js +++ b/EmberLib/QualifiedFunction.js @@ -16,7 +16,7 @@ class QualifiedFunction extends QualifiedElement { constructor(path, func) { super(path); this.func = func; - this._seqID = BER.APPLICATION(20); + this._seqID = QualifiedFunction.BERID; } /** @@ -39,7 +39,7 @@ class QualifiedFunction extends QualifiedElement { * @param {*} params */ invoke(params) { - const invocation = new Invocation() + const invocation = new Invocation(Invocation.newInvocationID()); invocation.arguments = params; const qualifiedFunctionNode = this.getCommand(COMMAND_INVOKE, "invocation", invocation); //qualifiedFunctionNode.getElementByNumber(this.getNumber()).getElementByNumber(COMMAND_INVOKE).invocation = invocation @@ -54,7 +54,7 @@ class QualifiedFunction extends QualifiedElement { */ static decode(ber) { const qf = new QualifiedFunction(); - ber = ber.getSequence(BER.APPLICATION(20)); + ber = ber.getSequence(QualifiedFunction.BERID); while(ber.remain > 0) { let tag = ber.peek(); let seq = ber.getSequence(tag); @@ -72,6 +72,13 @@ class QualifiedFunction extends QualifiedElement { } return qf; } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(20); + } } module.exports = QualifiedFunction; \ No newline at end of file diff --git a/EmberLib/QualifiedMatrix.js b/EmberLib/QualifiedMatrix.js index a746846..98efd23 100755 --- a/EmberLib/QualifiedMatrix.js +++ b/EmberLib/QualifiedMatrix.js @@ -40,7 +40,7 @@ class QualifiedMatrix extends Matrix { * @param {BER} ber */ encode(ber) { - ber.startSequence(BER.APPLICATION(17)); + ber.startSequence(QualifiedMatrix.BERID); ber.startSequence(BER.CONTEXT(0)); ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); @@ -117,7 +117,7 @@ class QualifiedMatrix extends Matrix { */ static decode(ber) { const qm = new QualifiedMatrix(); - ber = ber.getSequence(BER.APPLICATION(17)); + ber = ber.getSequence(QualifiedMatrix.BERID); while(ber.remain > 0) { let tag = ber.peek(); let seq = ber.getSequence(tag); @@ -149,6 +149,13 @@ class QualifiedMatrix extends Matrix { } return qm; } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(17); + } } module.exports = QualifiedMatrix; \ No newline at end of file diff --git a/EmberLib/QualifiedNode.js b/EmberLib/QualifiedNode.js index 7dce7a5..d0ec097 100755 --- a/EmberLib/QualifiedNode.js +++ b/EmberLib/QualifiedNode.js @@ -8,7 +8,7 @@ const errors = require("../errors"); class QualifiedNode extends QualifiedElement { constructor (path) { super(path); - this._seqID = BER.APPLICATION(10); + this._seqID = QualifiedNode.BERID; } /** @@ -39,7 +39,7 @@ class QualifiedNode extends QualifiedElement { */ static decode(ber) { const qn = new QualifiedNode(); - ber = ber.getSequence(BER.APPLICATION(10)); + ber = ber.getSequence(QualifiedNode.BERID); while(ber.remain > 0) { let tag = ber.peek(); let seq = ber.getSequence(tag); @@ -56,6 +56,13 @@ class QualifiedNode extends QualifiedElement { } return qn; } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(10); + } } module.exports = QualifiedNode; \ No newline at end of file diff --git a/EmberLib/QualifiedParameter.js b/EmberLib/QualifiedParameter.js index aec7990..a079c03 100755 --- a/EmberLib/QualifiedParameter.js +++ b/EmberLib/QualifiedParameter.js @@ -12,7 +12,7 @@ class QualifiedParameter extends QualifiedElement { */ constructor(path) { super(path); - this._seqID = BER.APPLICATION(9); + this._seqID = QualifiedParameter.BERID; } /** @@ -84,7 +84,7 @@ class QualifiedParameter extends QualifiedElement { */ static decode(ber) { var qp = new QualifiedParameter(); - ber = ber.getSequence(BER.APPLICATION(9)); + ber = ber.getSequence(QualifiedParameter.BERID); while(ber.remain > 0) { var tag = ber.peek(); var seq = ber.getSequence(tag); @@ -101,6 +101,13 @@ class QualifiedParameter extends QualifiedElement { } return qp; } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(9); + } } module.exports = QualifiedParameter; \ No newline at end of file diff --git a/EmberLib/StreamDescription.js b/EmberLib/StreamDescription.js index 08087b3..81ac7e6 100755 --- a/EmberLib/StreamDescription.js +++ b/EmberLib/StreamDescription.js @@ -2,7 +2,7 @@ const Element = require("./Element"); const BER = require('../ber.js'); const StreamFormat = require("./StreamFormat"); -const errors = require("../errors"); +const Errors = require("../errors"); class StreamDescription extends Element{ /** @@ -17,17 +17,32 @@ class StreamDescription extends Element{ * @param {BER} ber */ encode(ber) { - ber.startSequence(BER.APPLICATION(12)); + ber.startSequence(StreamDescription.BERID); ber.writeIfDefinedEnum(this.format, StreamFormat, ber.writeInt, 0); ber.writeIfDefined(this.offset, ber.writeInt, 1); ber.endSequence(); } - + + /** + * + */ + toJSON() { + return { + format: this.format == null ? null : this.format.key, + offset: this.offset + }; + } + + /** + * + * @param {BER} ber + * @returns {StreamDescription} + */ static decode(ber) { const sd = new StreamDescription(); - ber = ber.getSequence(BER.APPLICATION(12)); + ber = ber.getSequence(StreamDescription.BERID); while(ber.remain > 0) { var tag = ber.peek(); @@ -37,11 +52,18 @@ class StreamDescription extends Element{ } else if(tag == BER.CONTEXT(1)) { sd.offset = seq.readInt(); } else { - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } return sd; } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(12); + } } module.exports = StreamDescription; \ No newline at end of file diff --git a/EmberLib/StringIntegerCollection.js b/EmberLib/StringIntegerCollection.js index a341145..21baa99 100755 --- a/EmberLib/StringIntegerCollection.js +++ b/EmberLib/StringIntegerCollection.js @@ -1,19 +1,27 @@ "use strict"; -const Element = require("./Element"); const BER = require('../ber.js'); -const errors = require("../errors"); +const StringIntegerPair = require("./StringIntegerPair"); +const Errors = require("../errors"); -class StringIntegerCollection extends Element { +class StringIntegerCollection { constructor() { - super(); - this._seqID = BER.APPLICATION(8); this._collection = new Map(); } + /** + * + * @param {string} key + * @param {StringIntegerPair} value + */ addEntry(key, value) { this._collection.set(key, value); } + /** + * + * @param {string} key + * @returns {StringIntegerPair} + */ get(key) { return this._collection.get(key); } @@ -23,51 +31,52 @@ class StringIntegerCollection extends Element { * @param {BER} ber */ encode(ber) { - ber.startSequence(BER.CONTEXT(15)); - ber.startSequence(BER.APPLICATION(8)); - ber.startSequence(BER.CONTEXT(0)); - for(let [key,value] of this._collection) { - ber.startSequence(BER.APPLICATION(7)); + ber.startSequence(StringIntegerCollection.BERID); + for(let [key,sp] of this._collection) { ber.startSequence(BER.CONTEXT(0)); - ber.writeString(key, BER.EMBER_STRING); - ber.endSequence(); - ber.startSequence(BER.CONTEXT(1)); - ber.writeInt(value); - ber.endSequence(); + sp.encode(ber); ber.endSequence(); - } - ber.endSequence(); + } ber.endSequence(); ber.endSequence(); } + /** + * + */ + toJSON() { + const collection = []; + for(let [key,sp] of this._collection) { + collection.push(sp.toJSON()); + } + } + /** * * @param {BER} ber + * @returns {StringIntegerCollection} */ static decode(ber) { const sc = new StringIntegerCollection(); - ber = ber.getSequence(BER.APPLICATION(8)); - while(ber.remain > 0) { - var seq = ber.getSequence(BER.CONTEXT(0)); - seq = seq.getSequence(BER.APPLICATION(7)); - var entryString, entryInteger; - while(seq.remain > 0) { - var tag = seq.peek(); - var dataSeq = seq.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - entryString = dataSeq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - entryInteger = dataSeq.readInt(); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } + const seq = ber.getSequence(StringIntegerCollection.BERID); + while(seq.remain > 0) { + const tag = seq.peek(); + if (tag != BER.CONTEXT(0)) { + throw new Errors.UnimplementedEmberTypeError(tag); } - - sc.addEntry(entryString,entryInteger); + const data = seq.getSequence(BER.CONTEXT(0)); + const sp = StringIntegerPair.decode(data) + sc.addEntry(sp.key, sp); } return sc; } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(8); + } } module.exports = StringIntegerCollection; \ No newline at end of file diff --git a/EmberLib/TreeNode.js b/EmberLib/TreeNode.js index 508da87..166b751 100755 --- a/EmberLib/TreeNode.js +++ b/EmberLib/TreeNode.js @@ -2,6 +2,7 @@ const BER = require('../ber.js'); const Command = require("./Command"); const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); +const Errors = require("../errors"); class TreeNode { constructor() { @@ -9,6 +10,25 @@ class TreeNode { this._parent = null; } + _isSubscribable(callback) { + return (callback != null && this.isParameter() && this.isStream()); + } + + _subscribe(callback) { + if (this.contents == null) { + throw new Errors.InvalidEmberNode(this.getPath(), "No content to subscribe"); + } + this.contents._subscribers.add(callback); + } + + _unsubscribe(callback) { + this.contents._subscribers.delete(callback); + } + + /** + * + * @param {TreeNode} child + */ addChild(child) { TreeNode.addElement(this, child); } @@ -222,26 +242,21 @@ class TreeNode { } } + getCommand(cmd) { + return this.getTreeBranch(new Command(cmd)); + } + + /** + * + * @param {function} callback + */ getDirectory(callback) { - if (callback != null && this.contents != null && !this.isStream()) { - this.contents._subscribers.add(callback); + if (this._isSubscribable(callback)) { + this._subscribe(callback); } - return this.getTreeBranch(new Command(COMMAND_GETDIRECTORY)); + return this.getCommand(COMMAND_GETDIRECTORY); } - subscribe(callback) { - if (callback != null && this.isParameter() && this.isStream()) { - this.contents._subscribers.add(callback); - } - return this.getTreeBranch(new Command(COMMAND_SUBSCRIBE)); - } - - unsubscribe(callback) { - if (callback != null && this.isParameter() && this.isStream()) { - this.contents._subscribers.delete(callback); - } - return this.getTreeBranch(new Command(COMMAND_UNSUBSCRIBE)); - } /** * @returns {TreeNode[]} @@ -473,6 +488,26 @@ class TreeNode { /** * + * @param {function} callback + */ + subscribe(callback) { + if (this._isSubscribable(callback)) { + this._subscribe(callback); + } + return this.getCommand(COMMAND_SUBSCRIBE); + } + + /** + * + * @param {*} callback + */ + unsubscribe(callback) { + this._unsubscribe(callback); + return this.getCommand(COMMAND_UNSUBSCRIBE); + } + + /** + * * @param {TreeNode} other */ update(other) { @@ -481,9 +516,18 @@ class TreeNode { this.contents = other.contents; } else { + let modified = false; for (var key in other.contents) { - if (other.contents.hasOwnProperty(key)) { + if (key[0] === "_") { continue; } + if (other.contents.hasOwnProperty(key) && + this.contents[key] != other.contents[key]) { this.contents[key] = other.contents[key]; + modified = true; + } + } + if (modified && this.contents._subscribers != null) { + for(let cb of this.contents._subscribers) { + cb(this); } } } diff --git a/EmberLib/index.js b/EmberLib/index.js index 6da638d..1bdf67f 100755 --- a/EmberLib/index.js +++ b/EmberLib/index.js @@ -28,6 +28,7 @@ const QualifiedFunction = require("./QualifiedFunction"); const QualifiedMatrix = require("./QualifiedMatrix"); const QualifiedNode = require("./QualifiedNode"); const QualifiedParameter = require("./QualifiedParameter"); +const StringIntegerPair = require("./StringIntegerPair"); const StringIntegerCollection = require("./StringIntegerCollection"); const StreamFormat = require("./StreamFormat"); const StreamDescription = require("./StreamDescription"); @@ -149,6 +150,7 @@ module.exports = { QualifiedParameter, StreamFormat, StreamDescription, + StringIntegerPair, StringIntegerCollection, Subscribe,COMMAND_SUBSCRIBE, Unsubscribe,COMMAND_UNSUBSCRIBE, diff --git a/ber.js b/ber.js index 03d22d9..08bfbe9 100755 --- a/ber.js +++ b/ber.js @@ -79,6 +79,16 @@ ExtendedReader.prototype.getSequence = function(tag) { return new ExtendedReader(buf); } +/** +Value ::= + CHOICE { + integer Integer64, + real REAL, + string EmberString, + boolean BOOLEAN, + octets OCTET STRING, + null NULL + } */ ExtendedReader.prototype.readValue = function() { var tag = this.peek(); diff --git a/embrionix.ember b/embrionix.ember deleted file mode 100755 index f2bacc41f99bb2fbb230da1009756e52616e14f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41743 zcmeHQX;<7v7ByhJ;Kob#J&Ln0jU}~uC7r|u$0jkPW3XdqBcfYC$Gt@@>^Lju%!m0K z4#|9(bLPz7ntw6#R8>6bm7pXTV`y7_jyb++T}f5-?t8Cm(|7B)*6*#?*Y6!39zJyF zk9A}H-m%P)Y3Fvc?i_;8#(D$3x5M9`%8XyO-Nqf;b+kFV?Q9&)j9#4mZ0_Pe{A&#T z%l|v#bNvr;huF0r%bcF?E_;l7-EsTPZpWva6Z(X%qz^MX+l?#6KD zkdijSXycg^yhQwYPiIa#?f!BvYwKA(pS5gui4pn1MWq~Om&jzsX6=r>;V;R&39?W8d94 znaPA6#!x=WQFak%w!UY3&HDCbiP(my^JDA+P=0X%Wl^@f$Z_c+s*-GXjWKqOEzpQ18-t)}(d zX4`h}YJI2P?KI$j<74gW?PkO2YNie!?dn49-1+M@ty~x%t5?OHOwfZqVNICl3YLGp zes|5SJK5!C3rb&UcIqunJ`-E}puTm~(u6^fR*}s>6c)9JdgMGW*Yc)G7 z)tggGQ*PF(m=$wUyVGoVt5qncKB=ub&6QQJYUm{Z&X0E8TXw;_xS*G+HmNn7o_{KQ zw=gwdou9rssnxr!E)>`C+`H8a3q`#=snuqyehpo!%}#3b^K;d+^9w%CntH|5jpA8g z*Vq{gwYf>H*@5Dv*KRf{qVjiC<+;RFo=dAdTIC@-kClPF)@Vj#P~1WWR^m1ZL@LRks9iCwb5;pt zaBgek0-b3Lwu6?W<_O)hcq@`;NFUP%PZeJb9bG;bks`3p(P51tw1FZ+)D zYu;kw;uZJ1cze>krNreg?RWV@0VA<`%ZZCu-tXe=N%I@*K?EE(hDsLWdlBsGEGd z(GJp0c_HnlOuH!~ULbkGx^2GtDEAD9^q6*2ChUbxom#b z`Qgl+lc&xDnK>sD>^`itn=)wPZPUKNZsN3?GHBvN zX;#`zxtBGttDflOnO53OnRZjMDJy$v!#nx0w1XbIw3|{rVQ;$}Y!JFB`J7>xsw`$x z-f4RERc!%gOeL5=$|n%R#Ar^wbZusOum~fkRtNvdT zZbP9Y!7ztVqCdcqDlwgh%_rG!_EDrtd>~5XksMb41YtN|a`L+EhvCMK!mwCr!NWVO zZJB4QBD9+8^kKmdOAJOr0etA+>#MLtY-Rl(+z$_l|s4aaWqW9+f; z80U`i0zzf_x46M>aU^`~0zY;Xk3m&U&&*G;GmrU4xBB1;KVEQXlJjiNeuCU;*o53_ zIQxKetHj;G$@`HK&34<%!K~f%F%0Hd-Oh4`)WTr7S-NN?ei;}>i}5j~g+*hbk;X!? zF#FMsg_t(*rNgk$dTQ0~bevY7r8Gm~ltzxB7(Y1|_ipKA2+?D3 zkS0SYy&K<1A@rL&wXpq+pUDS-2FAjo@zYG>XBt1(MaRI_@pCv){Ny`sVEoJteEdvj zn~C^&kY<}bRPi&lutBr1X#C8j@iUE|_alDt4W}@ES_$H3!SBB{4r>2xI-U1`PUqcW z3{9=f-!yFBdT|GJZwsZBMdPTI#?ds6-j6uS_gus{njic)ny#4$Ce$CKH4{n?7wyYE z;BhpyvH`TRXdKO_aWsvi_alztzQcSzQDN}oXz~d~>Hb*{a!Jd>6-QGm3*zWOTK=fC zvS=JFq;WKjqxU0@^3C}1_h@nO<7o1UMd>DmX&j~RRY*Mh+eob}h@%5Mzo?kT(KL?U zk2uO-PlIu^H285e`Sg!;i-k0f?lz95Ru;t3ftkq`{XJSr<7gU3??)UR-+ay6wC&m3 zzJ@Kl%;j5un$gV%Jk2QG#zwq#4mO+FYK!qlYPaq1l9<#Q)4kvC_lEboUw&Fn!}22^ zmi=F%;SVk;<#Ryb#i)D;b=LOW=8yiFd|hcf{OV=tEWSzCpKQO4zea~)lZkaUfN}bt z>pE=7;ljI|$B!Hu9s2t!zW&v}@Sp74j<~RYTUZ5mY1SbAqlCG*8e1 zLDvYnPS6(w-5}^DL0=N|6+vGU^bJAZ60}IrcLdo4EfG{Fs6miJ&@w?Q1g#R(BXw?DzN-4(|R zU4gu#9jn-dK^#8jIMpEnUOjSy39G<>8eV45MO+H82=SisDM zm}_XS|O&|bWTake2A$wp3@Q&t;dW};~9yF=3{2nHT#%EMEfzb>WY0_B9=D| z&&;ap^$AKe(c_p|b+tYz5z*zCS#_;GB@xl*m|3+ZpO%Q|bj++;l+Q>+^g3o%t;%O5 zBAN~ufLfN%NzB5g(E(E}%jYF#F~n3G&kGW>6k@8a=S7K$?gyS$Z9XqiW)9sCFxB?+ zvcyF915C96X%Z6+C>JBkS0o}DP%cK6XC)#UP%cK6uS!IWTe%onz9tdT_2gn?`MN|z z*OQBpwL+4X6+!%c~L*4X6+!%Xx{2v8xax%LR#uuBQ+q%WD!5T~8rKme(aBx}HLeEWeP5 z=z0nnM2ua< z7+E$L5#Hdi*=2~nrx>G4M`oh)DaHtMS!Sa5DaI&sMP{P=DaJ^1Rc509DaL5CDKpW4 zzzo#l{9dMF00XL8owsBvMlhhN<=K*{7{Y+6)@NI$q9X#TTA&??8gxDbRjtsjOhs1& zRJBBVG8Ij!6r<4}WG0$YDMqBO%tTWv#i+C|GclHxVr1&cO!P&i7@4lgOms%27@6Ld zndps5F*3a)GtnKDVr2TG#0KoEz!KCl{Yj=`Km)2;ra#M6jA%er%k&qS ziXjcCYMK5jQ_&>>RV~xsWGcEOpsHp1yG%uw1k^2M>W@nLADp9wBaHp-np<}g40*yJ z4-9=>Y{MH6mA{B0W1ay~-3<6hJOFMsy?;k_BSc0$gFC95ATsP35Y-JZ${HXv?imob ztbQC2jtqPTMD-|3WaKj-s(+wHhCTzLdXyzH_6e!UrU9r`93A`&QPsv19sLYZ)#ei& k{tQvo1{59t3{ll46deE!QPoBi9RUqdw=^TJ;hp{cAFtSbSpWb4 diff --git a/test/DeviceTree.test.js b/test/DeviceTree.test.js index 4f9e879..1c91f72 100755 --- a/test/DeviceTree.test.js +++ b/test/DeviceTree.test.js @@ -15,7 +15,7 @@ describe("EmberClient", () => { beforeAll(() => { return Promise.resolve() .then(() => new Promise((resolve, reject) => { - fs.readFile("./embrionix.ember", (e, data) => { + fs.readFile("./test/embrionix.ember", (e, data) => { if (e) { reject(e); } diff --git a/test/Ember.test.js b/test/Ember.test.js index 80c7789..be0c90f 100755 --- a/test/Ember.test.js +++ b/test/Ember.test.js @@ -58,9 +58,28 @@ describe("Ember", () => { } }); it("should have a toJSON", () => { - const command = new EmberLib.Command(EmberLib.COMMAND_GETDIRECTORY); - const jsonCommand = command.toJSON(); + const command = new EmberLib.Command(EmberLib.COMMAND_GETDIRECTORY); + let jsonCommand = command.toJSON(); expect(jsonCommand.number).toBe(EmberLib.COMMAND_GETDIRECTORY); + command.invocation = new EmberLib.Invocation(1, [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 1, "arg1") + ]); + jsonCommand = command.toJSON(); + expect(jsonCommand.invocation.arguments.length).toBe(1); + }); + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.Command.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Command.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } }); }); describe("Node", () => { @@ -92,6 +111,34 @@ describe("Ember", () => { expect(n.contents.identifier).toBe(identifier); expect(n.contents.description).toBe(description); }); + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.Node.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Node.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should through an error if unable to decode content", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.EMBER_SET); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.NodeContents.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); }); describe("Function", () => { let func; @@ -102,7 +149,39 @@ describe("Ember", () => { res.value = args[0].value + args[1].value; return [res]; }); - }) + }); + it("should be able to encode FunctionArgument with no name", () => { + const res = new EmberLib.FunctionArgument(); + res.type = EmberLib.ParameterType.integer; + const writer = new BER.Writer(); + res.encode(writer); + expect(writer.buffer.length > 0).toBeTruthy(); + }); + it("should throw an Error if encoding FunctionArgument with no type", () => { + const res = new EmberLib.FunctionArgument(); + const writer = new BER.Writer(); + try { + res.encode(writer); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidEmberNode); + } + }); + it("should throw an Error if unable to decode", () => { + const writer = new BER.Writer(); + try { + writer.startSequence(EmberLib.Function.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); // BER.CONTEXT(0) + writer.endSequence(); // BER.CONTEXT(0) + EmberLib.Function.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError); + } + }); it("should return true when calling isFunction", () => { expect(func.isFunction()).toBeTruthy(); }); @@ -112,8 +191,10 @@ describe("Ember", () => { expect(children.length).toBe(1); expect(children[0].isCommand()).toBeTruthy(); }); - it("should have a decoder", () => { + it("should have a encoder/decoder", () => { func.contents = new EmberLib.FunctionContent(identifier, description); + func.contents.templateReference = "1.2.3"; + func.addChild(new EmberLib.Node(1)); func.contents.arguments = [ new EmberLib.FunctionArgument(EmberLib.ParameterType.integer,null, "arg1"), new EmberLib.FunctionArgument(EmberLib.ParameterType.integer,null, "arg2") @@ -121,12 +202,68 @@ describe("Ember", () => { func.contents.result = [ new EmberLib.FunctionArgument(EmberLib.ParameterType.integer,null, "result") ]; - const writer = new BER.Writer(); + let writer = new BER.Writer(); func.encode(writer); - const f = EmberLib.Function.decode(new BER.Reader(writer.buffer)); + let f = EmberLib.Function.decode(new BER.Reader(writer.buffer)); expect(f.number).toBe(func.number); expect(f.contents.identifier).toBe(identifier); expect(f.contents.description).toBe(description); + expect(f.contents.result.length).toBe(1); + expect(f.contents.templateReference).toBe(func.contents.templateReference); + + writer = new BER.Writer(); + func.contents.identifier = null; + func.contents.arguments = null; + func.contents.result = null; + func.encode(writer); + f = EmberLib.Function.decode(new BER.Reader(writer.buffer)); + expect(f.number).toBe(func.number); + expect(f.contents.identifier == null).toBeTruthy(); + expect(f.contents.result == null || f.contents.result.length == 0).toBeTruthy(); + }); + it("should through an error if unable to decode result", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.EMBER_SET); + writer.startSequence(BER.CONTEXT(3)); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.FunctionContent.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should through an error if unable to decode content", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.EMBER_SET); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.FunctionContent.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should through an error if unable to decode FunctionArgument", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.FunctionArgument.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.FunctionArgument.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } }); }); describe("Parameter", () => { @@ -172,6 +309,61 @@ describe("Ember", () => { const newParameter = EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); expect(newParameter.getChildren().length).toBe(1); }); + it("should support type real", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE = 1.1; + parameter.contents = new EmberLib.ParameterContents(VALUE, "real"); + const writer = new BER.Writer(); + parameter.encode(writer); + const newParameter = EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); + expect(newParameter.contents.value).toBe(VALUE); + }); + it("should support type string", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE ="string support"; + parameter.contents = new EmberLib.ParameterContents(VALUE, "string"); + const writer = new BER.Writer(); + parameter.encode(writer); + const newParameter = EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); + expect(newParameter.contents.value).toBe(VALUE); + }); + it("should support type boolean", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE = true; + parameter.contents = new EmberLib.ParameterContents(VALUE, "boolean"); + const writer = new BER.Writer(); + parameter.encode(writer); + const newParameter = EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); + expect(newParameter.contents.value).toBe(VALUE); + }); + it("should through an error if fails to decode StringIntegerPair", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.StringIntegerPair.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.StringIntegerPair.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should through an error if fails to decode StringIntegerCollection", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.StringIntegerCollection.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.StringIntegerCollection.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); }); describe("Matrix", () => { describe("validateConnection", () => { @@ -252,6 +444,11 @@ describe("Ember", () => { matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; matrixNode.targets = [0, 3]; matrixNode.sources = [0, 3]; + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + } + const min = matrixNode.getMinimal(true); + expect(min.sources).toBeDefined(); try { EmberLib.Matrix.validateConnection(matrixNode, 1, [0]); throw new Error("Should not succeed"); @@ -398,6 +595,17 @@ describe("Ember", () => { const res = EmberLib.Matrix.canConnect(matrixNode, 0, [0,3]); expect(res).toBeFalsy(); }); + it("should always return true if NtoN and no limits", () => { + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.nToN, + EmberLib.MatrixMode.linear + ); + matrixNode.connections = null; + matrixNode.contents.maximumConnectsPerTarget = null; + matrixNode.contents.maximumTotalConnects = null; + const res = EmberLib.Matrix.canConnect(matrixNode, 0, [0,3]); + expect(res).toBeTruthy(); + }); }); describe("Matrix Non-Linear", () => { it("should have encoder / decoder", () => { @@ -406,8 +614,14 @@ describe("Ember", () => { EmberLib.MatrixType.onetoN, EmberLib.MatrixMode.nonLinear ); + matrixNode.contents.gainParameterNumber = 4; matrixNode.contents.identifier = "matrix"; matrixNode.contents.description = "matrix"; + matrixNode.contents.maximumTotalConnects = 5; + matrixNode.contents.maximumConnectsPerTarget = 1; + matrixNode.contents.parametersLocation = "1.2.3"; + matrixNode.contents.schemaIdentifiers = "de.l-s-b.emberplus.schema1"; + matrixNode.contents.templateReference = "0.1.2.3"; matrixNode.targets = [0,3]; matrixNode.sources = [1,2]; const writer = new BER.Writer(); @@ -430,6 +644,189 @@ describe("Ember", () => { const connect = matrixNode.connect({0: new EmberLib.MatrixConnection(0)}); expect(connect).toBeDefined(); }); + it("should through an error if can't decode", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.APPLICATION(13)); + writer.startSequence(BER.CONTEXT(0)); + writer.writeInt(1); + writer.endSequence(); // BER.CONTEXT(0) + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.MatrixNode.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + }); + describe("Label", () => { + it ("should throw an error if it fails to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.APPLICATION(18)); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Label.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it ("should throw an error if no basePath", () => { + const label = new EmberLib.Label(null, "test"); + const writer = new BER.Writer(); + try { + label.encode(writer); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); + it ("should throw an error if no description", () => { + const label = new EmberLib.Label("1.2.3", null); + const writer = new BER.Writer(); + try { + label.encode(writer); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); + it ("should be able to encode/decode a valid label", () => { + const label = new EmberLib.Label("1.2.3", "primary"); + const writer = new BER.Writer(); + label.encode(writer); + const reader = new BER.Reader(writer.buffer); + const newLabel = EmberLib.Label.decode(reader); + expect(newLabel.description).toBe(label.description); + expect(newLabel.basePath).toBe(label.basePath); + }); + }) + }); + describe("Invocation", () => { + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.Invocation.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Invocation.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("Should be able to encode even if no id", () => { + const invocation = new EmberLib.Invocation(); + const writer = new BER.Writer(); + invocation.encode(writer); + const i = EmberLib.Invocation.decode(new BER.Reader(writer.buffer)); + expect(i.id == null).toBeTruthy(); + }); + it("Should have a toJSON", () => { + const invocation = new EmberLib.Invocation(1, [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 10, "arg1" ) + ]); + let js = invocation.toJSON(); + expect(js.id).toBe(invocation.id); + expect(js.arguments.length).toBe(invocation.arguments.length); + invocation.arguments = null; + js = invocation.toJSON(); + expect(js.id).toBe(invocation.id); + expect(js.arguments).toBe(null); + }); + }); + describe("InvocationResult", () => { + it("should support all types of result", () => { + const invocationResult = new EmberLib.InvocationResult(); + invocationResult.invocationId = 100; + const valBuf = [0xa, 0x1, 0x2]; + invocationResult.setFailure(); + expect(invocationResult.success).toBeFalsy(); + invocationResult.setSuccess(); + expect(invocationResult.success).toBeTruthy(); + try { + invocationResult.setResult(new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 1)); + throw new Error("should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidResultFormat).toBeTruthy(); + } + expect(invocationResult.toQualified()).toBe(invocationResult); + invocationResult.setResult([ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 1), + new EmberLib.FunctionArgument(EmberLib.ParameterType.real, 1.1), + new EmberLib.FunctionArgument(EmberLib.ParameterType.string, "one"), + new EmberLib.FunctionArgument(EmberLib.ParameterType.boolean, false), + new EmberLib.FunctionArgument(EmberLib.ParameterType.octets, Buffer.from(valBuf)) + ]); + const writer = new BER.Writer(); + invocationResult.encode(writer); + const newInvocationRes = EmberLib.InvocationResult.decode(new BER.Reader(writer.buffer)); + expect(newInvocationRes.success).toBe(invocationResult.success); + expect(newInvocationRes.invocationId).toBe(invocationResult.invocationId); + expect(newInvocationRes.result.length).toBe(invocationResult.result.length); + expect(newInvocationRes.result[4].value.length).toBe(valBuf.length); + expect(newInvocationRes.result[4].value[0]).toBe(valBuf[0]); + }); + it("should be able to encode with not result, no success", () => { + const invocationResult = new EmberLib.InvocationResult(); + invocationResult.invocationId = 100; + invocationResult.result = null; + invocationResult.sucess = null; + const writer = new BER.Writer(); + invocationResult.encode(writer); + const newInvocationRes = EmberLib.InvocationResult.decode(new BER.Reader(writer.buffer)); + expect(newInvocationRes.result).not.toBeDefined(); + }); + it("should be able to encode with no invocationId", () => { + const invocationResult = new EmberLib.InvocationResult(); + invocationResult.invocationId = null; + invocationResult.result = null; + invocationResult.sucess = null; + const writer = new BER.Writer(); + invocationResult.encode(writer); + const newInvocationRes = EmberLib.InvocationResult.decode(new BER.Reader(writer.buffer)); + expect(newInvocationRes.invocationId == null).toBeTruthy(); + }); + it("should through an error if can't decode", () => { + let writer = new BER.Writer(); + writer.startSequence(EmberLib.InvocationResult.BERID); + writer.startSequence(BER.CONTEXT(3)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.InvocationResult.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + writer = new BER.Writer(); + writer.startSequence(EmberLib.InvocationResult.BERID); + writer.startSequence(BER.CONTEXT(2)); + writer.startSequence(BER.EMBER_SEQUENCE); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.InvocationResult.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } }); }); }); diff --git a/test/Server.test.js b/test/Server.test.js index 85a898a..428a341 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -19,28 +19,27 @@ describe("server", function() { expect(root).toBeDefined(); expect(root.elements).toBeDefined(); expect(root.elements.size).toBe(1); - console.log("root", root.getElementByNumber(0).contents); expect(root.getElementByNumber(0).contents.identifier).toBe("scoreMaster"); expect(root.getElementByNumber(0).elements.size).toBe(jsonTree[0].children.length); }); }); describe("Server - Client communication", function() { - let server,client; + let server,client,jsonTree; beforeAll(function() { jsonTree = jsonRoot(); const root = EmberServer.JSONtoTree(jsonTree); server = new EmberServer(LOCALHOST, PORT, root); server.on("error", e => { + // eslint-disable-next-line no-console console.log(e); }); server.on("clientError", e => { + // eslint-disable-next-line no-console console.log(e); }); //server._debug = true; - return server.listen().then(() => { - console.log("server listening"); - }); + return server.listen(); }); afterAll(function() { return server.close(); @@ -51,7 +50,6 @@ describe("server", function() { return Promise.resolve() .then(() => client.connect()) .then(() => { - console.log("client connected"); return client.getDirectory(); }) .then(() => { @@ -71,7 +69,7 @@ describe("server", function() { // Issue #33 EmberServer.handleGetDirectory does not subscribe to child parameters expect(server.subscribers["0.0.0"]).toBeDefined(); // Keepalive - return client.disconnect(); + return client.disconnect(); }); }); it("should be able to modify a parameter", () => { @@ -110,7 +108,6 @@ describe("server", function() { ]); }) .then(result => { - console.log(result); expect(result).toBeDefined(); expect(result.result).toBeDefined(); expect(result.result.length).toBe(1); @@ -119,7 +116,7 @@ describe("server", function() { }); }); - it("should be able to get child with tree.getNodeByPath", function() { + it("should be able to get child with tree.getNodeByPath", function() { //server._debug = true; client = new EmberClient(LOCALHOST, PORT); //client._debug = true; @@ -127,17 +124,14 @@ describe("server", function() { return Promise.resolve() .then(() => client.connect()) .then(() => { - console.log("client connected"); return client.getDirectory(); }) .then(() => client.getNodeByPath("scoreMaster/identity/product")) - .then(child => { - console.log(child); + .then(() => { return client.getNodeByPath("scoreMaster/router/labels/group 1"); }) - .then(child => { - console.log("router/labels", child); - return client.disconnect(); + .then(() => { + return client.disconnect(); }); }); it("should be able to get child with getElementByPath", function() { @@ -148,17 +142,14 @@ describe("server", function() { return Promise.resolve() .then(() => client.connect()) .then(() => { - console.log("client connected"); return client.getDirectory(); }) .then(() => client.getElementByPath("scoreMaster/identity/product")) - .then(child => { - console.log(child); + .then(() => { return client.getElementByPath("scoreMaster/router/labels/group 1"); }) - .then(child => { - console.log("router/labels", child); - return client.disconnect(); + .then(() => { + return client.disconnect(); }); }); it("should throw an error if getNodeByPath for unknown path", function() { @@ -167,12 +158,10 @@ describe("server", function() { return Promise.resolve() .then(() => client.connect()) .then(() => { - console.log("client connected"); return client.getDirectory(); }) .then(() => client.getNodeByPath("scoreMaster/router/labels/group")) - .then(child => { - console.log("router/labels", child); + .then(() => { throw new Error("Should not succeed"); }) .catch(e => { @@ -193,7 +182,6 @@ describe("server", function() { return client.getElementByPath(matrix.getPath()); }) .then(matrix => { - console.log(matrix); expect(matrix.connections['0'].sources).toBeDefined(); expect(matrix.connections['0'].sources.length).toBe(1); expect(matrix.connections['0'].sources[0]).toBe(1); @@ -244,7 +232,7 @@ describe("server", function() { .then(parameter => { server._subscribe = server.subscribe; let _resolve; - const p = new Promise((resolve, reject) => { + const p = new Promise((resolve) => { _resolve = resolve; }); server.subscribe = (c,e) => { @@ -312,7 +300,7 @@ describe("server", function() { it("should verify if connection allowed in 1-to-1", function() { const matrix = server.tree.getElementByPath("0.1.0"); let disconnectCount = 0; - const handleDisconnect = info => { + const handleDisconnect = () => { disconnectCount++; } server.on("matrix-disconnect", handleDisconnect.bind(this)); @@ -344,7 +332,7 @@ describe("server", function() { it("should disconnect if trying to connect same source and target in 1-to-1", function() { const matrix = server.tree.getElementByPath("0.1.0"); let disconnectCount = 0; - const handleDisconnect = info => { + const handleDisconnect = () => { disconnectCount++; } server.on("matrix-disconnect", handleDisconnect.bind(this)); @@ -360,7 +348,7 @@ describe("server", function() { it("should be able to lock a connection", function() { const matrix = server.tree.getElementByPath("0.1.0"); let disconnectCount = 0; - const handleDisconnect = info => { + const handleDisconnect = () => { disconnectCount++; } server.on("matrix-disconnect", handleDisconnect.bind(this)); @@ -429,9 +417,11 @@ describe("server", function() { it("should return modified answer on absolute connect", function() { let client; server.on("error", e => { + // eslint-disable-next-line no-console console.log(e); }); server.on("clientError", e => { + // eslint-disable-next-line no-console console.log(e); }); //server._debug = true; @@ -443,12 +433,8 @@ describe("server", function() { .then(() => client.connect()) .then(() => client.getDirectory()) .then(() => client.getNodeByPathnum("0.1.0")) - .then(matrix => { - console.log(matrix); - return client.matrixSetConnection(matrix, 0, [1]); - }) + .then(matrix => client.matrixSetConnection(matrix, 0, [1])) .then(result => { - console.log(result); expect(result).toBeDefined(); expect(result.connections).toBeDefined(); expect(result.connections[0]).toBeDefined(); @@ -456,7 +442,6 @@ describe("server", function() { return client.disconnect(); }) .then(() => { - console.log("closing server"); server.close(); }); }); @@ -469,9 +454,11 @@ describe("server", function() { const root = EmberServer.JSONtoTree(jsonTree); server = new EmberServer(LOCALHOST, PORT, root); server.on("error", e => { + // eslint-disable-next-line no-console console.log(e); }); server.on("clientError", e => { + // eslint-disable-next-line no-console console.log(e); }); return server.listen(); @@ -481,7 +468,6 @@ describe("server", function() { }); it("should not auto subscribe stream parameter", function() { const parameter = server.tree.getElementByPath("0.0.2"); - console.log(parameter); expect(parameter.isStream()).toBeTruthy(); expect(server.subscribers["0.0.2"]).not.toBeDefined(); }); @@ -498,13 +484,12 @@ describe("server", function() { }) .then(() => client.getNodeByPathnum("0.0.2")) .then(parameter => { - console.log(parameter); expect(server.subscribers["0.0.2"]).not.toBeDefined(); expect(parameter.contents._subscribers).toBeDefined(); expect(parameter.contents._subscribers.size).toBe(0); server._subscribe = server.subscribe; let _resolve; - const p = new Promise((resolve, reject) => { + const p = new Promise(resolve => { _resolve = resolve; }); server.subscribe = (c,e) => { @@ -523,24 +508,20 @@ describe("server", function() { expect(parameter.contents._subscribers.size).toBe(1); server._unsubscribe = server.unsubscribe; let _resolve; - const p = new Promise((resolve, reject) => { + const p = new Promise(resolve => { _resolve = resolve; }); server.unsubscribe = (c,e) => { - console.log("unsubscribe"); server._unsubscribe(c,e); _resolve(); }; - console.log(parameter); return client.unsubscribe(parameter, cb).then(() => (p)) }) .then(() => { - console.log(server.subscribers); expect(server.subscribers["0.0.2"]).toBeDefined(); return client.getNodeByPathnum("0.0.2"); }) .then(parameter => { - console.log(parameter); expect(server.subscribers["0.0.2"]).toBeDefined(); expect(server.subscribers["0.0.2"].size).toBe(0); expect(parameter.contents._subscribers).toBeDefined(); From 4ed624c294b3fcf597aeced0f548047d89418423 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 13 Jan 2020 18:37:15 +0100 Subject: [PATCH 45/64] Adding test code coverage. EmberLib 100% upto Matrix --- EmberLib/StringIntegerPair.js | 68 ++++++++++++++++++++++++++++++++++ test/embrionix.ember | Bin 0 -> 41743 bytes 2 files changed, 68 insertions(+) create mode 100755 EmberLib/StringIntegerPair.js create mode 100755 test/embrionix.ember diff --git a/EmberLib/StringIntegerPair.js b/EmberLib/StringIntegerPair.js new file mode 100755 index 0000000..187a6fb --- /dev/null +++ b/EmberLib/StringIntegerPair.js @@ -0,0 +1,68 @@ +"use strict"; +const BER = require('../ber.js'); +const errors = require("../errors"); + +class StringIntegerPair { + constructor(key,value) { + this.key = key; + this.value = value; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + if (this.key == null || this.value == null) { + throw new errors.InvalidEmberNode("", "Invalid key/value missing"); + } + ber.startSequence(StringIntegerPair.BERID); + ber.startSequence(BER.CONTEXT(0)); + ber.writeString(this.key, BER.EMBER_STRING); + ber.endSequence(); + ber.startSequence(BER.CONTEXT(1)); + ber.writeInt(this.value); + ber.endSequence(); + ber.endSequence(); + } + + /** + * + */ + toJSON() { + return { + key: this.key, + value: this.value + } + } + /** + * + * @param {BER} ber + * @returns {StringIntegerPair} + */ + static decode(ber) { + const sp = new StringIntegerPair(); + let seq = ber.getSequence(StringIntegerPair.BERID); + while(seq.remain > 0) { + const tag = seq.peek(); + const dataSeq = seq.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + sp.key = dataSeq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(1)) { + sp.value = dataSeq.readInt(); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return sp; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(7); + } +} + +module.exports = StringIntegerPair; \ No newline at end of file diff --git a/test/embrionix.ember b/test/embrionix.ember new file mode 100755 index 0000000000000000000000000000000000000000..f2bacc41f99bb2fbb230da1009756e52616e14f4 GIT binary patch literal 41743 zcmeHQX;<7v7ByhJ;Kob#J&Ln0jU}~uC7r|u$0jkPW3XdqBcfYC$Gt@@>^Lju%!m0K z4#|9(bLPz7ntw6#R8>6bm7pXTV`y7_jyb++T}f5-?t8Cm(|7B)*6*#?*Y6!39zJyF zk9A}H-m%P)Y3Fvc?i_;8#(D$3x5M9`%8XyO-Nqf;b+kFV?Q9&)j9#4mZ0_Pe{A&#T z%l|v#bNvr;huF0r%bcF?E_;l7-EsTPZpWva6Z(X%qz^MX+l?#6KD zkdijSXycg^yhQwYPiIa#?f!BvYwKA(pS5gui4pn1MWq~Om&jzsX6=r>;V;R&39?W8d94 znaPA6#!x=WQFak%w!UY3&HDCbiP(my^JDA+P=0X%Wl^@f$Z_c+s*-GXjWKqOEzpQ18-t)}(d zX4`h}YJI2P?KI$j<74gW?PkO2YNie!?dn49-1+M@ty~x%t5?OHOwfZqVNICl3YLGp zes|5SJK5!C3rb&UcIqunJ`-E}puTm~(u6^fR*}s>6c)9JdgMGW*Yc)G7 z)tggGQ*PF(m=$wUyVGoVt5qncKB=ub&6QQJYUm{Z&X0E8TXw;_xS*G+HmNn7o_{KQ zw=gwdou9rssnxr!E)>`C+`H8a3q`#=snuqyehpo!%}#3b^K;d+^9w%CntH|5jpA8g z*Vq{gwYf>H*@5Dv*KRf{qVjiC<+;RFo=dAdTIC@-kClPF)@Vj#P~1WWR^m1ZL@LRks9iCwb5;pt zaBgek0-b3Lwu6?W<_O)hcq@`;NFUP%PZeJb9bG;bks`3p(P51tw1FZ+)D zYu;kw;uZJ1cze>krNreg?RWV@0VA<`%ZZCu-tXe=N%I@*K?EE(hDsLWdlBsGEGd z(GJp0c_HnlOuH!~ULbkGx^2GtDEAD9^q6*2ChUbxom#b z`Qgl+lc&xDnK>sD>^`itn=)wPZPUKNZsN3?GHBvN zX;#`zxtBGttDflOnO53OnRZjMDJy$v!#nx0w1XbIw3|{rVQ;$}Y!JFB`J7>xsw`$x z-f4RERc!%gOeL5=$|n%R#Ar^wbZusOum~fkRtNvdT zZbP9Y!7ztVqCdcqDlwgh%_rG!_EDrtd>~5XksMb41YtN|a`L+EhvCMK!mwCr!NWVO zZJB4QBD9+8^kKmdOAJOr0etA+>#MLtY-Rl(+z$_l|s4aaWqW9+f; z80U`i0zzf_x46M>aU^`~0zY;Xk3m&U&&*G;GmrU4xBB1;KVEQXlJjiNeuCU;*o53_ zIQxKetHj;G$@`HK&34<%!K~f%F%0Hd-Oh4`)WTr7S-NN?ei;}>i}5j~g+*hbk;X!? zF#FMsg_t(*rNgk$dTQ0~bevY7r8Gm~ltzxB7(Y1|_ipKA2+?D3 zkS0SYy&K<1A@rL&wXpq+pUDS-2FAjo@zYG>XBt1(MaRI_@pCv){Ny`sVEoJteEdvj zn~C^&kY<}bRPi&lutBr1X#C8j@iUE|_alDt4W}@ES_$H3!SBB{4r>2xI-U1`PUqcW z3{9=f-!yFBdT|GJZwsZBMdPTI#?ds6-j6uS_gus{njic)ny#4$Ce$CKH4{n?7wyYE z;BhpyvH`TRXdKO_aWsvi_alztzQcSzQDN}oXz~d~>Hb*{a!Jd>6-QGm3*zWOTK=fC zvS=JFq;WKjqxU0@^3C}1_h@nO<7o1UMd>DmX&j~RRY*Mh+eob}h@%5Mzo?kT(KL?U zk2uO-PlIu^H285e`Sg!;i-k0f?lz95Ru;t3ftkq`{XJSr<7gU3??)UR-+ay6wC&m3 zzJ@Kl%;j5un$gV%Jk2QG#zwq#4mO+FYK!qlYPaq1l9<#Q)4kvC_lEboUw&Fn!}22^ zmi=F%;SVk;<#Ryb#i)D;b=LOW=8yiFd|hcf{OV=tEWSzCpKQO4zea~)lZkaUfN}bt z>pE=7;ljI|$B!Hu9s2t!zW&v}@Sp74j<~RYTUZ5mY1SbAqlCG*8e1 zLDvYnPS6(w-5}^DL0=N|6+vGU^bJAZ60}IrcLdo4EfG{Fs6miJ&@w?Q1g#R(BXw?DzN-4(|R zU4gu#9jn-dK^#8jIMpEnUOjSy39G<>8eV45MO+H82=SisDM zm}_XS|O&|bWTake2A$wp3@Q&t;dW};~9yF=3{2nHT#%EMEfzb>WY0_B9=D| z&&;ap^$AKe(c_p|b+tYz5z*zCS#_;GB@xl*m|3+ZpO%Q|bj++;l+Q>+^g3o%t;%O5 zBAN~ufLfN%NzB5g(E(E}%jYF#F~n3G&kGW>6k@8a=S7K$?gyS$Z9XqiW)9sCFxB?+ zvcyF915C96X%Z6+C>JBkS0o}DP%cK6XC)#UP%cK6uS!IWTe%onz9tdT_2gn?`MN|z z*OQBpwL+4X6+!%c~L*4X6+!%Xx{2v8xax%LR#uuBQ+q%WD!5T~8rKme(aBx}HLeEWeP5 z=z0nnM2ua< z7+E$L5#Hdi*=2~nrx>G4M`oh)DaHtMS!Sa5DaI&sMP{P=DaJ^1Rc509DaL5CDKpW4 zzzo#l{9dMF00XL8owsBvMlhhN<=K*{7{Y+6)@NI$q9X#TTA&??8gxDbRjtsjOhs1& zRJBBVG8Ij!6r<4}WG0$YDMqBO%tTWv#i+C|GclHxVr1&cO!P&i7@4lgOms%27@6Ld zndps5F*3a)GtnKDVr2TG#0KoEz!KCl{Yj=`Km)2;ra#M6jA%er%k&qS ziXjcCYMK5jQ_&>>RV~xsWGcEOpsHp1yG%uw1k^2M>W@nLADp9wBaHp-np<}g40*yJ z4-9=>Y{MH6mA{B0W1ay~-3<6hJOFMsy?;k_BSc0$gFC95ATsP35Y-JZ${HXv?imob ztbQC2jtqPTMD-|3WaKj-s(+wHhCTzLdXyzH_6e!UrU9r`93A`&QPsv19sLYZ)#ei& k{tQvo1{59t3{ll46deE!QPoBi9RUqdw=^TJ;hp{cAFtSbSpWb4 literal 0 HcmV?d00001 From 9af68b57a5915a77275d7077c3dee5057480275f Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Wed, 15 Jan 2020 20:25:06 +0100 Subject: [PATCH 46/64] Added support for Template and QualifiedTemplate. Improved code coverage. --- EmberClient/EmberClient.js | 2 +- EmberLib/Command.js | 2 +- EmberLib/Element.js | 14 +- EmberLib/Function.js | 2 +- EmberLib/FunctionArgument.js | 2 +- EmberLib/FunctionContent.js | 2 +- EmberLib/Invocation.js | 2 +- EmberLib/InvocationResult.js | 2 +- EmberLib/Label.js | 13 +- EmberLib/Matrix.js | 58 ++- EmberLib/MatrixConnection.js | 70 ++- EmberLib/MatrixContents.js | 2 +- EmberLib/MatrixNode.js | 4 +- EmberLib/Node.js | 2 +- EmberLib/NodeContents.js | 3 +- EmberLib/Parameter.js | 5 +- EmberLib/ParameterContents.js | 17 +- EmberLib/ParameterType.js | 4 +- EmberLib/QualifiedElement.js | 5 +- EmberLib/QualifiedFunction.js | 4 +- EmberLib/QualifiedMatrix.js | 42 +- EmberLib/QualifiedNode.js | 19 +- EmberLib/QualifiedParameter.js | 23 +- EmberLib/QualifiedTemplate.js | 78 ++++ EmberLib/StreamDescription.js | 2 +- EmberLib/StringIntegerCollection.js | 13 +- EmberLib/StringIntegerPair.js | 18 +- EmberLib/Template.js | 85 ++++ EmberLib/TemplateElement.js | 75 +++ EmberLib/TreeNode.js | 53 ++- EmberLib/index.js | 42 +- EmberServer/ElementHandlers.js | 2 +- EmberServer/EmberServer.js | 2 +- EmberServer/JSONParser.js | 2 +- EmberServer/QualifiedHandlers.js | 2 +- errors.js => Errors.js | 12 +- ber.js | 2 +- package.json | 4 +- test/Ember.test.js | 693 ++++++++++++++++++++++++++-- test/Server.test.js | 12 +- 40 files changed, 1145 insertions(+), 251 deletions(-) create mode 100755 EmberLib/QualifiedTemplate.js create mode 100755 EmberLib/Template.js create mode 100755 EmberLib/TemplateElement.js rename errors.js => Errors.js (94%) diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index f2d2d43..1d148fd 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -2,7 +2,7 @@ const EventEmitter = require('events').EventEmitter; const S101Socket = require('../EmberSocket').S101Socket; const ember = require('../EmberLib'); const BER = require('../ber.js'); -const errors = require('../errors.js'); +const errors = require('../Errors.js'); const winston = require("winston"); const DEFAULT_PORT = 9000; diff --git a/EmberLib/Command.js b/EmberLib/Command.js index f91b78b..a3fab00 100755 --- a/EmberLib/Command.js +++ b/EmberLib/Command.js @@ -3,7 +3,7 @@ const Enum = require('enum'); const {COMMAND_GETDIRECTORY, COMMAND_INVOKE} = require("./constants"); const BER = require('../ber.js'); const Invocation = require("./Invocation"); -const errors = require("../errors"); +const errors = require("../Errors"); const FieldFlags = new Enum({ sparse: -2, diff --git a/EmberLib/Element.js b/EmberLib/Element.js index 26bfa9d..00aede0 100755 --- a/EmberLib/Element.js +++ b/EmberLib/Element.js @@ -19,9 +19,7 @@ class Element extends TreeNode { encode(ber) { ber.startSequence(this._seqID); - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.number); - ber.endSequence(); // BER.CONTEXT(0) + this.encodeNumber(ber); if(this.contents != null) { ber.startSequence(BER.CONTEXT(1)); @@ -33,6 +31,16 @@ class Element extends TreeNode { ber.endSequence(); // BER.APPLICATION(3) } + + /** + * + * @param {BER} ber + */ + encodeNumber(ber) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.number); + ber.endSequence(); // BER.CONTEXT(0) + } } module.exports = Element; \ No newline at end of file diff --git a/EmberLib/Function.js b/EmberLib/Function.js index 4d3913c..8611c31 100755 --- a/EmberLib/Function.js +++ b/EmberLib/Function.js @@ -5,7 +5,7 @@ const BER = require('../ber.js'); const Command = require("./Command"); const {COMMAND_INVOKE} = require("./constants"); const FunctionContent = require("./FunctionContent"); -const Errors = require("../errors"); +const Errors = require("../Errors"); class Function extends Element { constructor(number, func) { diff --git a/EmberLib/FunctionArgument.js b/EmberLib/FunctionArgument.js index 38a7d3d..de1a792 100755 --- a/EmberLib/FunctionArgument.js +++ b/EmberLib/FunctionArgument.js @@ -1,7 +1,7 @@ "use strict"; const BER = require('../ber.js'); const {ParameterType} = require("./ParameterType"); -const Errors = require("../errors"); +const Errors = require("../Errors"); /* TupleDescription ::= diff --git a/EmberLib/FunctionContent.js b/EmberLib/FunctionContent.js index 37d434e..cd368b8 100755 --- a/EmberLib/FunctionContent.js +++ b/EmberLib/FunctionContent.js @@ -1,7 +1,7 @@ "use strict"; const BER = require('../ber.js'); const FunctionArgument = require("./FunctionArgument"); -const errors = require("../errors"); +const errors = require("../Errors"); class FunctionContent { constructor(identifier=null, description=null) { diff --git a/EmberLib/Invocation.js b/EmberLib/Invocation.js index 580a68c..945b828 100755 --- a/EmberLib/Invocation.js +++ b/EmberLib/Invocation.js @@ -2,7 +2,7 @@ const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); const BER = require('../ber.js'); const FunctionArgument = require("./FunctionArgument"); -const errors = require("../errors"); +const errors = require("../Errors"); let _id = 1; class Invocation { diff --git a/EmberLib/InvocationResult.js b/EmberLib/InvocationResult.js index f26ea41..b036b32 100755 --- a/EmberLib/InvocationResult.js +++ b/EmberLib/InvocationResult.js @@ -3,7 +3,7 @@ const BER = require('../ber.js'); const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); const FunctionArgument = require("./FunctionArgument"); -const Errors = require("../errors"); +const Errors = require("../Errors"); class InvocationResult { diff --git a/EmberLib/Label.js b/EmberLib/Label.js index 280203e..8a41235 100755 --- a/EmberLib/Label.js +++ b/EmberLib/Label.js @@ -1,6 +1,6 @@ "use strict"; const BER = require('../ber.js'); -const Errors = require("../errors"); +const Errors = require("../Errors"); class Label { constructor(path, description) { @@ -17,7 +17,7 @@ class Label { * @param {BER} ber */ encode(ber) { - ber.startSequence(BER.APPLICATION(18)); + ber.startSequence(Label.BERID); if (this.basePath == null) { throw new Errors.InvalidEmberNode("", "Missing label base path"); } @@ -41,7 +41,7 @@ class Label { static decode(ber) { var l = new Label(); - ber = ber.getSequence(BER.APPLICATION(18)); + ber = ber.getSequence(Label.BERID); while (ber.remain > 0) { var tag = ber.peek(); @@ -57,6 +57,13 @@ class Label { } return l; } + + /** + * + */ + static get BERID() { + return BER.APPLICATION(18); + } } module.exports = Label; \ No newline at end of file diff --git a/EmberLib/Matrix.js b/EmberLib/Matrix.js index c8e4d95..9add4f8 100755 --- a/EmberLib/Matrix.js +++ b/EmberLib/Matrix.js @@ -5,7 +5,7 @@ const BER = require('../ber.js'); const MatrixMode = require("./MatrixMode"); const MatrixOperation = require("./MatrixOperation"); const MatrixType = require("./MatrixType"); -const Errors = require("../errors"); +const Errors = require("../Errors"); class Matrix extends TreeNode { @@ -72,7 +72,7 @@ class Matrix extends TreeNode ber.startSequence(BER.CONTEXT(5)); ber.startSequence(BER.EMBER_SEQUENCE); - for(var id in this.connections) { + for(let id in this.connections) { if (this.connections.hasOwnProperty(id)) { ber.startSequence(BER.CONTEXT(0)); this.connections[id].encode(ber); @@ -93,7 +93,7 @@ class Matrix extends TreeNode ber.startSequence(BER.CONTEXT(4)); ber.startSequence(BER.EMBER_SEQUENCE); - for(var i=0; i 0) { - var seq = ber.getSequence(BER.CONTEXT(0)); + let seq = ber.getSequence(BER.CONTEXT(0)); seq = seq.getSequence(BER.APPLICATION(14)); seq = seq.getSequence(BER.CONTEXT(0)); targets.push(seq.readInt()); @@ -279,7 +278,7 @@ class Matrix extends TreeNode const sources = []; ber = ber.getSequence(BER.EMBER_SEQUENCE); while(ber.remain > 0) { - var seq = ber.getSequence(BER.CONTEXT(0)); + let seq = ber.getSequence(BER.CONTEXT(0)); seq = seq.getSequence(BER.APPLICATION(15)); seq = seq.getSequence(BER.CONTEXT(0)); sources.push(seq.readInt()); @@ -293,14 +292,12 @@ class Matrix extends TreeNode * @returns {Object} */ static decodeConnections(ber) { - let connections = {}; - let seq = ber.getSequence(BER.EMBER_SEQUENCE); + const connections = {}; + const seq = ber.getSequence(BER.EMBER_SEQUENCE); while(seq.remain > 0) { - var conSeq = seq.getSequence(BER.CONTEXT(0)); - var con = MatrixConnection.decode(conSeq); - if (con.target != null) { - connections[con.target] = (con); - } + const conSeq = seq.getSequence(BER.CONTEXT(0)); + const con = MatrixConnection.decode(conSeq); + connections[con.target] = (con); } return connections; } @@ -347,31 +344,44 @@ class Matrix extends TreeNode * * @param {QualifiedMatrix|MatrixNode} matrix * @param {QualifiedMatrix|MatrixNode} newMatrix + * @returns {boolean} - True if something changed */ - static MatrixUpdate(matrix, newMatrix) { + static MatrixUpdate(matrix, newMatrix) { + let modified = false; if (newMatrix.targets != null) { matrix.targets = newMatrix.targets; + modified = true; } if (newMatrix.sources != null) { matrix.sources = newMatrix.sources; + modified = true; } if (newMatrix.connections != null) { if (matrix.connections == null) { matrix.connections = {}; + modified = true; } for(let id in newMatrix.connections) { if (newMatrix.connections.hasOwnProperty(id)) { - let connection = newMatrix.connections[id]; + const connection = newMatrix.connections[id]; if ((connection.target < matrix.contents.targetCount) && (connection.target >= 0)) { if (matrix.connections[connection.target] == null) { matrix.connections[connection.target] = new MatrixConnection(connection.target); + modified = true; + } + if (matrix.connections[connection.target].isDifferent(connection.sources)) { + matrix.connections[connection.target].setSources(connection.sources); + modified = true; } - matrix.connections[connection.target].setSources(connection.sources); + } + else { + throw new Errors.InvalidMatrixSignal(connection.target, "Invalid target") } } } - } + } + return modified; } /** diff --git a/EmberLib/MatrixConnection.js b/EmberLib/MatrixConnection.js index 693eee2..45ec689 100755 --- a/EmberLib/MatrixConnection.js +++ b/EmberLib/MatrixConnection.js @@ -2,7 +2,7 @@ const BER = require('../ber.js'); const MatrixOperation = require("./MatrixOperation"); const MatrixDisposition = require("./MatrixDisposition"); -const errors = require("../errors"); +const Errors = require("../Errors"); class MatrixConnection { /** @@ -12,7 +12,9 @@ class MatrixConnection { constructor(target) { if (target) { let _target = Number(target); - if (isNaN(_target)) { _target = 0; } + if (isNaN(_target)) { + throw new Errors.InvalidMatrixSignal(target, "Can't create connection with invalid target.") + } this.target = _target; } else { @@ -26,14 +28,7 @@ class MatrixConnection { * @param {number[]} sources */ connectSources(sources) { - if (sources == null) { - return; - } - let s = new Set(this.sources); - for(let item of sources) { - s.add(item); - } - this.sources = [...s].sort(); + this.sources = this.validateSources(sources); } /** @@ -56,13 +51,13 @@ class MatrixConnection { * @param {BER} ber */ encode(ber) { - ber.startSequence(BER.APPLICATION(16)); + ber.startSequence(MatrixConnection.BERID); ber.startSequence(BER.CONTEXT(0)); ber.writeInt(this.target); ber.endSequence(); - if ((this.sources != null)&& (this.sources.length > 0)) { + if (this.sources != null) { ber.startSequence(BER.CONTEXT(1)); ber.writeRelativeOID(this.sources.join("."), BER.EMBER_RELATIVE_OID); ber.endSequence(); @@ -79,6 +74,31 @@ class MatrixConnection { } ber.endSequence(); } + + /** + * + * @param {number[]|null} sources + */ + isDifferent(sources) { + const newSources = this.validateSources(sources); + + if (this.sources == null && newSources == null) { + return false; + } + + if ((this.sources == null && newSources != null)|| + (this.sources != null && newSources == null) || + (this.sources.length != newSources.length)) { + return true; + } + // list are ordered, so we can simply parse 1 by 1. + for(let i = 0; i < this.sources.length; i++) { + if (this.sources[i] !== newSources[i]) { + return true; + } + } + return false; + } /** * @returns {boolean} @@ -103,8 +123,20 @@ class MatrixConnection { delete this.sources; return; } - let s = new Set(sources.map(i => Number(i))); - this.sources = [...s].sort(); // sources should be an array + this.sources = this.validateSources(sources); + } + + /** + * + * @param {number[]} sources + * @returns {number[]} - uniq and sorted + */ + validateSources(sources) { + if (sources == null) { + return null; + } + const s = new Set(sources.map(i => Number(i))); + return [...s].sort(); } /** @@ -121,7 +153,7 @@ class MatrixConnection { */ static decode(ber) { const c = new MatrixConnection(); - ber = ber.getSequence(BER.APPLICATION(16)); + ber = ber.getSequence(MatrixConnection.BERID); while (ber.remain > 0) { let tag = ber.peek(); let seq = ber.getSequence(tag); @@ -143,12 +175,18 @@ class MatrixConnection { c.disposition = MatrixDisposition.get(seq.readInt()); } else { - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } return c; } + /** + * + */ + static get BERID() { + return BER.APPLICATION(16); + } } module.exports = MatrixConnection; \ No newline at end of file diff --git a/EmberLib/MatrixContents.js b/EmberLib/MatrixContents.js index b7f75af..1bbcda8 100755 --- a/EmberLib/MatrixContents.js +++ b/EmberLib/MatrixContents.js @@ -4,7 +4,7 @@ const MatrixType = require("./MatrixType"); const MatrixMode = require("./MatrixMode"); const BER = require('../ber.js'); const Label = require("./Label"); -const errors = require("../errors"); +const errors = require("../Errors"); class MatrixContents { constructor(type = MatrixType.oneToN, mode = MatrixMode.linear) { diff --git a/EmberLib/MatrixNode.js b/EmberLib/MatrixNode.js index b368f15..7cf53d4 100755 --- a/EmberLib/MatrixNode.js +++ b/EmberLib/MatrixNode.js @@ -4,7 +4,7 @@ const Matrix = require("./Matrix"); const MatrixContents = require("./MatrixContents"); const QualifiedMatrix = require("./QualifiedMatrix"); const BER = require('../ber.js'); -const errors = require("../errors"); +const errors = require("../Errors"); class MatrixNode extends Matrix { constructor(number = undefined) { @@ -17,7 +17,7 @@ class MatrixNode extends Matrix { * @param {BER} ber */ encode(ber) { - ber.startSequence(BER.APPLICATION(13)); + ber.startSequence(MatrixNode.BERID); ber.startSequence(BER.CONTEXT(0)); ber.writeInt(this.number); diff --git a/EmberLib/Node.js b/EmberLib/Node.js index a42f001..e24c2eb 100755 --- a/EmberLib/Node.js +++ b/EmberLib/Node.js @@ -4,7 +4,7 @@ const Element = require("./Element"); const QualifiedNode = require("./QualifiedNode"); const NodeContents = require("./NodeContents"); const BER = require('../ber.js'); -const Errors = require("../errors"); +const Errors = require("../Errors"); class Node extends Element { /** diff --git a/EmberLib/NodeContents.js b/EmberLib/NodeContents.js index 68aeede..cb3c479 100755 --- a/EmberLib/NodeContents.js +++ b/EmberLib/NodeContents.js @@ -1,6 +1,6 @@ "use strict"; const BER = require('../ber.js'); -const errors = require("../errors"); +const errors = require("../Errors"); class NodeContents{ /** @@ -12,7 +12,6 @@ class NodeContents{ this.isOnline = true; this.identifier = identifier; this.description = description; - this._subscribers = new Set(); } /** diff --git a/EmberLib/Parameter.js b/EmberLib/Parameter.js index c0ba322..5e0dfd9 100755 --- a/EmberLib/Parameter.js +++ b/EmberLib/Parameter.js @@ -4,8 +4,7 @@ const Element = require("./Element"); const QualifiedParameter = require("./QualifiedParameter"); const BER = require('../ber.js'); const ParameterContents = require("./ParameterContents"); -const Errors = require("../errors"); -const {COMMAND_SUBSCRIBE} = require("./Command"); +const Errors = require("../Errors"); class Parameter extends Element { /** @@ -40,7 +39,7 @@ class Parameter extends Element { * @returns {QualifiedParameter} */ toQualified() { - let qp = new QualifiedParameter(this.getPath()); + const qp = new QualifiedParameter(this.getPath()); qp.update(this); return qp; } diff --git a/EmberLib/ParameterContents.js b/EmberLib/ParameterContents.js index 54548c6..4846b99 100755 --- a/EmberLib/ParameterContents.js +++ b/EmberLib/ParameterContents.js @@ -5,7 +5,7 @@ const ParameterAccess = require("./ParameterAccess"); const StringIntegerCollection = require("./StringIntegerCollection"); const StreamDescription = require("./StreamDescription"); const BER = require('../ber.js'); -const errors = require("../errors"); +const Errors = require("../Errors"); class ParameterContents { /** @@ -14,7 +14,6 @@ class ParameterContents { * @param {string} type */ constructor(value, type) { - this._subscribers = new Set(); if(value != null) { this.value = value; } @@ -48,8 +47,10 @@ class ParameterContents { ber.writeIfDefinedEnum(this.type, ParameterType, ber.writeInt, 13); ber.writeIfDefined(this.streamIdentifier, ber.writeInt, 14); - if(this.stringIntegerCollection != null) { - this.stringIntegerCollection.encode(ber); + if(this.enumMap != null) { + ber.startSequence(BER.CONTEXT(15)); + this.enumMap.encode(ber); + ber.endSequence(); } if(this.streamDescriptor != null) { @@ -106,16 +107,14 @@ class ParameterContents { } else if(tag == BER.CONTEXT(14)) { pc.streamIdentifier = seq.readInt(); } else if(tag == BER.CONTEXT(15)) { - pc.stringIntegerCollection = StringIntegerCollection.decode(seq); + pc.enumMap = StringIntegerCollection.decode(seq); } else if(tag == BER.CONTEXT(16)) { pc.streamDescriptor = StreamDescription.decode(seq); } else if(tag == BER.CONTEXT(17)) { pc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); - } else if (tag == null) { - break; - } + } else { - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } return pc; diff --git a/EmberLib/ParameterType.js b/EmberLib/ParameterType.js index 6b9fb04..421a1c9 100755 --- a/EmberLib/ParameterType.js +++ b/EmberLib/ParameterType.js @@ -1,6 +1,6 @@ const Enum = require('enum'); const BER = require('../ber.js'); -const Errors = require("../errors"); +const Errors = require("../Errors"); function ParameterTypetoBERTAG(type) { switch (type.value) { @@ -10,7 +10,7 @@ function ParameterTypetoBERTAG(type) { case 4: return BER.EMBER_BOOLEAN; case 7: return BER.EMBER_OCTETSTRING; default: - throw new Errors.InvalidBERFormat(`Unhandled ParameterType ${type}`); + throw new Errors.InvalidEmberNode("", `Unhandled ParameterType ${type}`); } } diff --git a/EmberLib/QualifiedElement.js b/EmberLib/QualifiedElement.js index f7a78c3..b233375 100755 --- a/EmberLib/QualifiedElement.js +++ b/EmberLib/QualifiedElement.js @@ -28,9 +28,7 @@ class QualifiedElement extends TreeNode { encode(ber) { ber.startSequence(this._seqID); - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); - ber.endSequence(); // BER.CONTEXT(0) + this.encodePath(ber); if(this.contents != null) { ber.startSequence(BER.CONTEXT(1)); @@ -43,7 +41,6 @@ class QualifiedElement extends TreeNode { ber.endSequence(); // BER.APPLICATION(3) } - /** * * @param {number} cmd diff --git a/EmberLib/QualifiedFunction.js b/EmberLib/QualifiedFunction.js index b34ca15..7a4d017 100755 --- a/EmberLib/QualifiedFunction.js +++ b/EmberLib/QualifiedFunction.js @@ -5,7 +5,7 @@ const FunctionContent = require("./FunctionContent"); const {COMMAND_GETDIRECTORY, COMMAND_INVOKE} = require("./constants"); const BER = require('../ber.js'); const Invocation = require("./Invocation"); -const errors = require("../errors"); +const Errors = require("../Errors"); class QualifiedFunction extends QualifiedElement { /** @@ -67,7 +67,7 @@ class QualifiedFunction extends QualifiedElement { qf.decodeChildren(seq); } else { - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } return qf; diff --git a/EmberLib/QualifiedMatrix.js b/EmberLib/QualifiedMatrix.js index 98efd23..ab4e8e4 100755 --- a/EmberLib/QualifiedMatrix.js +++ b/EmberLib/QualifiedMatrix.js @@ -1,12 +1,11 @@ "use strict"; const Matrix = require("./Matrix"); -const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); const BER = require('../ber.js'); const Command = require("./Command"); const MatrixContents = require("./MatrixContents"); const MatrixConnection = require("./MatrixConnection"); -const errors = require("../errors"); +const errors = require("../Errors"); class QualifiedMatrix extends Matrix { /** @@ -42,9 +41,7 @@ class QualifiedMatrix extends Matrix { encode(ber) { ber.startSequence(QualifiedMatrix.BERID); - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); - ber.endSequence(); // BER.CONTEXT(0) + this.encodePath(ber); if(this.contents != null) { ber.startSequence(BER.CONTEXT(1)); @@ -74,41 +71,6 @@ class QualifiedMatrix extends Matrix { return r; } - /** - * - * @param {function} callback - * @returns {TreeNode} - */ - getDirectory(callback) { - if (callback != null && !this.isStream()) { - this.contents._subscribers.add(callback); - } - return this.getCommand(COMMAND_GETDIRECTORY); - } - - /** - * - * @param {function} callback - * @returns {TreeNode} - */ - subscribe(callback) { - if (callback != null && this.isStream()) { - this.contents._subscribers.add(callback); - } - return this.getCommand(COMMAND_SUBSCRIBE); - } - - /** - * - * @param {function} callback - * @returns {TreeNode} - */ - unsubscribe(callback) { - if (callback != null && this.isStream()) { - this.contents._subscribers.delete(callback); - } - return this.getCommand(COMMAND_UNSUBSCRIBE); - } /** * diff --git a/EmberLib/QualifiedNode.js b/EmberLib/QualifiedNode.js index d0ec097..155e23c 100755 --- a/EmberLib/QualifiedNode.js +++ b/EmberLib/QualifiedNode.js @@ -2,8 +2,7 @@ const QualifiedElement = require("./QualifiedElement"); const BER = require('../ber.js'); const NodeContents = require("./NodeContents"); -const Node = require("./Node"); -const errors = require("../errors"); +const Errors = require("../Errors"); class QualifiedNode extends QualifiedElement { constructor (path) { @@ -18,20 +17,6 @@ class QualifiedNode extends QualifiedElement { return true; } - /** - * - * @param {boolean} complete - * @returns {QualifiedNode} - */ - getMinimal(complete = false) { - const number = this.getNumber(); - const n = new Node(number); - if (complete && (this.contents != null)) { - n.contents = this.contents; - } - return n; - } - /** * * @param {BER} ber @@ -51,7 +36,7 @@ class QualifiedNode extends QualifiedElement { } else if(tag == BER.CONTEXT(2)) { qn.decodeChildren(seq); } else { - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } return qn; diff --git a/EmberLib/QualifiedParameter.js b/EmberLib/QualifiedParameter.js index a079c03..9a14095 100755 --- a/EmberLib/QualifiedParameter.js +++ b/EmberLib/QualifiedParameter.js @@ -3,7 +3,7 @@ const QualifiedElement = require("./QualifiedElement"); const ParameterContents = require("./ParameterContents"); const BER = require('../ber.js'); -const Parameter = require("./Parameter"); +const Errors = require("../Errors"); class QualifiedParameter extends QualifiedElement { /** @@ -22,22 +22,6 @@ class QualifiedParameter extends QualifiedElement { return true; } - /** - * - * @param {boolean} complete - * @returns {Parameter} - */ - getMinimal(complete = false) { - const number = this.getNumber(); - const p = new Parameter(number); - if (complete) { - if (this.contents != null) { - p.contents = this.contents; - } - } - return p; - } - /** * * @param {number|string} value @@ -69,9 +53,6 @@ class QualifiedParameter extends QualifiedElement { this.contents[key] = other.contents[key]; } } - for(let cb of this.contents._subscribers) { - cb(this); - } } } return; @@ -96,7 +77,7 @@ class QualifiedParameter extends QualifiedElement { } else if(tag == BER.CONTEXT(2)) { qp.decodeChildren(seq); } else { - return qp; + throw new Errors.UnimplementedEmberTypeError(tag); } } return qp; diff --git a/EmberLib/QualifiedTemplate.js b/EmberLib/QualifiedTemplate.js new file mode 100755 index 0000000..a5a2d51 --- /dev/null +++ b/EmberLib/QualifiedTemplate.js @@ -0,0 +1,78 @@ +"use strict"; +const TemplateElement = require("./TemplateElement"); +const QualifiedElement = require("./QualifiedElement"); +const Template = require("./Template"); +const BER = require('../ber.js'); +const Errors = require("../Errors"); + +class QualifiedTemplate extends QualifiedElement { + /** + * + * @param {string} path + * @param {Node|Function|MatrixNode|Parameter} element + */ + constructor(path, element) { + super(path); + this.element = element; + this._seqID = QualifiedTemplate.BERID; + } + + /** + * @returns {boolean} + */ + isTemplate() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(QualifiedTemplate.BERID); + + this.encodePath(ber); + + TemplateElement.encodeContent(this, ber); + + ber.endSequence(); + } + + /** + * + * @param {Template} other + */ + update(other) { + this.element = other.element; + } + + /** + * + * @param {BER} ber + * @returns {QualifiedTemplate} + */ + static decode(ber) { + const qt = new QualifiedTemplate(); + ber = ber.getSequence(QualifiedTemplate.BERID); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + qt.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else { + TemplateElement.decodeContent(qt, tag, seq); + } + } + return qt; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(25); + } +} + +module.exports = QualifiedTemplate; \ No newline at end of file diff --git a/EmberLib/StreamDescription.js b/EmberLib/StreamDescription.js index 81ac7e6..53edb69 100755 --- a/EmberLib/StreamDescription.js +++ b/EmberLib/StreamDescription.js @@ -2,7 +2,7 @@ const Element = require("./Element"); const BER = require('../ber.js'); const StreamFormat = require("./StreamFormat"); -const Errors = require("../errors"); +const Errors = require("../Errors"); class StreamDescription extends Element{ /** diff --git a/EmberLib/StringIntegerCollection.js b/EmberLib/StringIntegerCollection.js index 21baa99..3fd3eb1 100755 --- a/EmberLib/StringIntegerCollection.js +++ b/EmberLib/StringIntegerCollection.js @@ -1,7 +1,7 @@ "use strict"; const BER = require('../ber.js'); const StringIntegerPair = require("./StringIntegerPair"); -const Errors = require("../errors"); +const Errors = require("../Errors"); class StringIntegerCollection { constructor() { @@ -14,6 +14,9 @@ class StringIntegerCollection { * @param {StringIntegerPair} value */ addEntry(key, value) { + if (!(value instanceof StringIntegerPair)) { + throw new Errors.InvalidStringPair(); + } this._collection.set(key, value); } @@ -32,23 +35,23 @@ class StringIntegerCollection { */ encode(ber) { ber.startSequence(StringIntegerCollection.BERID); - for(let [key,sp] of this._collection) { + for(let [,sp] of this._collection) { ber.startSequence(BER.CONTEXT(0)); sp.encode(ber); ber.endSequence(); } ber.endSequence(); - ber.endSequence(); } /** - * + * @returns {JSON_StringPair[]} */ toJSON() { const collection = []; - for(let [key,sp] of this._collection) { + for(let [,sp] of this._collection) { collection.push(sp.toJSON()); } + return collection; } /** diff --git a/EmberLib/StringIntegerPair.js b/EmberLib/StringIntegerPair.js index 187a6fb..15bbe1c 100755 --- a/EmberLib/StringIntegerPair.js +++ b/EmberLib/StringIntegerPair.js @@ -1,6 +1,6 @@ "use strict"; const BER = require('../ber.js'); -const errors = require("../errors"); +const Errors = require("../Errors"); class StringIntegerPair { constructor(key,value) { @@ -14,7 +14,7 @@ class StringIntegerPair { */ encode(ber) { if (this.key == null || this.value == null) { - throw new errors.InvalidEmberNode("", "Invalid key/value missing"); + throw new Errors.InvalidEmberNode("", "Invalid key/value missing"); } ber.startSequence(StringIntegerPair.BERID); ber.startSequence(BER.CONTEXT(0)); @@ -26,8 +26,18 @@ class StringIntegerPair { ber.endSequence(); } + /** + * @typedef {{ + * key: string + * value: number + * }} JSON_StringPair + */ + /** - * + * @returns {{ + * key: string + * value: number + * }} */ toJSON() { return { @@ -51,7 +61,7 @@ class StringIntegerPair { } else if(tag == BER.CONTEXT(1)) { sp.value = dataSeq.readInt(); } else { - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } return sp; diff --git a/EmberLib/Template.js b/EmberLib/Template.js new file mode 100755 index 0000000..44a3950 --- /dev/null +++ b/EmberLib/Template.js @@ -0,0 +1,85 @@ +"use strict"; + +const Element = require("./Element"); +const QualifiedTemplate = require("./QualifiedTemplate"); +const BER = require('../ber.js'); +const TemplateElement = require("./TemplateElement"); +const Errors = require("../Errors"); + +class Template extends Element { + /** + * + * @param {number} number + * @param {Node|Function|MatrixNode|Parameter} element + */ + constructor(number, element) { + super(); + this.number = number; + this.element = element; + this._seqID = Template.BERID; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(Template.BERID); + this.encodeNumber(ber); + TemplateElement.encodeContent(this, ber); + ber.endSequence(); + } + + /** + * @returns {boolean} + */ + isTemplate() { + return true; + } + + /** + * @returns {QualifiedParameter} + */ + toQualified() { + const qp = new QualifiedTemplate(this.getPath()); + qp.update(this); + return qp; + } + + /** + * + * @param {Template} other + */ + update(other) { + this.element = other.element; + } + + /** + * + * @param {BER} ber + * @returns {Template} + */ + static decode(ber) { + const template = new Template(); + ber = ber.getSequence(Template.BERID); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + template.number = seq.readInt(); + } else { + TemplateElement.decodeContent(template, tag, seq); + } + } + return template; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(24); + } +} + +module.exports = Template; \ No newline at end of file diff --git a/EmberLib/TemplateElement.js b/EmberLib/TemplateElement.js new file mode 100755 index 0000000..1642ac0 --- /dev/null +++ b/EmberLib/TemplateElement.js @@ -0,0 +1,75 @@ +"use strict"; + +const BER = require('../ber.js'); +const Parameter = require("./Parameter"); +const Node = require("./Node"); +const MatrixNode = require("./MatrixNode"); +const Function = require("./Function"); +const Errors = require("../Errors"); + +/* +TemplateElement ::= + CHOICE { + parameter Parameter, + node Node, + matrix Matrix, + function Function + } +*/ +class TemplateElement { + /** + * + * @param {Node|Function|Parameter|MatrixNode} ber + */ + static decode(ber) { + const tag = ber.peek(); + if (tag == BER.APPLICATION(1)) { + return Parameter.decode(ber); + } else if(tag == BER.APPLICATION(3)) { + return Node.decode(ber); + } else if(tag == BER.APPLICATION(19)) { + return Function.decode(ber); + } else if(tag == BER.APPLICATION(13)) { + return MatrixNode.decode(ber); + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + + /** + * + * @param {Template|QualifiedTemplate} template + * @param {number} tag + * @param {BER} ber + */ + static decodeContent(template, tag, ber) { + if(tag == BER.CONTEXT(1)) { + template.element = TemplateElement.decode(ber); + } else if(tag == BER.CONTEXT(2)) { + template.description = ber.readString(BER.EMBER_STRING); + } else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + /** + * + * @param {Template|QualifiedTemplate} template + * @param {BER} ber + */ + static encodeContent(template, ber) { + if(template.element != null) { + ber.startSequence(BER.CONTEXT(1)); + template.element.encode(ber); + ber.endSequence(); + } + + if (template.description != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.writeString(template.description, BER.EMBER_STRING); + ber.endSequence(); + } + } +} + +module.exports = TemplateElement; \ No newline at end of file diff --git a/EmberLib/TreeNode.js b/EmberLib/TreeNode.js index 166b751..a7344b1 100755 --- a/EmberLib/TreeNode.js +++ b/EmberLib/TreeNode.js @@ -2,27 +2,26 @@ const BER = require('../ber.js'); const Command = require("./Command"); const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); -const Errors = require("../errors"); class TreeNode { constructor() { /** @type {TreeNode} */ - this._parent = null; + this._parent = null; + this._subscribers = new Set(); } _isSubscribable(callback) { - return (callback != null && this.isParameter() && this.isStream()); + return (callback != null && + ((this.isParameter() && this.isStream()) || + this.isMatrix())); } _subscribe(callback) { - if (this.contents == null) { - throw new Errors.InvalidEmberNode(this.getPath(), "No content to subscribe"); - } - this.contents._subscribers.add(callback); + this._subscribers.add(callback); } _unsubscribe(callback) { - this.contents._subscribers.delete(callback); + this._subscribers.delete(callback); } /** @@ -119,6 +118,18 @@ class TreeNode { } } + /** + * + * @param {BER} ber + */ + encodePath(ber) { + if (this.isQualified()) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); + ber.endSequence(); // BER.CONTEXT(0) + } + } + /** * @returns {TreeNode} */ @@ -182,6 +193,14 @@ class TreeNode { return this.contents != null && this.contents.streamIdentifier != null; } + + /** + * @returns {boolean} + */ + isTemplate() { + return false; + } + /** * @returns {TreeNode} */ @@ -511,12 +530,13 @@ class TreeNode { * @param {TreeNode} other */ update(other) { + let modified = false; if ((other != null) && (other.contents != null)) { if (this.contents == null) { this.contents = other.contents; + modified = true; } else { - let modified = false; for (var key in other.contents) { if (key[0] === "_") { continue; } if (other.contents.hasOwnProperty(key) && @@ -525,14 +545,17 @@ class TreeNode { modified = true; } } - if (modified && this.contents._subscribers != null) { - for(let cb of this.contents._subscribers) { - cb(this); - } - } } } - return; + return modified; + } + + updateSubscribers() { + if (this._subscribers != null) { + for(let cb of this._subscribers) { + cb(this); + } + } } /** diff --git a/EmberLib/index.js b/EmberLib/index.js index 1bdf67f..f890109 100755 --- a/EmberLib/index.js +++ b/EmberLib/index.js @@ -1,7 +1,7 @@ const {Subscribe,COMMAND_SUBSCRIBE,Unsubscribe,COMMAND_UNSUBSCRIBE, GetDirectory,COMMAND_GETDIRECTORY,Invoke,COMMAND_INVOKE, COMMAND_STRINGS} = require("./constants"); const BER = require('../ber.js'); -const errors = require("../errors"); +const Errors = require("../Errors"); const TreeNode = require("./TreeNode"); const Command = require("./Command"); const Function = require("./Function"); @@ -32,6 +32,9 @@ const StringIntegerPair = require("./StringIntegerPair"); const StringIntegerCollection = require("./StringIntegerCollection"); const StreamFormat = require("./StreamFormat"); const StreamDescription = require("./StreamDescription"); +const Template = require("./Template"); +const TemplateElement = require("./TemplateElement"); +const QualifiedTemplate = require("./QualifiedTemplate"); const rootDecode = function(ber) { const r = new TreeNode(); @@ -62,7 +65,7 @@ const rootDecode = function(ber) { else { // StreamCollection BER.APPLICATION(6) // InvocationResult BER.APPLICATION(23) - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } else if (tag === BER.CONTEXT(0)) { @@ -76,7 +79,7 @@ const rootDecode = function(ber) { } } else { - throw new errors.UnimplementedEmberTypeError(tag); + throw new Errors.UnimplementedEmberTypeError(tag); } } return r; @@ -84,29 +87,31 @@ const rootDecode = function(ber) { const childDecode = function(ber) { const tag = ber.peek(); - if (tag == BER.APPLICATION(1)) { + if (tag == Parameter.BERID) { return Parameter.decode(ber); - } else if(tag == BER.APPLICATION(3)) { + } else if(tag == Node.BERID) { return Node.decode(ber); - } else if(tag == BER.APPLICATION(2)) { + } else if(tag == Command.BERID) { return Command.decode(ber); - } else if(tag == BER.APPLICATION(9)) { + } else if(tag == QualifiedParameter.BERID) { return QualifiedParameter.decode(ber); - } else if(tag == BER.APPLICATION(10)) { + } else if(tag == QualifiedNode.BERID) { return QualifiedNode.decode(ber); - } else if(tag == BER.APPLICATION(13)) { + } else if(tag == MatrixNode.BERID) { return MatrixNode.decode(ber); - } else if(tag == BER.APPLICATION(17)) { + } else if(tag == QualifiedMatrix.BERID) { return QualifiedMatrix.decode(ber); - } else if(tag == BER.APPLICATION(19)) { + } else if(tag == Function.BERID) { return Function.decode(ber); - } else if (tag == BER.APPLICATION(20)) { + } else if (tag == QualifiedFunction.BERID) { return QualifiedFunction.decode(ber); - } else if(tag == BER.APPLICATION(24)) { - // Template - throw new errors.UnimplementedEmberTypeError(tag); - } else { - throw new errors.UnimplementedEmberTypeError(tag); + } else if(tag == Template.BERID) { + return Template.decode(ber); + } else if (tag == QualifiedTemplate.BERID) { + return QualifiedTemplate.decode(ber) + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); } } @@ -148,10 +153,13 @@ module.exports = { QualifiedMatrix, QualifiedNode, QualifiedParameter, + QualifiedTemplate, StreamFormat, StreamDescription, StringIntegerPair, StringIntegerCollection, + Template, + TemplateElement, Subscribe,COMMAND_SUBSCRIBE, Unsubscribe,COMMAND_UNSUBSCRIBE, GetDirectory,COMMAND_GETDIRECTORY, diff --git a/EmberServer/ElementHandlers.js b/EmberServer/ElementHandlers.js index a318500..ece5ce0 100755 --- a/EmberServer/ElementHandlers.js +++ b/EmberServer/ElementHandlers.js @@ -2,7 +2,7 @@ const QualifiedHandlers = require("./QualifiedHandlers"); const EmberLib = require('../EmberLib'); const ServerEvents = require("./ServerEvents"); -const Errors = require("../errors"); +const Errors = require("../Errors"); const winston = require("winston"); class ElementHandlers extends QualifiedHandlers{ diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js index 9fe7c73..99f74bf 100755 --- a/EmberServer/EmberServer.js +++ b/EmberServer/EmberServer.js @@ -5,7 +5,7 @@ const JSONParser = require("./JSONParser"); const ElementHandlers = require("./ElementHandlers"); const ServerEvents = require("./ServerEvents"); const winston = require("winston"); -const Errors = require("../errors"); +const Errors = require("../Errors"); class TreeServer extends EventEmitter{ /** diff --git a/EmberServer/JSONParser.js b/EmberServer/JSONParser.js index 9fcd617..126164a 100755 --- a/EmberServer/JSONParser.js +++ b/EmberServer/JSONParser.js @@ -1,6 +1,6 @@ "use strict"; const ember = require('../EmberLib'); -const Errors = require("../errors"); +const Errors = require("../Errors"); class JSONParser { /** diff --git a/EmberServer/QualifiedHandlers.js b/EmberServer/QualifiedHandlers.js index 1589087..21ec5c2 100755 --- a/EmberServer/QualifiedHandlers.js +++ b/EmberServer/QualifiedHandlers.js @@ -1,6 +1,6 @@ "use strict"; const MatrixHandlers = require("./MatrixHandlers"); -const Errors = require("../errors"); +const Errors = require("../Errors"); class QualifiedHandlers extends MatrixHandlers { /** diff --git a/errors.js b/Errors.js similarity index 94% rename from errors.js rename to Errors.js index e89d463..89c85ad 100755 --- a/errors.js +++ b/Errors.js @@ -187,4 +187,14 @@ class InvalidMatrixSignal extends Error { super(`Invalid Matrix Signal ${value}: ${info}`); } } -module.exports.InvalidMatrixSignal = InvalidMatrixSignal; \ No newline at end of file +module.exports.InvalidMatrixSignal = InvalidMatrixSignal; + +class InvalidStringPair extends Error { + /** + * + */ + constructor() { + super("Invalid StringPair Value"); + } +} +module.exports.InvalidStringPair = InvalidStringPair; \ No newline at end of file diff --git a/ber.js b/ber.js index 08bfbe9..72ae011 100755 --- a/ber.js +++ b/ber.js @@ -22,7 +22,7 @@ ***************************************************************************/ const BER = require('asn1').Ber; -const errors = require('./errors.js'); +const errors = require('./Errors.js'); const util = require('util'); const Long = require('long'); diff --git a/package.json b/package.json index bb05bd1..5832de4 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.3.0", + "version": "2.4.0", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { @@ -19,7 +19,7 @@ }, "license": "MIT", "dependencies": { - "asn1": "github:evs-broadcast/node-asn1#date_2018_01_02", + "asn1": "github:evs-broadcast/node-asn1#date_20190114", "enum": "^2.4.0", "long": "^3.2.0", "smart-buffer": "^3.0.3", diff --git a/test/Ember.test.js b/test/Ember.test.js index be0c90f..4195c7e 100755 --- a/test/Ember.test.js +++ b/test/Ember.test.js @@ -4,16 +4,16 @@ const s101Buffer = Buffer.from("fe000e0001c001021f026082008d6b820089a0176a15a005 const errorBuffer = Buffer.from("76fe000e0001c001021f026082008d6b820089a0176a15a0050d03010201a10c310aa0080c066c6162656c73a01b6a19a0050d03010202a110310ea00c0c0a706172616d6574657273a051714fa0050d03010203a1463144a0080c066d6174726978a403020104a503020104aa183016a0147212a0050d03010201a1090c075072696d617279a203020102a303020101a8050d03010202a903020101f24cff", "hex"); const ember = require("../EmberLib"); const BER = require('../ber.js'); -const Errors = require('../errors.js'); +const Errors = require('../Errors.js'); const EmberLib = require("../EmberLib"); - +const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("../EmberLib/ParameterType"); const identifier = "node_identifier"; const description = "node_description"; describe("Ember", () => { describe("generic", () => { let client; - beforeAll(() => { + beforeEach(() => { client = new S101Client(); }); @@ -93,10 +93,14 @@ describe("Ember", () => { node.contents.schemaIdentifiers = "schema1"; const root = new EmberLib.Node(0); root.addChild(node); - const writer = new BER.Writer(); + let writer = new BER.Writer(); + root.encode(writer); + expect(writer.buffer.size).not.toBe(0); + node.contents.isOnline = null; + node.contents.identifier = null; + writer = new BER.Writer(); root.encode(writer); expect(writer.buffer.size).not.toBe(0); - }); it("should have a decoder", () => { const node = new EmberLib.Node(0); @@ -125,7 +129,7 @@ describe("Ember", () => { expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); } }); - it("should through an error if unable to decode content", () => { + it("should throw an error if unable to decode content", () => { const writer = new BER.Writer(); writer.startSequence(BER.EMBER_SET); writer.startSequence(BER.CONTEXT(99)); @@ -142,7 +146,7 @@ describe("Ember", () => { }); describe("Function", () => { let func; - beforeAll(() => { + beforeEach(() => { func = new EmberLib.Function(0, args => { const res = new EmberLib.FunctionArgument(); res.type = EmberLib.ParameterType.integer; @@ -221,7 +225,7 @@ describe("Ember", () => { expect(f.contents.identifier == null).toBeTruthy(); expect(f.contents.result == null || f.contents.result.length == 0).toBeTruthy(); }); - it("should through an error if unable to decode result", () => { + it("should throw an error if unable to decode result", () => { const writer = new BER.Writer(); writer.startSequence(BER.EMBER_SET); writer.startSequence(BER.CONTEXT(3)); @@ -237,7 +241,7 @@ describe("Ember", () => { expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); } }); - it("should through an error if unable to decode content", () => { + it("should throw an error if unable to decode content", () => { const writer = new BER.Writer(); writer.startSequence(BER.EMBER_SET); writer.startSequence(BER.CONTEXT(99)); @@ -251,7 +255,7 @@ describe("Ember", () => { expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); } }); - it("should through an error if unable to decode FunctionArgument", () => { + it("should throw an error if unable to decode FunctionArgument", () => { const writer = new BER.Writer(); writer.startSequence(EmberLib.FunctionArgument.BERID); writer.startSequence(BER.CONTEXT(99)); @@ -267,25 +271,39 @@ describe("Ember", () => { }); }); describe("Parameter", () => { + it("should through an error if decoding unknown parameter type", () => { + try { + ParameterTypefromBERTAG(99); + } + catch(e) { + expect(e instanceof Errors.InvalidBERFormat).toBeTruthy(); + } + try { + ParameterTypetoBERTAG(99); + } + catch(e) { + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); it("should have an update function", () => { const parameter = new EmberLib.Parameter(0); const VALUE = 1; - let count = 0; parameter.contents = new EmberLib.ParameterContents(VALUE, "integer"); - parameter.contents._subscribers.add(() => {count++;}); const newParameter = new EmberLib.Parameter(0); const NEW_VALUE = VALUE + 1; newParameter.contents = new EmberLib.ParameterContents(NEW_VALUE, "integer"); parameter.update(newParameter); - expect(count).toBe(1); expect(parameter.contents.value).toBe(NEW_VALUE); }); it("should have setValue function", () => { const parameter = new EmberLib.Parameter(0); const VALUE = 1; parameter.contents = new EmberLib.ParameterContents(VALUE, "integer"); - const NEW_VALUE = VALUE + 1; - const setVal = parameter.setValue(NEW_VALUE); + let NEW_VALUE = VALUE + 1; + let setVal = parameter.setValue(NEW_VALUE); + expect(setVal.contents.value).toBe(NEW_VALUE); + NEW_VALUE = NEW_VALUE + 1; + setVal = parameter.setValue(new EmberLib.ParameterContents(NEW_VALUE)); expect(setVal.contents.value).toBe(NEW_VALUE); }); it("should have decoder function", () => { @@ -299,15 +317,35 @@ describe("Ember", () => { parameter.contents.factor = 10; parameter.contents.isOnline = true; parameter.contents.formula = "x10"; - parameter.contents.step = 2; - parameter.contents.default = 0; + const STEP = 2; + parameter.contents.step = STEP; + const DEFAULT = 0; + parameter.contents.default = DEFAULT; parameter.contents.type = EmberLib.ParameterType.integer; + parameter.contents.enumeration = "enumeration"; + parameter.contents.description = "description"; + parameter.contents.enumMap = new EmberLib.StringIntegerCollection(); + const KEY = "one"; + const KEY_VAL = 1; + parameter.contents.enumMap.addEntry(KEY, new EmberLib.StringIntegerPair(KEY, KEY_VAL)); + parameter.contents.streamDescriptor = new EmberLib.StreamDescription(); + parameter.contents.streamDescriptor.format = EmberLib.StreamFormat.signedInt8; + const OFFSET = 4; + parameter.contents.streamDescriptor.offset = OFFSET; + + const SCHEMA = "schema"; + parameter.contents.schemaIdentifiers = SCHEMA; const node = new EmberLib.Node(0); parameter.addChild(node); const writer = new BER.Writer(); parameter.encode(writer); const newParameter = EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); expect(newParameter.getChildren().length).toBe(1); + expect(newParameter.contents.streamDescriptor.offset).toBe(OFFSET); + expect(newParameter.contents.step).toBe(STEP); + expect(newParameter.contents.default).toBe(DEFAULT); + expect(newParameter.contents.enumMap.get(KEY).value).toBe(KEY_VAL); + expect(newParameter.contents.schemaIdentifiers).toBe(SCHEMA); }); it("should support type real", () => { const parameter = new EmberLib.Parameter(0); @@ -336,7 +374,21 @@ describe("Ember", () => { const newParameter = EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); expect(newParameter.contents.value).toBe(VALUE); }); - it("should through an error if fails to decode StringIntegerPair", () => { + it("should throw an error if fails to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.Parameter.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should throw an error if fails to decode StringIntegerPair", () => { const writer = new BER.Writer(); writer.startSequence(EmberLib.StringIntegerPair.BERID); writer.startSequence(BER.CONTEXT(99)); @@ -350,7 +402,7 @@ describe("Ember", () => { expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); } }); - it("should through an error if fails to decode StringIntegerCollection", () => { + it("should throw an error if fails to decode StringIntegerCollection", () => { const writer = new BER.Writer(); writer.startSequence(EmberLib.StringIntegerCollection.BERID); writer.startSequence(BER.CONTEXT(99)); @@ -364,14 +416,31 @@ describe("Ember", () => { expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); } }); + it("should throw an error if fails to decode ParameterContents", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.EMBER_SET); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.ParameterContents.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); }); describe("Matrix", () => { describe("validateConnection", () => { + const PATH = "0.0.0"; let matrixNode; + let qMatrixNode; const TARGETCOUNT = 5; const SOURCECOUNT = 5; - beforeAll(() => { + beforeEach(() => { matrixNode = new EmberLib.MatrixNode(0); + qMatrixNode = new EmberLib.QualifiedMatrix(PATH); matrixNode.contents = new EmberLib.MatrixContents( EmberLib.MatrixType.onetoN, EmberLib.MatrixMode.linear @@ -380,8 +449,46 @@ describe("Ember", () => { matrixNode.contents.description = "matrix"; matrixNode.contents.targetCount = TARGETCOUNT; matrixNode.contents.sourceCount = SOURCECOUNT; + qMatrixNode.contents = matrixNode.contents; + }); + it("should have encoder/decoder", () => { + matrixNode.addChild(new EmberLib.Node(0)); + let writer = new BER.Writer(); + matrixNode.encode(writer); + let newMatrixNode = EmberLib.MatrixNode.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.getChildren().length).toBe(1); + + writer = new BER.Writer(); + qMatrixNode.encode(writer); + newMatrixNode = EmberLib.QualifiedMatrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.path).toBe(PATH); + + matrixNode.contents.identifier = null; + matrixNode.contents.type = null; + matrixNode.contents.mode = null; + writer = new BER.Writer(); + matrixNode.encode(writer); + newMatrixNode = EmberLib.MatrixNode.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.contents.identifier == null).toBeTruthy(); + + writer = new BER.Writer(); + qMatrixNode.encode(writer); + newMatrixNode = EmberLib.QualifiedMatrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.contents.identifier == null).toBeTruthy(); + + matrixNode.contents = null; + writer = new BER.Writer(); + matrixNode.encode(writer); + newMatrixNode = EmberLib.MatrixNode.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.contents == null).toBeTruthy(); + + qMatrixNode.contents = null; + writer = new BER.Writer(); + qMatrixNode.encode(writer); + newMatrixNode = EmberLib.QualifiedMatrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.contents == null).toBeTruthy(); }); - it("should through an error if target is negative", () => { + it("should throw an error if target is negative", () => { try { EmberLib.Matrix.validateConnection(matrixNode, -1, []); throw new Error("Should not succeed"); @@ -390,7 +497,7 @@ describe("Ember", () => { expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); } }); - it("should through an error if source is negative", () => { + it("should throw an error if source is negative", () => { try { EmberLib.Matrix.validateConnection(matrixNode, 0, [-1]); throw new Error("Should not succeed"); @@ -399,7 +506,7 @@ describe("Ember", () => { expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); } }); - it("should through an error if target higher than max target", () => { + it("should throw an error if target higher than max target", () => { try { EmberLib.Matrix.validateConnection(matrixNode, TARGETCOUNT, [0]); throw new Error("Should not succeed"); @@ -408,7 +515,7 @@ describe("Ember", () => { expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); } }); - it("should through an error if target higher than max target", () => { + it("should throw an error if target higher than max target", () => { try { EmberLib.Matrix.validateConnection(matrixNode, 0, [SOURCECOUNT]); throw new Error("Should not succeed"); @@ -417,7 +524,7 @@ describe("Ember", () => { expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); } }); - it("should through an error if non-Linear Matrix without targets", () => { + it("should throw an error if non-Linear Matrix without targets", () => { matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; try { EmberLib.Matrix.validateConnection(matrixNode, 0, [0]); @@ -428,7 +535,7 @@ describe("Ember", () => { expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); } }); - it("should through an error if non-Linear Matrix without sources", () => { + it("should throw an error if non-Linear Matrix without sources", () => { matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; matrixNode.targets = [0, 3]; try { @@ -440,7 +547,7 @@ describe("Ember", () => { expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); } }); - it("should through an error if non-Linear Matrix and not valid target", () => { + it("should throw an error if non-Linear Matrix and not valid target", () => { matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; matrixNode.targets = [0, 3]; matrixNode.sources = [0, 3]; @@ -458,7 +565,13 @@ describe("Ember", () => { expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); } }); - it("should through an error if non-Linear Matrix and not valid source", () => { + it("should have getMinimal function", () => { + matrixNode.contents = null; + matrixNode.connections = null; + const min = matrixNode.getMinimal(true); + expect(min.number).toBe(matrixNode.getNumber()); + }); + it("should throw an error if non-Linear Matrix and not valid source", () => { matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; matrixNode.targets = [0, 3]; matrixNode.sources = [0, 3]; @@ -471,7 +584,7 @@ describe("Ember", () => { expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); } }); - it("should not through an error on valid non-linear connect", () => { + it("should not throw an error on valid non-linear connect", () => { let error = null; matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; matrixNode.targets = [0, 3]; @@ -484,12 +597,26 @@ describe("Ember", () => { } expect(error == null).toBeTruthy(); }); + it("should not throw an error if can't decode MatrixContent", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.EMBER_SET); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.MatrixContents.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); }); describe("MatrixUpdate", () => { let matrixNode; const TARGETCOUNT = 5; const SOURCECOUNT = 5; - beforeAll(() => { + beforeEach(() => { matrixNode = new EmberLib.MatrixNode(0); matrixNode.contents = new EmberLib.MatrixContents( EmberLib.MatrixType.onetoN, @@ -500,7 +627,69 @@ describe("Ember", () => { matrixNode.contents.targetCount = TARGETCOUNT; matrixNode.contents.sourceCount = SOURCECOUNT; }); - it("should not through an error on valid non-linear connect", () => { + it("should update connections", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; + + const newMatrixNode = new EmberLib.MatrixNode(0); + newMatrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + newMatrixNode.contents.identifier = "matrix"; + newMatrixNode.contents.description = "matrix"; + matrixNode.connections[0].sources = [1]; + newMatrixNode.connections = { + 0: matrixNode.connections[0], + 1: new EmberLib.MatrixConnection(1) + }; + EmberLib.Matrix.MatrixUpdate(matrixNode, newMatrixNode); + expect(matrixNode.connections[1]).toBeDefined(); + matrixNode.connections = null; + EmberLib.Matrix.MatrixUpdate(matrixNode, newMatrixNode); + expect(matrixNode.connections[1]).toBeDefined(); + }); + it("should ignore empty connections request", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; + + const newMatrixNode = new EmberLib.MatrixNode(0); + newMatrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + newMatrixNode.contents.identifier = "matrix"; + newMatrixNode.contents.description = "matrix"; + newMatrixNode.connections = null; + EmberLib.Matrix.MatrixUpdate(matrixNode, newMatrixNode); + expect(matrixNode.connections[0]).toBeDefined(); + }); + it("should throw error if invalid target inside new connections", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; + + const newMatrixNode = new EmberLib.MatrixNode(0); + newMatrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + newMatrixNode.contents.identifier = "matrix"; + newMatrixNode.contents.description = "matrix"; + newMatrixNode.connections = { + 7: new EmberLib.MatrixConnection(7) + }; + try { + EmberLib.Matrix.MatrixUpdate(matrixNode, newMatrixNode); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should not throw an error on valid non-linear connect", () => { let error = null; const newMatrixNode = new EmberLib.MatrixNode(0); newMatrixNode.contents = new EmberLib.MatrixContents( @@ -531,7 +720,7 @@ describe("Ember", () => { let matrixNode; const TARGETCOUNT = 5; const SOURCECOUNT = 5; - beforeAll(() => { + beforeEach(() => { matrixNode = new EmberLib.MatrixNode(0); matrixNode.contents = new EmberLib.MatrixContents( EmberLib.MatrixType.onetoN, @@ -545,12 +734,46 @@ describe("Ember", () => { it("should generate the connection structure if not existent", () => { EmberLib.Matrix.disconnectSources(matrixNode, 0, [1]); }); + it("should disconnect existing connection", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; + EmberLib.Matrix.connectSources(matrixNode, 0, [1]); + EmberLib.Matrix.connectSources(matrixNode, 1, [1]); + expect(matrixNode._numConnections).toBe(2); + EmberLib.Matrix.disconnectSources(matrixNode, 0, [1]); + expect(matrixNode.connections[0]).toBeDefined(); + expect(matrixNode.connections[0].sources.length).toBe(0); + expect(matrixNode._numConnections).toBe(1); + }); + it("should ignore disconnect with no source", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; + EmberLib.Matrix.connectSources(matrixNode, 0, [1]); + expect(matrixNode._numConnections).toBe(1); + EmberLib.Matrix.disconnectSources(matrixNode, 0, null); + expect(matrixNode.connections[0]).toBeDefined(); + expect(matrixNode.connections[0].sources.length).toBe(1); + }); + it("should ignore disconnect with not connected source", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; + EmberLib.Matrix.connectSources(matrixNode, 0, [1]); + EmberLib.Matrix.connectSources(matrixNode, 1, [0]); + expect(matrixNode._numConnections).toBe(2); + EmberLib.Matrix.disconnectSources(matrixNode, 0, [0]); + expect(matrixNode.connections[0]).toBeDefined(); + expect(matrixNode.connections[0].sources.length).toBe(1); + expect(matrixNode._numConnections).toBe(2); + }); }); describe("decodeConnections", () => { let matrixNode; const TARGETCOUNT = 5; const SOURCECOUNT = 5; - beforeAll(() => { + beforeEach(() => { matrixNode = new EmberLib.MatrixNode(0); matrixNode.contents = new EmberLib.MatrixContents( EmberLib.MatrixType.onetoN, @@ -574,11 +797,20 @@ describe("Ember", () => { expect(connections[0].sources[0]).toBe(SOURCEID); }); }); + describe("encodeConnections", () => { + it ("should ignore empty/null connections", () => { + const matrixNode = new EmberLib.MatrixNode(0); + matrixNode.connections = null; + const writer = new BER.Writer(); + matrixNode.encodeConnections(writer); + expect(writer.buffer.length).toBe(0); + }); + }); describe("canConnect", () => { let matrixNode; const TARGETCOUNT = 5; const SOURCECOUNT = 5; - beforeAll(() => { + beforeEach(() => { matrixNode = new EmberLib.MatrixNode(0); matrixNode.contents = new EmberLib.MatrixContents( EmberLib.MatrixType.onetoN, @@ -589,6 +821,13 @@ describe("Ember", () => { matrixNode.contents.targetCount = TARGETCOUNT; matrixNode.contents.sourceCount = SOURCECOUNT; }); + it("should consider default type as 1toN", () => { + matrixNode.connections = null; + matrixNode.contents.type = null; + matrixNode.contents.maximumTotalConnects = 1; + const res = EmberLib.Matrix.canConnect(matrixNode, 0, [0,3]); + expect(res).toBeFalsy(); + }); it("should return false if more than 1 source in 1toN", () => { matrixNode.connections = null; matrixNode.contents.maximumTotalConnects = 1; @@ -606,10 +845,50 @@ describe("Ember", () => { const res = EmberLib.Matrix.canConnect(matrixNode, 0, [0,3]); expect(res).toBeTruthy(); }); + it("should check maximumTotalConnects in NtoN and reject on limit pass", () => { + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.nToN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.maximumConnectsPerTarget = null; + matrixNode.contents.maximumTotalConnects = 2; + EmberLib.Matrix.connectSources(matrixNode, 0, [1,2]); + const res = EmberLib.Matrix.canConnect(matrixNode, 1, [3]); + expect(res).toBeFalsy(); + }); + it("should check maximumTotalConnects in NtoN and accept if below limit", () => { + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.nToN, + EmberLib.MatrixMode.linear + ); + matrixNode.connections = null; + matrixNode.contents.maximumConnectsPerTarget = null; + matrixNode.contents.maximumTotalConnects = 2; + const res = EmberLib.Matrix.canConnect(matrixNode, 1, [3]); + expect(res).toBeTruthy(); + }); + it("should check locked connection", () => { + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.nToN, + EmberLib.MatrixMode.linear + ); + matrixNode.connections = null; + matrixNode.contents.maximumConnectsPerTarget = null; + matrixNode.contents.maximumTotalConnects = 2; + EmberLib.Matrix.connectSources(matrixNode, 0, [1]); + matrixNode.connections[0].lock(); + let res = EmberLib.Matrix.canConnect(matrixNode, 0, [3]); + expect(res).toBeFalsy(); + matrixNode.connections[0].unlock(); + res = EmberLib.Matrix.canConnect(matrixNode, 0, [3]); + expect(res).toBeTruthy(); + }); }); describe("Matrix Non-Linear", () => { it("should have encoder / decoder", () => { + const PATH = "0.1.2"; const matrixNode = new EmberLib.MatrixNode(0); + const qMatrixNode = new EmberLib.QualifiedMatrix(PATH); matrixNode.contents = new EmberLib.MatrixContents( EmberLib.MatrixType.onetoN, EmberLib.MatrixMode.nonLinear @@ -622,12 +901,29 @@ describe("Ember", () => { matrixNode.contents.parametersLocation = "1.2.3"; matrixNode.contents.schemaIdentifiers = "de.l-s-b.emberplus.schema1"; matrixNode.contents.templateReference = "0.1.2.3"; + qMatrixNode.contents = matrixNode.contents; matrixNode.targets = [0,3]; + qMatrixNode.targets = matrixNode.targets; matrixNode.sources = [1,2]; - const writer = new BER.Writer(); + qMatrixNode.sources = matrixNode.sources; + let writer = new BER.Writer(); + matrixNode.encode(writer); + let newMatrixNode = EmberLib.Matrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.targets).toBeDefined(); + + writer = new BER.Writer(); + qMatrixNode.encode(writer); + newMatrixNode = EmberLib.QualifiedMatrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.targets).toBeDefined(); + + + // Should support int + matrixNode.contents.parametersLocation = 123; + writer = new BER.Writer(); matrixNode.encode(writer); - const newMatrixNode = EmberLib.Matrix.decode(new BER.Reader(writer.buffer)); + newMatrixNode = EmberLib.Matrix.decode(new BER.Reader(writer.buffer)); expect(newMatrixNode.targets).toBeDefined(); + expect(newMatrixNode.contents.parametersLocation).toBe(matrixNode.contents.parametersLocation); }); it("should have connect function", () => { const root = new EmberLib.Root(); @@ -644,7 +940,7 @@ describe("Ember", () => { const connect = matrixNode.connect({0: new EmberLib.MatrixConnection(0)}); expect(connect).toBeDefined(); }); - it("should through an error if can't decode", () => { + it("should throw an error if can't decode", () => { const writer = new BER.Writer(); writer.startSequence(BER.APPLICATION(13)); writer.startSequence(BER.CONTEXT(0)); @@ -798,7 +1094,7 @@ describe("Ember", () => { const newInvocationRes = EmberLib.InvocationResult.decode(new BER.Reader(writer.buffer)); expect(newInvocationRes.invocationId == null).toBeTruthy(); }); - it("should through an error if can't decode", () => { + it("should throw an error if can't decode", () => { let writer = new BER.Writer(); writer.startSequence(EmberLib.InvocationResult.BERID); writer.startSequence(BER.CONTEXT(3)); @@ -829,4 +1125,325 @@ describe("Ember", () => { } }); }); + describe("MatrixConnection", () => { + it("should have a decoder and throw error if can't decode", () => { + let writer = new BER.Writer(); + writer.startSequence(EmberLib.MatrixConnection.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.MatrixConnection.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should decode connection with no source", () => { + const matrixConnection = new EmberLib.MatrixConnection(0); + matrixConnection.sources = []; + const writer = new BER.Writer(); + matrixConnection.encode(writer); + const newMC = EmberLib.MatrixConnection.decode(new BER.Reader(writer.buffer)); + expect(newMC.sources).toBeDefined(); + expect(newMC.sources.length).toBe(0); + }); + it("should throw an error if invalid target", () => { + try { + new EmberLib.MatrixConnection("zero"); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + }); + describe("QualifiedFunction", () => { + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.QualifiedFunction.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.QualifiedFunction.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + }); + describe("QualifiedMatrix", () => { + const PATH = "1.2.3"; + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.QualifiedMatrix.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.QualifiedMatrix.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("Should have a subscribe/unsubscribe function", () => { + const qMatrixNode = new EmberLib.QualifiedMatrix(PATH); + qMatrixNode.contents = new EmberLib.MatrixContents(); + const cb = function() {}; + let cmd = qMatrixNode.subscribe(cb); + expect(cmd).toBeDefined(); + expect(cmd instanceof EmberLib.Root).toBeTruthy(); + }); + }); + describe("QualifiedNode", () => { + const PATH = "0.1.2"; + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.QualifiedNode.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.QualifiedNode.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("Should return true to isNode() call", () => { + const qNode = new EmberLib.QualifiedNode(PATH); + expect(qNode.isNode()).toBeTruthy(); + }); + }); + describe("QualifiedParameter", () => { + const PATH = "0.1.2"; + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.QualifiedParameter.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.QualifiedParameter.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should update and ignore key starting with _", () => { + const NEW_VAL = 15; + const qp = new EmberLib.QualifiedParameter(PATH); + qp.contents = new EmberLib.ParameterContents(5, "integer"); + const dup = new EmberLib.QualifiedParameter(PATH); + dup.contents = new EmberLib.ParameterContents(NEW_VAL, "integer"); + dup.contents["_ignore"] = "test"; + qp.update(dup); + expect(qp.contents._ignore).not.toBeDefined(); + expect(qp.contents.value).toBe(NEW_VAL); + }); + + it("Should return true to isParameter() call", () => { + const qNode = new EmberLib.QualifiedParameter(PATH); + expect(qNode.isParameter()).toBeTruthy(); + }); + }); + describe("StreamDescription", () => { + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.StreamDescription.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.StreamDescription.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("Should have a toJSON", () => { + const streamDescriptor = new EmberLib.StreamDescription(); + streamDescriptor.format = EmberLib.StreamFormat.signedInt8; + const OFFSET = 4; + streamDescriptor.offset = OFFSET; + + let js = streamDescriptor.toJSON(); + expect(js).toBeDefined(); + expect(js.format).toBeDefined(); + expect(js.offset).toBe(OFFSET); + + streamDescriptor.format = null; + js = streamDescriptor.toJSON(); + expect(js).toBeDefined(); + expect(js.format).toBe(null); + expect(js.offset).toBe(OFFSET); + }); + }); + describe("StringIntegerCollection", () => { + it("should reject invalid value", () => { + const sic = new EmberLib.StringIntegerCollection(); + try { + sic.addEntry("test", 4); + } + catch(e) { + expect(e instanceof Errors.InvalidStringPair).toBeTruthy(); + } + }); + it("should have a toJSON", () => { + const KEY = "test"; + const VAL = 4; + const sic = new EmberLib.StringIntegerCollection(); + sic.addEntry("test", new EmberLib.StringIntegerPair(KEY, VAL)); + const js = sic.toJSON(); + expect(js).toBeDefined(); + expect(js.length).toBe(1); + }); + }); + describe("StringIntegerPair", () => { + it("should throw an error if trying to encode invalid key/value", () => { + const sp = new EmberLib.StringIntegerPair(); + const writer = new BER.Writer(); + try { + sp.encode(writer); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); + }); + describe("rootDecode", () => { + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + try { + EmberLib.rootDecode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + }); + describe("QualifiedTemplate", () => { + const PATH = "0.1.2"; + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.QualifiedTemplate.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.QualifiedTemplate.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should have encoder/decoder", () => { + const qp = new EmberLib.QualifiedTemplate(PATH, new EmberLib.Node(0)); + let writer = new BER.Writer(); + qp.encode(writer); + let dup = EmberLib.QualifiedTemplate.decode(new BER.Reader(writer.buffer)); + expect(dup).toBeDefined(); + expect(dup.getPath()).toBe(PATH); + expect(dup.element instanceof EmberLib.Node).toBeTruthy(); + + const DESCRIPTION = "description"; + qp.description = DESCRIPTION; + writer = new BER.Writer(); + qp.encode(writer); + dup = EmberLib.QualifiedTemplate.decode(new BER.Reader(writer.buffer)); + expect(dup).toBeDefined(); + expect(dup.getPath()).toBe(PATH); + expect(dup.element instanceof EmberLib.Node).toBeTruthy(); + expect(dup.description).toBe(DESCRIPTION); + }); + + it("Should return true to isTemplate() call", () => { + const qp = new EmberLib.QualifiedTemplate(PATH, new EmberLib.Node(0)); + expect(qp.isTemplate()).toBeTruthy(); + }); + }); + describe("Template", () => { + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.Template.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Template.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should have encoder/decoder", () => { + const qp = new EmberLib.Template(10, new EmberLib.Node(0)); + let writer = new BER.Writer(); + qp.encode(writer); + let dup = EmberLib.Template.decode(new BER.Reader(writer.buffer)); + expect(dup).toBeDefined(); + expect(dup.getNumber()).toBe(10); + + const DESCRIPTION = "description"; + qp.description = DESCRIPTION; + writer = new BER.Writer(); + qp.encode(writer); + dup = EmberLib.Template.decode(new BER.Reader(writer.buffer)); + expect(dup).toBeDefined(); + expect(dup.element instanceof EmberLib.Node).toBeTruthy(); + expect(dup.description).toBe(DESCRIPTION); + + writer = new BER.Writer(); + qp.element = new EmberLib.Function(0, null); + qp.encode(writer); + dup = EmberLib.Template.decode(new BER.Reader(writer.buffer)); + expect(dup.element instanceof EmberLib.Function).toBeTruthy(); + + writer = new BER.Writer(); + qp.element = new EmberLib.Parameter(0); + qp.encode(writer); + dup = EmberLib.Template.decode(new BER.Reader(writer.buffer)); + expect(dup.element instanceof EmberLib.Parameter).toBeTruthy(); + + writer = new BER.Writer(); + qp.element = new EmberLib.MatrixNode(0); + qp.encode(writer); + dup = EmberLib.Template.decode(new BER.Reader(writer.buffer)); + expect(dup.element instanceof EmberLib.MatrixNode).toBeTruthy(); + + }); + + it("Should return true to isTemplate() call", () => { + const qp = new EmberLib.Template(10, new EmberLib.Node(0)); + expect(qp.isTemplate()).toBeTruthy(); + }); + + it("Should have toQualified function", () => { + const template = new EmberLib.Template(10, new EmberLib.Node(0)); + const qp = template.toQualified(); + expect(qp.isTemplate()).toBeTruthy(); + }); + + it("Should have update function", () => { + const template = new EmberLib.Template(10, new EmberLib.Node(0)); + const DUP_NUM = 5; + const dup = new EmberLib.Template(10, new EmberLib.Node(DUP_NUM)); + template.update(dup); + expect(template.element.getNumber()).toBe(DUP_NUM); + }); + }); }); diff --git a/test/Server.test.js b/test/Server.test.js index 428a341..05f5dc5 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -485,8 +485,8 @@ describe("server", function() { .then(() => client.getNodeByPathnum("0.0.2")) .then(parameter => { expect(server.subscribers["0.0.2"]).not.toBeDefined(); - expect(parameter.contents._subscribers).toBeDefined(); - expect(parameter.contents._subscribers.size).toBe(0); + expect(parameter._subscribers).toBeDefined(); + expect(parameter._subscribers.size).toBe(0); server._subscribe = server.subscribe; let _resolve; const p = new Promise(resolve => { @@ -504,8 +504,8 @@ describe("server", function() { return client.getNodeByPathnum("0.0.2"); }) .then(parameter => { - expect(parameter.contents._subscribers).toBeDefined(); - expect(parameter.contents._subscribers.size).toBe(1); + expect(parameter._subscribers).toBeDefined(); + expect(parameter._subscribers.size).toBe(1); server._unsubscribe = server.unsubscribe; let _resolve; const p = new Promise(resolve => { @@ -524,8 +524,8 @@ describe("server", function() { .then(parameter => { expect(server.subscribers["0.0.2"]).toBeDefined(); expect(server.subscribers["0.0.2"].size).toBe(0); - expect(parameter.contents._subscribers).toBeDefined(); - expect(parameter.contents._subscribers.size).toBe(0); + expect(parameter._subscribers).toBeDefined(); + expect(parameter._subscribers.size).toBe(0); }) .then(() => client.disconnect()); }); From 53646255b7e6d7bc68e975148af4a8d703cbcb86 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 20 Jan 2020 11:34:01 +0100 Subject: [PATCH 47/64] 92% code coverage --- EmberClient/EmberClient.js | 188 ++++---- EmberLib/Command.js | 26 +- EmberLib/Function.js | 9 - EmberLib/Matrix.js | 3 + EmberLib/Parameter.js | 3 +- EmberLib/ParameterContents.js | 96 +++-- EmberLib/QualifiedElement.js | 26 +- EmberLib/QualifiedFunction.js | 25 +- EmberLib/QualifiedMatrix.js | 15 - EmberLib/QualifiedParameter.js | 5 +- EmberLib/TreeNode.js | 136 +++--- EmberLib/index.js | 41 +- EmberServer/ElementHandlers.js | 30 +- EmberServer/EmberServer.js | 71 ++-- EmberServer/ServerEvents.js | 3 +- EmberSocket/S101Socket.js | 31 +- Errors.js | 26 +- package.json | 2 +- test/DeviceTree.test.js | 20 +- test/Ember.test.js | 181 +++++++- test/Server.test.js | 753 +++++++++++++++++++++++++++------ test/utils.js | 4 +- 22 files changed, 1166 insertions(+), 528 deletions(-) diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index 1d148fd..e1426f5 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -2,7 +2,7 @@ const EventEmitter = require('events').EventEmitter; const S101Socket = require('../EmberSocket').S101Socket; const ember = require('../EmberLib'); const BER = require('../ber.js'); -const errors = require('../Errors.js'); +const Errors = require('../Errors.js'); const winston = require("winston"); const DEFAULT_PORT = 9000; @@ -79,6 +79,7 @@ class EmberClient extends EventEmitter { _finishRequest() { this._clearTimeout(); + this._callback = undefined; this._activeRequest = null; try { this._makeRequest(); @@ -95,7 +96,7 @@ class EmberClient extends EventEmitter { if (this._activeRequest == null && this._pendingRequests.length > 0) { this._activeRequest = this._pendingRequests.shift(); const req = `${ this._requestID++} - ${this._activeRequest.node.getPath()}`; - this._activeRequest.timeoutError = new errors.EmberTimeoutError(`Request ${req} timed out`) + this._activeRequest.timeoutError = new Errors.EmberTimeoutError(`Request ${req} timed out`) winston.debug(`Making request ${req}`, Date.now()); this._timeout = setTimeout(() => { @@ -239,12 +240,13 @@ class EmberClient extends EventEmitter { } /** - * + * @returns {Promise} */ disconnect() { if (this._client != null) { return this._client.disconnect(); } + return Promise.resolve(); } /** @@ -255,33 +257,32 @@ class EmberClient extends EventEmitter { */ expand(node, callback = null) { if (node == null) { - return Promise.reject(new errors.InvalidEmberNode("Invalid null node")); + return Promise.reject(new Errors.InvalidEmberNode("Invalid null node")); } if (node.isParameter() || node.isMatrix() || node.isFunction()) { return this.getDirectory(node); } - return this.getDirectory(node, callback).then((res) => { - let children = node.getChildren(); - if ((res === undefined) || (children === undefined) || (children === null)) { + return this.getDirectory(node, callback).then(res => { + const children = node.getChildren(); + if ((res == null) || (children == null)) { winston.debug("No more children for ", node); return; } - let p = Promise.resolve(); + let directChildren = Promise.resolve(); for (let child of children) { if (child.isParameter()) { // Parameter can only have a single child of type Command. continue; } - winston.debug("Expanding child", child); - p = p.then(() => { - return this.expand(child).catch((e) => { + directChildren = directChildren.then(() => { + return this.expand(child, callback).catch((e) => { // We had an error on some expansion // let's save it on the child itthis child.error = e; }); }); } - return p; + return directChildren }); } @@ -291,7 +292,7 @@ class EmberClient extends EventEmitter { * @param {function} callback=null * @returns {Promise} */ - getDirectory(qnode, callback = null) { + getDirectory(qnode, callback = null) { if (qnode == null) { this.root.clear(); qnode = this.root; @@ -300,28 +301,26 @@ class EmberClient extends EventEmitter { this.addRequest({node: qnode, func: error => { if (error) { this._finishRequest(); - reject(error); - return; + return reject(error); } this._callback = (error, node) => { const requestedPath = qnode.getPath(); if (node == null) { winston.debug(`received null response for ${requestedPath}`); + // ignore return; } if (error) { winston.debug("Received getDirectory error", error); - this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); - reject(error); - return; + return reject(error); } if (qnode.isRoot()) { const elements = qnode.getChildren(); if (elements == null || elements.length === 0) { winston.debug("getDirectory response", node); - return this._callback(new errors.InvalidEmberNode()); + return reject(new Errors.InvalidEmberNode()); } const nodeElements = node == null ? null : node.getChildren(); @@ -329,16 +328,14 @@ class EmberClient extends EventEmitter { if (nodeElements != null && nodeElements.every(el => el._parent instanceof ember.Root)) { winston.debug("Received getDirectory response", node); - this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); - resolve(node); // make sure the info is treated before going to next request. + return resolve(node); // make sure the info is treated before going to next request. } else { - return this._callback(new errors.InvalidEmberResponse(`getDirectory ${requestedPath}`)); + return this._callback(new Errors.InvalidEmberResponse(`getDirectory ${requestedPath}`)); } } else if (node.getElementByPath(requestedPath) != null) { - this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); return resolve(node); // make sure the info is treated before going to next request. } @@ -348,7 +345,6 @@ class EmberClient extends EventEmitter { ((qnode.isMatrix() && nodeElements.length === 1 && nodeElements[0].getPath() === requestedPath) || (!qnode.isMatrix() && nodeElements.every(el => isDirectSubPathOf(el.getPath(), requestedPath))))) { winston.debug("Received getDirectory response", node); - this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); return resolve(node); // make sure the info is treated before going to next request. } @@ -359,106 +355,79 @@ class EmberClient extends EventEmitter { } }; winston.debug("Sending getDirectory", qnode); - this._client.sendBERNode(qnode.getDirectory(callback)); + try { + this._client.sendBERNode(qnode.getDirectory(callback)); + } + catch(e) { + reject(e); + } }}); }); } /** - * @deprecated + * * @param {string} path ie: "path/to/destination" * @param {function} callback=null * @returns {Promise} */ getElementByPath(path, callback=null) { + const pathError = new Errors.PathDiscoveryFailure(path); + const TYPE_NUM = 1; + const TYPE_ID = 2; + let type = TYPE_NUM; + let pathArray = []; if (path.indexOf("/") >= 0) { - return this.getNodeByPath(path, callback); + type = TYPE_ID; + pathArray = path.split("/"); } else { - return this.getNodeByPathnum(path, callback); - } - } - - /** - * @deprecated - * @param {string} path ie: "path/to/destination" - * @param {function} callback=null - * @returns {Promise} - */ - getNodeByPath(path, callback = null) { - if (typeof path === 'string') { - path = path.split('/'); + pathArray = path.split("."); + if (pathArray.length === 1) { + if (isNaN(Number(pathArray[0]))) { + type = TYPE_ID; + } + } } - var pathError = new errors.PathDiscoveryFailure(path.slice(0, pos + 1).join("/")); - var pos = 0; - var lastMissingPos = -1; - var currentNode = this.root; + let pos = 0; + let lastMissingPos = -1; + let currentNode = this.root; const getNext = () => { return Promise.resolve() - .then(() => { - const children = currentNode.getChildren(); - const identifier = path[pos]; - if (children != null) { - for (let i = 0; i < children.length; i++) { - var node = children[i]; - if (node.contents != null && node.contents.identifier === identifier) { - // We have this part already. - pos++; - if (pos >= path.length) { - return node; + .then(() => { + let node; + if (type === TYPE_NUM) { + const number = Number(pathArray[pos]); + node = currentNode.getElementByNumber(number); + } + else { + const children = currentNode.getChildren(); + const identifier = pathArray[pos]; + if (children != null) { + let i = 0; + for (i = 0; i < children.length; i++) { + node = children[i]; + if (node.contents != null && node.contents.identifier === identifier) { + break; } - currentNode = node; - return getNext(); + } + if (i >= children.length) { + node = null; } } } - // We do not have that node yet. - if (lastMissingPos === pos) { - throw pathError; - } - lastMissingPos = pos; - return this.getDirectory(currentNode, callback).then(() => getNext()); - }); - } - return getNext(); - } - - /** - * @deprecated - * @param {string|number[]} path ie: 1.0.2 - * @param {function} callback=null - * @returns {Promise} - */ - getNodeByPathnum(path, callback = null) { - if (typeof path === 'string') { - path = path.split('.'); - } - var pathnumError = new errors.PathDiscoveryFailure(path.slice(0, pos).join("/")); - var pos = 0; - var lastMissingPos = -1; - var currentNode = this.root; - const getNext = () => { - return Promise.resolve() - .then(() => { - const children = currentNode.getChildren(); - const number = Number(path[pos]); - if (children != null) { - for (let i = 0; i < children.length; i++) { - var node = children[i]; - if (node.getNumber() === number) { - // We have this part already. - pos++; - if (pos >= path.length) { - return node; - } - currentNode = node; - return getNext(); - } + if (node != null) { + // We have this part already. + pos++; + if (pos >= pathArray.length) { + return node; } + currentNode = node; + return getNext(); } // We do not have that node yet. if (lastMissingPos === pos) { - throw pathnumError; + throw pathError; } lastMissingPos = pos; return this.getDirectory(currentNode, callback).then(() => getNext()); @@ -466,7 +435,7 @@ class EmberClient extends EventEmitter { } return getNext(); } - + /** * * @param {TreeNode} fnNode @@ -516,7 +485,7 @@ class EmberClient extends EventEmitter { matrixOPeration(matrixNode, targetID, sources, operation = ember.MatrixOperation.connect) { return new Promise((resolve, reject) => { if (!Array.isArray(sources)) { - return reject(new errors.InvalidSourcesFormat()); + return reject(new Errors.InvalidSourcesFormat()); } try { matrixNode.validateConnection(targetID, sources); @@ -617,12 +586,10 @@ class EmberClient extends EventEmitter { */ setValue(node, value) { return new Promise((resolve, reject) => { - if ((!(node instanceof ember.Parameter)) && - (!(node instanceof ember.QualifiedParameter))) { - reject(new errors.EmberAccessError('not a property')); + if (!node.isParameter()) { + reject(new Errors.EmberAccessError('not a Parameter')); } else { - // if (this._debug) { console.log('setValue', node.getPath(), value); } this.addRequest({node: node, func: error => { if (error) { this._finishRequest(); @@ -630,19 +597,18 @@ class EmberClient extends EventEmitter { return; } - let cb = (error, node) => { - this._clearTimeout(); + this._callback = (error, node) => { this._finishRequest(); + this._callback = null; if (error) { reject(error); } else { + resolve(node); } }; - - this._callback = cb; - winston.debug('setValue sending ...', node.getPath(), value); + winston.debug('setValue sending ...', node.getPath(), value); this._client.sendBERNode(node.setValue(value)); }}); } diff --git a/EmberLib/Command.js b/EmberLib/Command.js index a3fab00..73bf653 100755 --- a/EmberLib/Command.js +++ b/EmberLib/Command.js @@ -4,6 +4,7 @@ const {COMMAND_GETDIRECTORY, COMMAND_INVOKE} = require("./constants"); const BER = require('../ber.js'); const Invocation = require("./Invocation"); const errors = require("../Errors"); +const ElementInterface = require("./ElementInterface"); const FieldFlags = new Enum({ sparse: -2, @@ -16,12 +17,13 @@ const FieldFlags = new Enum({ connections: 5 }); -class Command { +class Command extends ElementInterface{ /** * * @param {number} number */ constructor(number) { + super(); this.number = number; if(number == COMMAND_GETDIRECTORY) { this.fieldFlags = FieldFlags.all; @@ -109,6 +111,28 @@ class Command { return c; } + + /** + * + * @param {number} cmd + * @param {string} key + * @param {string|value|object} value + */ + static getCommand(cmd, key, value) { + const command = new Command(cmd); + if (key != null) { + command[key] = value; + } + return command; + } + + /** + * + * @param {Invocation} invocation + */ + static getInvocationCommand(invocation) { + return this.getCommand(COMMAND_INVOKE, "invocation", invocation); + } /** * @returns {number} diff --git a/EmberLib/Function.js b/EmberLib/Function.js index 8611c31..54c777b 100755 --- a/EmberLib/Function.js +++ b/EmberLib/Function.js @@ -22,15 +22,6 @@ class Function extends Element { return true; } - /** - * @returns {Root} - */ - invoke() { - return this.getTreeBranch(undefined, (m) => { - m.addChild(new Command(COMMAND_INVOKE)) - }); - } - /** * @returns {QualifiedFunction} */ diff --git a/EmberLib/Matrix.js b/EmberLib/Matrix.js index 9add4f8..9f8ac2f 100755 --- a/EmberLib/Matrix.js +++ b/EmberLib/Matrix.js @@ -409,6 +409,9 @@ class Matrix extends TreeNode if (targetID < 0) { throw new Errors.InvalidMatrixSignal(targetID, "target"); } + if (sources == null) { + throw new Errors.InvalidSourcesFormat(); + } for(let i = 0; i < sources.length; i++) { if (sources[i] < 0) { throw new Errors.InvalidMatrixSignal(sources[i], `Source at index ${i}`); diff --git a/EmberLib/Parameter.js b/EmberLib/Parameter.js index 5e0dfd9..a7a07c8 100755 --- a/EmberLib/Parameter.js +++ b/EmberLib/Parameter.js @@ -25,7 +25,8 @@ class Parameter extends Element { } /** - * + * Generate a Root of a partial tree containing the Parameter and its new value. + * Should be sent to the Provider to update the value. * @param {string|number} value * @returns {Root} */ diff --git a/EmberLib/ParameterContents.js b/EmberLib/ParameterContents.js index 4846b99..07ad48a 100755 --- a/EmberLib/ParameterContents.js +++ b/EmberLib/ParameterContents.js @@ -76,45 +76,63 @@ class ParameterContents { while(ber.remain > 0) { let tag = ber.peek(); let seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - pc.identifier = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - pc.description = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(2)) { - pc.value = seq.readValue(); - } else if(tag == BER.CONTEXT(3)) { - pc.minimum = seq.readValue(); - } else if(tag == BER.CONTEXT(4)) { - pc.maximum = seq.readValue(); - } else if(tag == BER.CONTEXT(5)) { - pc.access = ParameterAccess.get(seq.readInt()); - } else if(tag == BER.CONTEXT(6)) { - pc.format = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(7)) { - pc.enumeration = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(8)) { - pc.factor = seq.readInt(); - } else if(tag == BER.CONTEXT(9)) { - pc.isOnline = seq.readBoolean(); - } else if(tag == BER.CONTEXT(10)) { - pc.formula = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(11)) { - pc.step = seq.readInt(); - } else if(tag == BER.CONTEXT(12)) { - pc.default = seq.readValue(); - } else if(tag == BER.CONTEXT(13)) { - pc.type = ParameterType.get(seq.readInt()); - } else if(tag == BER.CONTEXT(14)) { - pc.streamIdentifier = seq.readInt(); - } else if(tag == BER.CONTEXT(15)) { - pc.enumMap = StringIntegerCollection.decode(seq); - } else if(tag == BER.CONTEXT(16)) { - pc.streamDescriptor = StreamDescription.decode(seq); - } else if(tag == BER.CONTEXT(17)) { - pc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); - } - else { - throw new Errors.UnimplementedEmberTypeError(tag); + switch(tag) { + case BER.CONTEXT(0): + pc.identifier = seq.readString(BER.EMBER_STRING); + break; + case BER.CONTEXT(1): + pc.description = seq.readString(BER.EMBER_STRING); + break; + case BER.CONTEXT(2): + pc.value = seq.readValue(); + break; + case BER.CONTEXT(3): + pc.minimum = seq.readValue(); + break; + case BER.CONTEXT(4): + pc.maximum = seq.readValue(); + break; + case BER.CONTEXT(5): + pc.access = ParameterAccess.get(seq.readInt()); + break; + case BER.CONTEXT(6): + pc.format = seq.readString(BER.EMBER_STRING); + break; + case BER.CONTEXT(7): + pc.enumeration = seq.readString(BER.EMBER_STRING); + break; + case BER.CONTEXT(8): + pc.factor = seq.readInt(); + break; + case BER.CONTEXT(9): + pc.isOnline = seq.readBoolean(); + break; + case BER.CONTEXT(10): + pc.formula = seq.readString(BER.EMBER_STRING); + break; + case BER.CONTEXT(11): + pc.step = seq.readInt(); + break; + case BER.CONTEXT(12): + pc.default = seq.readValue(); + break; + case BER.CONTEXT(13): + pc.type = ParameterType.get(seq.readInt()); + break; + case BER.CONTEXT(14): + pc.streamIdentifier = seq.readInt(); + break; + case BER.CONTEXT(15): + pc.enumMap = StringIntegerCollection.decode(seq); + break; + case BER.CONTEXT(16): + pc.streamDescriptor = StreamDescription.decode(seq); + break; + case BER.CONTEXT(17): + pc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); + break; + default: + throw new Errors.UnimplementedEmberTypeError(tag); } } return pc; diff --git a/EmberLib/QualifiedElement.js b/EmberLib/QualifiedElement.js index b233375..98e45f4 100755 --- a/EmberLib/QualifiedElement.js +++ b/EmberLib/QualifiedElement.js @@ -2,7 +2,7 @@ const TreeNode = require("./TreeNode"); const BER = require('../ber.js'); const Command = require("./Command"); -const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); +const {COMMAND_GETDIRECTORY} = require("./constants"); class QualifiedElement extends TreeNode { /** @@ -43,35 +43,17 @@ class QualifiedElement extends TreeNode { /** * - * @param {number} cmd - * @param {string} key - * @param {string} value + * @param {Command} cmd * @returns {TreeNode} */ - getCommand(cmd, key, value) { + getCommand(cmd) { const r = this.getNewTree(); const qn = new this.constructor(); qn.path = this.getPath(); r.addElement(qn); - const command = new Command(cmd); - if (key != null) { - command[key] = value; - } - qn.addChild(command); + qn.addChild(cmd); return r; } - - /** - * - * @param {function} callback - * @returns {TreeNode} - */ - getDirectory(callback) { - if (callback != null && !this.isStream()) { - this.contents._subscribers.add(callback); - } - return this.getCommand(COMMAND_GETDIRECTORY); - } } module.exports = QualifiedElement; \ No newline at end of file diff --git a/EmberLib/QualifiedFunction.js b/EmberLib/QualifiedFunction.js index 7a4d017..8e4ac84 100755 --- a/EmberLib/QualifiedFunction.js +++ b/EmberLib/QualifiedFunction.js @@ -2,9 +2,9 @@ const QualifiedElement = require("./QualifiedElement"); const FunctionContent = require("./FunctionContent"); -const {COMMAND_GETDIRECTORY, COMMAND_INVOKE} = require("./constants"); +const {COMMAND_GETDIRECTORY} = require("./constants"); const BER = require('../ber.js'); -const Invocation = require("./Invocation"); +const Command = require("./Command"); const Errors = require("../Errors"); class QualifiedFunction extends QualifiedElement { @@ -19,14 +19,6 @@ class QualifiedFunction extends QualifiedElement { this._seqID = QualifiedFunction.BERID; } - /** - * - * @returns {TreeNode} - */ - getDirectory() { - return this.getCommand(COMMAND_GETDIRECTORY); - } - /** * @returns {boolean} */ @@ -34,19 +26,6 @@ class QualifiedFunction extends QualifiedElement { return true; } - /** - * - * @param {*} params - */ - invoke(params) { - const invocation = new Invocation(Invocation.newInvocationID()); - invocation.arguments = params; - const qualifiedFunctionNode = this.getCommand(COMMAND_INVOKE, "invocation", invocation); - //qualifiedFunctionNode.getElementByNumber(this.getNumber()).getElementByNumber(COMMAND_INVOKE).invocation = invocation - return qualifiedFunctionNode; - } - - /** * * @param {BER} ber diff --git a/EmberLib/QualifiedMatrix.js b/EmberLib/QualifiedMatrix.js index ab4e8e4..3d4f890 100755 --- a/EmberLib/QualifiedMatrix.js +++ b/EmberLib/QualifiedMatrix.js @@ -57,21 +57,6 @@ class QualifiedMatrix extends Matrix { ber.endSequence(); // BER.APPLICATION(3) } - /** - * - * @param {number} cmd - * @returns {TreeNode} - */ - getCommand(cmd) { - const r = this.getNewTree(); - const qn = new QualifiedMatrix(); - qn.path = this.getPath(); - r.addElement(qn); - qn.addChild(new Command(cmd)); - return r; - } - - /** * * @param {BER} ber diff --git a/EmberLib/QualifiedParameter.js b/EmberLib/QualifiedParameter.js index 9a14095..3871823 100755 --- a/EmberLib/QualifiedParameter.js +++ b/EmberLib/QualifiedParameter.js @@ -23,10 +23,11 @@ class QualifiedParameter extends QualifiedElement { } /** - * + * Generate a Root containing a minimal QualifiedParameter and its new value. + * Should be sent to the Provider to update the value. * @param {number|string} value * @returns {TreeNode} - */ + */ setValue(value) { let r = this.getNewTree(); let qp = new QualifiedParameter(this.path); diff --git a/EmberLib/TreeNode.js b/EmberLib/TreeNode.js index a7344b1..2cb7919 100755 --- a/EmberLib/TreeNode.js +++ b/EmberLib/TreeNode.js @@ -1,10 +1,14 @@ "use strict"; const BER = require('../ber.js'); +const ElementInterface = require("./ElementInterface"); +const Invocation = require("./Invocation"); const Command = require("./Command"); -const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE} = require("./constants"); +const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE, COMMAND_INVOKE} = require("./constants"); +const Errors = require("../Errors"); -class TreeNode { +class TreeNode extends ElementInterface { constructor() { + super(); /** @type {TreeNode} */ this._parent = null; this._subscribers = new Set(); @@ -144,48 +148,13 @@ class TreeNode { return this.elements != null && this.elements.size > 0; } - /** - * @returns {boolean} - */ - isCommand() { - return false; - } - /** - * @returns {boolean} - */ - isNode() { - return false; - } - /** - * @returns {boolean} - */ - isMatrix() { - return false; - } - /** - * @returns {boolean} - */ - isParameter() { - return false; - } - /** - * @returns {boolean} - */ - isFunction() { - return false; - } /** * @returns {boolean} */ isRoot() { return this._parent == null; } - /** - * @returns {boolean} - */ - isQualified() { - return false; - } + /** * @returns {boolean} */ @@ -194,13 +163,6 @@ class TreeNode { this.contents.streamIdentifier != null; } - /** - * @returns {boolean} - */ - isTemplate() { - return false; - } - /** * @returns {TreeNode} */ @@ -261,8 +223,12 @@ class TreeNode { } } + /** + * + * @param {Command} cmd + */ getCommand(cmd) { - return this.getTreeBranch(new Command(cmd)); + return this.getTreeBranch(cmd); } /** @@ -273,7 +239,7 @@ class TreeNode { if (this._isSubscribable(callback)) { this._subscribe(callback); } - return this.getCommand(COMMAND_GETDIRECTORY); + return this.getCommand(new Command(COMMAND_GETDIRECTORY)); } @@ -314,7 +280,14 @@ class TreeNode { getElementByPath(path) { if (this.elements == null || this.elements.size === 0) { return null; - } + } + if (this.isRoot()) { + // check if we have QualifiedElement + const node = this.elements.get(path); + if (node != null) { + return node; + } + } const myPath = this.getPath(); if (path == myPath) { return this; @@ -322,7 +295,7 @@ class TreeNode { const myPathArray = this.isRoot() ? [] : myPath.split("."); let pathArray = path.split("."); - if (pathArray.length <= myPathArray.length) { + if (pathArray.length < myPathArray.length) { // We are lower in the tree than the requested path return null; } @@ -369,7 +342,7 @@ class TreeNode { getElementByIdentifier(identifier) { const children = this.getChildren(); if (children == null) return null; - for(var i = 0; i < children.length; i++) { + for(let i = 0; i < children.length; i++) { if(children[i].contents != null && children[i].contents.identifier == identifier) { return children[i]; @@ -390,34 +363,7 @@ class TreeNode { return this.getElementByIdentifier(id); } } - - getNodeByPath(client, path, callback) { - if(path.length === 0) { - callback(null, this); - return; - } - - let child = this.getElement(path[0]); - if(child !== null) { - child.getNodeByPath(client, path.slice(1), callback); - } else { - const cmd = this.getDirectory((error, node) => { - if(error) { - callback(error); - } - child = node.getElement(path[0]); - if(child === null) { - //DO NOT REJECT !!!! We could still be updating the tree. - return; - } else { - child.getNodeByPath(client, path.slice(1), callback); - } - }); - if(cmd !== null) { - client.sendBERNode(cmd); - } - } - } + /** * @returns {string} @@ -442,6 +388,21 @@ class TreeNode { } } + /** + * + * @param {FunctionArgument[]} params + * @returns {TreeNode} + */ + invoke(params) { + if (!this.isFunction()) { + throw new Errors.InvalidEmberNode(this.getPath(), "Invoke only for Ember Function"); + } + const invocation = new Invocation(Invocation.newInvocationID()); + invocation.arguments = params; + const req = this.getCommand(Command.getInvocationCommand(invocation)); + return req; + } + /** * */ @@ -513,7 +474,7 @@ class TreeNode { if (this._isSubscribable(callback)) { this._subscribe(callback); } - return this.getCommand(COMMAND_SUBSCRIBE); + return this.getCommand(new Command(COMMAND_SUBSCRIBE)); } /** @@ -522,7 +483,7 @@ class TreeNode { */ unsubscribe(callback) { this._unsubscribe(callback); - return this.getCommand(COMMAND_UNSUBSCRIBE); + return this.getCommand(new Command(COMMAND_UNSUBSCRIBE)); } /** @@ -564,12 +525,25 @@ class TreeNode { * @param {TreeNode} element */ static addElement(parent, element) { + /* + Store element hashed by number direct to the parent. + But if QualifiedElement, it could be directly attached to the root. + In this case, use the path instead of number. + However, if the path is a single number, it is equivalent to number. + */ element._parent = parent; if(parent.elements == null) { parent.elements = new Map(); } + if (parent.isRoot() && element.isQualified()) { + const path = element.getPath().split("."); + if (path.length > 1) { + parent.elements.set(element.getPath(), element); + return; + } + } parent.elements.set(element.getNumber(), element); - } + } static path2number(path) { try { diff --git a/EmberLib/index.js b/EmberLib/index.js index f890109..d3f10f8 100755 --- a/EmberLib/index.js +++ b/EmberLib/index.js @@ -85,33 +85,28 @@ const rootDecode = function(ber) { return r; } +const TreeNodeDecoders = { + [Parameter.BERID]: Parameter.decode, + [Node.BERID]: Node.decode, + [Command.BERID]: Command.decode, + [QualifiedParameter.BERID]: QualifiedParameter.decode, + [QualifiedNode.BERID]: QualifiedNode.decode, + [MatrixNode.BERID]: MatrixNode.decode, + [QualifiedMatrix.BERID]: QualifiedMatrix.decode, + [Function.BERID]: Function.decode, + [QualifiedFunction.BERID]: QualifiedFunction.decode, + [Template.BERID]: Template.decode, + [QualifiedTemplate.BERID]: QualifiedTemplate +}; + const childDecode = function(ber) { const tag = ber.peek(); - if (tag == Parameter.BERID) { - return Parameter.decode(ber); - } else if(tag == Node.BERID) { - return Node.decode(ber); - } else if(tag == Command.BERID) { - return Command.decode(ber); - } else if(tag == QualifiedParameter.BERID) { - return QualifiedParameter.decode(ber); - } else if(tag == QualifiedNode.BERID) { - return QualifiedNode.decode(ber); - } else if(tag == MatrixNode.BERID) { - return MatrixNode.decode(ber); - } else if(tag == QualifiedMatrix.BERID) { - return QualifiedMatrix.decode(ber); - } else if(tag == Function.BERID) { - return Function.decode(ber); - } else if (tag == QualifiedFunction.BERID) { - return QualifiedFunction.decode(ber); - } else if(tag == Template.BERID) { - return Template.decode(ber); - } else if (tag == QualifiedTemplate.BERID) { - return QualifiedTemplate.decode(ber) + const decode = TreeNodeDecoders[tag]; + if (decode == null) { + throw new Errors.UnimplementedEmberTypeError(tag); } else { - throw new Errors.UnimplementedEmberTypeError(tag); + return decode(ber); } } diff --git a/EmberServer/ElementHandlers.js b/EmberServer/ElementHandlers.js index ece5ce0..e48d60f 100755 --- a/EmberServer/ElementHandlers.js +++ b/EmberServer/ElementHandlers.js @@ -39,7 +39,7 @@ class ElementHandlers extends QualifiedHandlers{ this.server.emit("event", ServerEvents.UNSUBSCRIBE(identifier, element.getPath(), src)); this.handleUnSubscribe(client, element); break; - case EmberLib.COMMAND_INVOKE: + case EmberLib.COMMAND_INVOKE: this.server.emit("event", ServerEvents.INVOKE(identifier, element.getPath(), src)); this.handleInvoke(client, cmd.invocation, element); break; @@ -118,39 +118,40 @@ class ElementHandlers extends QualifiedHandlers{ let element = node; let path = []; while(element != null) { + if (element.isCommand()) { + break; + } if (element.number == null) { this.server.emit("error", new Errors.MissingElementNumber()); return; } - if (element.isCommand()) { - break; - } + path.push(element.number); - let children = element.getChildren(); + const children = element.getChildren(); if ((! children) || (children.length === 0)) { break; } - element = element.children[0]; + element = children[0]; } let cmd = element; if (cmd == null) { this.server.emit("error", new Errors.InvalidRequest()); - return this.server.handleError(client); - } + this.server.handleError(client); + return path; + } element = this.server.tree.getElementByPath(path.join(".")); - + if (element == null) { this.server.emit("error", new Errors.UnknownElement(path.join("."))); return this.server.handleError(client); } - if (cmd.isCommand()) { this.handleCommand(client, element, cmd); - } - else if ((cmd.isCommand()) && (cmd.connections != null)) { + return path; + } else if ((cmd.isMatrix()) && (cmd.connections != null)) { this.handleMatrixConnections(client, element, cmd.connections); } else if ((cmd.isParameter()) && @@ -166,6 +167,7 @@ class ElementHandlers extends QualifiedHandlers{ winston.debug("invalid request format"); return this.server.handleError(client, element.getTreeBranch()); } + // for logging purpose, return the path. return path; } @@ -175,7 +177,7 @@ class ElementHandlers extends QualifiedHandlers{ * @param {TreeNode} root */ handleSubscribe(client, element) { - winston.debug("subscribe"); + winston.debug("subscribe", element); this.server.subscribe(client, element); } @@ -185,7 +187,7 @@ class ElementHandlers extends QualifiedHandlers{ * @param {TreeNode} root */ handleUnSubscribe(client, element) { - winston.debug("unsubscribe"); + winston.debug("unsubscribe", element); this.server.unsubscribe(client, element); } } diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js index 99f74bf..8f1997f 100755 --- a/EmberServer/EmberServer.js +++ b/EmberServer/EmberServer.js @@ -1,6 +1,6 @@ const EventEmitter = require('events').EventEmitter; const S101Server = require('../EmberSocket').S101Server; -const ember = require('../EmberLib'); +const EmberLib = require('../EmberLib'); const JSONParser = require("./JSONParser"); const ElementHandlers = require("./ElementHandlers"); const ServerEvents = require("./ServerEvents"); @@ -79,20 +79,26 @@ class TreeServer extends EventEmitter{ */ close() { return new Promise((resolve, reject) => { - this.callback = (e) => { + const cb = e => { if (e == null) { return resolve(); } return reject(e); }; - this.server.server.close(); + if (this.server.server != null) { + this.server.server.close(cb); + } + else { + cb(); + } this.clients.clear(); }); } /** * - * @param {TreeNode} element + * @param {TreeNode} element + * @returns {TreeNode} */ getResponse(element) { return element.getTreeBranch(undefined, node => { @@ -103,9 +109,6 @@ class TreeServer extends EventEmitter{ node.addChild(children[i].getDuplicate()); } } - else { - winston.debug("getResponse","no children"); - } }); } @@ -114,7 +117,7 @@ class TreeServer extends EventEmitter{ * @param {TreeNode} element */ getQualifiedResponse(element) { - const res = new ember.Root(); + const res = new EmberLib.Root(); let dup; if (element.isRoot() === false) { dup = element.toQualified(); @@ -175,7 +178,8 @@ class TreeServer extends EventEmitter{ */ listen() { return new Promise((resolve, reject) => { - this.callback = (e) => { + this.callback = e => { + this.callback = null; if (e == null) { return resolve(); } @@ -192,7 +196,7 @@ class TreeServer extends EventEmitter{ * @param {number[]} sources */ matrixConnect(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.connect); + doMatrixOperation(this, path, target, sources, EmberLib.MatrixOperation.connect); } /** @@ -202,7 +206,7 @@ class TreeServer extends EventEmitter{ * @param {number[]} sources */ matrixDisconnect(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.disconnect); + doMatrixOperation(this, path, target, sources, EmberLib.MatrixOperation.disconnect); } /** @@ -212,7 +216,7 @@ class TreeServer extends EventEmitter{ * @param {number[]} sources */ matrixSet(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.absolute); + doMatrixOperation(this, path, target, sources, EmberLib.MatrixOperation.absolute); } /** @@ -220,22 +224,21 @@ class TreeServer extends EventEmitter{ * @param {TreeNode} element */ replaceElement(element) { - let path = element.getPath(); - let parent = this.tree.getElementByPath(path); - if ((parent == null)||(parent._parent == null)) { + const path = element.getPath(); + const existingElement = this.tree.getElementByPath(path); + if (existingElement == null) { throw new Errors.UnknownElement(path); } - parent = parent._parent; - const children = parent.getChildren(); - for(let i = 0; i <= children.length; i++) { - if (children[i] && children[i].getPath() == path) { - element._parent = parent; // move it to new tree. - children[i] = element; - let res = this.getResponse(element); - this.updateSubscribers(path,res); - return; - } + const parent = existingElement._parent; + if (parent == null) { + throw new Errors.InvalidEmberNode(path, "No parent. Can't execute replaceElement"); } + // Replace the element at the parent + parent.elements.set(existingElement.getNumber(), element); + // point the new element to parent + element._parent = parent; + const res = this.getResponse(element); + this.updateSubscribers(path,res); } /** @@ -341,7 +344,7 @@ class TreeServer extends EventEmitter{ * @returns {TreeNode} */ static JSONtoTree(obj) { - const tree = new ember.Root(); + const tree = new EmberLib.Root(); JSONParser.parseObj(tree, obj); return tree; } @@ -355,26 +358,18 @@ const validateMatrixOperation = function(matrix, target, sources) { if (matrix.contents == null) { throw new Errors.MissingElementContents(matrix.getPath()); } - if (matrix.contents.targetCount == null) { - throw new Errors.InvalidEmberNode(matrix.getPath(), "no targetCount"); - } - if ((target < 0) || (target >= matrix.contents.targetCount)) { - throw new Errors.InvalidEmberNode(matrix.getPath(), `target id ${target} out of range 0 - ${matrix.contents.targetCount}`); - } - if (sources.length == null) { - throw new Errors.InvalidSourcesFormat(); - } + matrix.validateConnection(target, sources); } const doMatrixOperation = function(server, path, target, sources, operation) { - let matrix = server.tree.getElementByPath(path); + const matrix = server.tree.getElementByPath(path); validateMatrixOperation(matrix, target, sources); - let connection = new ember.MatrixConnection(target); + const connection = new EmberLib.MatrixConnection(target); connection.sources = sources; connection.operation = operation; - server.handleMatrixConnections(undefined, matrix, [connection], false); + server._handlers.handleMatrixConnections(undefined, matrix, [connection], false); } module.exports = TreeServer; diff --git a/EmberServer/ServerEvents.js b/EmberServer/ServerEvents.js index 6e95910..62b19a5 100755 --- a/EmberServer/ServerEvents.js +++ b/EmberServer/ServerEvents.js @@ -113,8 +113,9 @@ class ServerEvents { * @param {number[]} sources */ static MATRIX_CONNECTION(identifier, path, src, target, sources) { + const sourcesInfo = sources == null || sources.length === 0 ? "empty" : sources.toString(); return new ServerEvents( - `Matrix connection to ${identifier}(path: ${path}) target ${target} connections: ${sources.toString()} from ${src}`, + `Matrix connection to ${identifier}(path: ${path}) target ${target} connections: ${sourcesInfo} from ${src}`, Types.MATRIX_CONNECTION ); } diff --git a/EmberSocket/S101Socket.js b/EmberSocket/S101Socket.js index dfab372..22307f7 100755 --- a/EmberSocket/S101Socket.js +++ b/EmberSocket/S101Socket.js @@ -88,21 +88,34 @@ class S101Socket extends EventEmitter{ } /** - * + * @param {number} timeout=2 */ - disconnect() { + disconnect(timeout = 2) { if (!this.isConnected()) { return Promise.resolve(); } return new Promise((resolve, reject) => { - this.socket.once('close', () => { - this.codec = null; - this.socket = null; - resolve(); - }); - this.socket.once('error', reject); clearInterval(this.keepaliveIntervalTimer); - this.socket.end(); + let done = false; + const cb = (data, error) => { + if (done) { return; } + done = true; + if (timer != null) { + clearTimeout(timer); + timer = null; + } + if (error == null) { + resolve(); + } + else { + reject(error); + } + }; + let timer; + if (timeout != null && (!isNaN(timeout)) && timeout > 0) { + timer = setTimeout(cb, 100 * timeout); + } + this.socket.end(cb); this.status = "disconnected"; } ); diff --git a/Errors.js b/Errors.js index 89c85ad..01273d6 100755 --- a/Errors.js +++ b/Errors.js @@ -143,7 +143,19 @@ class PathDiscoveryFailure extends Error { * @param {string} path */ constructor(path) { - super(`Failed path discovery at ${path}`); + super(PathDiscoveryFailure.getMessage(path)); + } + + /** + * + * @param {string} path + */ + setPath(path) { + this.message = PathDiscoveryFailure.getMessage(path); + } + + static getMessage(path) { + return `Failed path discovery at ${path}`; } } module.exports.PathDiscoveryFailure = PathDiscoveryFailure; @@ -197,4 +209,14 @@ class InvalidStringPair extends Error { super("Invalid StringPair Value"); } } -module.exports.InvalidStringPair = InvalidStringPair; \ No newline at end of file +module.exports.InvalidStringPair = InvalidStringPair; + +class InvalidRequesrFormat extends Error { + /** + * @param {string} path + */ + constructor(path) { + super(`Can't process request for node ${path}`); + } +} +module.exports.InvalidRequesrFormat = InvalidRequesrFormat; \ No newline at end of file diff --git a/package.json b/package.json index 5832de4..8439bf6 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.4.0", + "version": "2.4.1", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/test/DeviceTree.test.js b/test/DeviceTree.test.js index 1c91f72..a1c645e 100755 --- a/test/DeviceTree.test.js +++ b/test/DeviceTree.test.js @@ -63,25 +63,23 @@ describe("EmberClient", () => { tree.on("error", () => { }); const expectedTimeoutInSec = 2; - const initialTime = performance.now(); + const initialTime = Date.now(); return tree.connect(expectedTimeoutInSec) .then(() => { throw new Error("Should have thrown"); }, error => { - const durationMs = performance.now() - initialTime; + const durationMs = Date.now() - initialTime; const deltaMs = Math.abs(expectedTimeoutInSec * 1000 - durationMs); expect(deltaMs).toBeLessThan(10); expect(error.message).toBe(`Could not connect to ${UNKNOWN_HOST}:${PORT} after a timeout of ${expectedTimeoutInSec} seconds`) }); - }); - - + }); it("should gracefully connect and getDirectory", () => { let tree = new EmberClient(LOCALHOST, PORT); - tree.on("error", e => { - console.log(e); - }) + tree.on("error", () => { + // ignore + }); let stub = sinon.stub(tree._client, "sendBER"); tree._debug = true; server._debug = true; @@ -95,10 +93,10 @@ describe("EmberClient", () => { .then(() => { stub.restore(); tree.disconnect(); - }, error => { + }, () => { stub.restore(); - tree.disconnect(); - console.log(error); + tree.disconnect(); + // do nothinf }); }, 10000); }); diff --git a/test/Ember.test.js b/test/Ember.test.js index 4195c7e..f96b136 100755 --- a/test/Ember.test.js +++ b/test/Ember.test.js @@ -34,6 +34,146 @@ describe("Ember", () => { var ber = new BER.Reader(errorBuffer); expect(() => ember.Root.decode(ber)).toThrow(Errors.UnimplementedEmberTypeError); }); + it("Should have a toJSON()", () => { + const node = new EmberLib.Node(); + node.addChild(new EmberLib.Node(0)); + node.getElementByNumber(0).addChild(new EmberLib.Parameter(1)); + const matrix = new EmberLib.MatrixNode(2); + matrix.targets = [0,3,6,7]; + matrix.sources = [2,6,8]; + matrix.contents = new EmberLib.MatrixContents(EmberLib.MatrixType.oneToN, EmberLib.MatrixMode.nonLinear); + node.getElementByNumber(0).addChild(matrix); + const js = node.toJSON(); + expect(js).toBeDefined(); + expect(js.elements.length).toBe(1); + expect(js.elements[0].number).toBe(0); + expect(js.elements[0].children[0].number).toBe(1); + expect(js.elements[0].children[1].number).toBe(2); + expect(js.elements[0].children[1].targets.length).toBe(matrix.targets.length); + }); + it("should have a getElement()", () => { + const node = new EmberLib.Node(); + node.addChild(new EmberLib.Node(0)); + let res = node.getElement(0); + expect(res).toBeDefined(); + }); + it("should have a isCommand(), isRoot() ... functions", () => { + const root = new EmberLib.Root(); + const node = new EmberLib.Node(); + root.addElement(node); + expect(node.isCommand()).toBeFalsy(); + expect(node.isRoot()).toBeFalsy(); + expect(node.isStream()).toBeFalsy(); + expect(node.isTemplate()).toBeFalsy(); + }); + it("should have function getElement", () => { + const node = new EmberLib.Node(0); + const identifier = "node_identifier"; + const description = "node_description"; + node.contents = new EmberLib.NodeContents(identifier, description); + const root = new EmberLib.Root(); + root.addElement(node); + let res = root.getElement(identifier); + expect(res).toBeDefined(); + expect(res.contents.identifier).toBe(identifier); + }); + + it("should throw error if function getElement called from a node with longer parh", () => { + const root = new EmberLib.Root(); + root.addChild(new EmberLib.Node(0)); + root.getElement(0).addChild(new EmberLib.Node(1)); + root.getElementByPath("0.1").addChild(new EmberLib.Node(1)); + const node = new EmberLib.Node(0); + root.getElementByPath("0.1.1").addChild(node); + const identifier = "node_identifier"; + const description = "node_description"; + node.contents = new EmberLib.NodeContents(identifier, description); + let res = root.getElementByPath("0.1").getElementByPath("0"); + expect(res).toBe(null); + + res = root.getElementByPath("0.1").getElementByPath("0.2.0"); + expect(res).toBe(null); + + res = root.getElementByPath("0.1").getElementByPath("0.1"); + expect(res).toBeDefined(); + }); + it("should have a getRoot function", () => { + const root = new EmberLib.Root(); + root.addChild(new EmberLib.Node(0)); + root.getElement(0).addChild(new EmberLib.Node(1)); + root.getElementByPath("0.1").addChild(new EmberLib.Node(1)); + const node = new EmberLib.Node(0); + root.getElementByPath("0.1.1").addChild(node); + let res = node.getRoot(); + expect(res).toBe(root); + }); + it("should have a getDirectory() and accept a callback for subscribers", () => { + const parameter = new EmberLib.Parameter(0); + parameter.contents = new EmberLib.ParameterContents(7, "integer"); + parameter.contents.streamIdentifier = 12345; + let res = parameter.getDirectory(0, () => {}); + expect(res).toBeDefined(); + expect(parameter._subscribers.size).toBe(1); + }); + it("should have a getDuplicate function", () => { + const parameter = new EmberLib.Parameter(0); + parameter.contents = new EmberLib.ParameterContents("test", "string"); + let res = parameter.getDuplicate(); + expect(res).toBeDefined(); + + const qp = new EmberLib.QualifiedParameter("0.1"); + qp.contents = parameter.contents; + res = qp.getDuplicate(); + expect(res).toBeDefined(); + }); + it("should decode continuation messages", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.CONTEXT(0)); + const qp = new EmberLib.QualifiedParameter("0.1"); + qp.encode(writer); + writer.endSequence(); + const res = EmberLib.rootDecode(new BER.Reader(writer.buffer)); + expect(res).toBeDefined(); + expect(res.getElementByPath("0.1")).toBeDefined(); + }); + it("should throw an error if not able to decode root", () => { + let writer = new BER.Writer(); + writer.startSequence(BER.CONTEXT(0)); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.rootDecode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(error){ + expect(error instanceof Errors.UnimplementedEmberTypeError); + } + + writer = new BER.Writer(); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + try { + EmberLib.rootDecode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(error){ + expect(error instanceof Errors.UnimplementedEmberTypeError); + } + + writer = new BER.Writer(); + writer.startSequence(BER.APPLICATION(0)); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.rootDecode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(error){ + expect(error instanceof Errors.UnimplementedEmberTypeError); + } + }); }); describe("Command", () => { it("should throw error if unknown context found", () => { @@ -81,6 +221,20 @@ describe("Ember", () => { expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); } }); + it("should have a getElementByIdentifier", () => { + const node = new EmberLib.Node(0); + const identifier = "node_identifier"; + const description = "node_description"; + node.contents = new EmberLib.NodeContents(identifier, description); + const root = new EmberLib.Root(); + root.addElement(node); + let res = root.getElementByIdentifier(identifier); + expect(res).toBeDefined(); + expect(res.contents.identifier).toBe(identifier); + + res = root.getElementByIdentifier("unknown"); + expect(res).toBe(null); + }); }); describe("Node", () => { it("should have an encoder", () => { @@ -1248,11 +1402,36 @@ describe("Ember", () => { expect(qp.contents._ignore).not.toBeDefined(); expect(qp.contents.value).toBe(NEW_VAL); }); - it("Should return true to isParameter() call", () => { const qNode = new EmberLib.QualifiedParameter(PATH); expect(qNode.isParameter()).toBeTruthy(); }); + it("should have setValue function", () => { + const qp = new EmberLib.QualifiedParameter(PATH); + const VALUE = 1; + qp.contents = new EmberLib.ParameterContents(VALUE, "integer"); + let NEW_VALUE = VALUE + 1; + let setVal = qp.setValue(NEW_VALUE); + let dup = setVal.getElementByPath(PATH); + expect(dup).toBeDefined(); + expect(dup.contents.value).toBe(NEW_VALUE); + NEW_VALUE = NEW_VALUE + 1; + setVal = qp.setValue(new EmberLib.ParameterContents(NEW_VALUE)); + expect(setVal.getElementByPath(PATH).contents.value).toBe(NEW_VALUE); + }); + it("should accept subscribers and have a function to update them", () => { + const qp = new EmberLib.QualifiedParameter(PATH); + const VALUE = 1; + qp.contents = new EmberLib.ParameterContents(VALUE, "integer"); + qp.contents.streamIdentifier = 12345; + let updatedValue = null; + const handleUpdate = function(param) { + updatedValue = param.contents.value; + } + qp.subscribe(handleUpdate); + qp.updateSubscribers(); + expect(updatedValue).toBe(VALUE); + }); }); describe("StreamDescription", () => { it("Should throw an error if unable to decode", () => { diff --git a/test/Server.test.js b/test/Server.test.js index 05f5dc5..5dac151 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -1,17 +1,18 @@ const expect = require("expect"); const {EmberServer, ServerEvents} = require("../EmberServer"); const EmberClient = require("../EmberClient"); -const ember = require("../EmberLib"); +const EmberLib = require("../EmberLib"); const {jsonRoot} = require("./utils"); const MatrixHandlers = require("../EmberServer/MatrixHandlers"); +const Errors = require("../Errors"); const LOCALHOST = "127.0.0.1"; -const PORT = 9009; +let PORT = 9009; describe("server", function() { describe("JSONtoTree", function() { let jsonTree; - beforeAll(function() { + beforeEach(function() { jsonTree = jsonRoot(); }); it("should generate an ember tree from json", function() { @@ -22,36 +23,74 @@ describe("server", function() { expect(root.getElementByNumber(0).contents.identifier).toBe("scoreMaster"); expect(root.getElementByNumber(0).elements.size).toBe(jsonTree[0].children.length); }); + it("should throw an error if invalid matrix mode", function() { + jsonTree[0].children[1].children[0].mode = "invalid"; + let error; + try { + const root = EmberServer.JSONtoTree(jsonTree); + } + catch(e) { + error = e; + } + expect(error).toBeDefined(); + expect(error instanceof Errors.InvalidEmberNode).toBeDefined(); + }); + it("should support matrix type nToN nonLinear", function() { + jsonTree[0].children[1].children[0].type = "nToN"; + jsonTree[0].children[1].children[0].mode = "nonLinear"; + jsonTree[0].children[1].children[0].maximumConnectsPerTarget = 10; + jsonTree[0].children[1].children[0].maximumTotalConnects = 100; + const root = EmberServer.JSONtoTree(jsonTree); + const matrix = root.getElementByPath("0.1.0"); + expect(matrix).toBeDefined(); + expect(matrix.contents.maximumConnectsPerTarget).toBe(jsonTree[0].children[1].children[0].maximumConnectsPerTarget); + expect(matrix.contents.maximumTotalConnects).toBe(jsonTree[0].children[1].children[0].maximumTotalConnects); + }); + it("should support matrix type oneToOne", function() { + jsonTree[0].children[1].children[0].type = "oneToOne"; + const root = EmberServer.JSONtoTree(jsonTree); + const matrix = root.getElementByPath("0.1.0"); + expect(matrix).toBeDefined(); + expect(matrix.contents.type).toBe(EmberLib.MatrixType.oneToOne); + }); + it("should throw an error if invalid matrix type", function() { + jsonTree[0].children[1].children[0].type = "invalid"; + let error; + try { + const root = EmberServer.JSONtoTree(jsonTree); + } + catch(e) { + error = e; + } + expect(error).toBeDefined(); + expect(error instanceof Errors.InvalidEmberNode).toBeDefined(); + }); }); describe("Server - Client communication", function() { let server,client,jsonTree; - beforeAll(function() { + beforeEach(() => { jsonTree = jsonRoot(); const root = EmberServer.JSONtoTree(jsonTree); server = new EmberServer(LOCALHOST, PORT, root); server.on("error", e => { - // eslint-disable-next-line no-console - console.log(e); + // ignore }); server.on("clientError", e => { - // eslint-disable-next-line no-console - console.log(e); + // ignore }); //server._debug = true; return server.listen(); }); - afterAll(function() { + afterEach(() => { return server.close(); }); - it("should receive and decode the full tree", function () { + it("should receive and decode the full tree", () => { client = new EmberClient(LOCALHOST, PORT); //client._debug = true; return Promise.resolve() .then(() => client.connect()) - .then(() => { - return client.getDirectory(); - }) + .then(() => client.getDirectory()) .then(() => { expect(client.root).toBeDefined(); expect(client.root.elements).toBeDefined(); @@ -68,43 +107,32 @@ describe("server", function() { expect(client.root.getElementByPath("0.0.3").contents.identifier).toBe("author"); // Issue #33 EmberServer.handleGetDirectory does not subscribe to child parameters expect(server.subscribers["0.0.0"]).toBeDefined(); - // Keepalive return client.disconnect(); }); }); - it("should be able to modify a parameter", () => { + it("should be able to modify a parameter", async () => { client = new EmberClient(LOCALHOST, PORT); - //client._debug = true; - return Promise.resolve() - .then(() => client.connect()) - .then(() => { - return client.getDirectory(); - }) - .then(() => client.expand(client.root.getElementByNumber(0))) - .then(() => { - expect(server.tree.getElementByPath("0.0.1").contents.value).not.toBe("gdnet"); - return client.setValue(client.root.getElementByPath("0.0.1"), "gdnet"); - }) - .then(() => { - expect(server.tree.getElementByPath("0.0.1").contents.value).toBe("gdnet"); - return client.disconnect(); - }); + await client.connect() + await client.getDirectory(); + await client.getElementByPath("0.0.1"); + expect(server.tree.getElementByPath("0.0.1").contents.value).not.toBe("gdnet"); + await client.setValue(client.root.getElementByPath("0.0.1"), "gdnet"); + expect(server.tree.getElementByPath("0.0.1").contents.value).toBe("gdnet"); + console.log("result", server.tree.getElementByPath("0.0.1").contents.value) + return client.disconnect().then(() => { console.log("disconnected")}); }); it("should be able to call a function with parameters", () => { client = new EmberClient(LOCALHOST, PORT); //client._debug = true; - return Promise.resolve() - .then(() => client.connect()) - .then(() => { - return client.getDirectory(); - }) - .then(() => client.expand(client.root.getElementByNumber(0))) + return client.connect() + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("0.2")) .then(() => { const func = client.root.getElementByPath("0.2"); return client.invokeFunction(func, [ - new ember.FunctionArgument(ember.ParameterType.integer, 1), - new ember.FunctionArgument(ember.ParameterType.integer, 7) + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 1), + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 7) ]); }) .then(result => { @@ -116,51 +144,30 @@ describe("server", function() { }); }); - it("should be able to get child with tree.getNodeByPath", function() { - //server._debug = true; + it("should be able to get child with client.getElement", function() { client = new EmberClient(LOCALHOST, PORT); - //client._debug = true; - //client._debug = true; return Promise.resolve() .then(() => client.connect()) - .then(() => { - return client.getDirectory(); - }) - .then(() => client.getNodeByPath("scoreMaster/identity/product")) - .then(() => { - return client.getNodeByPath("scoreMaster/router/labels/group 1"); - }) - .then(() => { - return client.disconnect(); - }); + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("scoreMaster/identity/product")) + .then(() => client.getElementByPath("scoreMaster/router/labels/group 1")) + .then(() => client.disconnect()); }); it("should be able to get child with getElementByPath", function() { - //server._debug = true; client = new EmberClient(LOCALHOST, PORT); - //client._debug = true; - //client._debug = true; return Promise.resolve() .then(() => client.connect()) - .then(() => { - return client.getDirectory(); - }) + .then(() => client.getDirectory()) .then(() => client.getElementByPath("scoreMaster/identity/product")) - .then(() => { - return client.getElementByPath("scoreMaster/router/labels/group 1"); - }) - .then(() => { - return client.disconnect(); - }); + .then(() => client.getElementByPath("scoreMaster/router/labels/group 1")) + .then(() => client.disconnect()); }); - it("should throw an error if getNodeByPath for unknown path", function() { - //server._debug = true; + it("should throw an error if getElementByPath for unknown path", function() { client = new EmberClient(LOCALHOST, PORT); return Promise.resolve() .then(() => client.connect()) - .then(() => { - return client.getDirectory(); - }) - .then(() => client.getNodeByPath("scoreMaster/router/labels/group")) + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("scoreMaster/router/labels/group")) .then(() => { throw new Error("Should not succeed"); }) @@ -173,14 +180,10 @@ describe("server", function() { client = new EmberClient(LOCALHOST, PORT); return Promise.resolve() .then(() => client.connect()) - .then(() => { - return client.getDirectory(); - }) + .then(() => client.getDirectory()) .then(() => client.getElementByPath("0.1.0")) .then(matrix => client.matrixConnect(matrix, 0, [1])) - .then(matrix => { - return client.getElementByPath(matrix.getPath()); - }) + .then(matrix => client.getElementByPath(matrix.getPath())) .then(matrix => { expect(matrix.connections['0'].sources).toBeDefined(); expect(matrix.connections['0'].sources.length).toBe(1); @@ -220,15 +223,15 @@ describe("server", function() { count = 0; const func = client.root.getElementByPath("0.2"); return client.invokeFunction(func, [ - new ember.FunctionArgument(ember.ParameterType.integer, 1), - new ember.FunctionArgument(ember.ParameterType.integer, 7) + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 1), + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 7) ]); }) .then(() => { expect(count).toBe(1); expect(receivedEvent.type).toBe(ServerEvents.Types.INVOKE); }) - .then(() => client.getNodeByPathnum("0.0.2")) + .then(() => client.getElementByPath("0.0.2")) .then(parameter => { server._subscribe = server.subscribe; let _resolve; @@ -260,6 +263,9 @@ describe("server", function() { const root = EmberServer.JSONtoTree(jsonTree); server = new EmberServer(LOCALHOST, PORT, root); }); + afterEach(() => { + return server.close(); + }); it("should verify if connection allowed in 1-to-N", function() { let disconnectCount = 0; const handleDisconnect = () => { @@ -267,15 +273,15 @@ describe("server", function() { } server.on("matrix-disconnect", handleDisconnect.bind(this)); const matrix = server.tree.getElementByPath("0.1.0"); - let connection = new ember.MatrixConnection(0); + let connection = new EmberLib.MatrixConnection(0); connection.setSources([1]); - connection.operation = ember.MatrixOperation.connect; + connection.operation = EmberLib.MatrixOperation.connect; let res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); matrix.setSources(0, [0]); res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeFalsy(); - connection.operation = ember.MatrixOperation.absolute; + connection.operation = EmberLib.MatrixOperation.absolute; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); // We can't connect. But server will disconnect existing source and connect new one. @@ -291,8 +297,8 @@ describe("server", function() { expect(disconnectCount).toBe(2); expect(matrix.connections[0].sources[0]).toBe(222); matrix.setSources(0, [0]); - connection = new ember.MatrixConnection(1); - connection.operation = ember.MatrixOperation.absolute; + connection = new EmberLib.MatrixConnection(1); + connection.operation = EmberLib.MatrixOperation.absolute; connection.setSources([1]); res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); @@ -304,10 +310,10 @@ describe("server", function() { disconnectCount++; } server.on("matrix-disconnect", handleDisconnect.bind(this)); - matrix.contents.type = ember.MatrixType.oneToOne; - const connection = new ember.MatrixConnection(0); + matrix.contents.type = EmberLib.MatrixType.oneToOne; + const connection = new EmberLib.MatrixConnection(0); connection.setSources([1]); - connection.operation = ember.MatrixOperation.connect; + connection.operation = EmberLib.MatrixOperation.connect; let res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); matrix.setSources(0, [0]); @@ -320,7 +326,7 @@ describe("server", function() { // But if connecting same source and dest. just disconnect and do not reconnect. server._handlers.handleMatrixConnections(null, matrix, {0: connection}); expect(disconnectCount).toBe(2); - connection.operation = ember.MatrixOperation.absolute; + connection.operation = EmberLib.MatrixOperation.absolute; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); matrix.setSources(0, []); @@ -336,11 +342,11 @@ describe("server", function() { disconnectCount++; } server.on("matrix-disconnect", handleDisconnect.bind(this)); - matrix.contents.type = ember.MatrixType.oneToOne; + matrix.contents.type = EmberLib.MatrixType.oneToOne; matrix.setSources(0, [1]); - const connection = new ember.MatrixConnection(0); + const connection = new EmberLib.MatrixConnection(0); connection.setSources([1]); - connection.operation = ember.MatrixOperation.connect; + connection.operation = EmberLib.MatrixOperation.connect; server._handlers.handleMatrixConnections(null, matrix, {0: connection}); expect(matrix.connections[0].sources.length).toBe(0); expect(disconnectCount).toBe(1); @@ -352,12 +358,12 @@ describe("server", function() { disconnectCount++; } server.on("matrix-disconnect", handleDisconnect.bind(this)); - matrix.contents.type = ember.MatrixType.oneToOne; + matrix.contents.type = EmberLib.MatrixType.oneToOne; matrix.setSources(0, [1]); matrix.connections[0].lock(); - const connection = new ember.MatrixConnection(0); + const connection = new EmberLib.MatrixConnection(0); connection.setSources([0]); - connection.operation = ember.MatrixOperation.connect; + connection.operation = EmberLib.MatrixOperation.connect; server._handlers.handleMatrixConnections(null, matrix, {0: connection}); expect(matrix.connections[0].sources.length).toBe(1); expect(matrix.connections[0].sources[0]).toBe(1); @@ -365,13 +371,13 @@ describe("server", function() { }); it("should verify if connection allowed in N-to-N", function() { const matrix = server.tree.getElementByPath("0.1.0"); - matrix.contents.type = ember.MatrixType.nToN; + matrix.contents.type = EmberLib.MatrixType.nToN; matrix.contents.maximumTotalConnects = 2; matrix.setSources(0, [0,1]); - const connection = new ember.MatrixConnection(0); + const connection = new EmberLib.MatrixConnection(0); connection.setSources([2]); - connection.operation = ember.MatrixOperation.connect; + connection.operation = EmberLib.MatrixOperation.connect; let res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeFalsy(); @@ -387,7 +393,7 @@ describe("server", function() { matrix.setSources(0, [1,2]); matrix.setSources(1, []); - connection.operation = ember.MatrixOperation.absolute; + connection.operation = EmberLib.MatrixOperation.absolute; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); @@ -399,7 +405,7 @@ describe("server", function() { matrix.setSources(1, [1]); matrix.setSources(0, [0]); connection.setSources([2]); - connection.operation = ember.MatrixOperation.connect; + connection.operation = EmberLib.MatrixOperation.connect; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeFalsy(); @@ -409,20 +415,18 @@ describe("server", function() { expect(res).toBeTruthy(); matrix.setSources(0, [0]); - connection.operation = ember.MatrixOperation.absolute; + connection.operation = EmberLib.MatrixOperation.absolute; res = matrix.canConnect(connection.target,connection.sources,connection.operation); expect(res).toBeTruthy(); }); it("should return modified answer on absolute connect", function() { let client; - server.on("error", e => { - // eslint-disable-next-line no-console - console.log(e); + server.on("error", () => { + // ignore }); - server.on("clientError", e => { - // eslint-disable-next-line no-console - console.log(e); + server.on("clientError", () => { + // ignore }); //server._debug = true; return server.listen() @@ -432,38 +436,33 @@ describe("server", function() { }) .then(() => client.connect()) .then(() => client.getDirectory()) - .then(() => client.getNodeByPathnum("0.1.0")) + .then(() => client.getElementByPath("0.1.0")) .then(matrix => client.matrixSetConnection(matrix, 0, [1])) .then(result => { expect(result).toBeDefined(); expect(result.connections).toBeDefined(); expect(result.connections[0]).toBeDefined(); - expect(result.connections[0].disposition).toBe(ember.MatrixDisposition.modified); + expect(result.connections[0].disposition).toBe(EmberLib.MatrixDisposition.modified); return client.disconnect(); - }) - .then(() => { - server.close(); }); }); }); describe("Parameters subscribe/unsubscribe", function( ){ let jsonTree; let server; - beforeAll(function() { + beforeEach(function() { jsonTree = jsonRoot(); const root = EmberServer.JSONtoTree(jsonTree); server = new EmberServer(LOCALHOST, PORT, root); - server.on("error", e => { - // eslint-disable-next-line no-console - console.log(e); + server.on("error", () => { + // ignore }); - server.on("clientError", e => { - // eslint-disable-next-line no-console - console.log(e); + server.on("clientError", () => { + // ignore }); return server.listen(); }); - afterAll(function() { + afterEach(function() { return server.close(); }); it("should not auto subscribe stream parameter", function() { @@ -482,7 +481,7 @@ describe("server", function() { .then(() => { return client.getDirectory(); }) - .then(() => client.getNodeByPathnum("0.0.2")) + .then(() => client.getElementByPath("0.0.2")) .then(parameter => { expect(server.subscribers["0.0.2"]).not.toBeDefined(); expect(parameter._subscribers).toBeDefined(); @@ -501,7 +500,7 @@ describe("server", function() { .then(() => { expect(server.subscribers["0.0.2"]).toBeDefined(); expect(server.subscribers["0.0.2"].size).toBe(1); - return client.getNodeByPathnum("0.0.2"); + return client.getElementByPath("0.0.2"); }) .then(parameter => { expect(parameter._subscribers).toBeDefined(); @@ -519,7 +518,7 @@ describe("server", function() { }) .then(() => { expect(server.subscribers["0.0.2"]).toBeDefined(); - return client.getNodeByPathnum("0.0.2"); + return client.getElementByPath("0.0.2"); }) .then(parameter => { expect(server.subscribers["0.0.2"]).toBeDefined(); @@ -530,4 +529,514 @@ describe("server", function() { .then(() => client.disconnect()); }); }); + describe("Handlers", () => { + let jsonTree; + let server; + beforeEach(function() { + jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + server.on("error", () => { + //ignore + }); + server.on("clientError", () => { + //ignore + }); + return server.listen(); + }); + afterEach(function() { + return server.close(); + }); + it("Should through an error if can't process request", () => { + const root = new EmberLib.Root(); + root.addElement(new EmberLib.Node(0)); + let error; + const errorHandler = function(e) { + error = e; + } + server.on("error", errorHandler); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + server.handleRoot(client._client, root); + expect(error instanceof Errors.InvalidRequesrFormat); + client.disconnect(); + }); + }); + it("should ignore empty or null tree", () => { + const root = new EmberLib.Root(); + let error; + try { + server.handleRoot(null, root); + } + catch(e) { + error = e; + } + expect(error).not.toBeDefined(); + }); + it("should generate responses which include children", () => { + const node = server.tree.getElementByNumber(0); + server.getResponse(node); + expect(node.getChildren().length > 0).toBeTruthy(); + }); + it("Should update parameter value if new parameter value received", () => { + const root = new EmberLib.Root(); + const parameter = new EmberLib.Parameter(2); + const VALUE = "3.4.5"; + parameter.contents = new EmberLib.ParameterContents(VALUE, "string"); + root.addElement(new EmberLib.Node(0)); + root.getElement(0).addChild(new EmberLib.Node(0)); + root.getElementByPath("0.0").addChild(parameter); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + server.handleRoot(client._client, root); + const res = server.tree.getElementByPath("0.0.2"); + expect(res.contents.value).toBe(VALUE); + return client.disconnect(); + }); + }); + it("Should throw an error if element not found during request process", () => { + const root = new EmberLib.Root(); + const parameter = new EmberLib.Parameter(99); + const VALUE = "3.4.5"; + parameter.contents = new EmberLib.ParameterContents(VALUE, "string"); + root.addElement(new EmberLib.Node(0)); + root.getElement(0).addChild(new EmberLib.Node(0)); + root.getElementByPath("0.0").addChild(parameter); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + let count = 0; + server.handleError = () => { + count++; + } + server.handleRoot(client._client, root); + expect(count).toBe(1); + return client.disconnect(); + }); + }); + it("Should throw an error if element contains null child", () => { + const root = new EmberLib.Root(); + const node = new EmberLib.Node(0); + root.addElement(node); + node.elements = new Map(); + node.elements.set(0, null); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + let count = 0; + server.handleError = () => { + count++; + } + server.handleRoot(client._client, root); + expect(count).toBe(1); + return client.disconnect(); + }); + }); + it("should handle commands embedded in Node", () => { + const root = new EmberLib.Root(); + const node = new EmberLib.Node(0); + root.addElement(node); + node.elements = new Map(); + node.elements.set(EmberLib.COMMAND_GETDIRECTORYGETDIRECTORY, new EmberLib.Command(EmberLib.COMMAND_GETDIRECTORYGETDIRECTORY)); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + let count = 0; + server._handlers.handleCommand = () => { + count++; + } + server.handleRoot(client._client, root); + expect(count).toBe(1); + return client.disconnect(); + }); + }); + it("should catch unknown commands", () => { + const command = new EmberLib.Command(99); + let count = 0; + server.on("error", e => { + expect(e instanceof Errors.InvalidCommand); + count++; + }); + server._handlers.handleCommand(null, new EmberLib.Root(), command); + expect(count).toBe(1); + }); + it("should catch invalid node with no number", () => { + const node = new EmberLib.Node(99); + node.number = null; + let count = 0; + server.on("error", e => { + expect(e instanceof Errors.MissingElementNumber); + count++; + }); + server._handlers.handleNode(null, node); + expect(count).toBe(1); + }); + it("should handle matrix connections embedded in Node", () => { + const root = new EmberLib.Root(); + const node = new EmberLib.Node(0); + root.addElement(node); + const matrix = new EmberLib.MatrixNode(0); + matrix.connections = [ + new EmberLib.MatrixConnection(0) + ]; + node.elements = new Map(); + node.elements.set(0, matrix); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + let count = 0; + server._handlers.handleMatrixConnections = () => { + count++; + } + server.handleRoot(client._client, root); + expect(count).toBe(1); + return client.disconnect(); + }); + }); + it("should catch function invocation errors and set success to false", () => { + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + const root = new EmberLib.Root(); + const func = new EmberLib.Function(0, () => { throw Error("function error")}); + root.addElement(func); + server.tree = root; + return client.invokeFunction(func, []); + }) + .then(result => { + expect(result).toBeDefined(); + expect(result.success).toBeFalsy(); + }) + .then(() => client.disconnect()); + }); + it("should catch invoke to non function", () => { + const client = new EmberClient(LOCALHOST, PORT); + let result; + client.sendBERNode = function(res) { + result = res; + } + const root = new EmberLib.Root(); + const func = new EmberLib.Node(0); + root.addElement(func); + server.tree = root; + const command = EmberLib.Command.getInvocationCommand(new EmberLib.Invocation(1, [])); + server._handlers.handleInvoke(client, func, command); + expect(result).toBeDefined(); + expect(result.success).toBeFalsy(); + return client.disconnect(); + }); + }); + describe("Matrix", () => { + let jsonTree; + const MATRIX_PATH = "0.1.0"; + /** @type {EmberServer} */ + let server; + beforeEach(function() { + jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + server.on("error", () => { + //ignore + }); + server.on("clientError", () => { + //ignore + }); + return server.listen(); + }); + afterEach(function() { + return server.close(); + }); + it("should generate connections structure if none provided when calling JSONtoStree", () => { + const js = jsonRoot(); + js[0].children[1].children[0].connections = null; + const tree = EmberServer.JSONtoTree(js); + const matrix = tree.getElementByPath(MATRIX_PATH); + expect(matrix.connections).toBeDefined(); + for(let i = 0; i < matrix.contents.targetCount; i++) { + expect(matrix.connections[i]).toBeDefined(); + expect(matrix.connections[i].target).toBe(i); + } + }); + it("should have a matrixConnect function", () => { + const matrix = server.tree.getElementByPath(MATRIX_PATH); + matrix.connections[0].setSources([]); + server.matrixConnect(MATRIX_PATH, 0, [1]); + expect(matrix.connections[0].sources).toBeDefined(); + expect(matrix.connections[0].sources.length).toBe(1); + expect(matrix.connections[0].sources[0]).toBe(1); + }); + it("should throw an error if can't find matrix", () => { + try { + server.matrixConnect("0.99.0", 0, [1]); + throw new Error("Should not succeed"); + } + catch(error) { + expect(error instanceof Errors.UnknownElement); + } + }); + it("should throw an error if invalid matrix", () => { + const matrix = server.tree.getElementByPath(MATRIX_PATH); + matrix.contents = null; + try { + server.matrixConnect(MATRIX_PATH, 0, [1]); + throw new Error("Should not succeed"); + } + catch(error) { + expect(error instanceof Errors.MissingElementContents); + } + }); + it("should have a matrixSet operation on matrix", () => { + const matrix = server.tree.getElementByPath(MATRIX_PATH); + matrix.connections[0].setSources([0]); + server.matrixSet(MATRIX_PATH, 0, [1]); + expect(matrix.connections[0].sources).toBeDefined(); + expect(matrix.connections[0].sources.length).toBe(1); + expect(matrix.connections[0].sources[0]).toBe(1); + }); + it("should have a matrixDisconnect operation on matrix", () => { + const matrix = server.tree.getElementByPath(MATRIX_PATH); + matrix.contents.type = EmberLib.MatrixType.nToN; + matrix.connections[0].setSources([1]); + server.matrixDisconnect(MATRIX_PATH, 0, [1]); + expect(matrix.connections[0].sources).toBeDefined(); + expect(matrix.connections[0].sources.length).toBe(0); + }); + }); + describe("subscribers", () => { + let jsonTree; + const PARAMETER_PATH = "0.0.1"; + const MATRIX_PATH = "0.1.0"; + /** @type {EmberServer} */ + let server; + beforeEach(function() { + jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + server.on("error", () => { + //ignore + }); + server.on("clientError", () => { + //ignore + }); + return server.listen(); + }); + afterEach(function() { + return server.close(); + }); + it("should accept client to subscribe to parameter and update those who subscribed", () => { + const client = new EmberClient(LOCALHOST, PORT); + const VALUE = "The new Value"; + return client.connect() + .then(() => client.getDirectory()) + .then(() => client.getElementByPath(PARAMETER_PATH)) + .then(() => { + // A get directory on non stream is auto subscribe + expect(server.subscribers).toBeDefined(); + expect(server.subscribers[PARAMETER_PATH]).toBeDefined(); + expect(server.subscribers[PARAMETER_PATH].size).toBe(1); + let res; + for(let c of server.subscribers[PARAMETER_PATH]) { + c.queueMessage = message => { + res = message; + } + } + server.setValue(server.tree.getElementByPath(PARAMETER_PATH), VALUE, null, null); + expect(res).toBeDefined(); + const resParam = res.getElementByPath(PARAMETER_PATH); + expect(resParam).toBeDefined(); + expect(resParam.getPath()).toBe(PARAMETER_PATH); + expect(resParam.contents).toBeDefined(); + expect(resParam.contents.value).toBe(VALUE); + return client.disconnect(); + }); + }); + it("should accept client to subscribe to matrix and update those who subscribed", () => { + const client = new EmberClient(LOCALHOST, PORT); + const VALUE = 17; + const MatrixParamName = "maximumTotalConnects"; + server.tree.getElementByPath(MATRIX_PATH).contents[MatrixParamName] = 0; + return client.connect() + .then(() => client.getDirectory()) + .then(() => client.getElementByPath(MATRIX_PATH)) + .then(() => { + // A get directory on non stream is auto subscribe + expect(server.subscribers).toBeDefined(); + expect(server.subscribers[MATRIX_PATH]).toBeDefined(); + expect(server.subscribers[MATRIX_PATH].size).toBe(1); + let res; + for(let c of server.subscribers[MATRIX_PATH]) { + c.queueMessage = message => { + res = message; + } + } + server.setValue(server.tree.getElementByPath(MATRIX_PATH), VALUE, null, MatrixParamName); + expect(res).toBeDefined(); + const resParam = res.getElementByPath(MATRIX_PATH); + expect(resParam).toBeDefined(); + expect(resParam.getPath()).toBe(MATRIX_PATH); + expect(resParam.contents).toBeDefined(); + expect(resParam.contents[MatrixParamName]).toBe(VALUE); + return client.disconnect(); + }); + }); + it("should clean up subscribers if client not connected anymore", () => { + const client = new EmberClient(LOCALHOST, PORT); + const VALUE = "The new Value"; + server.subscribers[PARAMETER_PATH] = new Set(); + server.subscribers[PARAMETER_PATH].add(client); + expect(server.subscribers[PARAMETER_PATH].size).toBe(1); + let res; + for(let c of server.subscribers[PARAMETER_PATH]) { + c.queueMessage = message => { + res = message; + } + } + server.setValue(server.tree.getElementByPath(PARAMETER_PATH), VALUE, null, null); + expect(res).not.toBeDefined(); + expect(server.subscribers[PARAMETER_PATH].has(client)).toBeFalsy(); + }); + it("should ignore unsubscribe if no subcribers", () => { + const client = new EmberClient(LOCALHOST, PORT); + let error; + try { + server.unsubscribe(client, server.tree.getElementByPath(PARAMETER_PATH)); + } + catch(e) { + error =e ; + } + expect(error).not.toBeDefined(); + }); + it("should ignore serValue on element with no contents", () => { + const param = server.tree.getElementByPath(PARAMETER_PATH); + const VALUE = "The new Value"; + param.contents = null; + let error; + try { + server.setValue(param, VALUE, null, null); + } + catch(e) { + error =e ; + } + expect(error).not.toBeDefined(); + }); + }); + describe("EmberServer toJSON", () => { + it("should have a toJSON", () => { + const PARAMETER_PATH = "0.0.1"; + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + const js = server.toJSON(); + expect(js[0].children[0].children[1].path).toBe(PARAMETER_PATH); + }); + it("should have a toJSON and return empty array if no tree", () => { + server = new EmberServer(LOCALHOST, PORT, null); + const js = server.toJSON(); + expect(js).toBeDefined(); + expect(js.length).toBe(0); + }); + }); + describe("replaceElement", () => { + it("should replace existing element with new one", () => { + const PARAMETER_PATH = "0.0.1"; + const VALUE = "Gilles Dufour" + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + const newParam = new EmberLib.Parameter(1); + newParam.contents = new EmberLib.ParameterContents(VALUE); + newParam.path = PARAMETER_PATH; + server.replaceElement(newParam); + expect(server.tree.getElementByPath(PARAMETER_PATH).contents.value).toBe(VALUE); + }); + it("should throw an error if unknown element path", () => { + const VALUE = "Gilles Dufour" + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + const newParam = new EmberLib.Parameter(1); + newParam.contents = new EmberLib.ParameterContents(VALUE); + let error; + try { + server.replaceElement(newParam); + } + catch(e) { + error = e; + } + expect(error).toBeDefined(); + expect(error instanceof Errors.UnknownElement).toBeTruthy(); + }); + it("should throw an error if trying to replace root or unattached element", () => { + const PARAMETER_PATH = "0.0.1"; + const VALUE = "Gilles Dufour" + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + server.tree.getElementByPath(PARAMETER_PATH)._parent = null; + const newParam = new EmberLib.Parameter(1); + newParam.contents = new EmberLib.ParameterContents(VALUE); + newParam.path = PARAMETER_PATH; + let error; + try { + server.replaceElement(newParam); + } + catch(e) { + error = e; + } + expect(error).toBeDefined(); + expect(error instanceof Errors.InvalidEmberNode).toBeTruthy(); + }); + }); + describe("Events", () => { + it("should catch error emitted by internal tcp server", () => { + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + const ERROR_MESSAGE = "gdnet internal error"; + server = new EmberServer(LOCALHOST, PORT, root); + let error; + server.on("error", e => {error = e;}); + server.server.emit("error", new Error(ERROR_MESSAGE)); + expect(error).toBeDefined(); + expect(error.message).toBe(ERROR_MESSAGE); + }); + it("should catch error and use callback if present", () => { + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + const ERROR_MESSAGE = "gdnet internal error"; + server = new EmberServer(LOCALHOST, PORT, root); + let error; + server.on("error", e => {}); + server.callback = e => {error = e;} + server.server.emit("error", new Error(ERROR_MESSAGE)); + expect(error).toBeDefined(); + expect(error.message).toBe(ERROR_MESSAGE); + }); + it("should catch tcp server disconnected message, and clean up clients", () => { + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + server.clients.add(new EmberClient(LOCALHOST, PORT)); + let count = 0; + server.on("disconnected", () => {count++;}); + server.server.emit("disconnected"); + expect(count).toBe(1); + expect(server.clients.size).toBe(0); + }); + it("should catch error from connection to clients", () => { + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + const ERROR_MESSAGE = "gdnet internal error"; + server = new EmberServer(LOCALHOST, PORT, root); + const client = new EmberClient(LOCALHOST, PORT); + client.remoteAddress = () => {return "address";} + let info; + server.on("clientError", data => {info = data;}); + server.server.emit("connection", client); + client.emit("error", new Error(ERROR_MESSAGE)); + expect(info).toBeDefined(); + expect(info.error.message).toBe(ERROR_MESSAGE); + }); + }); }); diff --git a/test/utils.js b/test/utils.js index 7fb6f65..f776d62 100755 --- a/test/utils.js +++ b/test/utils.js @@ -39,7 +39,7 @@ const init = function(_src,_tgt) { // path "0.0" identifier: "identity", children: [ - {identifier: "product", value: "S-CORE Master"}, + {identifier: "product", value: "S-CORE Master", type: "string"}, {identifier: "company", value: "EVS", access: "readWrite"}, {identifier: "version", value: "1.2.0", access: "readWrite", streamIdentifier: 1234567}, {identifier: "author", value: "g.dufour@evs.com"}, @@ -55,7 +55,7 @@ const init = function(_src,_tgt) { type: "oneToN", mode: "linear", targetCount: targets.length, - sourceCount: sources.length, + sourceCount: sources.length, connections: buildConnections(sources, targets), labels: [{basePath: "0.1.1000", description: "primary"}], }, From a06f5cdbbbec552d1a7681a1b10d48789c561cf0 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 20 Jan 2020 12:04:05 +0100 Subject: [PATCH 48/64] Add missing files. Reformat S101Socket and S101Client --- EmberClient/EmberClient.js | 28 ++--- EmberLib/ElementInterface.js | 61 +++++++++++ EmberSocket/S101Client.js | 123 ++++++++------------- EmberSocket/S101Server.js | 7 +- EmberSocket/S101Socket.js | 201 +++++++++++++++++++---------------- README.md | 25 +++-- test/EmberClient.test.js | 66 ++++++++++++ 7 files changed, 315 insertions(+), 196 deletions(-) create mode 100755 EmberLib/ElementInterface.js create mode 100755 test/EmberClient.test.js diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index e1426f5..245251b 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -1,5 +1,5 @@ const EventEmitter = require('events').EventEmitter; -const S101Socket = require('../EmberSocket').S101Socket; +const S101Client = require('../EmberSocket').S101Client; const ember = require('../EmberLib'); const BER = require('../ber.js'); const Errors = require('../Errors.js'); @@ -8,10 +8,14 @@ const winston = require("winston"); const DEFAULT_PORT = 9000; const DEFAULT_TIMEOUT = 3000; -/** @typedef {{ +/** + * @typedef {{ * node: TreeNode, * func: function - * }} REQUEST*/ + * }} REQUEST + * + */ + class EmberClient extends EventEmitter { /** * @@ -28,7 +32,7 @@ class EmberClient extends EventEmitter { this._timeout = null; this._callback = undefined; this._requestID = 0; - this._client = new S101Socket(host, port); + this._client = new S101Client(host, port); this.timeoutValue = DEFAULT_TIMEOUT; /** @type {Root} */ this.root = new ember.Root(); @@ -132,7 +136,7 @@ class EmberClient extends EventEmitter { * @param {TreeNode} node */ _handleNode(parent, node) { - var n = parent.getElementByNumber(node.getNumber()); + let n = parent.getElementByNumber(node.getNumber()); if (n === null) { parent.addChild(node); n = node; @@ -140,9 +144,9 @@ class EmberClient extends EventEmitter { n.update(node); } - var children = node.getChildren(); + const children = node.getChildren(); if (children !== null) { - for (var i = 0; i < children.length; i++) { + for (let i = 0; i < children.length; i++) { this._handleNode(n, children[i]); } } @@ -158,13 +162,13 @@ class EmberClient extends EventEmitter { * @param {TreeNode} node */ _handleQualifiedNode(parent, node) { - var element = parent.getElementByPath(node.path); + let element = parent.getElementByPath(node.path); if (element !== null) { this.emit("value-change", node); element.update(node); } else { - var path = node.path.split("."); + const path = node.path.split("."); if (path.length === 1) { this.root.addChild(node); } @@ -181,9 +185,9 @@ class EmberClient extends EventEmitter { element = node; } - var children = node.getChildren(); + const children = node.getChildren(); if (children !== null) { - for (var i = 0; i < children.length; i++) { + for (let i = 0; i < children.length; i++) { if (children[i].isQualified()) { this._handleQualifiedNode(element, children[i]); } @@ -205,7 +209,7 @@ class EmberClient extends EventEmitter { this.root.update(root); if (root.elements != null) { const elements = root.getChildren(); - for (var i = 0; i < elements.length; i++) { + for (let i = 0; i < elements.length; i++) { if (elements[i].isQualified()) { this._handleQualifiedNode(this.root, elements[i]); } diff --git a/EmberLib/ElementInterface.js b/EmberLib/ElementInterface.js new file mode 100755 index 0000000..0d93c6c --- /dev/null +++ b/EmberLib/ElementInterface.js @@ -0,0 +1,61 @@ +"use strict"; + +class ElementType { + /** + * @returns {boolean} + */ + isCommand() { + return false; + } + /** + * @returns {boolean} + */ + isNode() { + return false; + } + /** + * @returns {boolean} + */ + isMatrix() { + return false; + } + /** + * @returns {boolean} + */ + isParameter() { + return false; + } + /** + * @returns {boolean} + */ + isFunction() { + return false; + } + /** + * @returns {boolean} + */ + isRoot() { + return false; + } + /** + * @returns {boolean} + */ + isQualified() { + return false; + } + /** + * @returns {boolean} + */ + isStream() { + return false; + } + + /** + * @returns {boolean} + */ + isTemplate() { + return false; + } +} + +module.exports = ElementType; \ No newline at end of file diff --git a/EmberSocket/S101Client.js b/EmberSocket/S101Client.js index d87ca87..d34ce97 100755 --- a/EmberSocket/S101Client.js +++ b/EmberSocket/S101Client.js @@ -1,97 +1,58 @@ "use strict"; - +const net = require('net'); const S101Socket = require("./S101Socket"); -const S101Codec = require('../s101.js'); -const BER = require('../ber.js'); -const ember = require("../EmberLib"); +const DEFAULT_PORT = 9000; class S101Client extends S101Socket { /** * - * @param {Socket} socket - * @param {S101Server} server + * @param {string} address + * @param {number} port=9000 */ - constructor(socket, server) { - super() - this.request = null; - /** @type {S101Server} */ - this.server = server; - /** @type {Socket} */ - this.socket = socket; - - this.pendingRequests = []; - this.activeRequest = null; - - this.status = "connected"; - - this.codec = new S101Codec(); - this.codec.on('keepaliveReq', () => { - this.sendKeepaliveResponse(); - }); - - this.codec.on('emberPacket', (packet) => { - this.emit('emberPacket', packet); - const ber = new BER.Reader(packet); - try { - const root = ember.rootDecode(ber); - if (root != null) { - this.emit('emberTree', root); - } - } catch (e) { - this.emit("error", e); - } - }); - - if (socket != null) { - this.socket.on('data', (data) => { - this.codec.dataIn(data); - }); - - this.socket.on('close', () => { - this.emit('disconnected'); - this.status = "disconnected"; - this.socket = null; - }); - - this.socket.on('error', (e) => { - this.emit("error", e); - }); - } + constructor(address, port=DEFAULT_PORT) { + super(); + this.address = address; + this.port = port; } - /** + /** * - * @param {function} cb + * @param {number} timeout */ - addRequest(cb) { - this.pendingRequests.push(cb); - this._makeRequest(); - } - - _makeRequest() { - if (this.activeRequest === null && this.pendingRequests.length > 0) { - this.activeRequest = this.pendingRequests.shift(); - this.activeRequest(); - this.activeRequest = null; - } - } - - /** - * - * @param {TreeNode} node - */ - queueMessage(node) { - this.addRequest(() => this.sendBERNode(node)); - } - - /** - * @returns {string} - ie: "10.1.1.1:9000" - */ - remoteAddress() { - if (this.socket === undefined) { + connect(timeout = 2) { + if (this.status !== "disconnected") { return; } - return `${this.socket.remoteAddress}:${this.socket.remotePort}` + this.emit('connecting'); + const connectTimeoutListener = () => { + this.socket.destroy(); + this.emit("error", new Error(`Could not connect to ${this.address}:${this.port} after a timeout of ${timeout} seconds`)); + }; + + this.socket = net.createConnection({ + port: this.port, + host: this.address, + timeout: 1000 * timeout + }, + () => { + // Disable connect timeout to hand-over to keepalive mechanism + this.socket.removeListener("timeout", connectTimeoutListener); + this.socket.setTimeout(0); + this.startKeepAlive(); + this.emit('connected'); + }) + .on('error', (e) => { + this.emit("error", e); + }) + .once("timeout", connectTimeoutListener) + .on('data', (data) => { + if (this.isConnected()) { + this.codec.dataIn(data); + } + }) + .on('close', this.handleClose) + .on("end", this.handleClose); + this._initSocket(); } } diff --git a/EmberSocket/S101Server.js b/EmberSocket/S101Server.js index c712292..9660931 100755 --- a/EmberSocket/S101Server.js +++ b/EmberSocket/S101Server.js @@ -1,6 +1,6 @@ "use strict"; const EventEmitter = require('events').EventEmitter; -const S101Client = require("./S101Client"); +const S101Socket = require("./S101Socket"); const net = require('net'); class S101Server extends EventEmitter { @@ -18,10 +18,11 @@ class S101Server extends EventEmitter { } /** * - * @param {Socket} socket + * @param {Socket} socket - tcp socket */ addClient(socket) { - const client = new S101Client(socket, this); + // Wrap the tcp socket into an S101Socket. + const client = new S101Socket(socket); this.emit("connection", client); } /** diff --git a/EmberSocket/S101Socket.js b/EmberSocket/S101Socket.js index 22307f7..9035be7 100755 --- a/EmberSocket/S101Socket.js +++ b/EmberSocket/S101Socket.js @@ -1,90 +1,97 @@ "use strict"; const EventEmitter = require('events').EventEmitter; -const net = require('net'); const BER = require('../ber.js'); const ember = require('../EmberLib'); const S101Codec = require('../s101.js'); class S101Socket extends EventEmitter{ - constructor(address, port) { + /** + * + * @param {Socket} socket + */ + constructor(socket = null) { super(); - this.address = address; - this.port = port; - this.socket = null; + this.socket = socket; this.keepaliveInterval = 10; - this.codec = null; - this.status = "disconnected"; + this.keepaliveIntervalTimer = null; + this.pendingRequests = []; + this.activeRequest = null; + this.status = this.isConnected() ? "connected" : "disconnected"; + + this.codec = new S101Codec(); + this.codec.on('keepaliveReq', () => { + this.sendKeepaliveResponse(); + }); + + this.codec.on('emberPacket', (packet) => { + this.emit('emberPacket', packet); + const ber = new BER.Reader(packet); + try { + const root = ember.rootDecode(ber); + if (root != null) { + this.emit('emberTree', root); + } + } catch (e) { + this.emit("error", e); + } + }); + + this._initSocket(); + } + + _initSocket() { + if (this.socket != null) { + this.socket.on('data', (data) => { + this.codec.dataIn(data); + }); + + this.socket.on('close', () => { + this.emit('disconnected'); + this.status = "disconnected"; + this.socket = null; + }); + + this.socket.on('error', (e) => { + this.emit("error", e); + }); + } + } + + /** + * + * @param {function} cb + */ + addRequest(cb) { + this.pendingRequests.push(cb); + this._makeRequest(); + } + + _makeRequest() { + if (this.activeRequest === null && this.pendingRequests.length > 0) { + this.activeRequest = this.pendingRequests.shift(); + this.activeRequest(); + this.activeRequest = null; + } } /** * - * @param {number} timeout + * @param {TreeNode} node + */ + queueMessage(node) { + this.addRequest(() => this.sendBERNode(node)); + } + + /** + * @returns {string} - ie: "10.1.1.1:9000" */ - connect(timeout = 2) { - if (this.status !== "disconnected") { - return; + remoteAddress() { + if (this.socket == null) { + return "not connected"; } - - this.emit('connecting'); - - this.codec = new S101Codec(); - - const connectTimeoutListener = () => { - this.socket.destroy(); - this.emit("error", new Error(`Could not connect to ${this.address}:${this.port} after a timeout of ${timeout} seconds`)); - }; - - this.socket = net.createConnection({ - port: this.port, - host: this.address, - timeout: 1000 * timeout - }, - () => { - // Disable connect timeout to hand-over to keepalive mechanism - this.socket.removeListener("timeout", connectTimeoutListener); - this.socket.setTimeout(0); - - this.keepaliveIntervalTimer = setInterval(() => { - try { - this.sendKeepaliveRequest(); - } catch (e) { - this.emit("error", e); - } - }, 1000 * this.keepaliveInterval); - - this.codec.on('keepaliveReq', () => { - this.sendKeepaliveResponse(); - }); - - this.codec.on('emberPacket', (packet) => { - this.emit('emberPacket', packet); - - const ber = new BER.Reader(packet); - try { - const root = ember.rootDecode(ber); - if (root != null) { - this.emit('emberTree', root); - } - } catch (e) { - this.emit("error", e); - } - }); - - this.emit('connected'); - }) - .on('error', (e) => { - this.emit("error", e); - }) - .once("timeout", connectTimeoutListener) - .on('data', (data) => { - if (this.isConnected()) { - this.codec.dataIn(data); - } - }) - .on('close', this.handleClose) - .on("end", this.handleClose); + return `${this.socket.remoteAddress}:${this.socket.remotePort}` } /** @@ -95,30 +102,32 @@ class S101Socket extends EventEmitter{ return Promise.resolve(); } return new Promise((resolve, reject) => { + if (this.keepaliveIntervalTimer != null) { clearInterval(this.keepaliveIntervalTimer); - let done = false; - const cb = (data, error) => { - if (done) { return; } - done = true; - if (timer != null) { - clearTimeout(timer); - timer = null; - } - if (error == null) { - resolve(); - } - else { - reject(error); - } - }; - let timer; - if (timeout != null && (!isNaN(timeout)) && timeout > 0) { - timer = setTimeout(cb, 100 * timeout); + this.keepaliveIntervalTimer = null; + } + let done = false; + const cb = (data, error) => { + if (done) { return; } + done = true; + if (timer != null) { + clearTimeout(timer); + timer = null; + } + if (error == null) { + resolve(); } - this.socket.end(cb); - this.status = "disconnected"; + else { + reject(error); + } + }; + let timer; + if (timeout != null && (!isNaN(timeout)) && timeout > 0) { + timer = setTimeout(cb, 100 * timeout); } - ); + this.socket.end(cb); + this.status = "disconnected"; + }); } /** @@ -194,6 +203,16 @@ class S101Socket extends EventEmitter{ node.encode(writer); this.sendBER(writer.buffer); } + + startKeepAlive() { + this.keepaliveIntervalTimer = setInterval(() => { + try { + this.sendKeepaliveRequest(); + } catch (e) { + this.emit("error", e); + } + }, 1000 * this.keepaliveInterval); + } } module.exports = S101Socket; \ No newline at end of file diff --git a/README.md b/README.md index 39c937b..e8de727 100755 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ Server has been added in version 1.6.0. ## Example usage ### Client + Get Full tree: + ```javascript const EmberClient = require('node-emberplus').EmberClient; const client = new EmberClient("10.9.8.7", 9000); @@ -33,12 +35,12 @@ client.connect() // Get a Specific Node .then(() => client.getElementByPath("0.0.2")) .then(node => { - console.log(node); + console.log(node); }) // Get a node by its path identifiers .then(() => client.getElementByPath("path/to/node")) .then(node => { - console.log(node); + console.log(node); }) // Expand entire tree under node 0 .then(() => client.expand(client.root.getElementByNumber(0))) @@ -48,6 +50,7 @@ client.connect() ``` Subsribe to changes + ```javascript const {EmberClient, EmberLib} = require('node-emberplus'); @@ -59,14 +62,14 @@ client.connect()) .then(node => { // For streams, use subscribe return client.subscribe(node, update => { - console.log(udpate); - }); + console.log(udpate); + }); }) .then(() => client.getElementByPath("0.2")) .then(node => { // For non-streams a getDirectory will automatically subscribe for update return client.getDirectory(node, update => { - console.log(udpate); + console.log(udpate); }); }) // You can also provide a callback to the getElementByPath @@ -76,6 +79,7 @@ client.connect()) ``` ### Invoking Function + ```javascript const {EmberClient, EmberLib} = require('node-emberplus'); @@ -95,6 +99,7 @@ client.connect()) ``` ### Matrix Connection + ```javascript const {EmberClient, EmberLib} = require('node-emberplus'); @@ -104,7 +109,7 @@ client.connect() .then(() => client.getDirectory()) .then(() => client.getElementByPath("0.1.0")) .then(matrix => { - console.log("Connecting source 1 to target 0); + console.log("Connecting source 1 to target 0); return client.matrixConnect(matrix, 0, [1]); }) .then(() => client.matrixDisconnect(matrix, 0, [1])) @@ -115,6 +120,7 @@ client.connect() ``` ### Packet decoder + ```javascript // Simple packet decoder const Decoder = require('node-emberplus').Decoder; @@ -124,6 +130,7 @@ fs.readFile("tree.ember", (e,data) => { var root = Decoder(data); }); ``` + ### Server ```javascript @@ -152,6 +159,7 @@ server.listen().then(() => { console.log("listening"); }).catch((e) => { console ``` ### Construct Tree + ```javascript const EmberServer = require("node-emberplus").EmberServer; const {ParameterType, FunctionArgument} = require("node-emberplus").EmberLib; @@ -159,8 +167,8 @@ const {ParameterType, FunctionArgument} = require("node-emberplus").EmberLib; const targets = [ "tgt1", "tgt2", "tgt3" ]; const sources = [ "src1", "src2", "src3" ]; const defaultSources = [ - {identifier: "t-0", value: -1, access: "readWrite" }, - {identifier: "t-1", value: 0, access: "readWrite"}, + {identifier: "t-0", value: -1, access: "readWrite" }, + {identifier: "t-1", value: 0, access: "readWrite"}, {identifier: "t-2", value: 0, access: "readWrite"} ]; const labels = function(endpoints, type) { @@ -277,4 +285,3 @@ const jsonTree = [ ]; const root = EmberServer.JSONtoTree(jsonTree); ``` - diff --git a/test/EmberClient.test.js b/test/EmberClient.test.js new file mode 100755 index 0000000..c9f20a7 --- /dev/null +++ b/test/EmberClient.test.js @@ -0,0 +1,66 @@ +const fs = require("fs"); +const {EmberServer} = require("../EmberServer"); +const Decoder = require('../EmberLib').DecodeBuffer; +const EmberClient = require("../EmberClient"); + +const HOST = "127.0.0.1"; +const PORT = 9008; + +function getRoot() { + return new Promise((resolve, reject) => { + fs.readFile("test/embrionix.ember", (e, data) => { + if (e) { + reject(e); + } + try { + resolve(Decoder(data)); + } + catch(error) { + reject(error); + } + }); + }); +} + +let server; +describe("EmberClient", () => { + + beforeEach(() => { + return getRoot() + .then(root => { + server = new EmberServer(HOST, PORT, root); + //server._debug = true; + server.on("error", e => { + console.log("Server Error", e); + }); + server.on("clientError", info => { + console.log("clientError", info.error); + }); + server.on("event", event => { + console.log("Event: " + event); + }); + return server.listen() + }); + }); + + afterEach(() => { + server.close(); + }); + + it("should be able to fetch a specific node", () => { + const client = new EmberClient(HOST, PORT); + const PATH = "0.1"; + client.on("error", () => { + // ignore + }); + return Promise.resolve() + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => client.getElementByPath(PATH)) + .then(node => { + expect(node).toBeDefined(); + expect(node.getPath()).toBe(PATH); + return client.disconnect(); + }); + }); +}); From 662bf20b651687c6e1a1a520c34c82f988ceb481 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Mon, 20 Jan 2020 12:04:22 +0100 Subject: [PATCH 49/64] v2.4.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8439bf6..99f73e4 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.4.1", + "version": "2.4.2", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { From aac8f99a536bd39c95b93d86a77c394e457abbee Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Tue, 21 Jan 2020 08:40:35 +0100 Subject: [PATCH 50/64] use latest asn1 lib --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 99f73e4..1ad608a 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.4.2", + "version": "2.4.3", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { @@ -19,7 +19,7 @@ }, "license": "MIT", "dependencies": { - "asn1": "github:evs-broadcast/node-asn1#date_20190114", + "asn1": "github:evs-broadcast/node-asn1", "enum": "^2.4.0", "long": "^3.2.0", "smart-buffer": "^3.0.3", From 8145c3b3bf5bf903406432376bb642b83fb6b308 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Thu, 23 Jan 2020 09:43:16 +0100 Subject: [PATCH 51/64] Fixed some client missing promise return. Add server preMatrixConnect to provide above app a way to decide if connection allowed or not --- EmberClient/EmberClient.js | 20 ++-- EmberServer/EmberServer.js | 156 +++++++++++++++++++++++++++ EmberServer/MatrixHandlers.js | 178 ++++++++----------------------- EmberServer/QualifiedHandlers.js | 2 +- package.json | 2 +- test/EmberClient.test.js | 4 +- 6 files changed, 218 insertions(+), 144 deletions(-) diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index 245251b..b5a61f3 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -484,7 +484,8 @@ class EmberClient extends EventEmitter { * @param {Matrix} matrixNode * @param {number} targetID * @param {number[]} sources - * @param {MatrixOperation} operation + * @param {MatrixOperation} operation + * @returns {Promise} */ matrixOPeration(matrixNode, targetID, sources, operation = ember.MatrixOperation.connect) { return new Promise((resolve, reject) => { @@ -547,6 +548,7 @@ class EmberClient extends EventEmitter { * @param {Matrix} matrixNode * @param {number} targetID * @param {number[]} sources + * @returns {Promise} */ matrixConnect(matrixNode, targetID, sources) { return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.connect) @@ -556,7 +558,8 @@ class EmberClient extends EventEmitter { * * @param {Matrix} matrixNode * @param {number} targetID - * @param {number[]} sources + * @param {number[]} sources + * @returns {Promise} */ matrixDisconnect(matrixNode, targetID, sources) { return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.disconnect) @@ -567,6 +570,7 @@ class EmberClient extends EventEmitter { * @param {Matrix} matrixNode * @param {number} targetID * @param {number[]} sources + * @returns {Promise} */ matrixSetConnection(matrixNode, targetID, sources) { return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.absolute) @@ -622,7 +626,8 @@ class EmberClient extends EventEmitter { /** * * @param {TreeNode} qnode - * @param {function} callback + * @param {function} callback + * @returns {Promise} */ subscribe(qnode, callback) { if ((qnode.isParameter() || qnode.isMatrix()) && qnode.isStream()) { @@ -641,15 +646,15 @@ class EmberClient extends EventEmitter { resolve(); }}); }); - } else { - qnode.addCallback(callback); } + return Promise.resolve(); } /** * * @param {TreeNode} qnode - * @param {function} callback + * @param {function} callback + * @returns {Promise} */ unsubscribe(qnode, callback) { if (qnode.isParameter() && qnode.isStream()) { @@ -664,7 +669,8 @@ class EmberClient extends EventEmitter { resolve(); }}); }); - } + } + return Promise.resolve(); } } diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js index 8f1997f..1fc82c2 100755 --- a/EmberServer/EmberServer.js +++ b/EmberServer/EmberServer.js @@ -95,6 +95,16 @@ class TreeServer extends EventEmitter{ }); } + /** + * + * @param {Matrix} matrix + * @param {number} targetID + * @returns {number} + */ + getDisconnectSource(matrix, targetID) { + return this._handlers.getDisconnectSource(matrix, targetID); + } + /** * * @param {TreeNode} element @@ -219,6 +229,152 @@ class TreeServer extends EventEmitter{ doMatrixOperation(this, path, target, sources, EmberLib.MatrixOperation.absolute); } + /** + * + * @param {Matrix} matrix + * @param {number} target + * @param {number[]} sources + * @param {S101Socket} client + * @param {boolean} response + */ + disconnectMatrixTarget(matrix, target, sources, client, response) { + const disconnect = new EmberLib.MatrixConnection(target); + disconnect.setSources([]); + disconnect.disposition = EmberLib.MatrixDisposition.modified; + matrix.setSources(target, []); + if (response) { + this.emit("matrix-disconnect", { + target: target, + sources: sources, + client: client == null ? null : client.remoteAddress() + }); + } + return disconnect; + } + + /** + * + * @param {Matrix} matrix + * @param {number} target + * @param {number[]} sources + * @param {S101Socket} client + * @param {boolean} response + */ + disconnectSources(matrix, target, sources, client, response) { + const disconnect = new EmberLib.MatrixConnection(target); + disconnect.disposition = EmberLib.MatrixDisposition.modified; + matrix.disconnectSources(target, sources); + if (response) { + this.emit("matrix-disconnect", { + target: target, + sources: sources, + client: client == null ? null : client.remoteAddress() + }); + } + return disconnect; + } + + /** + * + * @param {Matrix} matrix + * @param {MatrixConnection} connection + * @param {Matrix} res - result + * @param {S101Socket} client + * @param {boolean} response + */ + preMatrixConnect(matrix, connection, res, client, response) { + const conResult = res[connection.target]; + + if (matrix.contents.type !== EmberLib.MatrixType.nToN && + connection.operation !== EmberLib.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length === 1) { + if (matrix.contents.type === EmberLib.MatrixType.oneToOne) { + // if the source is being used already, disconnect it from current target. + const currentTargets = matrix.getSourceConnections(connection.sources[0]); + if (currentTargets.length === 1 && currentTargets[0] !== connection.target) { + res.connections[currentTargets[0]] = + this.disconnectMatrixTarget(matrix, currentTargets[0], connection.sources, client, response); + } + } + // if the target is connected already, disconnect it + if (matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length === 1) { + if (matrix.contents.type === EmberLib.MatrixType.oneToN) { + const disconnectSource = this.getDisconnectSource(matrix, connection.target); + if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { + if (disconnectSource >= 0 && disconnectSource != connection.sources[0]) { + connection.sources = [disconnectSource]; + } + else { + // do nothing => set disposition to bypass further processing + conResult.disposition = EmberLib.MatrixDisposition.tally; + } + } + } + if (matrix.connections[connection.target].sources[0] !== connection.sources[0]) { + this.disconnectMatrixTarget(matrix, connection.target, matrix.connections[connection.target].sources, client, response) + } + else if (matrix.contents.type === EmberLib.MatrixType.oneToOne) { + // let's change the request into a disconnect + connection.operation = EmberLib.MatrixOperation.disconnect; + } + } + } + } + + applyMatrixConnect(matrix, connection, conResult, client, response) { + // Apply changes + let emitType; + if ((connection.operation == null) || + (connection.operation.value == EmberLib.MatrixOperation.absolute)) { + matrix.setSources(connection.target, connection.sources); + emitType = "matrix-change"; + } + else if (connection.operation == EmberLib.MatrixOperation.connect) { + matrix.connectSources(connection.target, connection.sources); + emitType = "matrix-connect"; + } + conResult.disposition = EmberLib.MatrixDisposition.modified; + if (response && emitType != null) { + // We got a request so emit something. + this.emit(emitType, { + target: connection.target, + sources: connection.sources, + client: client == null ? null : client.remoteAddress() + }); + } + } + + /** + * + * @param {Matrix} matrix + * @param {MatrixConnection} connection + * @param {Matrix} res - result + * @param {S101Socket} client + * @param {boolean} response + */ + applyMatrixOneToNDisconnect(matrix, connection, res, client, response) { + const disconnectSource = this.getDisconnectSource(matrix, connection.target); + if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { + const conResult = res[connection.target]; + if (disconnectSource >= 0 && disconnectSource != connection.sources[0]) { + if (response) { + this.server.emit("matrix-disconnect", { + target: connection.target, + sources: matrix.connections[connection.target].sources, + client: client == null ? null : client.remoteAddress() + }); + } + matrix.setSources(connection.target, [disconnectSource]); + conResult.disposition = EmberLib.MatrixDisposition.modified; + } + else { + // do nothing + conResult.disposition = EmberLib.MatrixDisposition.tally; + } + } + } + /** * * @param {TreeNode} element diff --git a/EmberServer/MatrixHandlers.js b/EmberServer/MatrixHandlers.js index 1e47d8e..72ff80a 100755 --- a/EmberServer/MatrixHandlers.js +++ b/EmberServer/MatrixHandlers.js @@ -1,5 +1,5 @@ "use strict"; -const ember = require('../EmberLib'); +const EmberLib = require('../EmberLib'); const ServerEvents = require("./ServerEvents"); const winston = require("winston"); @@ -15,7 +15,8 @@ class MatrixHandlers { /** * * @param {Matrix} matrix - * @param {number} targetID + * @param {number} targetID + * @return {number} */ getDisconnectSource(matrix, targetID) { if (matrix.defaultSources) { @@ -35,7 +36,7 @@ class MatrixHandlers { return matrix.defaultSources[targetID].contents.value; } } - return null; + return -1; } /** @@ -46,17 +47,18 @@ class MatrixHandlers { * @param {boolean} response=true */ handleMatrixConnections(client, matrix, connections, response = true) { - let res,conResult; + let res; // response + let conResult; let root; // ember message root winston.debug("Handling Matrix Connection"); if (client != null && client.request.isQualified()) { - root = new ember.Root(); - res = new ember.QualifiedMatrix(matrix.getPath()); + root = new EmberLib.Root(); + res = new EmberLib.QualifiedMatrix(matrix.getPath()); //root.elements = [res]; // do not use addchild or the element will get removed from the tree. root.addElement(res); } else { - res = new ember.MatrixNode(matrix.number); + res = new EmberLib.MatrixNode(matrix.number); root = matrix._parent.getTreeBranch(res); } res.connections = {}; @@ -64,155 +66,65 @@ class MatrixHandlers { if (!connections.hasOwnProperty(id)) { continue; } - let connection = connections[id]; + const connection = connections[id]; const src = client == null ? "local" : `${client.socket.remoteAddress}:${client.socket.remotePort}`; this.server.emit("event", ServerEvents.MATRIX_CONNECTION( matrix.contents.identifier,matrix.getPath(),src,id,connection.sources )); - conResult = new ember.MatrixConnection(connection.target); - let emitType; + conResult = new EmberLib.MatrixConnection(connection.target); res.connections[connection.target] = conResult; if (matrix.connections[connection.target].isLocked()) { - conResult.disposition = ember.MatrixDisposition.locked; + conResult.disposition = EmberLib.MatrixDisposition.locked; } - else if (matrix.contents.type !== ember.MatrixType.nToN && - connection.operation !== ember.MatrixOperation.disconnect && - connection.sources != null && connection.sources.length === 1) { - if (matrix.contents.type === ember.MatrixType.oneToOne) { - // if the source is being used already, disconnect it. - const targets = matrix.getSourceConnections(connection.sources[0]); - if (targets.length === 1 && targets[0] !== connection.target) { - const disconnect = new ember.MatrixConnection(targets[0]); - disconnect.setSources([]); - disconnect.disposition = ember.MatrixDisposition.modified; - res.connections[targets[0]] = disconnect; - matrix.setSources(targets[0], []); - if (response) { - this.server.emit("matrix-disconnect", { - target: targets[0], - sources: connection.sources, - client: client == null ? null : client.remoteAddress() - }); - } - } - } - // if the target is connected already, disconnect it - if (matrix.connections[connection.target].sources != null && - matrix.connections[connection.target].sources.length === 1) { - if (matrix.contents.type === ember.MatrixType.oneToN) { - const disconnectSource = this.getDisconnectSource(matrix, connection.target); - if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { - if (disconnectSource != null && disconnectSource != -1 && - disconnectSource != connection.sources[0]) { - connection.sources = [disconnectSource]; - } - else { - // do nothing - connection.operarion = ember.MatrixOperation.tally; - } - } - } - if (matrix.connections[connection.target].sources[0] !== connection.sources[0]) { - const source = matrix.connections[connection.target].sources[0]; - matrix.setSources(connection.target, []); - if (response) { - this.server.emit("matrix-disconnect", { - target: connection.target, - sources: [source], - client: client == null ? null : client.remoteAddress() - }); - } - } - else if (matrix.contents.type === ember.MatrixType.oneToOne) { - // let's change the request into a disconnect - connection.operation = ember.MatrixOperation.disconnect; - } - } + else { + // Call pre-processing function + this.server.preMatrixConnect(matrix, connection, res, client, response); } - if (connection.operation !== ember.MatrixOperation.disconnect && - connection.sources != null && connection.sources.length > 0 && - matrix.canConnect(connection.target,connection.sources,connection.operation)) { - // Apply changes - if ((connection.operation == null) || - (connection.operation.value == ember.MatrixOperation.absolute)) { - matrix.setSources(connection.target, connection.sources); - emitType = "matrix-change"; + if (conResult.disposition == null) { + // No decision made yet + if (connection.operation !== EmberLib.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length > 0 && + matrix.canConnect(connection.target,connection.sources,connection.operation)) { + this.server.applyMatrixConnect(matrix, connection, conResult, client, response); } - else if (connection.operation == ember.MatrixOperation.connect) { - matrix.connectSources(connection.target, connection.sources); - emitType = "matrix-connect"; + else if (connection.operation !== EmberLib.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length === 0 && + matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length > 0) { + // let's disconnect + conResult = this.server.disconnectMatrixTarget( + matrix, connection.target, + matrix.connections[connection.target].sources, + client, + response); } - conResult.disposition = ember.MatrixDisposition.modified; - } - else if (connection.operation !== ember.MatrixOperation.disconnect && - connection.sources != null && connection.sources.length === 0 && - matrix.connections[connection.target].sources != null && - matrix.connections[connection.target].sources.length > 0) { - // let's disconnect - if (response) { - this.server.emit("matrix-disconnect", { - target: connection.target, - sources: matrix.connections[connection.target].sources, - client: client == null ? null : client.remoteAddress() - }); - } - matrix.setSources(connection.target, []); - conResult.disposition = ember.MatrixDisposition.modified; - } - else if (connection.operation === ember.MatrixOperation.disconnect && - matrix.connections[connection.target].sources != null && - matrix.connections[connection.target].sources.length > 0) { - // Disconnect - if (matrix.contents.type === ember.MatrixType.oneToN) { - const disconnectSource = this.getDisconnectSource(matrix, connection.target); - if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { - if (disconnectSource != null && disconnectSource != -1 && - disconnectSource != connection.sources[0]) { - if (response) { - this.server.emit("matrix-disconnect", { - target: connection.target, - sources: matrix.connections[connection.target].sources, - client: client == null ? null : client.remoteAddress() - }); - } - matrix.setSources(connection.target, [disconnectSource]); - connection.operarion = ember.MatrixOperation.modified; - } - else { - // do nothing - connection.operarion = ember.MatrixOperation.tally; - } + else if (connection.operation === EmberLib.MatrixOperation.disconnect && + matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length > 0) { + // Disconnect + if (matrix.contents.type === EmberLib.MatrixType.oneToN) { + this.server.applyMatrixOneToNDisconnect() + } + else { + conResult = this.server.disconnectSources(matrix, connection.target, connection.sources, client, response); } - } - else { - matrix.disconnectSources(connection.target, connection.sources); - conResult.disposition = ember.MatrixDisposition.modified; - emitType = "matrix-disconnect"; } } - else if (conResult.disposition !== ember.MatrixDisposition.locked){ + if (conResult.disposition == null){ winston.debug(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); - conResult.disposition = ember.MatrixDisposition.tally; + conResult.disposition = EmberLib.MatrixDisposition.tally; } // Send response or update subscribers. - conResult.sources = matrix.connections[connection.target].sources; - if (response) { - // We got a request so emit something. - this.server.emit(emitType, { - target: connection.target, - sources: connection.sources, - client: client == null ? null : client.remoteAddress() - }); - } + conResult.sources = matrix.connections[connection.target].sources; } if (client != null) { client.sendBERNode(root); } - if (conResult != null && conResult.disposition !== ember.MatrixDisposition.tally) { + if (conResult != null && conResult.disposition !== EmberLib.MatrixDisposition.tally) { winston.debug("Updating subscribers for matrix change"); this.server.updateSubscribers(matrix.getPath(), root, client); } @@ -220,4 +132,4 @@ class MatrixHandlers { } -module.exports = MatrixHandlers; \ No newline at end of file +module.exports = MatrixHandlers; diff --git a/EmberServer/QualifiedHandlers.js b/EmberServer/QualifiedHandlers.js index 21ec5c2..db73742 100755 --- a/EmberServer/QualifiedHandlers.js +++ b/EmberServer/QualifiedHandlers.js @@ -40,7 +40,7 @@ class QualifiedHandlers extends MatrixHandlers { if (node.hasChildren()) { for(let child of node.children) { if (child.isCommand()) { - this.handleCommand(client, element, child); + this.server._handlers.handleCommand(client, element, child); } break; } diff --git a/package.json b/package.json index 1ad608a..c050650 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.4.3", + "version": "2.5.0", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/test/EmberClient.test.js b/test/EmberClient.test.js index c9f20a7..66f66cd 100755 --- a/test/EmberClient.test.js +++ b/test/EmberClient.test.js @@ -4,7 +4,7 @@ const Decoder = require('../EmberLib').DecodeBuffer; const EmberClient = require("../EmberClient"); const HOST = "127.0.0.1"; -const PORT = 9008; +const PORT = 9010; function getRoot() { return new Promise((resolve, reject) => { @@ -44,7 +44,7 @@ describe("EmberClient", () => { }); afterEach(() => { - server.close(); + return server.close(); }); it("should be able to fetch a specific node", () => { From e68ecaad15d2393ff22a1b929fa330e599633739 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Thu, 23 Jan 2020 11:21:22 +0100 Subject: [PATCH 52/64] Fix matrix handling on server side --- EmberServer/EmberServer.js | 4 ++-- EmberServer/MatrixHandlers.js | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js index 1fc82c2..b60b50c 100755 --- a/EmberServer/EmberServer.js +++ b/EmberServer/EmberServer.js @@ -283,7 +283,7 @@ class TreeServer extends EventEmitter{ * @param {boolean} response */ preMatrixConnect(matrix, connection, res, client, response) { - const conResult = res[connection.target]; + const conResult = res.connections[connection.target]; if (matrix.contents.type !== EmberLib.MatrixType.nToN && connection.operation !== EmberLib.MatrixOperation.disconnect && @@ -356,7 +356,7 @@ class TreeServer extends EventEmitter{ applyMatrixOneToNDisconnect(matrix, connection, res, client, response) { const disconnectSource = this.getDisconnectSource(matrix, connection.target); if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { - const conResult = res[connection.target]; + const conResult = res.connections[connection.target]; if (disconnectSource >= 0 && disconnectSource != connection.sources[0]) { if (response) { this.server.emit("matrix-disconnect", { diff --git a/EmberServer/MatrixHandlers.js b/EmberServer/MatrixHandlers.js index 72ff80a..0bb9f9c 100755 --- a/EmberServer/MatrixHandlers.js +++ b/EmberServer/MatrixHandlers.js @@ -105,7 +105,7 @@ class MatrixHandlers { matrix.connections[connection.target].sources.length > 0) { // Disconnect if (matrix.contents.type === EmberLib.MatrixType.oneToN) { - this.server.applyMatrixOneToNDisconnect() + this.server.applyMatrixOneToNDisconnect(matrix, connection, res, client, response); } else { conResult = this.server.disconnectSources(matrix, connection.target, connection.sources, client, response); @@ -113,7 +113,7 @@ class MatrixHandlers { } } if (conResult.disposition == null){ - winston.debug(`Invalid Matrix operation ${connection.operarion} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); + winston.debug(`Invalid Matrix operation ${connection.operation} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); conResult.disposition = EmberLib.MatrixDisposition.tally; } diff --git a/package.json b/package.json index c050650..61328cb 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.5.0", + "version": "2.5.1", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { From ec83eb6d98b2f087cc06db27fe283025b081ef96 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Thu, 23 Jan 2020 16:11:03 +0100 Subject: [PATCH 53/64] Fixed tests. Changed server listen function to return promise --- EmberServer/EmberServer.js | 19 +------------------ EmberSocket/S101Server.js | 39 +++++++++++++++++++------------------- Errors.js | 6 ++++++ package.json | 2 +- test/Server.test.js | 26 +++++++------------------ 5 files changed, 35 insertions(+), 57 deletions(-) diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js index b60b50c..23fb37d 100755 --- a/EmberServer/EmberServer.js +++ b/EmberServer/EmberServer.js @@ -17,7 +17,6 @@ class TreeServer extends EventEmitter{ constructor(host, port, tree) { super(); this._debug = false; - this.callback = undefined; this.timeoutValue = 2000; this.server = new S101Server(host, port); this.tree = tree; @@ -28,10 +27,6 @@ class TreeServer extends EventEmitter{ this.server.on('listening', () => { winston.debug("listening"); this.emit('listening'); - if (this.callback != null) { - this.callback(); - this.callback = undefined; - } }); this.server.on('connection', client => { @@ -68,9 +63,6 @@ class TreeServer extends EventEmitter{ this.server.on("error", (e) => { this.emit("error", e); - if (this.callback != null) { - this.callback(e); - } }); } @@ -187,16 +179,7 @@ class TreeServer extends EventEmitter{ * @returns {Promise} */ listen() { - return new Promise((resolve, reject) => { - this.callback = e => { - this.callback = null; - if (e == null) { - return resolve(); - } - return reject(e); - }; - this.server.listen(); - }); + return this.server.listen(); } /** diff --git a/EmberSocket/S101Server.js b/EmberSocket/S101Server.js index 9660931..d4e80ec 100755 --- a/EmberSocket/S101Server.js +++ b/EmberSocket/S101Server.js @@ -2,6 +2,7 @@ const EventEmitter = require('events').EventEmitter; const S101Socket = require("./S101Socket"); const net = require('net'); +const Errors = require("../Errors"); class S101Server extends EventEmitter { /** @@ -26,27 +27,27 @@ class S101Server extends EventEmitter { this.emit("connection", client); } /** - * + * @returns {Promise} */ - listen () { - if (this.status !== "disconnected") { - return; - } - - this.server = net.createServer((socket) => { - this.addClient(socket); - }); - - this.server.on("error", (e) => { - this.emit("error", e); + listen() { + return new Promise((resolve, reject) => { + if (this.status !== "disconnected") { + return reject(new Errors.S101SocketError("Already listening")); + } + this.server = net.createServer((socket) => { + this.addClient(socket); + }).on("error", (e) => { + this.emit("error", e); + if (this.status === "disconnected") { + return reject(e); + } + }).on("listening", () => { + this.emit("listening"); + this.status = "listening"; + resolve(); + }); + this.server.listen(this.port, this.address); }); - - this.server.on("listening", () => { - this.emit("listening"); - this.status = "listening"; - }); - - this.server.listen(this.port, this.address); } } diff --git a/Errors.js b/Errors.js index 01273d6..dcffe41 100755 --- a/Errors.js +++ b/Errors.js @@ -25,6 +25,12 @@ class UnimplementedEmberTypeError extends Error { module.exports.UnimplementedEmberTypeError = UnimplementedEmberTypeError; +class S101SocketError extends Error { + constructor(message) { + super(message); + } +} +module.exports.S101SocketError = S101SocketError; class ASN1Error extends Error { constructor(message) { diff --git a/package.json b/package.json index 61328cb..0252389 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.5.1", + "version": "2.5.2", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/test/Server.test.js b/test/Server.test.js index 5dac151..458b29b 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -927,12 +927,12 @@ describe("server", function() { const PARAMETER_PATH = "0.0.1"; const jsonTree = jsonRoot(); const root = EmberServer.JSONtoTree(jsonTree); - server = new EmberServer(LOCALHOST, PORT, root); + const server = new EmberServer(LOCALHOST, PORT, root); const js = server.toJSON(); expect(js[0].children[0].children[1].path).toBe(PARAMETER_PATH); }); it("should have a toJSON and return empty array if no tree", () => { - server = new EmberServer(LOCALHOST, PORT, null); + const server = new EmberServer(LOCALHOST, PORT, null); const js = server.toJSON(); expect(js).toBeDefined(); expect(js.length).toBe(0); @@ -944,7 +944,7 @@ describe("server", function() { const VALUE = "Gilles Dufour" const jsonTree = jsonRoot(); const root = EmberServer.JSONtoTree(jsonTree); - server = new EmberServer(LOCALHOST, PORT, root); + const server = new EmberServer(LOCALHOST, PORT, root); const newParam = new EmberLib.Parameter(1); newParam.contents = new EmberLib.ParameterContents(VALUE); newParam.path = PARAMETER_PATH; @@ -955,7 +955,7 @@ describe("server", function() { const VALUE = "Gilles Dufour" const jsonTree = jsonRoot(); const root = EmberServer.JSONtoTree(jsonTree); - server = new EmberServer(LOCALHOST, PORT, root); + const server = new EmberServer(LOCALHOST, PORT, root); const newParam = new EmberLib.Parameter(1); newParam.contents = new EmberLib.ParameterContents(VALUE); let error; @@ -973,7 +973,7 @@ describe("server", function() { const VALUE = "Gilles Dufour" const jsonTree = jsonRoot(); const root = EmberServer.JSONtoTree(jsonTree); - server = new EmberServer(LOCALHOST, PORT, root); + const server = new EmberServer(LOCALHOST, PORT, root); server.tree.getElementByPath(PARAMETER_PATH)._parent = null; const newParam = new EmberLib.Parameter(1); newParam.contents = new EmberLib.ParameterContents(VALUE); @@ -1001,22 +1001,10 @@ describe("server", function() { expect(error).toBeDefined(); expect(error.message).toBe(ERROR_MESSAGE); }); - it("should catch error and use callback if present", () => { - const jsonTree = jsonRoot(); - const root = EmberServer.JSONtoTree(jsonTree); - const ERROR_MESSAGE = "gdnet internal error"; - server = new EmberServer(LOCALHOST, PORT, root); - let error; - server.on("error", e => {}); - server.callback = e => {error = e;} - server.server.emit("error", new Error(ERROR_MESSAGE)); - expect(error).toBeDefined(); - expect(error.message).toBe(ERROR_MESSAGE); - }); it("should catch tcp server disconnected message, and clean up clients", () => { const jsonTree = jsonRoot(); const root = EmberServer.JSONtoTree(jsonTree); - server = new EmberServer(LOCALHOST, PORT, root); + const server = new EmberServer(LOCALHOST, PORT, root); server.clients.add(new EmberClient(LOCALHOST, PORT)); let count = 0; server.on("disconnected", () => {count++;}); @@ -1028,7 +1016,7 @@ describe("server", function() { const jsonTree = jsonRoot(); const root = EmberServer.JSONtoTree(jsonTree); const ERROR_MESSAGE = "gdnet internal error"; - server = new EmberServer(LOCALHOST, PORT, root); + const server = new EmberServer(LOCALHOST, PORT, root); const client = new EmberClient(LOCALHOST, PORT); client.remoteAddress = () => {return "address";} let info; From 5b1377d16c82d1c5d95844f836fa8f609d98546c Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Wed, 29 Jan 2020 09:16:59 +0100 Subject: [PATCH 54/64] Added example for setValue --- README.md | 18 +++++++++++++++++- package.json | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e8de727..eec1086 100755 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ communication protocols and user interfaces; this module allows those to be integrated with Ember+ somewhat more easily than the reference libember C++ implementation. -This version support following ember objects : Node, Parameter, Matrix, QualifiedNode, +This version support following ember objects : Node, Parameter, Matrix, Function, QualifiedNode, QualifiedParameter, QualifiedMatrix, QualifiedFunction. It has been tested with EVS XT4k and Embrionix IP solutions. @@ -17,6 +17,10 @@ the way the lib is used so that now it uses Promise Server has been added in version 1.6.0. +Support for StreamCollection and UDP is available in a private branch. +If you want access to full version and/or would like support for new features or integration +with your projects, please contact Gilles Dufour - dufour.gilles@gmail.com + ## Example usage ### Client @@ -78,6 +82,18 @@ client.connect()) ; ``` +### Setting new value + +```javascript +client = new EmberClient(LOCALHOST, PORT); +await client.connect() +await client.getDirectory(); +await client.getElementByPath("0.0.1"); +await client.setValue(client.root.getElementByPath("0.0.1"), "gdnet"); +console.log("result", server.tree.getElementByPath("0.0.1").contents.value) +return client.disconnect().then(() => { console.log("disconnected")}); +``` + ### Invoking Function ```javascript diff --git a/package.json b/package.json index 0252389..bc95542 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.5.2", + "version": "2.5.3", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { From 767779f23b0eeb04685e11e24b3cd41f6bc7fc55 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sat, 1 Feb 2020 15:17:23 +0100 Subject: [PATCH 55/64] Fix duplicate dataIn --- EmberSocket/S101Client.js | 12 +----------- package.json | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/EmberSocket/S101Client.js b/EmberSocket/S101Client.js index d34ce97..f4dee32 100755 --- a/EmberSocket/S101Client.js +++ b/EmberSocket/S101Client.js @@ -41,17 +41,7 @@ class S101Client extends S101Socket { this.startKeepAlive(); this.emit('connected'); }) - .on('error', (e) => { - this.emit("error", e); - }) - .once("timeout", connectTimeoutListener) - .on('data', (data) => { - if (this.isConnected()) { - this.codec.dataIn(data); - } - }) - .on('close', this.handleClose) - .on("end", this.handleClose); + once("timeout", connectTimeoutListener); this._initSocket(); } } diff --git a/package.json b/package.json index bc95542..fc1fe93 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.5.3", + "version": "2.5.4", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { From a339f14388d59ec53f2fefa02fb79ed77034389c Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sat, 1 Feb 2020 17:13:02 +0100 Subject: [PATCH 56/64] Fix FunctionContent result encoding --- EmberLib/FunctionContent.js | 4 +++- package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/EmberLib/FunctionContent.js b/EmberLib/FunctionContent.js index cd368b8..ec13aaf 100755 --- a/EmberLib/FunctionContent.js +++ b/EmberLib/FunctionContent.js @@ -44,12 +44,14 @@ class FunctionContent { if(this.result != null && this.result.length > 0) { ber.startSequence(BER.CONTEXT(3)); + ber.startSequence(BER.EMBER_SEQUENCE); for(let i = 0; i < this.result.length; i++) { ber.startSequence(BER.CONTEXT(0)); /** @type {FunctionArgument} */ this.result[i].encode(ber); ber.endSequence(); - } + } + ber.endSequence(); ber.endSequence(); // BER.CONTEXT(3) } diff --git a/package.json b/package.json index fc1fe93..9557785 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.5.4", + "version": "2.5.5", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { From ed89db5468e6dfe7227dbb863761959bd7de28f4 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Fri, 14 Feb 2020 17:16:31 +0100 Subject: [PATCH 57/64] fix expand and function content encoding/decoding --- EmberClient/EmberClient.js | 10 +++------- EmberLib/FunctionContent.js | 11 ++++++----- EmberSocket/S101Client.js | 2 +- README.md | 3 ++- package.json | 2 +- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index b5a61f3..4fa9502 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -137,7 +137,7 @@ class EmberClient extends EventEmitter { */ _handleNode(parent, node) { let n = parent.getElementByNumber(node.getNumber()); - if (n === null) { + if (n == null) { parent.addChild(node); n = node; } else { @@ -260,14 +260,11 @@ class EmberClient extends EventEmitter { * @returns {Promise} */ expand(node, callback = null) { - if (node == null) { - return Promise.reject(new Errors.InvalidEmberNode("Invalid null node")); - } - if (node.isParameter() || node.isMatrix() || node.isFunction()) { + if (node != null && (node.isParameter() || node.isMatrix() || node.isFunction())) { return this.getDirectory(node); } return this.getDirectory(node, callback).then(res => { - const children = node.getChildren(); + const children = node == null ? res == null ? null : res.getChildren() : node.getChildren(); if ((res == null) || (children == null)) { winston.debug("No more children for ", node); return; @@ -289,7 +286,6 @@ class EmberClient extends EventEmitter { return directChildren }); } - /** * * @param {TreeNode} qnode diff --git a/EmberLib/FunctionContent.js b/EmberLib/FunctionContent.js index ec13aaf..737beae 100755 --- a/EmberLib/FunctionContent.js +++ b/EmberLib/FunctionContent.js @@ -81,18 +81,19 @@ class FunctionContent { fc.description = seq.readString(BER.EMBER_STRING); } else if(tag == BER.CONTEXT(2)) { fc.arguments = []; - let dataSeq = seq.getSequence(BER.EMBER_SEQUENCE); + let dataSeq = seq.getSequence(BER.EMBER_SEQUENCE); while(dataSeq.remain > 0) { seq = dataSeq.getSequence(BER.CONTEXT(0)); fc.arguments.push(FunctionArgument.decode(seq)); } } else if(tag == BER.CONTEXT(3)) { fc.result = []; - while(seq.remain > 0) { - tag = seq.peek(); - const dataSeq = seq.getSequence(tag); + let dataSeq = seq.getSequence(BER.EMBER_SEQUENCE); + while(dataSeq.remain > 0) { + tag = dataSeq.peek(); if (tag === BER.CONTEXT(0)) { - fc.result.push(FunctionArgument.decode(dataSeq)); + const fcSeq = dataSeq.getSequence(tag); + fc.result.push(FunctionArgument.decode(fcSeq)); } else { throw new errors.UnimplementedEmberTypeError(tag); diff --git a/EmberSocket/S101Client.js b/EmberSocket/S101Client.js index f4dee32..d1d2527 100755 --- a/EmberSocket/S101Client.js +++ b/EmberSocket/S101Client.js @@ -41,7 +41,7 @@ class S101Client extends S101Socket { this.startKeepAlive(); this.emit('connected'); }) - once("timeout", connectTimeoutListener); + .once("timeout", connectTimeoutListener); this._initSocket(); } } diff --git a/README.md b/README.md index eec1086..e2c77ae 100755 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ the way the lib is used so that now it uses Promise Server has been added in version 1.6.0. -Support for StreamCollection and UDP is available in a private branch. +Support for StreamCollection, UDP, packet stats/rate, support for tree with size higher than 8M +is available in a private branch. If you want access to full version and/or would like support for new features or integration with your projects, please contact Gilles Dufour - dufour.gilles@gmail.com diff --git a/package.json b/package.json index 9557785..9e9bc19 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.5.5", + "version": "2.5.6", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { From 3b382b7352dfde79e295b94956f1f76a765dd5e8 Mon Sep 17 00:00:00 2001 From: olzzon Date: Wed, 26 Feb 2020 08:56:13 +0100 Subject: [PATCH 58/64] feat: hack setValue to immediately resolve - added setValueWithHacksaw() function from NRKNO fork --- EmberClient/EmberClient.js | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index 4fa9502..ed8ce53 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -619,6 +619,51 @@ class EmberClient extends EventEmitter { }); } + /** + * + * @param {TreeNode} node + * @param {string|number} value + * @returns {Promise} + */ + setValueWithHacksaw(node, value) { + return new Promise((resolve, reject) => { + if (!node.isParameter()) { + reject(new Errors.EmberAccessError('not a Parameter')); + } + else { + this.addRequest({node: node, func: error => { + if (error) { + this._finishRequest(); + reject(error); + return; + } + + this._callback = (error, node) => { + // this._finishRequest(); + // this._callback = null; + if (error) { + reject(error); + } + else { + + // resolve(node); + } + }; + winston.debug('setValue sending ...', node.getPath(), value); + this._client.sendBERNode(node.setValue(value)); + + // We now immediately finish & resolve so we can't get any timeouts ever + // This is a pretty ugly hack, but as far as I can tell it doesn't bring + // any negative consequences regarding the execution and resolving of other + // functions. We need this because if the node already has the value we are + // setting it too, it will cause a timeout. + this._finishRequest(); + this._callback = null; + }}); + } + }); + } + /** * * @param {TreeNode} qnode From 82618c3ccc13b6355c893a47bafd1d226c7e86ae Mon Sep 17 00:00:00 2001 From: olzzon Date: Wed, 26 Feb 2020 13:03:57 +0100 Subject: [PATCH 59/64] feat: setValueNoAck - rename of function and cleanup --- EmberClient/EmberClient.js | 46 ++++++++++++-------------------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index ed8ce53..8a93d1f 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -625,43 +625,25 @@ class EmberClient extends EventEmitter { * @param {string|number} value * @returns {Promise} */ - setValueWithHacksaw(node, value) { + setValueNoAck(node, value) { return new Promise((resolve, reject) => { if (!node.isParameter()) { reject(new Errors.EmberAccessError('not a Parameter')); + return; } - else { - this.addRequest({node: node, func: error => { - if (error) { - this._finishRequest(); - reject(error); - return; - } - - this._callback = (error, node) => { - // this._finishRequest(); - // this._callback = null; - if (error) { - reject(error); - } - else { - - // resolve(node); - } - }; - winston.debug('setValue sending ...', node.getPath(), value); - this._client.sendBERNode(node.setValue(value)); - - // We now immediately finish & resolve so we can't get any timeouts ever - // This is a pretty ugly hack, but as far as I can tell it doesn't bring - // any negative consequences regarding the execution and resolving of other - // functions. We need this because if the node already has the value we are - // setting it too, it will cause a timeout. + this.addRequest({node: node, func: error => { + if (error) { this._finishRequest(); - this._callback = null; - }}); - } - }); + reject(error); + return; + } + winston.debug('setValue sending ...', node.getPath(), value); + this._client.sendBERNode(node.setValue(value)); + + this._finishRequest(); + this._callback = null; + }}); + }) } /** From 91100007882c08b814b84424726ea21543e4345d Mon Sep 17 00:00:00 2001 From: olzzon Date: Wed, 26 Feb 2020 13:21:36 +0100 Subject: [PATCH 60/64] =?UTF-8?q?fix:=20promise=20didn=C2=B4t=20resolve=20?= =?UTF-8?q?doc:=20Added=20comments=20to=20explain=20reason=20for=20hack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EmberClient/EmberClient.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index 8a93d1f..541e91b 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -626,6 +626,11 @@ class EmberClient extends EventEmitter { * @returns {Promise} */ setValueNoAck(node, value) { + // This function immediately finish & resolve so we can't get any timeouts ever + // This is a pretty ugly hack, but it doesn't look to bring + // any negative consequences regarding the execution and resolving of other + // functions. It´s needed this because if the node already has the value we are + // setting it too, it will cause a timeout. return new Promise((resolve, reject) => { if (!node.isParameter()) { reject(new Errors.EmberAccessError('not a Parameter')); @@ -642,6 +647,7 @@ class EmberClient extends EventEmitter { this._finishRequest(); this._callback = null; + return resolve(node) }}); }) } From fe9153e750bb08e308f56bbde8e0ddbbc4717777 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Sun, 1 Mar 2020 08:42:32 +0100 Subject: [PATCH 61/64] Fix client parameter subscribe --- EmberClient/EmberClient.js | 24 +++++--- EmberLib/QualifiedParameter.js | 22 ------- EmberLib/StreamCollection.js | 102 +++++++++++++++++++++++++++++++ EmberLib/StreamEntry.js | 70 +++++++++++++++++++++ EmberLib/TreeNode.js | 7 +-- EmberServer/EmberServer.js | 9 ++- EmberServer/QualifiedHandlers.js | 1 - package.json | 5 +- 8 files changed, 199 insertions(+), 41 deletions(-) create mode 100755 EmberLib/StreamCollection.js create mode 100755 EmberLib/StreamEntry.js diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js index 4fa9502..c1b33e9 100755 --- a/EmberClient/EmberClient.js +++ b/EmberClient/EmberClient.js @@ -73,7 +73,7 @@ class EmberClient extends EventEmitter { } } catch(e) { - winston.debug(e, root); + winston.error(e, root); if (this._callback) { this._callback(e); } @@ -88,7 +88,7 @@ class EmberClient extends EventEmitter { try { this._makeRequest(); } catch(e) { - winston.debug(e); + winston.error(e); if (this._callback != null) { this._callback(e); } @@ -140,8 +140,8 @@ class EmberClient extends EventEmitter { if (n == null) { parent.addChild(node); n = node; - } else { - n.update(node); + } else if (n.update(node)) { + n.updateSubscribers(); } const children = node.getChildren(); @@ -165,7 +165,9 @@ class EmberClient extends EventEmitter { let element = parent.getElementByPath(node.path); if (element !== null) { this.emit("value-change", node); - element.update(node); + if (element.update(node)) { + element.updateSubscribers(); + } } else { const path = node.path.split("."); @@ -344,17 +346,16 @@ class EmberClient extends EventEmitter { if (nodeElements != null && ((qnode.isMatrix() && nodeElements.length === 1 && nodeElements[0].getPath() === requestedPath) || (!qnode.isMatrix() && nodeElements.every(el => isDirectSubPathOf(el.getPath(), requestedPath))))) { - winston.debug("Received getDirectory response", node); + winston.debug("Received getDirectory response", node); this._finishRequest(); return resolve(node); // make sure the info is treated before going to next request. } else { winston.debug(node); - winston.debug(new Error(requestedPath)); + winston.error(new Error(requestedPath)); } } }; - winston.debug("Sending getDirectory", qnode); try { this._client.sendBERNode(qnode.getDirectory(callback)); } @@ -420,6 +421,9 @@ class EmberClient extends EventEmitter { // We have this part already. pos++; if (pos >= pathArray.length) { + if (callback) { + node.getDirectory(callback); + } return node; } currentNode = node; @@ -514,7 +518,7 @@ class EmberClient extends EventEmitter { return; } if (error) { - winston.debug("Received getDirectory error", error); + winston.error("Received getDirectory error", error); this._clearTimeout(); // clear the timeout now. The resolve below may take a while. this._finishRequest(); reject(error); @@ -612,7 +616,7 @@ class EmberClient extends EventEmitter { resolve(node); } }; - winston.debug('setValue sending ...', node.getPath(), value); + winston.debug('setValue sending ...', node.getPath(), value); this._client.sendBERNode(node.setValue(value)); }}); } diff --git a/EmberLib/QualifiedParameter.js b/EmberLib/QualifiedParameter.js index 3871823..2ff6d8c 100755 --- a/EmberLib/QualifiedParameter.js +++ b/EmberLib/QualifiedParameter.js @@ -36,28 +36,6 @@ class QualifiedParameter extends QualifiedElement { return r; } - /** - * - * @param {QualifiedParameter} other - */ - update(other) { - if ((other != null) && (other.contents != null)) { - if (this.contents == null) { - this.contents = other.contents; - } - else { - for (var key in other.contents) { - if (key[0] === "_") { - continue; - } - if (other.contents.hasOwnProperty(key)) { - this.contents[key] = other.contents[key]; - } - } - } - } - return; - } /** * diff --git a/EmberLib/StreamCollection.js b/EmberLib/StreamCollection.js new file mode 100755 index 0000000..4576809 --- /dev/null +++ b/EmberLib/StreamCollection.js @@ -0,0 +1,102 @@ +const BER = require("../ber"); +const StreamEntry = require("./StreamEntry"); + +class StreamCollection { + /** + * + */ + constructor() { + /** @type {Map} */ + this.elements = new Map(); + } + /** + * + * @param {StreamEntry} entry + */ + addEntry(entry) { + this.elements.set(entry.identifier, entry); + } + /** + * + * @param {StreamEntry} entry + */ + removeEntry(entry) { + this.elements.delete(entry.identifier); + } + /** + * + * @param {number} identifier + * @returns {StreamEntry} + */ + getEntry(identifier) { + return this.elements.get(identifier); + } + + /** + * @returns {StreamEntry} + */ + [Symbol.iterator]() { + return this.elements.values(); + } + + /** + * @retuns {number} + */ + size() { + return this.elements.size; + } + + /** + * + * @param {BER.Writer} ber + */ + encode(ber) { + ber.startSequence(StreamCollection.BERID); + for(let [, entry] of this.elements) { + ber.startSequence(BER.CONTEXT(0)); + entry.encode(ber); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * @returns { + * {identifier: number, value: string|number|boolean|Buffer}[] + * } + */ + toJSON() { + const js = []; + for(let [, entry] of this.elements) { + js.push(entry.toJSON()); + } + return js; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(5); + } + + /** + * + * @param {BER.ExtendedReader} ber + * @returns {StreamCollection} + */ + static decode(ber) { + const streamCollection = new StreamCollection(); + const seq = ber.getSequence(this.BERID); + while (seq.remain > 0) { + const rootReader = seq.getSequence(BER.CONTEXT(0)); + while (rootReader.remain > 0) { + const entry = StreamEntry.decode(rootReader); + streamCollection.addEntry(entry); + } + } + return streamCollection; + } +} + +module.exports = StreamCollection; \ No newline at end of file diff --git a/EmberLib/StreamEntry.js b/EmberLib/StreamEntry.js new file mode 100755 index 0000000..2e6b10e --- /dev/null +++ b/EmberLib/StreamEntry.js @@ -0,0 +1,70 @@ +const BER = require("../ber"); +const Errors = require("../Errors"); + +class StreamEntry { + /** + * + * @param {number} identifier + * @param {string|number|boolean|Buffer} value + */ + constructor(identifier, value ) { + this.identifier = identifier; + this.value = value; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(StreamEntry.BERID); + if (this.identifier != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.identifier); + ber.endSequence(); + } + if (this.value != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeValue(this.value); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * @returns {{ + * identifier: number, + * value: string|number + * }} + */ + toJSON() { + return { + identifier: this.identifier, + value: this.value + } + } + + static get BERID() { + return BER.APPLICATION(5); + } + + static decode(ber) { + const entry = new StreamEntry(); + const seq = ber.getSequence(this.BERID); + while(seq.remain > 0) { + const tag = seq.peek(); + const data = seq.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + entry.identifier = data.readInt(); + } else if(tag == BER.CONTEXT(1)) { + entry.value = data.readValue(); + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return entry; + } +} + +module.exports = StreamEntry; \ No newline at end of file diff --git a/EmberLib/TreeNode.js b/EmberLib/TreeNode.js index 2cb7919..69b6ebe 100755 --- a/EmberLib/TreeNode.js +++ b/EmberLib/TreeNode.js @@ -16,8 +16,7 @@ class TreeNode extends ElementInterface { _isSubscribable(callback) { return (callback != null && - ((this.isParameter() && this.isStream()) || - this.isMatrix())); + (this.isParameter() || this.isMatrix())); } _subscribe(callback) { @@ -236,7 +235,7 @@ class TreeNode extends ElementInterface { * @param {function} callback */ getDirectory(callback) { - if (this._isSubscribable(callback)) { + if (this._isSubscribable(callback) && !this.isStream()) { this._subscribe(callback); } return this.getCommand(new Command(COMMAND_GETDIRECTORY)); @@ -471,7 +470,7 @@ class TreeNode extends ElementInterface { * @param {function} callback */ subscribe(callback) { - if (this._isSubscribable(callback)) { + if (this._isSubscribable(callback) && this.isStream()) { this._subscribe(callback); } return this.getCommand(new Command(COMMAND_SUBSCRIBE)); diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js index 23fb37d..741f045 100755 --- a/EmberServer/EmberServer.js +++ b/EmberServer/EmberServer.js @@ -4,8 +4,8 @@ const EmberLib = require('../EmberLib'); const JSONParser = require("./JSONParser"); const ElementHandlers = require("./ElementHandlers"); const ServerEvents = require("./ServerEvents"); -const winston = require("winston"); const Errors = require("../Errors"); +const winston = require("winston"); class TreeServer extends EventEmitter{ /** @@ -16,7 +16,7 @@ class TreeServer extends EventEmitter{ */ constructor(host, port, tree) { super(); - this._debug = false; + this._debug = true; this.timeoutValue = 2000; this.server = new S101Server(host, port); this.tree = tree; @@ -390,6 +390,7 @@ class TreeServer extends EventEmitter{ setValue(element, value, origin, key) { return new Promise(resolve => { // Change the element value if write access permitted. + winston.debug("New Setvalue request"); if (element.contents == null) { return resolve(); } @@ -398,6 +399,7 @@ class TreeServer extends EventEmitter{ (element.contents.access != null) && (element.contents.access.value > 1)) { element.contents.value = value; + winston.debug("New value ", value, "path", element.getPath()); const res = this.getResponse(element); this.updateSubscribers(element.getPath(),res, origin); } @@ -460,6 +462,7 @@ class TreeServer extends EventEmitter{ */ updateSubscribers(path, response, origin) { if (this.subscribers[path] == null) { + winston.debug("No subscribers for", path); return; } @@ -468,10 +471,12 @@ class TreeServer extends EventEmitter{ continue; // already sent the response to origin } if (this.clients.has(client)) { + winston.debug("Sending new value to", client.remoteAddress()); client.queueMessage(response); } else { // clean up subscribers - client is gone + winston.debug("deleting client"); this.subscribers[path].delete(client); } } diff --git a/EmberServer/QualifiedHandlers.js b/EmberServer/QualifiedHandlers.js index db73742..43755ff 100755 --- a/EmberServer/QualifiedHandlers.js +++ b/EmberServer/QualifiedHandlers.js @@ -68,7 +68,6 @@ class QualifiedHandlers extends MatrixHandlers { this.server.setValue(element, parameter.contents.value, client); let res = this.server.getQualifiedResponse(element); client.sendBERNode(res) - this.server.updateSubscribers(element.getPath(), res, client); } } } diff --git a/package.json b/package.json index 9e9bc19..3c39074 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.5.6", + "version": "2.5.7", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { @@ -24,7 +24,8 @@ "long": "^3.2.0", "smart-buffer": "^3.0.3", "winston": "^2.1.1", - "winston-color": "^1.0.0" + "winston-color": "^1.0.0", + "yargs": "^15.1.0" }, "devDependencies": { "eslint": "^5.5.0", From 48037abe55b34072c8c7a1cb35d288af4c2b24e3 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Thu, 5 Mar 2020 15:43:59 +0100 Subject: [PATCH 62/64] Add support for streamDescriptor parsing from json --- .gitignore | 3 +- EmberLib/StreamDescription.js | 10 ++++--- EmberLib/index.js | 2 ++ EmberServer/JSONParser.js | 9 ++++++ package.json | 2 +- serve.js | 55 +++++++++++++++++++++++++++++++++++ test/Server.test.js | 4 ++- test/utils.js | 13 +++++++++ 8 files changed, 91 insertions(+), 7 deletions(-) create mode 100755 serve.js diff --git a/.gitignore b/.gitignore index c882358..e77824c 100755 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # intellij, pycharm, webstorm... /.idea/* - +.vscode node_modules .*.swp +coverage diff --git a/EmberLib/StreamDescription.js b/EmberLib/StreamDescription.js index 53edb69..4915551 100755 --- a/EmberLib/StreamDescription.js +++ b/EmberLib/StreamDescription.js @@ -1,15 +1,17 @@ "use strict"; -const Element = require("./Element"); const BER = require('../ber.js'); const StreamFormat = require("./StreamFormat"); const Errors = require("../Errors"); -class StreamDescription extends Element{ +class StreamDescription { /** * + * @param {number} offset + * @param {StreamFormat} format */ - constructor() { - super(); + constructor(offset, format) { + this.offset = offset; + this.format = format; } /** diff --git a/EmberLib/index.js b/EmberLib/index.js index d3f10f8..9dc3813 100755 --- a/EmberLib/index.js +++ b/EmberLib/index.js @@ -32,6 +32,7 @@ const StringIntegerPair = require("./StringIntegerPair"); const StringIntegerCollection = require("./StringIntegerCollection"); const StreamFormat = require("./StreamFormat"); const StreamDescription = require("./StreamDescription"); +const StreamCollection = require("./StreamCollection"); const Template = require("./Template"); const TemplateElement = require("./TemplateElement"); const QualifiedTemplate = require("./QualifiedTemplate"); @@ -151,6 +152,7 @@ module.exports = { QualifiedTemplate, StreamFormat, StreamDescription, + StreamCollection, StringIntegerPair, StringIntegerCollection, Template, diff --git a/EmberServer/JSONParser.js b/EmberServer/JSONParser.js index 126164a..dfa5015 100755 --- a/EmberServer/JSONParser.js +++ b/EmberServer/JSONParser.js @@ -90,6 +90,15 @@ class JSONParser { else { emberElement.contents.access = ember.ParameterAccess.read; } + if (content.streamDescriptor != null) { + if (content.streamDescriptor.offset == null || content.streamDescriptor.format == null) { + throw new Error("Missing offset or format for streamDescriptor"); + } + emberElement.contents.streamDescriptor = new ember.StreamDescription(); + emberElement.contents.streamDescriptor.offset = content.streamDescriptor.offset; + emberElement.contents.streamDescriptor.format = ember.StreamFormat.get(content.streamDescriptor.format); + delete content.streamDescriptor; + } } else if (content.func != null) { emberElement = new ember.Function(number, content.func); diff --git a/package.json b/package.json index 3c39074..b46f60d 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.5.7", + "version": "2.5.8", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/serve.js b/serve.js new file mode 100755 index 0000000..9447250 --- /dev/null +++ b/serve.js @@ -0,0 +1,55 @@ +const yargs = require('yargs'); +const { EmberServer, Decoder } = require('./index'); +const { readFileSync } = require('fs'); + +const argv = yargs.options({ + host: { + alias: 'h', + description: 'host name|ip', + default: '0.0.0.0' + }, + + port: { + alias: 'p', + default: 9000, + type: 'number', + description: 'port', + demandOption: true + }, + + file: { + alias: 'f', + description: 'file containing the ber (default) or json tree', + demandOption: true + }, + + json: { + alias: 'j', + type: 'boolean', + description: 'file format is json' + }, + debug: { + alias: 'd', + type: 'boolean', + description: 'debug' + } + +}).help().argv; + +const main = async () => { + const data = readFileSync(argv.file); + const tree = argv.json ? EmberServer.JSONtoTree(JSON.parse(data.toString())) : Decoder(data); + const server = new EmberServer(argv.host, argv.port, tree); + server._debug = true; + console.log(Date.now(), 'starting server'); + if (argv.debug) { + server._debug = true; + } + try { + server.listen(); + } catch (e) { + console.log(e); + } +}; + +main(); diff --git a/test/Server.test.js b/test/Server.test.js index 458b29b..46b0107 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -19,9 +19,11 @@ describe("server", function() { const root = EmberServer.JSONtoTree(jsonTree); expect(root).toBeDefined(); expect(root.elements).toBeDefined(); - expect(root.elements.size).toBe(1); + expect(root.elements.size).toBe(jsonTree.length); expect(root.getElementByNumber(0).contents.identifier).toBe("scoreMaster"); expect(root.getElementByNumber(0).elements.size).toBe(jsonTree[0].children.length); + expect(root.getElementByNumber(1).contents.streamDescriptor instanceof EmberLib.StreamDescription).toBeTruthy(); + expect(root.getElementByNumber(1).contents.streamDescriptor.offset).toBe(jsonTree[1].streamDescriptor.offset); }); it("should throw an error if invalid matrix mode", function() { jsonTree[0].children[1].children[0].mode = "invalid"; diff --git a/test/utils.js b/test/utils.js index f776d62..9cca4a4 100755 --- a/test/utils.js +++ b/test/utils.js @@ -119,6 +119,19 @@ const init = function(_src,_tgt) { ] } ] + }, + { + identifier: "PeakValue_2", + type: 2, + streamIdentifier: 4, + streamDescriptor: { + format: "ieeeFloat32LittleEndian", + offset: 4 + }, + access: 1, + maximum: 20, + minimum: -200, + value: -200 } ]; } From f4624aee4b99af83fac00b007036ba96804c5797 Mon Sep 17 00:00:00 2001 From: Gilles Dufour Date: Wed, 11 Mar 2020 11:35:01 +0100 Subject: [PATCH 63/64] Fixed tests --- package.json | 2 +- test/Server.test.js | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index b46f60d..2d5206e 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-emberplus", - "version": "2.5.8", + "version": "2.5.9", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { diff --git a/test/Server.test.js b/test/Server.test.js index 46b0107..0ec6105 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -17,6 +17,8 @@ describe("server", function() { }); it("should generate an ember tree from json", function() { const root = EmberServer.JSONtoTree(jsonTree); + // JSONtoTree will modify the json object. + jsonTree = jsonRoot(); expect(root).toBeDefined(); expect(root.elements).toBeDefined(); expect(root.elements.size).toBe(jsonTree.length); @@ -47,6 +49,10 @@ describe("server", function() { expect(matrix).toBeDefined(); expect(matrix.contents.maximumConnectsPerTarget).toBe(jsonTree[0].children[1].children[0].maximumConnectsPerTarget); expect(matrix.contents.maximumTotalConnects).toBe(jsonTree[0].children[1].children[0].maximumTotalConnects); + expect(matrix.contents.type).toBe(EmberLib.MatrixType.nToN); + const jMatrix = matrix.toJSON(); + expect(jMatrix.type).toBeDefined(); + expect(jMatrix.mode).toBeDefined(); }); it("should support matrix type oneToOne", function() { jsonTree[0].children[1].children[0].type = "oneToOne"; @@ -67,6 +73,19 @@ describe("server", function() { expect(error).toBeDefined(); expect(error instanceof Errors.InvalidEmberNode).toBeDefined(); }); + it("should generate a matrix with a valid toJSON", function() { + const root = EmberServer.JSONtoTree(jsonTree); + const matrix = root.getElementByPath("0.1.0"); + matrix.connectSources(0, [0]); + matrix.connectSources(1, [1]); + const jMatrix = matrix.toJSON(); + expect(jMatrix.type).toBeDefined(); + expect(jMatrix.type).toBe(matrix.contents.type.value); + expect(jMatrix.mode).toBeDefined(); + expect(jMatrix.mode).toBe(matrix.contents.mode.value); + expect(jMatrix.connections[0].sources.length).toBe(1); + expect(jMatrix.connections[0].sources[0]).toBe(0); + }); }); describe("Server - Client communication", function() { @@ -96,7 +115,7 @@ describe("server", function() { .then(() => { expect(client.root).toBeDefined(); expect(client.root.elements).toBeDefined(); - expect(client.root.elements.size).toBe(1); + expect(client.root.elements.size).toBe(jsonTree.length); expect(client.root.getElementByNumber(0).contents.identifier).toBe("scoreMaster"); return client.getDirectory(client.root.getElementByNumber(0)); }) @@ -958,7 +977,7 @@ describe("server", function() { const jsonTree = jsonRoot(); const root = EmberServer.JSONtoTree(jsonTree); const server = new EmberServer(LOCALHOST, PORT, root); - const newParam = new EmberLib.Parameter(1); + const newParam = new EmberLib.Parameter(1000); newParam.contents = new EmberLib.ParameterContents(VALUE); let error; try { From 089121967153349c37d22298e5b8b1796881071a Mon Sep 17 00:00:00 2001 From: olzzon Date: Sat, 28 Mar 2020 19:34:30 +0100 Subject: [PATCH 64/64] feat: export BER in index.js for manual ASN.1 encoding --- index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 1de9a28..6727f4f 100755 --- a/index.js +++ b/index.js @@ -4,4 +4,5 @@ const Decoder = EmberLib.DecodeBuffer; const S101 = require("./s101"); const {EmberServer,ServerEvents} = require("./EmberServer"); const {S101Client} = require("./EmberSocket"); -module.exports = {EmberClient, Decoder, EmberLib, EmberServer,ServerEvents, S101, S101Client}; +const BER = require('./ber.js') +module.exports = {EmberClient, Decoder, EmberLib, EmberServer,ServerEvents, S101, S101Client, BER};