From f5bdad1efdfe34db6a862343b8f48b1e875bfad8 Mon Sep 17 00:00:00 2001 From: Thomas Silvestre Date: Thu, 10 Sep 2020 16:28:31 +0200 Subject: [PATCH] merge back Gilles Dufour version (#39) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added supported for Function on server side. Fixed invokationresult decoding * new documentation * Added all matrixConnect and Disconnect functions. Fixed all tests. Modified the Doc. * Add description to label when converting json to tree * Adding validation of connection request on server * fix contributors * Fixing getDirectory when root has more than 1 elements * Added support for subscribe and unsubscribe of streams * fixing matrix absolute connect * Better error when doing getNodeByPath * handle connection for 1-to-1 matrix * add maximumTotalConnects * fix 1 to N matrix connect * Fix 1toN. Disconnect old source and connect to new one * Server should emit disconnect when connecting target with new source * Update doc * Do a disconnect if attempting to connect same src and target * fix disconnect * handle connect with empty sources * added lock/unlock of connection * Fix disconnect for 1toN * Fix function result missing * Added -1 for defaultSources to disable disconnect * fix disconnect when mismatch client server * Fix bug in command handling * fix continuation message * Ignore empty request on server * catch write error and disconnect * Removed all ref to callabcks...using different approach...Added support for subscribe * Fix toJSON for root * version 2 * Finalized version 2.0.0 * version 2 final fixes and good to go * Fixing missing EmberLib file. Added server events * Change server event format to include source ip:port * Fix setValue event to be inline with the other events * Fixed eslint errors * Adding Streams. Fixing bugs. Event/Errors using static value * Added code coverage for Parameter * Matrix code coverage. Bug fixing * Adding test code coverage. EmberLib 100% upto Matrix * Added support for Template and QualifiedTemplate. Improved code coverage. * 92% code coverage * Add missing files. Reformat S101Socket and S101Client * v2.4.2 * use latest asn1 lib * Fixed some client missing promise return. Add server preMatrixConnect to provide above app a way to decide if connection allowed or not * Fix matrix handling on server side * Fixed tests. Changed server listen function to return promise * Added example for setValue * Fix duplicate dataIn * Fix FunctionContent result encoding * fix expand and function content encoding/decoding * feat: hack setValue to immediately resolve - added setValueWithHacksaw() function from NRKNO fork * feat: setValueNoAck - rename of function and cleanup * fix: promise didn´t resolve doc: Added comments to explain reason for hack * Fix client parameter subscribe * Add support for streamDescriptor parsing from json * Fixed tests * feat: export BER in index.js for manual ASN.1 encoding Co-authored-by: Gilles Dufour Co-authored-by: Kasper Olsson Hans --- .gitignore | 3 +- EmberClient/EmberClient.js | 715 ++++++ EmberClient/index.js | 1 + EmberLib/Command.js | 145 ++ EmberLib/Element.js | 46 + EmberLib/ElementInterface.js | 61 + EmberLib/Function.js | 68 + EmberLib/FunctionArgument.js | 102 + EmberLib/FunctionContent.js | 113 + EmberLib/Invocation.js | 105 + EmberLib/InvocationResult.js | 128 ++ EmberLib/Label.js | 69 + EmberLib/Matrix.js | 460 ++++ EmberLib/MatrixConnection.js | 192 ++ EmberLib/MatrixContents.js | 154 ++ EmberLib/MatrixDisposition.js | 18 + EmberLib/MatrixMode.js | 8 + EmberLib/MatrixNode.js | 117 + EmberLib/MatrixOperation.js | 17 + EmberLib/MatrixType.js | 10 + EmberLib/Node.js | 70 + EmberLib/NodeContents.js | 87 + EmberLib/Parameter.js | 82 + EmberLib/ParameterAccess.js | 12 + EmberLib/ParameterContents.js | 142 ++ EmberLib/ParameterType.js | 53 + EmberLib/QualifiedElement.js | 59 + EmberLib/QualifiedFunction.js | 63 + EmberLib/QualifiedMatrix.js | 108 + EmberLib/QualifiedNode.js | 53 + EmberLib/QualifiedParameter.js | 73 + EmberLib/QualifiedTemplate.js | 78 + EmberLib/StreamCollection.js | 102 + EmberLib/StreamDescription.js | 71 + EmberLib/StreamEntry.js | 70 + EmberLib/StreamFormat.js | 25 + EmberLib/StringIntegerCollection.js | 85 + EmberLib/StringIntegerPair.js | 78 + EmberLib/Template.js | 85 + EmberLib/TemplateElement.js | 75 + EmberLib/TreeNode.js | 560 +++++ EmberLib/constants.js | 23 + EmberLib/index.js | 164 ++ EmberServer/ElementHandlers.js | 195 ++ EmberServer/EmberServer.js | 519 +++++ EmberServer/JSONParser.js | 170 ++ EmberServer/MatrixHandlers.js | 135 ++ EmberServer/QualifiedHandlers.js | 75 + EmberServer/ServerEvents.js | 124 ++ EmberServer/index.js | 4 + EmberSocket/S101Client.js | 49 + EmberSocket/S101Server.js | 55 + EmberSocket/S101Socket.js | 218 ++ EmberSocket/index.js | 7 + Errors.js | 228 ++ README.md | 297 ++- ber.js | 26 +- client.js | 291 --- device.js | 518 ----- ember.js | 2708 ----------------------- errors.js | 59 - index.js | 13 +- package.json | 37 +- serve.js | 55 + server.js | 611 ----- test/DeviceTree.test.js | 106 +- test/Ember.test.js | 1634 +++++++++++++- test/EmberClient.test.js | 66 + test/Server.test.js | 1140 ++++++++-- test/client.js | 21 - embrionix.ember => test/embrionix.ember | Bin test/utils.js | 71 +- 72 files changed, 9690 insertions(+), 4492 deletions(-) create mode 100755 EmberClient/EmberClient.js create mode 100755 EmberClient/index.js create mode 100755 EmberLib/Command.js create mode 100755 EmberLib/Element.js create mode 100755 EmberLib/ElementInterface.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/QualifiedElement.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/QualifiedTemplate.js create mode 100755 EmberLib/StreamCollection.js create mode 100755 EmberLib/StreamDescription.js create mode 100755 EmberLib/StreamEntry.js create mode 100755 EmberLib/StreamFormat.js create mode 100755 EmberLib/StringIntegerCollection.js create mode 100755 EmberLib/StringIntegerPair.js create mode 100755 EmberLib/Template.js create mode 100755 EmberLib/TemplateElement.js create mode 100755 EmberLib/TreeNode.js create mode 100755 EmberLib/constants.js create mode 100755 EmberLib/index.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/ServerEvents.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 create mode 100755 Errors.js delete mode 100755 client.js delete mode 100755 device.js delete mode 100755 ember.js delete mode 100755 errors.js create mode 100755 serve.js delete mode 100755 server.js create mode 100755 test/EmberClient.test.js delete mode 100755 test/client.js rename embrionix.ember => test/embrionix.ember (100%) 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/EmberClient/EmberClient.js b/EmberClient/EmberClient.js new file mode 100755 index 0000000..40f8a6a --- /dev/null +++ b/EmberClient/EmberClient.js @@ -0,0 +1,715 @@ +const EventEmitter = require('events').EventEmitter; +const S101Client = require('../EmberSocket').S101Client; +const ember = require('../EmberLib'); +const BER = require('../ber.js'); +const Errors = require('../Errors.js'); +const winston = require("winston"); + +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 != null) { + this._callback(); + } + }); + + this._client.on('disconnected', () => { + this.emit('disconnected'); + }); + + this._client.on("error", e => { + if (this._callback != null) { + this._callback(e); + } + this.emit("error", e); + }); + + this._client.on('emberTree', root => { + try { + if (root instanceof ember.InvocationResult) { + this.emit('invocationResult', root); + winston.debug("Received InvocationResult", root); + } else { + this._handleRoot(root); + winston.debug("Received root", root); + } + if (this._callback) { + this._callback(undefined, root); + } + } + catch(e) { + winston.error(e, root); + if (this._callback) { + this._callback(e); + } + } + }); + } + + _finishRequest() { + this._clearTimeout(); + this._callback = undefined; + this._activeRequest = null; + try { + this._makeRequest(); + } catch(e) { + winston.error(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`) + + winston.debug(`Making request ${req}`, Date.now()); + this._timeout = setTimeout(() => { + this._timeoutRequest(); + }, this.timeoutValue); + this._activeRequest.func(); + } + } + + _timeoutRequest() { + 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) { + let n = parent.getElementByNumber(node.getNumber()); + if (n == null) { + parent.addChild(node); + n = node; + } else if (n.update(node)) { + n.updateSubscribers(); + } + + const children = node.getChildren(); + if (children !== null) { + for (let 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) { + let element = parent.getElementByPath(node.path); + if (element !== null) { + this.emit("value-change", node); + if (element.update(node)) { + element.updateSubscribers(); + } + } + else { + const 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; + } + + const children = node.getChildren(); + if (children !== null) { + for (let 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) { + winston.debug("handling root", JSON.stringify(root)); + this.root.update(root); + if (root.elements != null) { + const elements = root.getChildren(); + for (let 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 != null) && (this._client.isConnected())) { + this._client.disconnect(); + } + this._client.connect(timeout); + }); + } + + /** + * @returns {Promise} + */ + disconnect() { + if (this._client != null) { + return this._client.disconnect(); + } + return Promise.resolve(); + } + + /** + * + * @param {TreeNode} qnode + * @param {function} callback=null + * @returns {Promise} + */ + expand(node, callback = null) { + if (node != null && (node.isParameter() || node.isMatrix() || node.isFunction())) { + return this.getDirectory(node); + } + return this.getDirectory(node, callback).then(res => { + const children = node == null ? res == null ? null : res.getChildren() : node.getChildren(); + if ((res == null) || (children == null)) { + winston.debug("No more children for ", node); + return; + } + let directChildren = Promise.resolve(); + for (let child of children) { + if (child.isParameter()) { + // Parameter can only have a single child of type Command. + continue; + } + 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 directChildren + }); + } + /** + * + * @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(); + 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._finishRequest(); + return reject(error); + } + if (qnode.isRoot()) { + const elements = qnode.getChildren(); + if (elements == null || elements.length === 0) { + winston.debug("getDirectory response", node); + return reject(new Errors.InvalidEmberNode()); + } + + const nodeElements = node == null ? null : node.getChildren(); + + if (nodeElements != null + && nodeElements.every(el => el._parent instanceof ember.Root)) { + winston.debug("Received getDirectory response", node); + this._finishRequest(); + return resolve(node); // make sure the info is treated before going to next request. + } + else { + return this._callback(new Errors.InvalidEmberResponse(`getDirectory ${requestedPath}`)); + } + } + else if (node.getElementByPath(requestedPath) != null) { + 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))))) { + 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.error(new Error(requestedPath)); + } + } + }; + try { + this._client.sendBERNode(qnode.getDirectory(callback)); + } + catch(e) { + reject(e); + } + }}); + }); + } + + /** + * + * @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) { + type = TYPE_ID; + pathArray = path.split("/"); + } + else { + pathArray = path.split("."); + if (pathArray.length === 1) { + if (isNaN(Number(pathArray[0]))) { + type = TYPE_ID; + } + } + } + let pos = 0; + let lastMissingPos = -1; + let currentNode = this.root; + const getNext = () => { + return Promise.resolve() + .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; + } + } + if (i >= children.length) { + node = null; + } + } + } + if (node != null) { + // We have this part already. + pos++; + if (pos >= pathArray.length) { + if (callback) { + node.getDirectory(callback); + } + 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 {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 { + winston.debug("InvocationResult", result); + resolve(result); + } + // cleaning callback and making next request. + this._finishRequest(); + }; + winston.debug("Invocking function", fnNode); + this._callback = cb; + this._client.sendBERNode(fnNode.invoke(params)); + }}); + }) + } + + /** + * @returns {boolean} + */ + isConnected() { + return ((this._client != null) && (this._client.isConnected())); + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + * @param {MatrixOperation} operation + * @returns {Promise} + */ + matrixOPeration(matrixNode, targetID, sources, operation = ember.MatrixOperation.connect) { + return new Promise((resolve, reject) => { + if (!Array.isArray(sources)) { + return reject(new Errors.InvalidSourcesFormat()); + } + 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) { + winston.debug(`received null response for ${requestedPath}`); + return; + } + if (error) { + winston.error("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.getElementByPath(requestedPath); + } + 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 { + winston.debug(`unexpected node response during matrix connect ${requestedPath}`, + matrix == null ? null : JSON.stringify(matrix.toJSON(), null, 4)); + } + } + this._client.sendBERNode(matrixNode.connect(connections)); + }}); + }); + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + * @returns {Promise} + */ + matrixConnect(matrixNode, targetID, sources) { + return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.connect) + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + * @returns {Promise} + */ + matrixDisconnect(matrixNode, targetID, sources) { + return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.disconnect) + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + * @returns {Promise} + */ + 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.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)); + }}); + } + }); + } + + /** + * + * @param {TreeNode} node + * @param {string|number} value + * @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')); + return; + } + this.addRequest({node: node, func: error => { + if (error) { + this._finishRequest(); + reject(error); + return; + } + winston.debug('setValue sending ...', node.getPath(), value); + this._client.sendBERNode(node.setValue(value)); + + this._finishRequest(); + this._callback = null; + return resolve(node) + }}); + }) + } + + /** + * + * @param {TreeNode} qnode + * @param {function} callback + * @returns {Promise} + */ + 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 (error != null) { + return reject(error); + } + winston.debug("Sending subscribe", qnode); + this._client.sendBERNode(qnode.subscribe(callback)); + this._finishRequest(); + resolve(); + }}); + }); + } + return Promise.resolve(); + } + + /** + * + * @param {TreeNode} qnode + * @param {function} callback + * @returns {Promise} + */ + unsubscribe(qnode, callback) { + if (qnode.isParameter() && qnode.isStream()) { + return new Promise((resolve, reject) => { + this.addRequest({node: qnode, func: (error) => { + if (error != null) { + return reject(error); + } + winston.debug("Sending subscribe", qnode); + this._client.sendBERNode(qnode.unsubscribe(callback)); + this._finishRequest(); + resolve(); + }}); + }); + } + return Promise.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 new file mode 100755 index 0000000..73bf653 --- /dev/null +++ b/EmberLib/Command.js @@ -0,0 +1,145 @@ +"use strict"; +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 ElementInterface = require("./ElementInterface"); + +const FieldFlags = new Enum({ + sparse: -2, + all: -1, + default: 0, + identifier: 1, + description: 2, + tree: 3, + value: 4, + connections: 5 +}); + +class Command extends ElementInterface{ + /** + * + * @param {number} number + */ + constructor(number) { + super(); + this.number = number; + if(number == COMMAND_GETDIRECTORY) { + this.fieldFlags = FieldFlags.all; + } + } + + /** + * @returns {boolean} + */ + isCommand() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(Command.BERID); + + 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) { + const c = new Command(); + ber = ber.getSequence(Command.BERID); + + while(ber.remain > 0) { + let tag = ber.peek(); + let 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; + } + + /** + * + * @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} + */ + static get BERID() { + return BER.APPLICATION(2); + } +} + +module.exports = Command; \ No newline at end of file diff --git a/EmberLib/Element.js b/EmberLib/Element.js new file mode 100755 index 0000000..00aede0 --- /dev/null +++ b/EmberLib/Element.js @@ -0,0 +1,46 @@ +"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); + + this.encodeNumber(ber); + + 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 {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/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/EmberLib/Function.js b/EmberLib/Function.js new file mode 100755 index 0000000..54c777b --- /dev/null +++ b/EmberLib/Function.js @@ -0,0 +1,68 @@ +"use strict"; +const Element = require("./Element"); +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 Element { + constructor(number, func) { + super(); + this.number = number; + this.func = func; + this._seqID = Function.BERID; + } + + /** + * @returns {boolean} + */ + isFunction() { + return true; + } + + /** + * @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(Function.BERID); + + 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); + } + } + 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 new file mode 100755 index 0000000..de1a792 --- /dev/null +++ b/EmberLib/FunctionArgument.js @@ -0,0 +1,102 @@ +"use strict"; +const BER = require('../ber.js'); +const {ParameterType} = require("./ParameterType"); +const Errors = require("../Errors"); + +/* +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(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); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * + */ + toJSON() { + return { + type: this.type, + name: this.name, + value: this.value + }; + } + + /** + * + * @param {BER} ber + * @returns {FunctionArgument} + */ + static decode(ber) { + const tuple = new FunctionArgument(); + ber = ber.getSequence(FunctionArgument.BERID); + 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); + } + 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 new file mode 100755 index 0000000..737beae --- /dev/null +++ b/EmberLib/FunctionContent.js @@ -0,0 +1,113 @@ +"use strict"; +const BER = require('../ber.js'); +const FunctionArgument = require("./FunctionArgument"); +const errors = require("../Errors"); + +class FunctionContent { + constructor(identifier=null, description=null) { + this.arguments = []; + this.result = []; + this.identifier = identifier; + this.description = description; + } + + /** + * + * @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 && 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) + } + + if(this.templateReference != null) { + ber.startSequence(BER.CONTEXT(4)); + ber.writeRelativeOID(this.templateReference, BER.EMBER_RELATIVE_OID); + 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 = []; + let dataSeq = seq.getSequence(BER.EMBER_SEQUENCE); + while(dataSeq.remain > 0) { + tag = dataSeq.peek(); + if (tag === BER.CONTEXT(0)) { + const fcSeq = dataSeq.getSequence(tag); + fc.result.push(FunctionArgument.decode(fcSeq)); + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + } 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..945b828 --- /dev/null +++ b/EmberLib/Invocation.js @@ -0,0 +1,105 @@ +"use strict"; +const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); +const BER = require('../ber.js'); +const FunctionArgument = require("./FunctionArgument"); +const errors = require("../Errors"); + +let _id = 1; +class Invocation { + /** + * + * @param {number} id + * @param {FunctionArgument[]} args + */ + constructor(id = null, args = []) { + this.id = id; + this.arguments = args; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + 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++) { + 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) + } + + /** + * + */ + toJSON() { + return { + id: this.id, + arguments: this.arguments == null ? null : this.arguments.map(a => a.toJSON()), + } + } + + /** + * + * @param {BER} ber + * @returns {Invocation} + */ + static decode(ber) { + 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)) { + invocation.id = seq.readInt(); + } + else if(tag == BER.CONTEXT(1)) { + 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; + } + + /** + * @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 new file mode 100755 index 0000000..b036b32 --- /dev/null +++ b/EmberLib/InvocationResult.js @@ -0,0 +1,128 @@ +"use strict"; + +const BER = require('../ber.js'); +const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); +const FunctionArgument = require("./FunctionArgument"); +const Errors = require("../Errors"); + + +class InvocationResult { + /** + * + * @param {number|null} invocationId=null + */ + constructor(invocationId = null) { + this.invocationId = invocationId; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(InvocationResult.BERID); + 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 Errors.InvalidResultFormat(); + } + this.result = result; + } + + toQualified() { + return this; + } + + /** + * + * @param {BER} ber + * @returns {InvocationResult} + */ + static decode(ber) { + const invocationResult = new InvocationResult(); + ber = ber.getSequence(InvocationResult.BERID); + while(ber.remain > 0) { + 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 + 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() + )); + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + continue + } else { + 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 new file mode 100755 index 0000000..8a41235 --- /dev/null +++ b/EmberLib/Label.js @@ -0,0 +1,69 @@ +"use strict"; +const BER = require('../ber.js'); +const Errors = require("../Errors"); + +class Label { + constructor(path, description) { + if (path) { + this.basePath = path; + } + if (description) { + this.description = description; + } + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(Label.BERID); + if (this.basePath == null) { + throw new Errors.InvalidEmberNode("", "Missing label base path"); + } + 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(); + } + + /** + * + * @param {BER} ber + * @returns {Label} + */ + static decode(ber) { + var l = new Label(); + + ber = ber.getSequence(Label.BERID); + + 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; + } + + /** + * + */ + 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 new file mode 100755 index 0000000..9f8ac2f --- /dev/null +++ b/EmberLib/Matrix.js @@ -0,0 +1,460 @@ +"use strict"; +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 Errors = require("../Errors"); + +class Matrix extends TreeNode +{ + constructor() { + super(); + this._connectedSources = {}; + this._numConnections = 0; + this.targets = null; + this.sources = null; + this.connections = {}; + } + + 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 != null) { + ber.startSequence(BER.CONTEXT(5)); + ber.startSequence(BER.EMBER_SEQUENCE); + + for(let 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(let i=0; i Number(i))); + + if (matrixNode.connections[targetID].isLocked()) { + return false; + } + if (type === MatrixType.oneToN && + matrixNode.contents.maximumTotalConnects == 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 - 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 == null) { + matrix.connections = {}; + } + 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) { + let 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) { + let 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) { + const connections = {}; + const seq = ber.getSequence(BER.EMBER_SEQUENCE); + while(seq.remain > 0) { + const conSeq = seq.getSequence(BER.CONTEXT(0)); + const con = MatrixConnection.decode(conSeq); + 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 + * @returns {boolean} - True if something changed + */ + 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)) { + 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; + } + } + else { + throw new Errors.InvalidMatrixSignal(connection.target, "Invalid target") + } + } + } + } + return modified; + } + + /** + * + * @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) { + this.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 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}`); + } + } + if (matrixNode.contents.mode === MatrixMode.linear) { + if (targetID >= 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.InvalidMatrixSignal(sources[i],`Source at index ${i} higher than max ${matrixNode.contents.sourceCount}`); + } + } + } + else if ((matrixNode.targets == null) || (matrixNode.sources == null)) { + throw new Errors.InvalidEmberNode(matrixNode.getPath(),"Non-Linear matrix should have targets and sources"); + } + else { + let found = false; + for(let i = 0; i < matrixNode.targets.length; i++) { + if (matrixNode.targets[i] === targetID) { + found = true; + break; + } + } + if (!found) { + throw new Errors.InvalidMatrixSignal(targetID, "Not part of existing targets"); + } + found = false; + for(let i = 0; i < sources.length; i++) { + for(let j = 0; j < matrixNode.sources.length; j++) { + if (matrixNode.sources[j] === sources[i]) { + found = true; + break; + } + } + if (!found) { + throw new Errors.InvalidMatrixSignal(sources[i],`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..45ec689 --- /dev/null +++ b/EmberLib/MatrixConnection.js @@ -0,0 +1,192 @@ +"use strict"; +const BER = require('../ber.js'); +const MatrixOperation = require("./MatrixOperation"); +const MatrixDisposition = require("./MatrixDisposition"); +const Errors = require("../Errors"); + +class MatrixConnection { + /** + * + * @param {number} target + */ + constructor(target) { + if (target) { + let _target = Number(target); + if (isNaN(_target)) { + throw new Errors.InvalidMatrixSignal(target, "Can't create connection with invalid target.") + } + this.target = _target; + } + else { + this.target = 0; + } + this._locked = false; + } + + /** + * + * @param {number[]} sources + */ + connectSources(sources) { + this.sources = this.validateSources(sources); + } + + /** + * + * @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(MatrixConnection.BERID); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.target); + ber.endSequence(); + + if (this.sources != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeRelativeOID(this.sources.join("."), BER.EMBER_RELATIVE_OID); + ber.endSequence(); + } + if (this.operation != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.writeInt(this.operation.value); + ber.endSequence(); + } + if (this.disposition != null) { + ber.startSequence(BER.CONTEXT(3)); + ber.writeInt(this.disposition.value); + ber.endSequence(); + } + 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} + */ + isLocked() { + return this._locked; + } + + /** + * + */ + lock() { + this._locked = true; + } + + /** + * + * @param {number[]} sources + */ + setSources(sources) { + if (sources == null) { + delete this.sources; + return; + } + 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(); + } + + /** + * + */ + unlock() { + this._locked = false; + } + + /** + * + * @param {BER} ber + * @returns {MatrixConnection} + */ + static decode(ber) { + const c = new MatrixConnection(); + ber = ber.getSequence(MatrixConnection.BERID); + 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; + } + + /** + * + */ + 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 new file mode 100755 index 0000000..1bbcda8 --- /dev/null +++ b/EmberLib/MatrixContents.js @@ -0,0 +1,154 @@ +"use strict"; + +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) { + this.type = type; + this.mode = mode; + } + + /** + * + * @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(); + } + if (this.description != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.description, BER.EMBER_STRING); + ber.endSequence(); + } + if (this.type != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.writeInt(this.type.value); + ber.endSequence(); + } + if (this.mode != null) { + ber.startSequence(BER.CONTEXT(3)); + ber.writeInt(this.mode.value); + ber.endSequence(); + } + if (this.targetCount != null) { + ber.startSequence(BER.CONTEXT(4)); + ber.writeInt(this.targetCount); + ber.endSequence(); + } + if (this.sourceCount != null) { + ber.startSequence(BER.CONTEXT(5)); + ber.writeInt(this.sourceCount); + ber.endSequence(); + } + if (this.maximumTotalConnects != null) { + ber.startSequence(BER.CONTEXT(6)); + ber.writeInt(this.maximumTotalConnects); + ber.endSequence(); + } + if (this.maximumConnectsPerTarget != null) { + ber.startSequence(BER.CONTEXT(7)); + ber.writeInt(this.maximumConnectsPerTarget); + ber.endSequence(); + } + if (this.parametersLocation != null) { + 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 != null) { + ber.startSequence(BER.CONTEXT(9)); + ber.writeInt(this.gainParameterNumber); + ber.endSequence(); + } + if (this.labels != null) { + 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 != null) { + ber.startSequence(BER.CONTEXT(11)); + ber.writeString(this.schemaIdentifiers, BER.EMBER_STRING); + ber.endSequence(); + } + if (this.templateReference != null) { + 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.readString(BER.EMBER_STRING); + } 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..7cf53d4 --- /dev/null +++ b/EmberLib/MatrixNode.js @@ -0,0 +1,117 @@ +"use strict"; + +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) { + super(); + this.number = number; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(MatrixNode.BERID); + + 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); + 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(MatrixNode.BERID); + 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); + } + } + return m; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(13); + } +} + +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..f211a0b --- /dev/null +++ b/EmberLib/MatrixOperation.js @@ -0,0 +1,17 @@ +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..e24c2eb --- /dev/null +++ b/EmberLib/Node.js @@ -0,0 +1,70 @@ +"use strict"; + +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 { + /** + * + * @param {number} number + */ + constructor(number) { + super(number); + this._seqID = Node.BERID; + /** @type {NodeContents} */ + this.contents = null; + } + + /** + * @returns {boolean} + */ + isNode() { + return true; + } + + /** + * @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(Node.BERID); + + 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); + } + } + return n; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(3); + } +} + +module.exports = Node; diff --git a/EmberLib/NodeContents.js b/EmberLib/NodeContents.js new file mode 100755 index 0000000..cb3c479 --- /dev/null +++ b/EmberLib/NodeContents.js @@ -0,0 +1,87 @@ +"use strict"; +const BER = require('../ber.js'); +const errors = require("../Errors"); + +class NodeContents{ + /** + * + * @param {string} identifier + * @param {string} description + */ + constructor(identifier=null, description=null) { + this.isOnline = true; + this.identifier = identifier; + this.description = description; + } + + /** + * + * @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.isRoot != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.writeBoolean(this.isRoot); + ber.endSequence(); // BER.CONTEXT(2) + } + + if(this.isOnline != null) { + ber.startSequence(BER.CONTEXT(3)); + ber.writeBoolean(this.isOnline); + ber.endSequence(); // BER.CONTEXT(3) + } + + if(this.schemaIdentifiers != null) { + 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..a7a07c8 --- /dev/null +++ b/EmberLib/Parameter.js @@ -0,0 +1,82 @@ +"use strict"; + +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 { + /** + * + * @param {number} number + */ + constructor(number) { + super(); + this.number = number; + this._seqID = Parameter.BERID; + } + + /** + * @returns {boolean} + */ + isParameter() { + return true; + } + + /** + * 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} + */ + setValue(value) { + return this.getTreeBranch(undefined, (m) => { + m.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); + }); + } + + /** + * @returns {QualifiedParameter} + */ + toQualified() { + const qp = new QualifiedParameter(this.getPath()); + qp.update(this); + return qp; + } + + /** + * + * @param {BER} ber + * @returns {Parameter} + */ + static decode(ber) { + const p = new Parameter(); + ber = ber.getSequence(Parameter.BERID); + + 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); + } + } + return p; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(1); + } +} + +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..07ad48a --- /dev/null +++ b/EmberLib/ParameterContents.js @@ -0,0 +1,142 @@ +"use strict"; + +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 { + /** + * + * @param {string|number} value + * @param {string} type + */ + constructor(value, type) { + if(value != null) { + this.value = value; + } + if(type != null) { + if((type = ParameterType.get(type)) != null){ + 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.enumMap != null) { + ber.startSequence(BER.CONTEXT(15)); + this.enumMap.encode(ber); + ber.endSequence(); + } + + if(this.streamDescriptor != null) { + 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); + 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; + } +} + +module.exports = ParameterContents; diff --git a/EmberLib/ParameterType.js b/EmberLib/ParameterType.js new file mode 100755 index 0000000..421a1c9 --- /dev/null +++ b/EmberLib/ParameterType.js @@ -0,0 +1,53 @@ +const Enum = require('enum'); +const BER = require('../ber.js'); +const Errors = require("../Errors"); + +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 Errors.InvalidEmberNode("", `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 Errors.InvalidBERFormat(`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/QualifiedElement.js b/EmberLib/QualifiedElement.js new file mode 100755 index 0000000..98e45f4 --- /dev/null +++ b/EmberLib/QualifiedElement.js @@ -0,0 +1,59 @@ +"use strict"; +const TreeNode = require("./TreeNode"); +const BER = require('../ber.js'); +const Command = require("./Command"); +const {COMMAND_GETDIRECTORY} = require("./constants"); + +class QualifiedElement extends TreeNode { + /** + * + * @param {string} path + */ + constructor(path) { + super(); + this.path = path; + } + + /** + * @returns {boolean} + */ + isQualified() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(this._seqID); + + this.encodePath(ber); + + 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 {Command} cmd + * @returns {TreeNode} + */ + getCommand(cmd) { + const r = this.getNewTree(); + const qn = new this.constructor(); + qn.path = this.getPath(); + r.addElement(qn); + qn.addChild(cmd); + return r; + } +} + +module.exports = QualifiedElement; \ No newline at end of file diff --git a/EmberLib/QualifiedFunction.js b/EmberLib/QualifiedFunction.js new file mode 100755 index 0000000..8e4ac84 --- /dev/null +++ b/EmberLib/QualifiedFunction.js @@ -0,0 +1,63 @@ +"use strict"; + +const QualifiedElement = require("./QualifiedElement"); +const FunctionContent = require("./FunctionContent"); +const {COMMAND_GETDIRECTORY} = require("./constants"); +const BER = require('../ber.js'); +const Command = require("./Command"); +const Errors = require("../Errors"); + +class QualifiedFunction extends QualifiedElement { + /** + * + * @param {string} path + * @param {function} func + */ + constructor(path, func) { + super(path); + this.func = func; + this._seqID = QualifiedFunction.BERID; + } + + /** + * @returns {boolean} + */ + isFunction() { + return true; + } + + /** + * + * @param {BER} ber + * @returns {QualifiedFunction} + */ + static decode(ber) { + const qf = new QualifiedFunction(); + ber = ber.getSequence(QualifiedFunction.BERID); + 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; + } + + /** + * @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 new file mode 100755 index 0000000..3d4f890 --- /dev/null +++ b/EmberLib/QualifiedMatrix.js @@ -0,0 +1,108 @@ +"use strict"; + +const Matrix = require("./Matrix"); +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 { + /** + * + * @param {string} path + */ + constructor(path) { + super(); + this.path = path; + } + + isQualified() { + return true; + } + /** + * + * @param {Object} connections + * @returns {Root} + */ + connect(connections) { + const r = this.getNewTree(); + const qn = new QualifiedMatrix(); + qn.path = this.path; + r.addElement(qn); + qn.connections = connections; + return r; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(QualifiedMatrix.BERID); + + this.encodePath(ber); + + 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 {BER} ber + * @returns {QualifiedMatrix} + */ + static decode(ber) { + const qm = new QualifiedMatrix(); + ber = ber.getSequence(QualifiedMatrix.BERID); + 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 = Matrix.decodeTargets(seq); + } else if (tag == BER.CONTEXT(4)) { + qm.sources = Matrix.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 != null) { + qm.connections[con.target] = con; + } + } + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + 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 new file mode 100755 index 0000000..155e23c --- /dev/null +++ b/EmberLib/QualifiedNode.js @@ -0,0 +1,53 @@ +"user strict"; +const QualifiedElement = require("./QualifiedElement"); +const BER = require('../ber.js'); +const NodeContents = require("./NodeContents"); +const Errors = require("../Errors"); + +class QualifiedNode extends QualifiedElement { + constructor (path) { + super(path); + this._seqID = QualifiedNode.BERID; + } + + /** + * @returns {boolean} + */ + isNode() { + return true; + } + + /** + * + * @param {BER} ber + * @returns {QualifiedNode} + */ + static decode(ber) { + const qn = new QualifiedNode(); + ber = ber.getSequence(QualifiedNode.BERID); + 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); + } + } + 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 new file mode 100755 index 0000000..2ff6d8c --- /dev/null +++ b/EmberLib/QualifiedParameter.js @@ -0,0 +1,73 @@ +"use strict"; + +const QualifiedElement = require("./QualifiedElement"); +const ParameterContents = require("./ParameterContents"); +const BER = require('../ber.js'); +const Errors = require("../Errors"); + +class QualifiedParameter extends QualifiedElement { + /** + * + * @param {string} path + */ + constructor(path) { + super(path); + this._seqID = QualifiedParameter.BERID; + } + + /** + * @returns {boolean} + */ + isParameter() { + return true; + } + + /** + * 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); + r.addElement(qp); + qp.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); + return r; + } + + + /** + * + * @param {BER} ber + * @returns {QualifiedParameter} + */ + static decode(ber) { + var qp = new QualifiedParameter(); + ber = ber.getSequence(QualifiedParameter.BERID); + 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 { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return qp; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(9); + } +} + +module.exports = QualifiedParameter; \ No newline at end of file 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/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/StreamDescription.js b/EmberLib/StreamDescription.js new file mode 100755 index 0000000..4915551 --- /dev/null +++ b/EmberLib/StreamDescription.js @@ -0,0 +1,71 @@ +"use strict"; +const BER = require('../ber.js'); +const StreamFormat = require("./StreamFormat"); +const Errors = require("../Errors"); + +class StreamDescription { + /** + * + * @param {number} offset + * @param {StreamFormat} format + */ + constructor(offset, format) { + this.offset = offset; + this.format = format; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + 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(StreamDescription.BERID); + + 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; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(12); + } +} + +module.exports = StreamDescription; \ 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/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/StringIntegerCollection.js b/EmberLib/StringIntegerCollection.js new file mode 100755 index 0000000..3fd3eb1 --- /dev/null +++ b/EmberLib/StringIntegerCollection.js @@ -0,0 +1,85 @@ +"use strict"; +const BER = require('../ber.js'); +const StringIntegerPair = require("./StringIntegerPair"); +const Errors = require("../Errors"); + +class StringIntegerCollection { + constructor() { + this._collection = new Map(); + } + + /** + * + * @param {string} key + * @param {StringIntegerPair} value + */ + addEntry(key, value) { + if (!(value instanceof StringIntegerPair)) { + throw new Errors.InvalidStringPair(); + } + this._collection.set(key, value); + } + + /** + * + * @param {string} key + * @returns {StringIntegerPair} + */ + get(key) { + return this._collection.get(key); + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(StringIntegerCollection.BERID); + for(let [,sp] of this._collection) { + ber.startSequence(BER.CONTEXT(0)); + sp.encode(ber); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * @returns {JSON_StringPair[]} + */ + toJSON() { + const collection = []; + for(let [,sp] of this._collection) { + collection.push(sp.toJSON()); + } + return collection; + } + + /** + * + * @param {BER} ber + * @returns {StringIntegerCollection} + */ + static decode(ber) { + const sc = new StringIntegerCollection(); + const seq = ber.getSequence(StringIntegerCollection.BERID); + while(seq.remain > 0) { + const tag = seq.peek(); + if (tag != BER.CONTEXT(0)) { + throw new Errors.UnimplementedEmberTypeError(tag); + } + 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/StringIntegerPair.js b/EmberLib/StringIntegerPair.js new file mode 100755 index 0000000..15bbe1c --- /dev/null +++ b/EmberLib/StringIntegerPair.js @@ -0,0 +1,78 @@ +"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(); + } + + /** + * @typedef {{ + * key: string + * value: number + * }} JSON_StringPair + */ + + /** + * @returns {{ + * key: string + * value: number + * }} + */ + 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/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 new file mode 100755 index 0000000..69b6ebe --- /dev/null +++ b/EmberLib/TreeNode.js @@ -0,0 +1,560 @@ +"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, COMMAND_INVOKE} = require("./constants"); +const Errors = require("../Errors"); + +class TreeNode extends ElementInterface { + constructor() { + super(); + /** @type {TreeNode} */ + this._parent = null; + this._subscribers = new Set(); + } + + _isSubscribable(callback) { + return (callback != null && + (this.isParameter() || this.isMatrix())); + } + + _subscribe(callback) { + this._subscribers.add(callback); + } + + _unsubscribe(callback) { + this._subscribers.delete(callback); + } + + /** + * + * @param {TreeNode} child + */ + 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(); + } + } + + /** + * + * @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} + */ + getNewTree() { + return new TreeNode(); + } + + /** + * @returns {boolean} + */ + hasChildren() { + return this.elements != null && this.elements.size > 0; + } + + /** + * @returns {boolean} + */ + isRoot() { + return this._parent == null; + } + + /** + * @returns {boolean} + */ + isStream() { + return this.contents != null && + this.contents.streamIdentifier != null; + } + + /** + * @returns {TreeNode} + */ + getMinimalContent() { + let obj; + if (this.isQualified()) { + obj = new this.constructor(this.path); + } + else { + obj = new this.constructor(this.number); + } + if (this.contents != null) { + obj.contents= this.contents; + } + return obj; + } + /** + * @returns {TreeNode} + */ + 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 != null) { + m.addChild(child); + } + + if(modifier != null) { + 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(); + } + } + + /** + * + * @param {Command} cmd + */ + getCommand(cmd) { + return this.getTreeBranch(cmd); + } + + /** + * + * @param {function} callback + */ + getDirectory(callback) { + if (this._isSubscribable(callback) && !this.isStream()) { + this._subscribe(callback); + } + return this.getCommand(new Command(COMMAND_GETDIRECTORY)); + } + + + /** + * @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; + } + + /** + * + * @param {string} path + * @returns {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; + } + 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; + } + if (node.isQualified() && node.path == path) { + return node; + } + 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(let i = 0; i < children.length; i++) { + if(children[i].contents != null && + 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); + } + } + + + /** + * @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; + } + } + + /** + * + * @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; + } + + /** + * + */ + toJSON() { + const res = {nodeType: this.constructor.name}; + 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 (prop[0] == "_" || node.contents[prop] == null) { + continue; + } + 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 != null) { + 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 {function} callback + */ + subscribe(callback) { + if (this._isSubscribable(callback) && this.isStream()) { + this._subscribe(callback); + } + return this.getCommand(new Command(COMMAND_SUBSCRIBE)); + } + + /** + * + * @param {*} callback + */ + unsubscribe(callback) { + this._unsubscribe(callback); + return this.getCommand(new Command(COMMAND_UNSUBSCRIBE)); + } + + /** + * + * @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 { + for (var key in other.contents) { + if (key[0] === "_") { continue; } + if (other.contents.hasOwnProperty(key) && + this.contents[key] != other.contents[key]) { + this.contents[key] = other.contents[key]; + modified = true; + } + } + } + } + return modified; + } + + updateSubscribers() { + if (this._subscribers != null) { + for(let cb of this._subscribers) { + cb(this); + } + } + } + + /** + * + * @param {TreeNode} parent + * @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 { + 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..e49d071 --- /dev/null +++ b/EmberLib/constants.js @@ -0,0 +1,23 @@ +const COMMAND_SUBSCRIBE = 30; +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, + COMMAND_GETDIRECTORY, + COMMAND_INVOKE, + Subscribe: COMMAND_SUBSCRIBE, + Unsubscribe: COMMAND_UNSUBSCRIBE, + GetDirectory: COMMAND_GETDIRECTORY, + Invoke: COMMAND_INVOKE, + COMMAND_STRINGS +}; \ No newline at end of file diff --git a/EmberLib/index.js b/EmberLib/index.js new file mode 100755 index 0000000..9dc3813 --- /dev/null +++ b/EmberLib/index.js @@ -0,0 +1,164 @@ +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 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 Matrix = require("./Matrix"); +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 MatrixDisposition = require("./MatrixDisposition"); +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 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"); + +const rootDecode = function(ber) { + const r = new TreeNode(); + let tag = undefined; + while(ber.remain > 0) { + tag = ber.peek(); + if (tag === BER.APPLICATION(0)) { + ber = ber.getSequence(BER.APPLICATION(0)); + tag = ber.peek(); + + if (tag === BER.APPLICATION(11)) { + 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) { + 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 childDecode(rootReader) + } + catch (e) { + return r; + } + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + 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(); + const decode = TreeNodeDecoders[tag]; + if (decode == null) { + throw new Errors.UnimplementedEmberTypeError(tag); + } + else { + return decode(ber); + } +} + +TreeNode.decode = childDecode; + +const DecodeBuffer = function (packet) { + const ber = new BER.Reader(packet); + return rootDecode(ber); +}; + +module.exports = { + Command, + COMMAND_STRINGS, + childDecode: childDecode, + rootDecode: rootDecode, + DecodeBuffer, + Root: TreeNode, + Function, + FunctionArgument, + FunctionContent, + Invocation, + InvocationResult, + Label, + Matrix, + MatrixNode, + MatrixMode, + MatrixType, + MatrixContents, + MatrixConnection, + MatrixDisposition, + MatrixOperation, + Node, + NodeContents, + Parameter, + ParameterContents, + ParameterAccess, + ParameterType, + QualifiedFunction , + QualifiedMatrix, + QualifiedNode, + QualifiedParameter, + QualifiedTemplate, + StreamFormat, + StreamDescription, + StreamCollection, + StringIntegerPair, + StringIntegerCollection, + Template, + TemplateElement, + Subscribe,COMMAND_SUBSCRIBE, + Unsubscribe,COMMAND_UNSUBSCRIBE, + GetDirectory,COMMAND_GETDIRECTORY, + Invoke,COMMAND_INVOKE +} \ No newline at end of file diff --git a/EmberServer/ElementHandlers.js b/EmberServer/ElementHandlers.js new file mode 100755 index 0000000..e48d60f --- /dev/null +++ b/EmberServer/ElementHandlers.js @@ -0,0 +1,195 @@ +"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{ + /** + * + * @param {EmberServer} server + */ + constructor(server) { + super(server); + } + /** + * + * @param {S101Client} client + * @param {TreeNode} root + * @param {Command} 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 Errors.InvalidCommand(cmd.number)); + return; + } + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleGetDirectory(client, element) { + if (client != null) { + 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); + winston.debug("getDirectory response", res); + client.sendBERNode(res); + } + } + + /** + * + * @param {S101Client} client + * @param {Invocation} invocation + * @param {TreeNode} element + */ + handleInvoke(client, invocation, element) { + const result = new EmberLib.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 EmberLib.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 != null) { + if (element.isCommand()) { + break; + } + if (element.number == null) { + this.server.emit("error", new Errors.MissingElementNumber()); + return; + } + + path.push(element.number); + + const children = element.getChildren(); + if ((! children) || (children.length === 0)) { + break; + } + element = children[0]; + } + let cmd = element; + + if (cmd == null) { + this.server.emit("error", new Errors.InvalidRequest()); + 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); + return path; + } else if ((cmd.isMatrix()) && (cmd.connections != null)) { + this.handleMatrixConnections(client, element, cmd.connections); + } + else if ((cmd.isParameter()) && + (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 Errors.InvalidRequesrFormat(path.join("."))); + winston.debug("invalid request format"); + return this.server.handleError(client, element.getTreeBranch()); + } + // for logging purpose, return the path. + return path; + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleSubscribe(client, element) { + winston.debug("subscribe", element); + this.server.subscribe(client, element); + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleUnSubscribe(client, element) { + winston.debug("unsubscribe", element); + 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..741f045 --- /dev/null +++ b/EmberServer/EmberServer.js @@ -0,0 +1,519 @@ +const EventEmitter = require('events').EventEmitter; +const S101Server = require('../EmberSocket').S101Server; +const EmberLib = require('../EmberLib'); +const JSONParser = require("./JSONParser"); +const ElementHandlers = require("./ElementHandlers"); +const ServerEvents = require("./ServerEvents"); +const Errors = require("../Errors"); +const winston = require("winston"); + +class TreeServer extends EventEmitter{ + /** + * + * @param {string} host + * @param {number} port + * @param {TreeNode} tree + */ + constructor(host, port, tree) { + super(); + this._debug = true; + 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', () => { + winston.debug("listening"); + this.emit('listening'); + }); + + this.server.on('connection', client => { + winston.debug("ember new connection from", client.remoteAddress()); + this.clients.add(client); + client.on("emberTree", (root) => { + winston.debug("ember new request from", client.remoteAddress(), root); + // Queue the action to make sure responses are sent in order. + client.addRequest(() => { + try { + const path = this.handleRoot(client, root); + this.emit("request", {client: client.remoteAddress(), root: root, path: path}); + } + catch(e) { + winston.debug(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.clients.clear(); + this.emit('disconnected'); + }); + + this.server.on("error", (e) => { + this.emit("error", e); + }); + } + + /** + * @returns {Promise} + */ + close() { + return new Promise((resolve, reject) => { + const cb = e => { + if (e == null) { + return resolve(); + } + return reject(e); + }; + if (this.server.server != null) { + this.server.server.close(cb); + } + else { + cb(); + } + this.clients.clear(); + }); + } + + /** + * + * @param {Matrix} matrix + * @param {number} targetID + * @returns {number} + */ + getDisconnectSource(matrix, targetID) { + return this._handlers.getDisconnectSource(matrix, targetID); + } + + /** + * + * @param {TreeNode} element + * @returns {TreeNode} + */ + getResponse(element) { + return element.getTreeBranch(undefined, node => { + node.update(element); + const children = element.getChildren(); + if (children != null) { + for (let i = 0; i < children.length; i++) { + node.addChild(children[i].getDuplicate()); + } + } + }); + } + + /** + * + * @param {TreeNode} element + */ + getQualifiedResponse(element) { + const res = new EmberLib.Root(); + let dup; + if (element.isRoot() === false) { + dup = element.toQualified(); + } + const 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 != null) { + 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 != null) { + 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 this.server.listen(); + } + + /** + * + * @param {string} path + * @param {number} target + * @param {number[]} sources + */ + matrixConnect(path, target, sources) { + doMatrixOperation(this, path, target, sources, EmberLib.MatrixOperation.connect); + } + + /** + * + * @param {string} path + * @param {number} target + * @param {number[]} sources + */ + matrixDisconnect(path, target, sources) { + doMatrixOperation(this, path, target, sources, EmberLib.MatrixOperation.disconnect); + } + + /** + * + * @param {string} path + * @param {number} target + * @param {number[]} sources + */ + matrixSet(path, target, sources) { + 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.connections[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.connections[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 + */ + replaceElement(element) { + const path = element.getPath(); + const existingElement = this.tree.getElementByPath(path); + if (existingElement == null) { + throw new Errors.UnknownElement(path); + } + 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); + } + + /** + * + * @param {TreeNode} element + * @param {string|number} value + * @param {S101Socket} origin + * @param {string} key + */ + 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(); + } + if (element.isParameter() || element.isMatrix()) { + if (element.isParameter() && + (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); + } + 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", ServerEvents.SETVALUE(element.contents.identifier,element.getPath(),src)); + } + 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) { + winston.debug("No subscribers for", path); + return; + } + + for (let client of this.subscribers[path]) { + if (client === origin) { + 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); + } + } + } + + /** + * + * @param {object} obj + * @returns {TreeNode} + */ + static JSONtoTree(obj) { + const tree = new EmberLib.Root(); + JSONParser.parseObj(tree, obj); + return tree; + } +} + + +const validateMatrixOperation = function(matrix, target, sources) { + if (matrix == null) { + throw new Errors.UnknownElement(`matrix not found`); + } + if (matrix.contents == null) { + throw new Errors.MissingElementContents(matrix.getPath()); + } + matrix.validateConnection(target, sources); +} + +const doMatrixOperation = function(server, path, target, sources, operation) { + const matrix = server.tree.getElementByPath(path); + + validateMatrixOperation(matrix, target, sources); + + const connection = new EmberLib.MatrixConnection(target); + connection.sources = sources; + connection.operation = operation; + server._handlers.handleMatrixConnections(undefined, matrix, [connection], false); +} + +module.exports = TreeServer; diff --git a/EmberServer/JSONParser.js b/EmberServer/JSONParser.js new file mode 100755 index 0000000..dfa5015 --- /dev/null +++ b/EmberServer/JSONParser.js @@ -0,0 +1,170 @@ +"use strict"; +const ember = require('../EmberLib'); +const Errors = require("../Errors"); + +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 Errors.InvalidEmberNode("", `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 Errors.InvalidEmberNode("",`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 != null ? 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; + } + 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); + 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..0bb9f9c --- /dev/null +++ b/EmberServer/MatrixHandlers.js @@ -0,0 +1,135 @@ +"use strict"; +const EmberLib = require('../EmberLib'); +const ServerEvents = require("./ServerEvents"); +const winston = require("winston"); + +class MatrixHandlers { + /** + * + * @param {EmberServer} server + */ + constructor(server) { + this.server = server; + } + + /** + * + * @param {Matrix} matrix + * @param {number} targetID + * @return {number} + */ + 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 -1; + } + + /** + * + * @param {S101Client} client + * @param {Matrix} matrix + * @param {Object} connections + * @param {boolean} response=true + */ + handleMatrixConnections(client, matrix, connections, response = true) { + let res; // response + let conResult; + let root; // ember message root + winston.debug("Handling Matrix Connection"); + if (client != null && client.request.isQualified()) { + 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 EmberLib.MatrixNode(matrix.number); + root = matrix._parent.getTreeBranch(res); + } + res.connections = {}; + for(let id in connections) { + if (!connections.hasOwnProperty(id)) { + continue; + } + 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 EmberLib.MatrixConnection(connection.target); + res.connections[connection.target] = conResult; + + if (matrix.connections[connection.target].isLocked()) { + conResult.disposition = EmberLib.MatrixDisposition.locked; + } + else { + // Call pre-processing function + this.server.preMatrixConnect(matrix, connection, res, client, response); + } + + 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 !== 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); + } + 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(matrix, connection, res, client, response); + } + else { + conResult = this.server.disconnectSources(matrix, connection.target, connection.sources, client, response); + } + } + } + if (conResult.disposition == null){ + winston.debug(`Invalid Matrix operation ${connection.operation} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); + conResult.disposition = EmberLib.MatrixDisposition.tally; + } + + // Send response or update subscribers. + conResult.sources = matrix.connections[connection.target].sources; + } + if (client != null) { + client.sendBERNode(root); + } + + if (conResult != null && conResult.disposition !== EmberLib.MatrixDisposition.tally) { + winston.debug("Updating subscribers for matrix change"); + this.server.updateSubscribers(matrix.getPath(), root, client); + } + } + +} + +module.exports = MatrixHandlers; diff --git a/EmberServer/QualifiedHandlers.js b/EmberServer/QualifiedHandlers.js new file mode 100755 index 0000000..43755ff --- /dev/null +++ b/EmberServer/QualifiedHandlers.js @@ -0,0 +1,75 @@ +"use strict"; +const MatrixHandlers = require("./MatrixHandlers"); +const Errors = require("../Errors"); + +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 Errors.UnknownElement(path)); + return this.server.handleError(client); + } + + if (node.hasChildren()) { + for(let child of node.children) { + if (child.isCommand()) { + this.server._handlers.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 != null) { + this.server.setValue(element, parameter.contents.value, client); + let res = this.server.getQualifiedResponse(element); + client.sendBERNode(res) + } + } +} + +module.exports = QualifiedHandlers; diff --git a/EmberServer/ServerEvents.js b/EmberServer/ServerEvents.js new file mode 100755 index 0000000..62b19a5 --- /dev/null +++ b/EmberServer/ServerEvents.js @@ -0,0 +1,124 @@ +"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) { + const sourcesInfo = sources == null || sources.length === 0 ? "empty" : sources.toString(); + return new ServerEvents( + `Matrix connection to ${identifier}(path: ${path}) target ${target} connections: ${sourcesInfo} from ${src}`, + Types.MATRIX_CONNECTION + ); + } +} + +module.exports = ServerEvents; \ No newline at end of file diff --git a/EmberServer/index.js b/EmberServer/index.js new file mode 100755 index 0000000..2a6ba5c --- /dev/null +++ b/EmberServer/index.js @@ -0,0 +1,4 @@ +module.exports = { + EmberServer: require("./EmberServer"), + ServerEvents: require("./ServerEvents") +}; \ No newline at end of file diff --git a/EmberSocket/S101Client.js b/EmberSocket/S101Client.js new file mode 100755 index 0000000..d1d2527 --- /dev/null +++ b/EmberSocket/S101Client.js @@ -0,0 +1,49 @@ +"use strict"; +const net = require('net'); +const S101Socket = require("./S101Socket"); + +const DEFAULT_PORT = 9000; +class S101Client extends S101Socket { + /** + * + * @param {string} address + * @param {number} port=9000 + */ + constructor(address, port=DEFAULT_PORT) { + super(); + this.address = address; + this.port = port; + } + + /** + * + * @param {number} timeout + */ + connect(timeout = 2) { + if (this.status !== "disconnected") { + return; + } + 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'); + }) + .once("timeout", connectTimeoutListener); + this._initSocket(); + } +} + +module.exports = S101Client; diff --git a/EmberSocket/S101Server.js b/EmberSocket/S101Server.js new file mode 100755 index 0000000..d4e80ec --- /dev/null +++ b/EmberSocket/S101Server.js @@ -0,0 +1,55 @@ +"use strict"; +const EventEmitter = require('events').EventEmitter; +const S101Socket = require("./S101Socket"); +const net = require('net'); +const Errors = require("../Errors"); + +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 - tcp socket + */ + addClient(socket) { + // Wrap the tcp socket into an S101Socket. + const client = new S101Socket(socket); + this.emit("connection", client); + } + /** + * @returns {Promise} + */ + 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); + }); + } + +} + +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..9035be7 --- /dev/null +++ b/EmberSocket/S101Socket.js @@ -0,0 +1,218 @@ +"use strict"; + +const EventEmitter = require('events').EventEmitter; +const BER = require('../ber.js'); +const ember = require('../EmberLib'); +const S101Codec = require('../s101.js'); + + +class S101Socket extends EventEmitter{ + /** + * + * @param {Socket} socket + */ + constructor(socket = null) { + super(); + this.socket = socket; + this.keepaliveInterval = 10; + 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 {TreeNode} node + */ + queueMessage(node) { + this.addRequest(() => this.sendBERNode(node)); + } + + /** + * @returns {string} - ie: "10.1.1.1:9000" + */ + remoteAddress() { + if (this.socket == null) { + return "not connected"; + } + return `${this.socket.remoteAddress}:${this.socket.remotePort}` + } + + /** + * @param {number} timeout=2 + */ + disconnect(timeout = 2) { + if (!this.isConnected()) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + if (this.keepaliveIntervalTimer != null) { + clearInterval(this.keepaliveIntervalTimer); + 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(); + } + else { + reject(error); + } + }; + let timer; + if (timeout != null && (!isNaN(timeout)) && timeout > 0) { + timer = setTimeout(cb, 100 * timeout); + } + this.socket.end(cb); + 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 != null)); + } + + /** + * + * @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); + } + + 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/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/Errors.js b/Errors.js new file mode 100755 index 0000000..dcffe41 --- /dev/null +++ b/Errors.js @@ -0,0 +1,228 @@ + +/**************************************************************************** + * UnimplementedEmberType 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; + +class S101SocketError extends Error { + constructor(message) { + super(message); + } +} +module.exports.S101SocketError = S101SocketError; + +class ASN1Error extends Error { + constructor(message) { + super(message); + } +} + +module.exports.ASN1Error = ASN1Error; + +class EmberAccessError extends Error { + constructor(message) { + super(message); + } +} + +module.exports.EmberAccessError = EmberAccessError; + +class EmberTimeoutError extends Error { + constructor(message) { + super(message); + } +} + +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(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; + +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; + +class InvalidMatrixSignal extends Error { + /** + * + * @param {number} value + * @param {string} info + */ + constructor(value, info) { + super(`Invalid Matrix Signal ${value}: ${info}`); + } +} +module.exports.InvalidMatrixSignal = InvalidMatrixSignal; + +class InvalidStringPair extends Error { + /** + * + */ + constructor() { + super("Invalid StringPair Value"); + } +} +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/README.md b/README.md index 2464cc9..e2c77ae 100755 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # 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. -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,37 +17,288 @@ the way the lib is used so that now it uses Promise Server has been added in version 1.6.0. +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 + ## 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); +const EmberClient = require('node-emberplus').EmberClient; +const client = new EmberClient("10.9.8.7", 9000); +client.on("error", e => { + console.log(e); }); +client.connect() + // Get Root info + .then(() => client.getDirectory()) + // Get a Specific Node + .then(() => client.getElementByPath("0.0.2")) + .then(node => { + console.log(node); + }) + // Get a node by its path identifiers + .then(() => client.getElementByPath("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); + }); +``` + +Subsribe to changes + +```javascript +const {EmberClient, EmberLib} = require('node-emberplus'); + +const client = new EmberClient(HOST, PORT); +client.connect()) + .then(() => client.getDirectory()) + .then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) + .then(() => client.getElementByPath("scoreMaster/router/labels/group 1")) + .then(node => { + // For streams, use subscribe + return client.subscribe(node, update => { + 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); + }); + }) + // You can also provide a callback to the getElementByPath + // Be carefull that subscription will be done for all elements in the path + .then(() => client.getElementByPath("0.3", update => {console.log(update);})) + ; +``` + +### 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 +const {EmberClient, EmberLib} = require('node-emberplus'); + +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.getElementByNumber(0))) + .then(() => { + 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 {EmberClient, EmberLib} = require('node-emberplus'); + + +const client = new EmberClient(HOST, PORT); +client.connect() + .then(() => client.getDirectory()) + .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.getElementByPath(matrix.getPath())) + .then(() => client.disconnect()); + +``` + +### 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) => { var root = Decoder(data); }); +``` +### Server + +```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); +}); +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.on("event", txt => { + console.log("event: " + txt); +}) server.listen().then(() => { console.log("listening"); }).catch((e) => { console.log(e.stack); }); +``` + +### Construct Tree + +```javascript +const EmberServer = require("node-emberplus").EmberServer; +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-2", value: 0, access: "readWrite"} +]; +const labels = function(endpoints, type) { + let labels = []; + for (let i = 0; i < endpoints.length; i++) { + let endpoint = endpoints[i]; + let l = { identifier: `${type}-${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: [{basePath: "0.1.1000", description: "primary"}] + }, + { + identifier: "labels", + // path "0.1.1000" + number: 1000, + children: [ + { + identifier: "targets", + // Must be 1 + number: 1, + children: labels(targets, "t") + }, + { + identifier: "sources", + // Must be 2 + number: 2, + children: labels(sources, "s") + }, + { + identifier: "group 1", + children: [ {identifier: "sdp A", value: "A"}, {identifier: "sdp B", value: "B"}] + } + ] + }, + { + identifier: "disconnect sources", + // must be labels + 1 + number: 1001, + children: defaultSources + } + ] + }, + { + // 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" + } + ], + result: [ + { + type: ParameterType.integer, + value: null, + name: "changeCount" + } + ] + } + ] + } +]; +const root = EmberServer.JSONtoTree(jsonTree); +``` diff --git a/ber.js b/ber.js index 8518c39..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'); @@ -74,17 +74,21 @@ 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); } +/** +Value ::= + CHOICE { + integer Integer64, + real REAL, + string EmberString, + boolean BOOLEAN, + octets OCTET STRING, + null NULL + } */ ExtendedReader.prototype.readValue = function() { var tag = this.peek(); @@ -108,7 +112,7 @@ ExtendedReader.prototype.readValue = function() { ExtendedReader.prototype.readReal = function(tag) { - if(tag !== undefined) { + if(tag != null) { tag = UNIVERSAL(9); } @@ -313,7 +317,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 +325,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/client.js b/client.js deleted file mode 100755 index a60916a..0000000 --- a/client.js +++ /dev/null @@ -1,291 +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('./ember.js'); - -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.Root.decode(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.Root.decode(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', () => { - clearInterval(self.keepaliveIntervalTimer); - self.emit('disconnected'); - self.status = "disconnected"; - self.socket = null; - }); -} - -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()) { - self.socket.write(self.codec.keepAliveRequest()); - winston.debug('sent keepalive request'); - } -} - -S101Socket.prototype.sendKeepaliveResponse = function () { - var self = this; - if (self.isConnected()) { - self.socket.write(self.codec.keepAliveResponse()); - winston.debug('sent keepalive response'); - } -} - -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]); - } - } -} - -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 a238066..0000000 --- a/device.js +++ /dev/null @@ -1,518 +0,0 @@ -const EventEmitter = require('events').EventEmitter; -const util = require('util'); -const S101Client = require('./client.js').S101Socket; -const ember = require('./ember.js'); -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) { - 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).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.lastIndexOf('.') === parent.length && path.startsWith(parent) -} - -DeviceTree.prototype.getDirectory = function (qnode) { - 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 instanceof ember.Root) { - if (qnode.elements == null || qnode.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; - - 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 { - const nodeElements = node == null ? null : node.elements; - 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(); - 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()); - }}); - }); -}; - -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); - } - 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 () { - var 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++); - 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.callback = undefined; - self.clearTimeout(); - self.activeRequest = null; - try { - self.makeRequest(); - } catch(e) { - console.log("warn:" + e.message) - } -}; - -DeviceTree.prototype.timeoutRequest = function (id) { - var self = this; - self.root.cancelCallbacks(); - self.activeRequest.func(new errors.EmberTimeoutError(`Request ${id !== undefined ? id : ""} timed out`)); -}; - -DeviceTree.prototype.handleRoot = function (root) { - var self = this; - - if (self._debug) { - console.log("handling root", JSON.stringify(root)); - } - var callbacks = 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])); - } - else { - callbacks = callbacks.concat(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](); - } - } -}; - -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); - } - 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 callbacks; - } - parent.addChild(node); - callbacks = parent.update(parent); - } - element = node; - } - - var children = node.getChildren(); - if (children !== null) { - for (var i = 0; i < children.length; i++) { - if (children[i].isQualified()) { - callbacks = callbacks.concat(this.handleQualifiedNode(element, children[i])); - } - else { - callbacks = callbacks.concat(this.handleNode(element, children[i])); - } - } - } - - return callbacks; -}; - -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); - } - - var children = node.getChildren(); - if (children !== null) { - for (var i = 0; i < children.length; i++) { - callbacks = callbacks.concat(this.handleNode(n, children[i])); - } - } - else { - self.emit("value-change", node); - } - - return callbacks; -}; - -DeviceTree.prototype.getNodeByPath = function (path, timeout = 2) { - var self = this; - if (typeof path === 'string') { - 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 timedOut = false; - var cb = (error, node) => { - if (timer) { - clearTimeout(timer); - } - if (timedOut) { return; } - if (error) { - reject(error); - } else { - resolve(node); - } - self.finishRequest(); - }; - var cbTimeout = () => { - timedOut = true; - reject(timeoutError); - } - var timer = timeout === 0 ? null : setTimeout(cbTimeout, timeout * 1000); - self.root.getNodeByPath(self.client, path, cb); - }}); - }); -}; - -DeviceTree.prototype.subscribe = function (node, callback) { - if (node instanceof ember.Parameter && node.isStream()) { - // TODO: implement - } 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.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 3e594bb..0000000 --- a/ember.js +++ /dev/null @@ -1,2708 +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; -}; - -/**************************************************************************** - * Root - ***************************************************************************/ - -function Root() { - Root.super_.call(this); - - //Object.defineProperty(this, '_parent', {value: null, enumerable: false}); -}; - -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)); - r.elements = []; - 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 { - return RootElement.decode(rootReader) - } - catch (e) { - console.log(e.stack); - return r; - } - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return r; -} - -Root.prototype.addElement = function(ele) { - ele._parent = this; - if(this.elements === undefined) { - this.elements = []; - } - this.elements.push(ele); -} - - - -Root.prototype.addChild = function(child) { - this.addElement(child); -} - -Root.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(0)); - if(this.elements !== undefined) { - ber.startSequence(BER.APPLICATION(11)); - for(var i=0; i { callback(error, node) }); - } - return this.getTreeBranch(new Command(COMMAND_GETDIRECTORY)); -} - -TreeNode.prototype.subscribe = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - return this.getTreeBranch(new Command(COMMAND_SUBSCRIBE)); -} - -TreeNode.prototype.unsubscribe = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - 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 currPath = pathArray.join("."); - var number = pathArray[pathArray.length - 1]; - - for (var i = 0; i < children.length; i++) { - if ((children[i].path == currPath)|| - (children[i].number == 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 (node.number) { - res.number = node.number - } - if (node.path) { - res.path = node.path; - } - 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.getElementByPath = function(path) { - var children = this.getChildren(); - if ((children === null)||(children === undefined)) { - 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(index) { - var children = this.getChildren(); - if(children === null) return null; - for(var i=0; i 0) { - (function(cb) { - callbacks.push(() => { - cb(null, self) - }); - })(self._directoryCallbacks.shift()); - } - - for(var i=0; i { - cb(self) - }); - })(self._callbacks[i]); - } - - return callbacks; -} - -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) { - callbacks = 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 callbacks; -} - -function QualifiedNodeCommand(self, cmd, callback) { - 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) -} - -QualifiedNode.prototype.subscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedNodeCommand(this, COMMAND_SUBSCRIBE, callback) -} - -QualifiedNode.prototype.unsubscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedNodeCommand(this, COMMAND_UNSUBSCRIBE, callback) -} - -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 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.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) { - 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; - } -} - -// 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.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)) { - c.sources = seq.readRelativeOID(BER.EMBER_RELATIVE_OID).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) { - if (path) { - this.basePath = path; - } -} - -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 !== undefined) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.basePath, BER.EMBER_RELATIVE_OID); - ber.endSequence(); - } - if (this.description !== undefined) { - 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; - } -} - -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.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) { - callbacks = QualifiedMatrix.super_.prototype.update.apply(this); - MatrixUpdate(this, other); - return callbacks; -} - -function QualifiedMatrixCommand(self, cmd, callback) { - 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); -} - -QualifiedMatrix.prototype.subscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedMatrixCommand(this, COMMAND_SUBSCRIBE, callback); -} - -QualifiedMatrix.prototype.unsubscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedMatrixCommand(this, COMMAND_UNSUBSCRIBE, callback); -} - -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() { -} - - -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 = []; - seq = seq.getSequence(BER.EMBER_SEQUENCE); - while(seq.remain > 0) { - tag = seq.peek(); - var dataSeq = seq.getSequence(BER.CONTEXT(0)); - if (tag === BER.CONTEXT(0)) { - fc.arguments.push(FunctionArgument.decode(dataSeq)); - } - } - } 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++) { - this.arguments[i].encode(ber); - } - 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; 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) { - QualifiedFunction.super_.call(this); - if (path != undefined) { - this.path = path; - } -} - -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) { - callbacks = 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 callbacks; -} - -function QualifiedFunctionCommand(self, cmd, callback) { - 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) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - var QualifiedFunctionNode = QualifiedFunctionCommand(this, COMMAND_INVOKE, callback); - 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) }); - } - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedFunctionCommand(this, COMMAND_GETDIRECTORY, callback); -} - -QualifiedFunction.prototype.subscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedFunctionCommand(this, COMMAND_SUBSCRIBE, callback); -} - -QualifiedFunction.prototype.unsubscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedFunctionCommand(this, COMMAND_UNSUBSCRIBE, callback); -} - -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 { callback(error, node) }); - } - - return this.getTreeBranch(undefined, (m) => { - m.addChild(new Command(COMMAND_INVOKE)) - }); -} - - - -/**************************************************************************** - * NodeContents - ***************************************************************************/ - -function NodeContents() { - this.isOnline = true; -}; - - - -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(ber); - } - else { - // TODO: options - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return c; -} - -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.prototype.decode = function(ber) { - let invocation = new Invocation(); - 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(); - } - if(tag == BER.CONTEXT(1)) { - invocation.arguments = []; - const seq = ber.getSequence(BER.EMBER_SEQUENCE); - while(seq.remain > 0) { - invocation.arguments.push(FunctionArgument.decode(seq)); - } - } - 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() { -} -module.exports.InvocationResult = InvocationResult; - -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(); - var resTag = res.getSequence(BER.CONTEXT(0)); - if (tag === BER.CONTEXT(0)) { - invocationResult.result.push(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 { 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); -} - -QualifiedParameter.prototype.subscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedParameterCommand(this, COMMAND_SUBSCRIBE, callback); -} - -QualifiedParameter.prototype.unsubscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedParameterCommand(this, COMMAND_UNSUBSCRIBE, callback); -} - -QualifiedParameter.prototype.setValue = function(value, callback) { - if(callback !== undefined) { - this._directoryCallbacks.push(callback); - } - - let r = new Root(); - let qp = new QualifiedParameter(this.path); - r.addElement(qp); - qp.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); - return r; -} - -/**************************************************************************** - * Parameter - ***************************************************************************/ - -function Parameter(number) { - Parameter.super_.call(this); - if(number !== undefined) - this.number = number; -} - -util.inherits(Parameter, TreeNode); -module.exports.Parameter = Parameter; - -Parameter.decode = function(ber) { - var p = new Parameter(); - ber = ber.getSequence(BER.APPLICATION(1)); - - while(ber.remain > 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) { - callbacks = 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 (other.contents.hasOwnProperty(key)) { - this.contents[key] = other.contents[key]; - } - } - } - } - return callbacks; -} - - -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}`); - } -} - -module.exports.ParameterAccess = ParameterAccess; -module.exports.ParameterType = ParameterType; - -function ParameterContents(value, type) { - 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/errors.js b/errors.js deleted file mode 100755 index 9aad982..0000000 --- a/errors.js +++ /dev/null @@ -1,59 +0,0 @@ -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); -module.exports.UnimplementedEmberTypeError = UnimplementedEmberTypeError; - - -function ASN1Error(message) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.message = 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"); - } -} - -util.inherits(EmberAccessError, Error); -module.exports.EmberAccessError = EmberAccessError; - -function EmberTimeoutError(message) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.message = message; -} - -util.inherits(EmberTimeoutError, Error); -module.exports.EmberTimeoutError = EmberTimeoutError; - diff --git a/index.js b/index.js index 924f192..6727f4f 100755 --- a/index.js +++ b/index.js @@ -1,7 +1,8 @@ -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,ServerEvents} = require("./EmberServer"); +const {S101Client} = require("./EmberSocket"); +const BER = require('./ber.js') +module.exports = {EmberClient, Decoder, EmberLib, EmberServer,ServerEvents, S101, S101Client, BER}; diff --git a/package.json b/package.json index 270514d..2d5206e 100755 --- a/package.json +++ b/package.json @@ -1,36 +1,47 @@ { - "name": "emberplus", - "version": "1.8.0", + "name": "node-emberplus", + "version": "2.5.9", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { - "test": "jest test", - "eslint": "eslint ./" + "test": "jest test --coverage", + "eslint": "eslint ./", + "start": "node server.js" }, - "author": "Brian Mayton (http://bdm.cc)", + "author": "Gilles Dufour (www.gdnet.be)", "contributors": [ - { - "name": "Gilles Dufour", - "email": "dufour.gilles@gmail.com" - } + "Gilles Dufour (www.gdnet.be)", + "Brian Mayton (http://bdm.cc)" ], "repository": { "type": "git", - "url": "https://github.com/bmayton/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", "enum": "^2.4.0", "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", "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/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/server.js b/server.js deleted file mode 100755 index 8bdfee5..0000000 --- a/server.js +++ /dev/null @@ -1,611 +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 === undefined) { - return resolve(); - } - return reject(e); - }; - this.server.listen(); - }); -}; - -TreeServer.prototype.close = function () { - return new Promise((resolve, reject) => { - this.callback = (e) => { - if (e === undefined) { - return resolve(); - } - return reject(e); - }; - this.server.server.close(); - }); -}; - -TreeServer.prototype.handleRoot = function(client, root) { - if ((root === undefined) || (root.elements === undefined) || (root.elements < 1)) { - this.emit("error", new Error("invalid request")); - return; - } - - - const node = root.elements[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.number); - 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) || (element === undefined)) { - this.emit("error", new Error(`unknown element at path ${path}`)); - return this.handleError(client); - } - - if ((node.children !== undefined) && (node.children.length === 1) && - (node.children[0] instanceof ember.Command)) { - this.handleCommand(client, element, node.children[0].number); - } - 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 === undefined) { - 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 === undefined) { - 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.number); - } - 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; - var root; // ember message root - if (matrix.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. - } - 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]; - let conResult = new ember.MatrixConnection(connection.target); - let emitType; - 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 - matrix.connections[connection.target].disconnectSources(connection.sources); - emitType = "matrix-disconnect"; - } - - // Send response or update subscribers. - - if (response) { - conResult.sources = matrix.connections[connection.target].sources; - conResult.disposition = ember.MatrixDisposition.modified; - // We got a request so emit something. - this.emit(emitType, { - target: connection.target, - sources: connection.sources, - client: 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.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); -} - -const validateMatrixOperation = function(matrix, target, sources) { - if (matrix === undefined) { - throw new Error(`matrix not found with path ${path}`); - } - if (matrix.contents === undefined) { - throw new Error(`invalid matrix at ${path} : no contents`); - } - if (matrix.contents.targetCount === undefined) { - 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) { - 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.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}`)); - } -} - -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) { - let 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.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); - } - } - } - } - - let res = this.getQualifiedResponse(element); - if (this._debug) { - console.log("getDirectory response", res); - } - client.sendBERNode(res); - } -} - -TreeServer.prototype.handleSubscribe = function(client, element) { - this.subscribe(client, element); -} - -TreeServer.prototype.handleUnSubscribe = function(client, element) { - this.unsubscribe(client, element); -} - - -TreeServer.prototype.subscribe = function(client, element) { - const path = element.getPath(); - if (this.subscribers[path] === undefined) { - this.subscribers[path] = new Set(); - } - this.subscribers[path].add(client); -} - -TreeServer.prototype.unsubscribe = function(client, element) { - const path = element.getPath(); - if (this.subscribers[path] === undefined) { - 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 !== undefined) { - if (element.isParameter()) { - if ((element.contents.access !== undefined) && - (element.contents.access.value > 1)) { - element.contents.value = value; - 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); - } - } - } - }); -} - -TreeServer.prototype.replaceElement = function(element) { - let path = element.getPath(); - let parent = this.tree.getElementByPath(path); - if ((parent === undefined)||(parent._parent === undefined)) { - 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.updateSubscribers = function(path, response, origin) { - if (this.subscribers[path] === undefined) { - 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++) { - 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; - } - 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) { - 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) { - 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.targetCount !== undefined) { - 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; - } - 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; - } - 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 ((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) || (!this.tree.elements) || (this.tree.elements.length == 0)) { - return []; - } - return [].push(this.tree.elements[0].toJSON()); -}; - -module.exports = TreeServer; diff --git a/test/DeviceTree.test.js b/test/DeviceTree.test.js index 85a0037..a1c645e 100755 --- a/test/DeviceTree.test.js +++ b/test/DeviceTree.test.js @@ -1,21 +1,21 @@ 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() .then(() => new Promise((resolve, reject) => { - fs.readFile("./embrionix.ember", (e, data) => { + fs.readFile("./test/embrionix.ember", (e, data) => { if (e) { reject(e); } @@ -23,17 +23,15 @@ describe("DeviceTree", () => { }); })) .then(root => { - server = new TreeServer(LOCALHOST, PORT, root); + server = new EmberServer(LOCALHOST, PORT, root); return server.listen(); }); }); - afterAll(() => server.close()); - }); 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()) @@ -41,12 +39,50 @@ 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 EmberClient(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 EmberClient(UNKNOWN_HOST, PORT); + tree.on("error", () => { + }); + const expectedTimeoutInSec = 2; + const initialTime = Date.now(); + return tree.connect(expectedTimeoutInSec) + .then(() => { + throw new Error("Should have thrown"); + }, + error => { + 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 DeviceTree(LOCALHOST, PORT); - let stub = sinon.stub(tree.client, "sendBER"); + let tree = new EmberClient(LOCALHOST, PORT); + tree.on("error", () => { + // ignore + }); + let stub = sinon.stub(tree._client, "sendBER"); + tree._debug = true; + server._debug = true; stub.onFirstCall().returns(); stub.onSecondCall().throws(new Error("blah")); stub.callThrough(); @@ -57,45 +93,11 @@ describe("DeviceTree", () => { .then(() => { stub.restore(); 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`) + // do nothinf }); - }); + }, 10000); + }); }); diff --git a/test/Ember.test.js b/test/Ember.test.js index e1c6e23..f96b136 100755 --- a/test/Ember.test.js +++ b/test/Ember.test.js @@ -1,32 +1,1628 @@ 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'); - +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", () => { - let client; + describe("generic", () => { + let client; + + beforeEach(() => { + 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 handle Errors in message", () => { + 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", () => { + 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"); + } + }); + it("should have a 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(); + } + }); + 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", () => { + const node = new EmberLib.Node(0); + 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); + 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); + 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)); + expect(n.number).toBe(node.number); + 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 throw 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; + beforeEach(() => { + 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 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(); + }); + 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 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") + ]; + func.contents.result = [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer,null, "result") + ]; + let writer = new BER.Writer(); + func.encode(writer); + 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); - beforeAll(() => { - client = new S101Client(); + 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 throw 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 throw 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 throw 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", () => { + 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; + parameter.contents = new EmberLib.ParameterContents(VALUE, "integer"); + const newParameter = new EmberLib.Parameter(0); + const NEW_VALUE = VALUE + 1; + newParameter.contents = new EmberLib.ParameterContents(NEW_VALUE, "integer"); + parameter.update(newParameter); + 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"); + 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", () => { + 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"; + 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; - it("should parse S101 message without error", (done) => { - client.on("emberPacket", () => { - done(); + 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); + 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); }); - client.on("error", e => { - console.log(e); - expect(e).toBeUndefined(); - done(); + 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 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)); + 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 throw 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(); + } + }); + 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(); + } }); - client.codec.dataIn(s101Buffer); }); + describe("Matrix", () => { + describe("validateConnection", () => { + const PATH = "0.0.0"; + let matrixNode; + let qMatrixNode; + const TARGETCOUNT = 5; + const SOURCECOUNT = 5; + beforeEach(() => { + matrixNode = new EmberLib.MatrixNode(0); + qMatrixNode = new EmberLib.QualifiedMatrix(PATH); + 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; + 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 throw 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 throw 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 throw 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 throw 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 throw 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 throw 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 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]; + 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"); + } + catch(e) { + matrixNode.contents.mode = EmberLib.MatrixMode.linear; + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + 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]; + 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 throw 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(); + }); + 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; + beforeEach(() => { + 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 update connections", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; - it("should handle errors in message", () => { - var ber = new BER.Reader(errorBuffer); - expect(() => ember.Root.decode(ber)).toThrow(errors.UnimplementedEmberTypeError); - }) + 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( + 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; + beforeEach(() => { + 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]); + }); + 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; + beforeEach(() => { + 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("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; + beforeEach(() => { + 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 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; + 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(); + }); + 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 + ); + 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"; + qMatrixNode.contents = matrixNode.contents; + matrixNode.targets = [0,3]; + qMatrixNode.targets = matrixNode.targets; + matrixNode.sources = [1,2]; + 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); + 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(); + 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(); + }); + it("should throw 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 throw 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(); + } + }); + }); + 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(); + }); + 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", () => { + 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/EmberClient.test.js b/test/EmberClient.test.js new file mode 100755 index 0000000..66f66cd --- /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 = 9010; + +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(() => { + return 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(); + }); + }); +}); diff --git a/test/Server.test.js b/test/Server.test.js index ed44416..0ec6105 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -1,211 +1,1051 @@ const expect = require("expect"); -const TreeServer = require("../server"); -const DeviceTree = require("../").DeviceTree; -const ember = require("../ember"); +const {EmberServer, ServerEvents} = require("../EmberServer"); +const EmberClient = require("../EmberClient"); +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; - -const wait = function(t) { - return new Promise(resolve => { - setTimeout(resolve, t); - }); -} +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() { - const root = TreeServer.JSONtoTree(jsonTree); + const root = EmberServer.JSONtoTree(jsonTree); + // JSONtoTree will modify the json object. + jsonTree = jsonRoot(); 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(2); + 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"; + 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); + 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"; + 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(); + }); + 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() { - let server,client; - beforeAll(function() { + let server,client,jsonTree; + beforeEach(() => { jsonTree = jsonRoot(); - const root = TreeServer.JSONtoTree(jsonTree); - server = new TreeServer(LOCALHOST, PORT, root); - //server._debug = true; - return server.listen().then(() => { - console.log("server listening"); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + server.on("error", e => { + // ignore + }); + server.on("clientError", e => { + // ignore }); + //server._debug = true; + return server.listen(); + }); + afterEach(() => { + return server.close(); }); - afterAll(function() { - client.disconnect(); - server.close(); - }) - it("should receive and decode the full tree", function () { - client = new DeviceTree(LOCALHOST, PORT); + it("should receive and decode the full tree", () => { + client = new EmberClient(LOCALHOST, PORT); //client._debug = true; return Promise.resolve() .then(() => client.connect()) - .then(() => { - console.log("client connected"); - return client.getDirectory(); - }) + .then(() => client.getDirectory()) .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(jsonTree.length); + 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(2); - 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.getElementByPath("0.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.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 - return client.disconnect(); + return client.disconnect(); }); }); - it("should be able to modify a parameter", () => { - client = new DeviceTree(LOCALHOST, PORT); + it("should be able to modify a parameter", async () => { + client = new EmberClient(LOCALHOST, PORT); + 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()) + return client.connect() + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("0.2")) .then(() => { - return client.getDirectory(); + const func = client.root.getElementByPath("0.2"); + return client.invokeFunction(func, [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 1), + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 7) + ]); }) - .then(() => client.expand(client.root.elements[0])) + .then(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 client.getElement", function() { + client = new EmberClient(LOCALHOST, PORT); + return Promise.resolve() + .then(() => client.connect()) + .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() { + client = new EmberClient(LOCALHOST, PORT); + return Promise.resolve() + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("scoreMaster/identity/product")) + .then(() => client.getElementByPath("scoreMaster/router/labels/group 1")) + .then(() => client.disconnect()); + }); + it("should throw an error if getElementByPath for unknown path", function() { + client = new EmberClient(LOCALHOST, PORT); + return Promise.resolve() + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("scoreMaster/router/labels/group")) .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"); + throw new Error("Should not succeed"); }) - .then(() => { - expect(server.tree.elements[0].children[0].children[1].contents.value).toBe("gdnet"); - return client.disconnect(); + .catch(e => { + expect(e.message).toMatch(/Failed path discovery/); + return client.disconnect(); }); }); it("should be able to make a matrix connection", () => { - client = new DeviceTree(LOCALHOST, PORT); - //client._debug = true; + client = new EmberClient(LOCALHOST, PORT); + return Promise.resolve() + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("0.1.0")) + .then(matrix => client.matrixConnect(matrix, 0, [1])) + .then(matrix => client.getElementByPath(matrix.getPath())) + .then(matrix => { + expect(matrix.connections['0'].sources).toBeDefined(); + expect(matrix.connections['0'].sources.length).toBe(1); + 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(() => 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 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()) + expect(count).toBe(1); + expect(receivedEvent.type).toBe(ServerEvents.Types.GETDIRECTORY); + return client.getElementByPath("0.1.0"); + }) + .then(matrix => { + count = 0; + return client.matrixConnect(matrix, 0, [1]); + }) .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); - } - }); - }); + expect(count).toBe(1); + expect(receivedEvent.type).toBe(ServerEvents.Types.MATRIX_CONNECTION); + }) + .then(() => { + count = 0; + const func = client.root.getElementByPath("0.2"); + return client.invokeFunction(func, [ + 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(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(() => client.getElementByPath("0.0.2")) + .then(parameter => { + server._subscribe = server.subscribe; + let _resolve; + const p = new Promise((resolve) => { + _resolve = resolve; }); + server.subscribe = (c,e) => { + server._subscribe(c,e); + _resolve(); + }; + count = 0; + return client.subscribe(parameter).then(() => (p)) }) - .then(child => { - console.log(child); - client.disconnect(); + .then(() => { + expect(count).toBe(1); + expect(receivedEvent.type).toBe(ServerEvents.Types.SUBSCRIBE); + }) + .then(() => { + server.off("event", eventHandler); + }) + .then(() => client.disconnect()); + }); + }); + describe("Matrix Connect", function() { + let jsonTree; + let server; + beforeEach(function() { + jsonTree = jsonRoot(); + 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 = () => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); + const matrix = server.tree.getElementByPath("0.1.0"); + let connection = new EmberLib.MatrixConnection(0); + connection.setSources([1]); + 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 = 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. + 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 + const matrixHandlers = new MatrixHandlers(server); + matrixHandlers.getDisconnectSource(matrix, 0); + matrix.defaultSources[0].contents.value = 222; + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); + expect(disconnectCount).toBe(2); + expect(matrix.connections[0].sources[0]).toBe(222); + matrix.setSources(0, [0]); + 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(); + }); + it("should verify if connection allowed in 1-to-1", function() { + const matrix = server.tree.getElementByPath("0.1.0"); + let disconnectCount = 0; + const handleDisconnect = () => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); + matrix.contents.type = EmberLib.MatrixType.oneToOne; + const connection = new EmberLib.MatrixConnection(0); + connection.setSources([1]); + 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(); + // We can't connect but in 1-on-1 server should disconnect existing source and connect new one. + 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._handlers.handleMatrixConnections(null, matrix, {0: connection}); + expect(disconnectCount).toBe(2); + connection.operation = EmberLib.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(); + 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.getElementByPath("0.1.0"); + let disconnectCount = 0; + const handleDisconnect = () => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); + matrix.contents.type = EmberLib.MatrixType.oneToOne; + matrix.setSources(0, [1]); + const connection = new EmberLib.MatrixConnection(0); + connection.setSources([1]); + connection.operation = EmberLib.MatrixOperation.connect; + 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.getElementByPath("0.1.0"); + let disconnectCount = 0; + const handleDisconnect = () => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); + matrix.contents.type = EmberLib.MatrixType.oneToOne; + matrix.setSources(0, [1]); + matrix.connections[0].lock(); + const connection = new EmberLib.MatrixConnection(0); + connection.setSources([0]); + 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); + expect(disconnectCount).toBe(0); + }); + it("should verify if connection allowed in N-to-N", function() { + const matrix = server.tree.getElementByPath("0.1.0"); + matrix.contents.type = EmberLib.MatrixType.nToN; + matrix.contents.maximumTotalConnects = 2; + matrix.setSources(0, [0,1]); + + const connection = new EmberLib.MatrixConnection(0); + connection.setSources([2]); + connection.operation = EmberLib.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 = EmberLib.MatrixOperation.absolute; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + + + 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 = EmberLib.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 = 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", () => { + // ignore + }); + server.on("clientError", () => { + // ignore + }); + //server._debug = true; + return server.listen() + .then(() => { + client = new EmberClient(LOCALHOST, PORT); + return Promise.resolve() + }) + .then(() => client.connect()) + .then(() => client.getDirectory()) + .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(EmberLib.MatrixDisposition.modified); + return client.disconnect(); }); - }); - it("should be able to get child with tree.getNodeByPath", function() { - //server._debug = true; - client = new DeviceTree(LOCALHOST, PORT); - //client._debug = true; + }); + }); + describe("Parameters subscribe/unsubscribe", function( ){ + 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 not auto subscribe stream parameter", function() { + const parameter = server.tree.getElementByPath("0.0.2"); + expect(parameter.isStream()).toBeTruthy(); + expect(server.subscribers["0.0.2"]).not.toBeDefined(); + }); + it("should be able subscribe to parameter changes", function() { + const client = new EmberClient(LOCALHOST, PORT); + const cb = () => { + return "updated"; + } //client._debug = true; 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); - return client.getNodeByPath("scoreMaster/router/labels/group 1"); - }) - .then(child => { - 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() - .then(() => client.connect()) - .then(() => { - console.log("client connected"); - return client.getDirectory(); + .then(() => client.getElementByPath("0.0.2")) + .then(parameter => { + expect(server.subscribers["0.0.2"]).not.toBeDefined(); + expect(parameter._subscribers).toBeDefined(); + expect(parameter._subscribers.size).toBe(0); + server._subscribe = server.subscribe; + let _resolve; + const p = new Promise(resolve => { + _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.getElementByPath("0.0.2"); }) - .then(() => client.getNodeByPath("scoreMaster/router/labels/group 1")) - .then(child => { - console.log("router/labels", child); - throw new Error("Should not succeed"); - }) - .catch(e => { - client.disconnect(); - console.log(e); - expect(e.message).toMatch(/timeout/); - }); - }); + .then(parameter => { + expect(parameter._subscribers).toBeDefined(); + expect(parameter._subscribers.size).toBe(1); + server._unsubscribe = server.unsubscribe; + let _resolve; + const p = new Promise(resolve => { + _resolve = resolve; + }); + server.unsubscribe = (c,e) => { + server._unsubscribe(c,e); + _resolve(); + }; + return client.unsubscribe(parameter, cb).then(() => (p)) + }) + .then(() => { + expect(server.subscribers["0.0.2"]).toBeDefined(); + return client.getElementByPath("0.0.2"); + }) + .then(parameter => { + expect(server.subscribers["0.0.2"]).toBeDefined(); + expect(server.subscribers["0.0.2"].size).toBe(0); + expect(parameter._subscribers).toBeDefined(); + expect(parameter._subscribers.size).toBe(0); + }) + .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); + 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", () => { + const 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); + const 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); + const server = new EmberServer(LOCALHOST, PORT, root); + const newParam = new EmberLib.Parameter(1000); + 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); + 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); + 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 tcp server disconnected message, and clean up clients", () => { + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + const 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"; + const 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/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/embrionix.ember b/test/embrionix.ember similarity index 100% rename from embrionix.ember rename to test/embrionix.ember diff --git a/test/utils.js b/test/utils.js index 74b6eb9..9cca4a4 100755 --- a/test/utils.js +++ b/test/utils.js @@ -1,13 +1,19 @@ -const {ParameterAccess} = require("../ember"); +const {ParameterType, FunctionArgument} = require("../EmberLib"); + 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 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++) { let endpoint = endpoints[i]; - let l = { identifier: `Label-${i}` }; + let l = { identifier: `${type}-${i}` }; if (endpoint) { l.value = endpoint; } @@ -33,9 +39,9 @@ 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"}, + {identifier: "version", value: "1.2.0", access: "readWrite", streamIdentifier: 1234567}, {identifier: "author", value: "g.dufour@evs.com"}, ] }, @@ -49,9 +55,9 @@ const init = function(_src,_tgt) { type: "oneToN", mode: "linear", targetCount: targets.length, - sourceCount: sources.length, + sourceCount: sources.length, connections: buildConnections(sources, targets), - labels: ["0.1.1000"] + labels: [{basePath: "0.1.1000", description: "primary"}], }, { identifier: "labels", @@ -62,23 +68,70 @@ 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", children: [ {identifier: "sdp A", value: "A"}, {identifier: "sdp B", value: "B"}] } ] + }, + { + identifier: "disconnect sources", + number: 1001, + children: defaultSources + } + ] + }, + { + // 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" + } + ], + result: [ + { + type: ParameterType.integer, + value: null, + name: "changeCount" } ] } ] + }, + { + identifier: "PeakValue_2", + type: 2, + streamIdentifier: 4, + streamDescriptor: { + format: "ieeeFloat32LittleEndian", + offset: 4 + }, + access: 1, + maximum: 20, + minimum: -200, + value: -200 } ]; }