diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100755 index 0000000..fe05738 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + "env": { + "es6": true, + "node": true, + "jest": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2015 + }, + "rules": { + "no-unused-vars": "warn", + "no-undef": "warn", + "no-redeclare": "warn", + "no-extra-semi": "warn", + "no-console": "warn", + "semi-style": + [ + "error", + "last" + ] + } +} +; diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index d6166ec..e77824c --- a/.gitignore +++ b/.gitignore @@ -1,2 +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..02d7a36 --- /dev/null +++ b/EmberLib/Label.js @@ -0,0 +1,71 @@ +"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"); + } + else { + 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..d72552b --- /dev/null +++ b/EmberLib/Matrix.js @@ -0,0 +1,443 @@ +"use strict"; +const MatrixConnection = require("./MatrixConnection"); +const TreeNode = require("./TreeNode"); +const BER = require('../ber'); +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; + /**@type number[] | null */ + this.targets = null; + /**@type number[] | 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]; + this.validateConnection(matrix, connection.target, connection.sources); + 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; + } + } + } + } + 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 { + if (!matrixNode.targets.includes(targetID)) { + throw new Errors.InvalidMatrixSignal(targetID, "Not part of existing targets"); + } + for(let i = 0; i < sources.length; i++) { + if (!matrixNode.sources.includes(sources[i])) { + 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..7c9be2c --- /dev/null +++ b/EmberLib/QualifiedElement.js @@ -0,0 +1,58 @@ +"use strict"; +const TreeNode = require("./TreeNode"); +const BER = require('../ber.js'); +const Command = require("./Command"); + +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..65384f8 --- /dev/null +++ b/EmberLib/QualifiedFunction.js @@ -0,0 +1,61 @@ +"use strict"; + +const QualifiedElement = require("./QualifiedElement"); +const FunctionContent = require("./FunctionContent"); +const BER = require('../ber.js'); +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..b3f2a6a --- /dev/null +++ b/EmberLib/QualifiedMatrix.js @@ -0,0 +1,107 @@ +"use strict"; + +const Matrix = require("./Matrix"); +const BER = require('../ber'); +const MatrixContents = require("./MatrixContents"); +const MatrixConnection = require("./MatrixConnection"); +const errors = require("../Errors"); +console.log(Matrix) +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..e84f314 --- /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} = require("./constants"); +const Errors = require("../Errors"); + +class TreeNode extends ElementInterface { + constructor() { + super(); + /** @type {TreeNode} */ + this._parent = null; + this._subscribers = new Set(); + this.hidden = false; + } + + _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: elements.map(e => e.toJSON())}: {elements: []}; + } + 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; + 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/._JSONParser.js b/EmberServer/._JSONParser.js new file mode 100755 index 0000000..56a2ca8 Binary files /dev/null and b/EmberServer/._JSONParser.js differ 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..b3ce9e0 --- /dev/null +++ b/EmberServer/EmberServer.js @@ -0,0 +1,522 @@ +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++) { + if (children[i].hidden) { + continue; + } + 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..52b66bf --- /dev/null +++ b/EmberServer/JSONParser.js @@ -0,0 +1,174 @@ +"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); + if (content.hidden === 'true' || content.hidden === true) { + emberElement.hidden = true; + } + delete content.hidden; + 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/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index d8463af..953ebf1 --- a/README.md +++ b/README.md @@ -1,45 +1,301 @@ # 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. -It is, at this point in time, still very much a work in progress. I am -developing it primarily to control our instance of [Virtual Patch -Bay](http://www.r3lay.com/product/vpb-virtual-patch-bay/), which seems to make -use of only a subset of of the Glow DTD. As such, I'm focusing on -implementing the parts of the DTD that are necessary for my use case. Please -consider this highly experimental code; any use in production environments is -entirely at your own risk. +This version support following ember objects : Node, Parameter, Matrix, Function, QualifiedNode, +QualifiedParameter, QualifiedMatrix, QualifiedFunction. -Basic trees of parameters should work. Streams aren't there yet, but -shouldn't be too far off. Everything else is probably missing at this point -(e.g. the newer matrix stuff). +It has been tested with EVS XT4k and Embrionix IP solutions. + +The current version has added new features to the initial commit but it also modified +the way the lib is used so that now it uses Promise + +Server has been added in version 1.6.0. + +This version doesn't have support for StreamCollection, UDP, packet stats/rate or for tree with size higher than 8M. ## Example usage +### Client + +Get Full tree: + +```javascript +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 DeviceTree = require('emberplus').DeviceTree; +const {EmberClient, EmberLib} = require('node-emberplus'); -var tree = new DeviceTree("localhost", 9998); +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")}); +``` -tree.on('ready', () => { - tree.getNodeByPath("EmberDevice/Sources/Monitor/Amplification").then((node) => { - - // Subscribe to parameter changes - tree.subscribe(node, (node) => { - console.log("Volume changed: %d", node.contents.value); - }); +### Invoking Function - // Change parameter value - tree.setValue(node, -20.0); +```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) + ]); + }); +``` - }).catch((e) => { - console.log("Failed to resolve node:", e); - }); +### 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('node-emberplus').Decoder; +const fs = require("fs"); + +fs.readFile("tree.ember", (e,data) => { + var root = Decoder(data); }); +``` + +### Server + +```javascript +// Server +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 old mode 100644 new mode 100755 index e1454e7..72ae011 --- 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'); @@ -30,15 +30,42 @@ var APPLICATION = function(x) { return x | 0x60; }; var CONTEXT = function(x) { return x | 0xa0; }; var UNIVERSAL = function(x) { return x; }; -const EMBER_SET = 0x20 | 17; -const EMBER_STRING = 12; +const EMBER_BOOLEAN = 1; +const EMBER_INTEGER = 2; +const EMBER_BITSTRING = 3; +const EMBER_OCTETSTRING = 4; +const EMBER_NULL = 5; +const EMBER_OBJECTIDENTIFIER = 6; +const EMBER_OBJECTDESCRIPTOR = 7; +const EMBER_EXTERNAL = 8; +const EMBER_REAL = 9; +const EMBER_ENUMERATED = 10; +const EMBER_EMBEDDED = 11; +const EMBER_STRING = 12; +const EMBER_RELATIVE_OID = 13; + +const EMBER_SEQUENCE = 0x20 | 16; +const EMBER_SET = 0x20 | 17; module.exports.APPLICATION = APPLICATION; module.exports.CONTEXT = CONTEXT; module.exports.UNIVERSAL = UNIVERSAL; module.exports.EMBER_SET = EMBER_SET; +module.exports.EMBER_SEQUENCE = EMBER_SEQUENCE; +module.exports.EMBER_BOOLEAN = EMBER_BOOLEAN; +module.exports.EMBER_INTEGER = EMBER_INTEGER; +module.exports.EMBER_BITSTRING = EMBER_BITSTRING; +module.exports.EMBER_OCTETSTRING = EMBER_OCTETSTRING; +module.exports.EMBER_NULL = EMBER_NULL; +module.exports.EMBER_OBJECTIDENTIFIER = EMBER_OBJECTIDENTIFIER; +module.exports.EMBER_OBJECTDESCRIPTOR = EMBER_OBJECTDESCRIPTOR; +module.exports.EMBER_EXTERNAL = EMBER_EXTERNAL; +module.exports.EMBER_REAL = EMBER_REAL; +module.exports.EMBER_ENUMERATED = EMBER_ENUMERATED; +module.exports.EMBER_EMBEDDED = EMBER_EMBEDDED; module.exports.EMBER_STRING = EMBER_STRING; +module.exports.EMBER_RELATIVE_OID = EMBER_RELATIVE_OID; function ExtendedReader(data) { ExtendedReader.super_.call(this, data); @@ -52,25 +79,40 @@ ExtendedReader.prototype.getSequence = function(tag) { return new ExtendedReader(buf); } +/** +Value ::= + CHOICE { + integer Integer64, + real REAL, + string EmberString, + boolean BOOLEAN, + octets OCTET STRING, + null NULL + } */ ExtendedReader.prototype.readValue = function() { - var tag = this.peek(tag); + var tag = this.peek(); + if(tag == EMBER_STRING) { return this.readString(EMBER_STRING); - } else if(tag == UNIVERSAL(2)) { + } else if(tag == EMBER_INTEGER) { return this.readInt(); - } else if(tag == UNIVERSAL(9)) { + } else if(tag == EMBER_REAL) { return this.readReal(); - } else if(tag == UNIVERSAL(1)) { + } else if(tag == EMBER_BOOLEAN) { return this.readBoolean(); - } else if(tag == UNIVERSAL(4)) { + } else if(tag == EMBER_OCTETSTRING) { return this.readString(UNIVERSAL(4), true); - } else { + } else if (tag === EMBER_RELATIVE_OID) { + return this.readOID(EMBER_RELATIVE_OID); + } + else { throw new errors.UnimplementedEmberTypeError(tag); } } + ExtendedReader.prototype.readReal = function(tag) { - if(tag !== undefined) { + if(tag != null) { tag = UNIVERSAL(9); } @@ -241,21 +283,41 @@ ExtendedWriter.prototype.writeReal = function(value, tag) { } ExtendedWriter.prototype.writeValue = function(value, tag) { - if(Number.isInteger(value)) { + // accepts Ember.ParameterContents for enforcing real types + if(typeof value === 'object' && value.type && value.type.key && value.type.key.length && typeof value.type.key === 'string') { + if(value.type.key === 'real') { + this.writeReal(value.value, tag); + return + } + } + + if(Number.isInteger(value)) { + if (tag === undefined) { + tag = EMBER_INTEGER; + } this.writeInt(value, tag); } else if(typeof value == 'boolean') { + if (tag === undefined) { + tag = EMBER_BOOLEAN; + } this.writeBoolean(value, tag); } else if(typeof value == 'number') { + if (tag === undefined) { + tag = EMBER_REAL; + } this.writeReal(value, tag); } else if(Buffer.isBuffer(value)) { this.writeBuffer(value, tag); } else { + if (tag === undefined) { + tag = EMBER_STRING; + } this.writeString(value.toString(), 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(); @@ -263,9 +325,9 @@ ExtendedWriter.prototype.writeIfDefined = function(property, writer, outer, inne } ExtendedWriter.prototype.writeIfDefinedEnum = function(property, type, writer, outer, inner) { - if(property !== undefined) { - this.startSequence(BER.CONTEXT(outer)); - if(property.value !== undefined) { + if(property != null) { + this.startSequence(CONTEXT(outer)); + 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 100644 index 995522a..0000000 --- a/client.js +++ /dev/null @@ -1,118 +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 S101Client(address, port) { - var self = this; - S101Client.super_.call(this); - - self.address = address; - self.port = port; - self.socket = null; - - self.codec = new S101Codec(); - self.connect(); - - setInterval(() => { - self.sendKeepaliveRequest(); - }, 10000); - - 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); - self.emit('emberTree', root); - } catch(e) { - console.log(e); - } - }); -} - -util.inherits(S101Client, EventEmitter); - -S101Client.prototype.connect = function() { - var self = this; - self.emit('connecting'); - console.log("socket connecting"); - - self.socket = net.createConnection(self.port, self.address, () => { - winston.debug('socket connected'); - self.emit('connected'); - }); - - self.socket.on('data', (data) => { - self.codec.dataIn(data); - }); - - self.socket.on('close', () => { - self.emit('disconnected'); - self.socket = null; - }); - - self.socket.on('error', (e) => { - //self.emit('disconnected'); - //self.socket = null; - console.log("Socket error", e); - }); -} - -S101Client.prototype.disconnect = function() { - var self = this; - if(self.socket !== null) { - self.socket.destroy(); - self.socket = null; - } -} - -S101Client.prototype.sendKeepaliveRequest = function() { - var self = this; - if(self.socket !== null) { - self.socket.write(self.codec.keepAliveRequest()); - winston.debug('sent keepalive request'); - } -} - -S101Client.prototype.sendKeepaliveResponse = function() { - var self = this; - if(self.socket !== null) { - self.socket.write(self.codec.keepAliveResponse()); - winston.debug('sent keepalive response'); - } -} - -S101Client.prototype.sendBER = function(data) { - var self = this; - var frames = self.codec.encodeBER(data); - for(var i=0; i { - self.emit('connecting'); - }); - - self.client.on('connected', () => { - self.root.clear(); - self.client.sendBERNode(self.root.getDirectory((node) => { - self.emit('ready'); - })); - }); - - self.client.on('disconnected', () => { - self.emit('disconnected'); - self.connectTimeout = setTimeout(() => { - self.client.connect(); - }, 10000); - }); - - self.client.on('emberTree', (root) => { - self.handleRoot(root); - }); -} - -util.inherits(DeviceTree, EventEmitter); - -DeviceTree.prototype.disconnect = function() { - this.client.disconnect(); -} - -DeviceTree.prototype.makeRequest = function() { - var self=this; - if(self.activeRequest === null && self.pendingRequests.length > 0) { - self.activeRequest = self.pendingRequests.shift(); - self.activeRequest(); - self.timeout = setTimeout(() => { - self.timeoutRequest(); - }, 300); - } -}; - -DeviceTree.prototype.addRequest = function(cb) { - var self=this; - self.pendingRequests.push(cb); - self.makeRequest(); -} - -DeviceTree.prototype.finishRequest = function() { - var self=this; - if(self.timeout != null) { - clearTimeout(self.timeout); - self.timeout = null; - } - self.activeRequest = null; - self.makeRequest(); -} - -DeviceTree.prototype.timeoutRequest = function() { - var self = this; - self.root.cancelCallbacks(); - self.activeRequest(new errors.EmberTimeoutError('Request timed out')); -} - -DeviceTree.prototype.handleRoot = function(root) { - var self=this; - - var callbacks = self.root.update(root); - - if(root.elements !== undefined) { - for(var i=0; i { - self.addRequest((error) => { - if(error) { - reject(error); - self.finishRequest(); - return; - } - self.root.getNodeByPath(self.client, path, (error, node) => { - if(error) { - reject(error); - } else { - resolve(node); - } - self.finishRequest(); - }); - }); - }); -} - -DeviceTree.prototype.subscribe = 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)) { - reject(new errors.EmberAccessError('not a property')); - } else if(node.contents !== undefined && node.contents.value == value) { - resolve(node); - } else { - console.log('setValue', node.getPath(), value); - self.addRequest((error) => { - if(error) { - reject(error); - self.finishRequest(); - return; - } - - - self.client.sendBERNode(node.setValue(value, (error, node) => { - if(error) { - reject(error); - } else { - resolve(node); - } - self.finishRequest(); - })); - }); - } - }); -} - -function TreePath(path) { - this.identifiers = []; - this.numbers = []; - - if(path !== undefined) { - for(var i=0; i 0) { - r.addElement(RootElement.decode(seq)); - } - - } else { - // StreamCollection BER.APPLICATION(6) - // InvocationResult BER.APPLICATION(23) - 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)); - ber.startSequence(BER.CONTEXT(0)); - for(var i=0; i { callback(error, node) }); - } - return this.getTreeBranch(new Command(32)); -} - -TreeNode.prototype.getChildren = function() { - if(this.children !== undefined) { - return this.children; - } - return null; -} - -TreeNode.prototype.getElementByNumber = function(index) { - var children = this.getChildren(); - if(children === null) return null; - for(var i=0; i 0) { - (function(cb) { - callbacks.push(() => { - console.log(this.constructor.name, "dir cb", self.getPath()); - cb(null, self) - }); - })(self._directoryCallbacks.shift()); - } - //} - - for(var i=0; i { - console.log(self.constructor.name, "cb", self.getPath()); - 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) { - console.log("inv:", path[0], self); - callback('invalid path'); - return; - } else { - child.getNodeByPath(client, path.slice(1), callback); - } - }); - if(cmd !== null) { - client.sendBERNode(cmd); - } - } -} - -TreeNode.prototype.getPath = function() { - 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)) { - // Parameter - return Parameter.decode(ber); - } else if(tag == BER.APPLICATION(3)) { - // Node - return Node.decode(ber); - } else if(tag == BER.APPLICATION(2)) { - // Command - return Command.decode(ber); - } else if(tag == BER.APPLICATION(13)) { - // Matrix - throw new errors.UnimplementedEmberTypeError(tag); - } else if(tag == BER.APPLICATION(19)) { - // Function - throw new errors.UnimplementedEmberTypeError(tag); - } else if(tag == BER.APPLICATION(24)) { - // Template - throw new errors.UnimplementedEmberTypeError(tag); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } -} - -/**************************************************************************** - * ElementCollection - ***************************************************************************/ - - - -/**************************************************************************** - * Node - ***************************************************************************/ - -function Node(number) { - Node.super_.call(this); - if(number !== undefined) - this.number = number; -}; - -util.inherits(Node, TreeNode); - -Node.decode = function(ber) { - var n = new Node(); - ber = ber.getSequence(BER.APPLICATION(3)); - - while(ber.remain > 0) { - var tag = ber.readSequence(); - if(tag == BER.CONTEXT(0)) { - n.number = ber.readInt(); - } else if(tag == BER.CONTEXT(1)) { - n.contents = NodeContents.decode(ber); - } else if(tag == BER.CONTEXT(2)) { - n.children = []; - var seq = ber.getSequence(BER.APPLICATION(4)); - while(seq.remain > 0) { - seq.readSequence(BER.CONTEXT(0)); - n.addChild(Element.decode(seq)); - } - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - 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.readSequence(); - if(tag == BER.CONTEXT(0)) { - nc.identifier = ber.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - nc.description = ber.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(2)) { - nc.isRoot = ber.readBoolean(); - } else if(tag == BER.CONTEXT(3)) { - nc.isOnline = ber.readBoolean(); - } else if(tag == BER.CONTEXT(4)) { - nc.schemaIdentifiers = ber.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; -} - -Command.decode = function(ber) { - var c = new Command(); - ber = ber.getSequence(BER.APPLICATION(2)); - - while(ber.remain > 0) { - var tag = ber.readSequence(); - if(tag == BER.CONTEXT(0)) { - c.number = ber.readInt(); - } 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) - - // TODO: options - - ber.endSequence(); // BER.APPLICATION(2) -} - -module.exports.Command = Command; - -/**************************************************************************** - * 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.readSequence(); - if(tag == BER.CONTEXT(0)) { - p.number = ber.readInt(); - } else if(tag == BER.CONTEXT(1)) { - p.contents = ParameterContents.decode(ber); - } else if(tag == BER.CONTEXT(2)) { - p.children = []; - var seq = ber.getSequence(BER.APPLICATION(4)); - while(seq.remain > 0) { - seq.readSequence(BER.CONTEXT(0)); - p.addChild(Element.decode(seq)); - } - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - 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 = new ParameterContents(value); - }); -} - -Parameter.prototype.update = function(other) { - callbacks = Parameter.super_.prototype.update.apply(this); - //console.log('update', this.getPath()); - //console.log(callbacks); - if(other.contents !== undefined) { - //console.log("other: ", other.contents); - for(var key in other.contents) { - //console.log(key, other.contents.hasOwnProperty(key)); - if(other.contents.hasOwnProperty(key)) { - this.contents[key] = other.contents[key]; - } - } - } - return callbacks; -} - -Parameter.prototype.isStream = function() { - return this.contents !== undefined && - this.contents.streamDescriptor !== undefined; -} - -var ParameterAccess = new Enum({ - none: 0, - read: 1, - write: 2, - readWrite: 3 -}); - -var ParameterType = new Enum({ - integer: 1, - real: 2, - string: 3, - boolean: 4, - trigger: 5, - enum: 6, - octets: 7 -}); - -function ParameterContents(value) { - if(value !== undefined) { - this.value = value; - } -}; - -ParameterContents.decode = function(ber) { - var pc = new ParameterContents(); - ber = ber.getSequence(BER.EMBER_SET); - - while(ber.remain > 0) { - var tag = ber.readSequence(); - if(tag == BER.CONTEXT(0)) { - pc.identifier = ber.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - pc.description = ber.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(2)) { - pc.value = ber.readValue(); - } else if(tag == BER.CONTEXT(3)) { - pc.minimum = ber.readValue(); - } else if(tag == BER.CONTEXT(4)) { - pc.maximum = ber.readValue(); - } else if(tag == BER.CONTEXT(5)) { - pc.access = ParameterAccess.get(ber.readInt()); - } else if(tag == BER.CONTEXT(6)) { - pc.format = ber.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(7)) { - pc.enumeration = ber.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(8)) { - pc.factor = ber.readInt(); - } else if(tag == BER.CONTEXT(9)) { - pc.isOnline = ber.readBoolean(); - } else if(tag == BER.CONTEXT(10)) { - pc.formula = ber.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(11)) { - pc.step = ber.readInt(); - } else if(tag == BER.CONTEXT(12)) { - pc.default = ber.readValue(); - } else if(tag == BER.CONTEXT(13)) { - pc.type = ParameterType.get(ber.readInt()); - } else if(tag == BER.CONTEXT(14)) { - pc.streamIdentifier = ber.readInt(); - } else if(tag == BER.CONTEXT(15)) { - pc.enumMap = StringIntegerCollection.decode(ber); - } else if(tag == BER.CONTEXT(16)) { - pc.streamDescriptor = StreamDescription.decode(ber); - } else if(tag == BER.CONTEXT(17)) { - pc.schemaIdentifiers = ber.readString(BER.EMBER_STRING); - } 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) { - ber.readSequence(BER.CONTEXT(0)); - var seq = ber.getSequence(BER.APPLICATION(7)); - var entryString, entryInteger; - while(seq.remain > 0) { - var tag = seq.readSequence(); - if(tag == BER.CONTEXT(0)) { - entryString = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - entryInteger = seq.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.readSequence(); - if(tag == BER.CONTEXT(0)) { - sd.format = StreamFormat.get(ber.readInt()); - } else if(tag == BER.CONTEXT(1)) { - sd.offset = ber.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 100644 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 old mode 100644 new mode 100755 index e5c946d..6727f4f --- a/index.js +++ b/index.js @@ -1,5 +1,8 @@ -const DeviceTree = require('./device.js'); - -module.exports = { - DeviceTree -} +const EmberClient = require('./EmberClient'); +const EmberLib = require("./EmberLib"); +const Decoder = EmberLib.DecodeBuffer; +const S101 = require("./s101"); +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 old mode 100644 new mode 100755 index 57b1ffb..b2e1cdb --- a/package.json +++ b/package.json @@ -1,23 +1,47 @@ { - "name": "emberplus", - "version": "1.0.0", + "name": "node-emberplus", + "version": "2.5.11", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "jest test --coverage", + "eslint": "eslint ./", + "start": "node server.js" }, - "author": "Brian Mayton (http://bdm.cc)", + "author": "Gilles Dufour (www.gdnet.be)", + "contributors": [ + "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": "^0.2.3", - "bluebird": "^3.5.0", + "asn1": "github:evs-broadcast/node-asn1", "enum": "^2.4.0", "long": "^3.2.0", "smart-buffer": "^3.0.3", - "winston-color": "^1.0.0" - } + "winston": "^2.1.1", + "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/s101.js b/s101.js old mode 100644 new mode 100755 index 19e12ac..e663335 --- a/s101.js +++ b/s101.js @@ -2,7 +2,6 @@ const EventEmitter = require('events').EventEmitter; const util = require('util'); const SmartBuffer = require('smart-buffer').SmartBuffer; const winston = require('winston'); -const BER = require('./ber.js'); const S101_BOF = 0xFE; const S101_EOF = 0xFF; @@ -31,37 +30,37 @@ const DTD_VERSION_MAJOR = 0x02; const DTD_VERSION_MINOR = 0x1F; const CRC_TABLE = [ - 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, - 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, - 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, - 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, - 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, - 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, - 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, - 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, - 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, - 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, - 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, - 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, - 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, - 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, - 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, - 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, - 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, - 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, - 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, - 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, - 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, - 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, - 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, - 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, - 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, - 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, - 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, - 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, - 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, - 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, - 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, + 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, + 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, + 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, + 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, + 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, + 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, + 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, + 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, + 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, + 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, + 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, + 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, + 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, + 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, + 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, + 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, + 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, + 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, + 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, + 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, + 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, + 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, + 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, + 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, + 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, + 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, + 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 ]; @@ -99,9 +98,8 @@ S101Codec.prototype.dataIn = function(buf) { S101Codec.prototype.handleFrame = function(frame) { var self = this; - //console.log(frame.toBuffer()); if(!validateFrame(frame.toBuffer())) { - winston.error('dropping frame of length %d with invalid CRC', + winston.error('dropping frame of length %d with invalid CRC', frame.length); return; } @@ -162,25 +160,22 @@ S101Codec.prototype.handleEmberFrame = function(frame) { } } - var payload = frame.readBuffer(); - if(flags & FLAG_SINGLE_PACKET) { - winston.debug('single ember packet'); - self.handleEmberPacket(SmartBuffer.fromBuffer(payload)); - self.emberbuf.clear(); - } else if(flags & FLAG_FIRST_MULTI_PACKET) { + var payload = frame.readBuffer(); + payload = payload.slice(0, payload.length - 2); + if(flags & FLAG_FIRST_MULTI_PACKET) { winston.debug('multi ember packet start'); self.emberbuf.clear(); + } + if ((flags & FLAG_EMPTY_PACKET) === 0) { + // not empty, save the payload self.emberbuf.writeBuffer(payload); - } else if(flags & FLAG_LAST_MULTI_PACKET) { + } + if(flags & FLAG_LAST_MULTI_PACKET) { winston.debug('multi ember packet end'); - self.emberbuf.writeBuffer(payload); self.emberbuf.moveTo(0); - self.handleEmberBuffer(self.emberbuf); + self.handleEmberPacket(self.emberbuf); self.emberbuf.clear(); - } else { - winston.debug('multi ember packet'); - self.emberbuf.writeBuffer(payload); } } @@ -188,7 +183,6 @@ S101Codec.prototype.handleEmberPacket = function(packet) { var self = this; winston.debug('ember packet'); - //console.log(packet); self.emit('emberPacket', packet.toBuffer()); } @@ -219,7 +213,7 @@ S101Codec.prototype.encodeBER = function(data) { encbuf.writeUInt8(S101_CE); encbuf.writeUInt8(b ^ S101_XOR); } - + if(encbuf.length >= 1024 && i < data.length-1) { if(frames.length == 0) { frames.push(makeBERFrame(FLAG_FIRST_MULTI_PACKET, encbuf.toBuffer())); @@ -245,6 +239,7 @@ S101Codec.prototype.keepAliveRequest = function() { packet.writeUInt8(SLOT); packet.writeUInt8(MSG_EMBER); packet.writeUInt8(CMD_KEEPALIVE_REQ); + packet.writeUInt8(VERSION); return finalizeBuffer(packet); } @@ -254,6 +249,7 @@ S101Codec.prototype.keepAliveResponse = function() { packet.writeUInt8(SLOT); packet.writeUInt8(MSG_EMBER); packet.writeUInt8(CMD_KEEPALIVE_RESP); + packet.writeUInt8(VERSION); return finalizeBuffer(packet); } @@ -261,21 +257,20 @@ var finalizeBuffer = function(smartbuf) { var crc = (~calculateCRCCE(smartbuf.toBuffer().slice(1, smartbuf.length))) & 0xFFFF; var crc_hi = crc >> 8; var crc_lo = crc & 0xFF; - if(crc_lo < S101_INV) { smartbuf.writeUInt8(crc_lo); } else { smartbuf.writeUInt8(S101_CE); smartbuf.writeUInt8(crc_lo ^ S101_XOR); } - + if(crc_hi < S101_INV) { smartbuf.writeUInt8(crc_hi); } else { smartbuf.writeUInt8(S101_CE); smartbuf.writeUInt8(crc_hi ^ S101_XOR); } - + smartbuf.writeUInt8(S101_EOF); return smartbuf.toBuffer(); } @@ -288,6 +283,7 @@ var calculateCRC = function(buf) { } return crc; } +S101Codec.prototype.calculateCRC = calculateCRC; var calculateCRCCE = function(buf) { var crc = 0xFFFF; @@ -301,8 +297,10 @@ var calculateCRCCE = function(buf) { return crc; } +S101Codec.prototype.calculateCRCCE = calculateCRCCE; + var validateFrame = function(buf) { - return calculateCRC(buf) == 0xF0B8; + return calculateCRC(buf) == 0xF0B8; } S101Codec.prototype.validateFrame = validateFrame; diff --git a/sample.js b/sample.js new file mode 100755 index 0000000..84d3741 --- /dev/null +++ b/sample.js @@ -0,0 +1,44 @@ +const Decoder = require('.').Decoder; +const DeviceTree = require(".").DeviceTree; +const fs = require("fs"); + +const LOCALHOST = "0.0.0.0"; +const PORT = 3336; + +fs.readFile("./embrionix.ember", (e, data) => { + let root = Decoder(data); + + const TreeServer = require("./").TreeServer; + const server = new TreeServer(LOCALHOST, PORT, root); + server.on("clientError", (e) => { + console.log(e); + }); + server.listen() + .then(() => { + console.log("listening"); + }) + .then(() => { + let tree = new DeviceTree(LOCALHOST, PORT); + return Promise.resolve() + .then(() => tree.connect()) + .then(() => { + return tree.getDirectory(); + }) + .then(() => tree.disconnect()) + .then(() => tree.connect()) + .then(() => { + return tree.getDirectory(); + }) + .catch((e) => { + console.log(e.stack); + }) + .then(() => tree.disconnect()) + }) + .catch((e) => { + console.log(e.stack); + }) + .then(() => { + console.log("close"); + server.close(); + }); +}); diff --git a/serve.js b/serve.js new file mode 100755 index 0000000..936346e --- /dev/null +++ b/serve.js @@ -0,0 +1,58 @@ +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.on('error', (e) => { + console.log(e); + }); + server._debug = true; + console.log(Date.now(), 'starting server'); + if (argv.debug) { + server._debug = true; + } + try { + server.listen(); + } catch (e) { + console.log(e); + } +}; + +main(); diff --git a/test/DeviceTree.test.js b/test/DeviceTree.test.js new file mode 100755 index 0000000..a1c645e --- /dev/null +++ b/test/DeviceTree.test.js @@ -0,0 +1,103 @@ +const fs = require("fs"); +const sinon = require("sinon"); +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("EmberClient", () => { + describe("With server", () => { + /** @type {EmberServer} */ + let server; + beforeAll(() => { + return Promise.resolve() + .then(() => new Promise((resolve, reject) => { + fs.readFile("./test/embrionix.ember", (e, data) => { + if (e) { + reject(e); + } + resolve(Decoder(data)); + }); + })) + .then(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 EmberClient(LOCALHOST, PORT); + return Promise.resolve() + .then(() => tree.connect()) + .then(() => tree.getDirectory()) + .then(() => tree.disconnect()) + .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 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(); + return Promise.resolve() + .then(() => tree.connect()) + .then(() => {tree.getDirectory().catch(() => {})}) + .then(() => tree.getDirectory()) + .then(() => { + stub.restore(); + tree.disconnect(); + }, () => { + stub.restore(); + tree.disconnect(); + // do nothinf + }); + }, 10000); + }); +}); diff --git a/test/Ember.test.js b/test/Ember.test.js new file mode 100755 index 0000000..2c4a205 --- /dev/null +++ b/test/Ember.test.js @@ -0,0 +1,1635 @@ +const expect = require("expect"); +const { S101Client } = require("../EmberSocket"); +const BER = require('../ber.js'); +const Errors = require('../Errors.js'); +const EmberLib = require("../EmberLib"); +const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("../EmberLib/ParameterType"); + +const s101Buffer = Buffer.from("fe000e0001c001021f026082008d6b820089a0176a15a0050d03010201a10c310aa0080c066c6162656c73a01b6a19a0050d03010202a110310ea00c0c0a706172616d6574657273a051714fa0050d03010203a1463144a0080c066d6174726978a403020104a503020104aa183016a0147212a0050d03010201a1090c075072696d617279a203020102a303020101a8050d03010202a903020101f24cff", "hex"); +const errorBuffer = Buffer.from("76fe000e0001c001021f026082008d6b820089a0176a15a0050d03010201a10c310aa0080c066c6162656c73a01b6a19a0050d03010202a110310ea00c0c0a706172616d6574657273a051714fa0050d03010203a1463144a0080c066d6174726978a403020104a503020104aa183016a0147212a0050d03010201a1090c075072696d617279a203020102a303020101a8050d03010202a903020101f24cff", "hex"); +const identifier = "node_identifier"; +const description = "node_description"; + + +describe("Ember", () => { + 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(() => EmberLib.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 path", () => { + 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("Root should have a toJSON function", () =>{ + const root = new EmberLib.Root(); + expect(root.toJSON()).toEqual({"elements": []}); + root.addChild(new EmberLib.Node(0)); + root.getElement(0).addChild(new EmberLib.Node(1)); + expect(root.toJSON()).toEqual({"elements": [{"children": [{"nodeType": "Node", "number": 1, "path": "0.1"}], "nodeType": "Node", "number": 0, "path": "0"}]}); + }); + it("should have a getDirectory() and accept a callback for subscribers", () => { + const parameter = new EmberLib.Parameter(0); + parameter.contents = new EmberLib.ParameterContents(7, "integer"); + let res = parameter.getDirectory(() => {}); + 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); + 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.name).toEqual('InvalidAsn1Error'); + } + }); + 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; + + 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); + }); + 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(); + } + }); + }); + 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) + }; + + 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 new file mode 100755 index 0000000..0ec6105 --- /dev/null +++ b/test/Server.test.js @@ -0,0 +1,1051 @@ +const expect = require("expect"); +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"; +let PORT = 9009; + +describe("server", function() { + describe("JSONtoTree", function() { + let jsonTree; + beforeEach(function() { + jsonTree = jsonRoot(); + }); + it("should generate an ember tree from json", function() { + const root = EmberServer.JSONtoTree(jsonTree); + // JSONtoTree will modify the json object. + jsonTree = jsonRoot(); + expect(root).toBeDefined(); + expect(root.elements).toBeDefined(); + expect(root.elements.size).toBe(jsonTree.length); + 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,jsonTree; + beforeEach(() => { + jsonTree = jsonRoot(); + 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(); + }); + it("should receive and decode the full tree", () => { + client = new EmberClient(LOCALHOST, PORT); + //client._debug = true; + return Promise.resolve() + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => { + expect(client.root).toBeDefined(); + expect(client.root.elements).toBeDefined(); + 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.getElementByNumber(0).elements.size).toBe(jsonTree[0].children.length); + return client.getDirectory(client.root.getElementByPath("0.0")); + }) + .then(() => { + 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(); + return client.disconnect(); + }); + }); + 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 client.connect() + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("0.2")) + .then(() => { + 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(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(() => { + throw new Error("Should not succeed"); + }) + .catch(e => { + expect(e.message).toMatch(/Failed path discovery/); + return client.disconnect(); + }); + }); + it("should be able to make a matrix connection", () => { + 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(() => { + 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(() => { + 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(() => 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(() => { + 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(); + }); + }); + }); + 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(() => { + 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(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/embrionix.ember b/test/embrionix.ember new file mode 100755 index 0000000..f2bacc4 Binary files /dev/null and b/test/embrionix.ember differ diff --git a/test/function.js b/test/function.js new file mode 100755 index 0000000..e518ff7 --- /dev/null +++ b/test/function.js @@ -0,0 +1,113 @@ +const EmberClient = require("../EmberClient"); +const EmberLib = require("../EmberLib"); +const TreeServer = require("../EmberServer").EmberServer; +const {jsonRoot} = require("./utils"); +const BER = require("../ber"); +const Decoder = EmberLib.DecodeBuffer; +const S101Client = require("../EmberSocket").S101Client; +const fs = require("fs"); + +let HOST; +let PORT; +const TINYEMBER = false; + + +const jsonTree = jsonRoot(); +const eee = TreeServer.JSONtoTree(jsonTree); + + +const PATH = "0.0.1"; + +// 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); +// } +// }); +// }); +// } + +const wait = function(t) { + return new Promise(resolve => { + setTimeout(resolve, t); + }); +} + +if (TINYEMBER) { + HOST = "192.168.4.4"; + PORT = 9092; + //HOST = "192.168.0.235"; + //PORT = 9000; +} +else { + HOST = "127.0.0.1"; + PORT = 9000; +} + + + + + +const client = new EmberClient(HOST, PORT); +const codec = new S101Client(); +const MATRIX_PATH = "0.1.0"; + +function clientData(node) { + console.log("received event for ", node.toJSON()); +} + +let server; +let start = Promise.resolve(); +start = start +//.then(() => getRoot()) +.then(root => { + root = TreeServer.JSONtoTree(jsonRoot()); + //root.addElement(new EmberLib.Node(1)); + server = new TreeServer(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() +}) + +.then(() => client.connect()) +.then(() => client.expand()) +.then(() => client.getElementByPath("1")) +.then(node1 => { + console.log(node1); + // const streamCollection = new EmberLib.StreamCollection(); + // const children = node1.getChildren(); + // let count = 0; + // for(let child of children) { + // client.subscribe(child, () => { count++; }); + // streamCollection.addEntry(new EmberLib.StreamEntry(child.contents.streamIdentifier, 999)); + // } + // // prepare StreamCollection + // const root = new EmberLib.Root(); + // root.setStreams(streamCollection); + // client._client.emit('emberTree', root); +}) +.catch(e => { + console.log(e.stack); + console.log(e); +}) +.then(() => { + client.disconnect(); + if (!TINYEMBER) { + server.close(); + } +}); diff --git a/test/matrixUpdate.test.js b/test/matrixUpdate.test.js new file mode 100755 index 0000000..f282bbe --- /dev/null +++ b/test/matrixUpdate.test.js @@ -0,0 +1,76 @@ +const { Matrix, MatrixContents, MatrixType, MatrixMode, MatrixConnection } = require("../EmberLib"); + +describe("Matrix Update", () => { + describe("linear matrix", () => { + let matrix; + beforeEach(() => { + matrix = new Matrix(); + matrix.contents = new MatrixContents(MatrixType.oneToN, MatrixMode.linear); + matrix.contents.targetCount = 5; + matrix.contents.sourceCount = 2; + matrix.connectSources(3, [1]); + }); + it("update matrix connections", () => { + const newMatrix = new Matrix(); + newMatrix.contents = new MatrixContents(MatrixType.oneToN, MatrixMode.linear); + newMatrix.contents.targetCount = 5; + newMatrix.contents.sourceCount = 2; + newMatrix.connectSources(3, [0]); + Matrix.MatrixUpdate(matrix, newMatrix); + expect(matrix.connections[3].sources).toHaveLength(1); + expect(matrix.connections[3].sources[0]).toBe(0); + }); + it("reject invalid connections (source) during matrix update", () => { + const newMatrix = new Matrix(); + newMatrix.contents = new MatrixContents(MatrixType.oneToN, MatrixMode.linear); + newMatrix.contents.targetCount = 5; + newMatrix.contents.sourceCount = 2; + newMatrix.connectSources(3, [2]); + expect(() => Matrix.MatrixUpdate(matrix, newMatrix)).toThrow + }); + it("reject invalid connections (target) during matrix update", () => { + const newMatrix = new Matrix(); + newMatrix.contents = new MatrixContents(MatrixType.oneToN, MatrixMode.linear); + newMatrix.contents.targetCount = 5; + newMatrix.contents.sourceCount = 2; + newMatrix.connectSources(14, [0]); + expect(() => Matrix.MatrixUpdate(matrix, newMatrix)).toThrow + }); + }); + describe("non-linear matrix", () => { + let matrix; + beforeEach(() => { + matrix = new Matrix(); + matrix.contents = new MatrixContents(MatrixType.oneToN, MatrixMode.nonLinear); + matrix.targets = [1, 3, 5, 6]; + matrix.sources = [0,1,4]; + matrix.connectSources(3, [0]); + }); + it("update matrix connections", () => { + const newMatrix = new Matrix(); + newMatrix.contents = new MatrixContents(MatrixType.oneToN, MatrixMode.linear); + newMatrix.targets = [1, 3, 5, 6]; + newMatrix.sources = [0,1,4]; + newMatrix.connectSources(3, [4]); + Matrix.MatrixUpdate(matrix, newMatrix); + expect(matrix.connections[3].sources).toHaveLength(1); + expect(matrix.connections[3].sources[0]).toBe(4); + }); + it("reject invalid connections (source) during matrix update", () => { + const newMatrix = new Matrix(); + newMatrix.contents = new MatrixContents(MatrixType.oneToN, MatrixMode.linear); + newMatrix.targets = [1, 3, 5, 6]; + newMatrix.sources = [0,1,4]; + newMatrix.connectSources(3, [2]); + expect(() => Matrix.MatrixUpdate(matrix, newMatrix)).toThrow + }); + it("reject invalid connections (target) during matrix update", () => { + const newMatrix = new Matrix(); + newMatrix.contents = new MatrixContents(MatrixType.oneToN, MatrixMode.linear); + newMatrix.targets = [1, 3, 5, 6]; + newMatrix.sources = [0,1,4]; + newMatrix.connectSources(4, [4]); + expect(() => Matrix.MatrixUpdate(matrix, newMatrix)).toThrow + }); + }); +}) \ No newline at end of file diff --git a/test/utils.js b/test/utils.js new file mode 100755 index 0000000..9cca4a4 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,141 @@ +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 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; + }; + + return [ + { + // path "0" + identifier: "scoreMaster", + children: [ + { + // path "0.0" + identifier: "identity", + children: [ + {identifier: "product", value: "S-CORE Master", type: "string"}, + {identifier: "company", value: "EVS", access: "readWrite"}, + {identifier: "version", value: "1.2.0", access: "readWrite", streamIdentifier: 1234567}, + {identifier: "author", value: "g.dufour@evs.com"}, + ] + }, + { + // 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", + 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 + } + ]; +} + +module.exports = { + jsonRoot: init +} \ No newline at end of file diff --git a/test/validateConnection.test.js b/test/validateConnection.test.js new file mode 100755 index 0000000..11f7552 --- /dev/null +++ b/test/validateConnection.test.js @@ -0,0 +1,53 @@ +const { Matrix, MatrixContents, MatrixNode, MatrixMode, MatrixType, MatrixConnection } = require("../EmberLib"); + +describe("validateConnection", () => { + describe("linear matrix", () => { + let matrixNode; + beforeEach(() => { + matrixNode = new MatrixNode(1); + matrixNode.contents = new MatrixContents(MatrixType.oneToN, MatrixMode.linear); + matrixNode.contents.targetCount = 3; + matrixNode.contents.sourceCount = 2; + }); + it("accept valid connection", () => { + const connection = new MatrixConnection(2); + connection.setSources([1]); + expect(() => Matrix.validateConnection(matrixNode, connection.target, connection.sources)).not.toThrow(); + }); + it("reject invalid source", () => { + const connection = new MatrixConnection(2); + connection.setSources([4]); + expect(() => Matrix.validateConnection(matrixNode, connection.target, connection.sources)).toThrow(); + }); + it("reject invalid target", () => { + const connection = new MatrixConnection(10); + connection.setSources([4]); + expect(() => Matrix.validateConnection(matrixNode, connection.target, connection.sources)).toThrow(); + }); + }); + describe("non-linear matrix", () => { + let matrixNode; + beforeEach(() => { + matrixNode = new MatrixNode(1); + matrixNode.contents = new MatrixContents(MatrixType.oneToN, MatrixMode.nonLinear); + matrixNode.targets = [1, 3, 7]; + matrixNode.sources = [1,2,4,8]; + }); + test.each([1,2,4,8]) + ("accept valid connection with source %p", (sourceID) => { + const connection = new MatrixConnection(3); + connection.setSources([sourceID]); + expect(() => Matrix.validateConnection(matrixNode, connection.target, connection.sources)).not.toThrow(); + }); + test.each([-1,3,5,18])("reject invalid source %p", (sourceID) => { + const connection = new MatrixConnection(1); + connection.setSources([sourceID]); + expect(() => Matrix.validateConnection(matrixNode, connection.target, connection.sources)).toThrow(); + }); + it("reject invalid target", () => { + const connection = new MatrixConnection(10); + connection.setSources([4]); + expect(() => Matrix.validateConnection(matrixNode, connection.target, connection.sources)).toThrow(); + }); + }); +}); \ No newline at end of file