diff --git a/.gitignore b/.gitignore index c882358..e77824c 100755 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # intellij, pycharm, webstorm... /.idea/* - +.vscode node_modules .*.swp +coverage diff --git a/EmberClient/EmberClient.js b/EmberClient/EmberClient.js new file mode 100755 index 0000000..40f8a6a --- /dev/null +++ b/EmberClient/EmberClient.js @@ -0,0 +1,715 @@ +const EventEmitter = require('events').EventEmitter; +const S101Client = require('../EmberSocket').S101Client; +const ember = require('../EmberLib'); +const BER = require('../ber.js'); +const Errors = require('../Errors.js'); +const winston = require("winston"); + +const DEFAULT_PORT = 9000; +const DEFAULT_TIMEOUT = 3000; + +/** + * @typedef {{ + * node: TreeNode, + * func: function + * }} REQUEST + * + */ + +class EmberClient extends EventEmitter { + /** + * + * @param {string} host + * @param {number} port + */ + constructor(host, port = DEFAULT_PORT) { + super(); + this._debug = false; + /** @type {REQUEST[]} */ + this._pendingRequests = []; + /** @type {REQUEST} */ + this._activeRequest = null; + this._timeout = null; + this._callback = undefined; + this._requestID = 0; + this._client = new S101Client(host, port); + this.timeoutValue = DEFAULT_TIMEOUT; + /** @type {Root} */ + this.root = new ember.Root(); + + this._client.on('connecting', () => { + this.emit('connecting'); + }); + + this._client.on('connected', () => { + this.emit('connected'); + if (this._callback != null) { + this._callback(); + } + }); + + this._client.on('disconnected', () => { + this.emit('disconnected'); + }); + + this._client.on("error", e => { + if (this._callback != null) { + this._callback(e); + } + this.emit("error", e); + }); + + this._client.on('emberTree', root => { + try { + if (root instanceof ember.InvocationResult) { + this.emit('invocationResult', root); + winston.debug("Received InvocationResult", root); + } else { + this._handleRoot(root); + winston.debug("Received root", root); + } + if (this._callback) { + this._callback(undefined, root); + } + } + catch(e) { + winston.error(e, root); + if (this._callback) { + this._callback(e); + } + } + }); + } + + _finishRequest() { + this._clearTimeout(); + this._callback = undefined; + this._activeRequest = null; + try { + this._makeRequest(); + } catch(e) { + winston.error(e); + if (this._callback != null) { + this._callback(e); + } + this.emit("error", e); + } + } + + _makeRequest() { + if (this._activeRequest == null && this._pendingRequests.length > 0) { + this._activeRequest = this._pendingRequests.shift(); + const req = `${ this._requestID++} - ${this._activeRequest.node.getPath()}`; + this._activeRequest.timeoutError = new Errors.EmberTimeoutError(`Request ${req} timed out`) + + winston.debug(`Making request ${req}`, Date.now()); + this._timeout = setTimeout(() => { + this._timeoutRequest(); + }, this.timeoutValue); + this._activeRequest.func(); + } + } + + _timeoutRequest() { + this._activeRequest.func(this._activeRequest.timeoutError); + } + + /** + * + * @param {function} req + */ + addRequest(req) { + this._pendingRequests.push(req); + this._makeRequest(); + } + + _clearTimeout() { + if (this._timeout != null) { + clearTimeout(this._timeout); + this._timeout = null; + } + } + + /** + * + * @param {TreeNode} parent + * @param {TreeNode} node + */ + _handleNode(parent, node) { + let n = parent.getElementByNumber(node.getNumber()); + if (n == null) { + parent.addChild(node); + n = node; + } else if (n.update(node)) { + n.updateSubscribers(); + } + + const children = node.getChildren(); + if (children !== null) { + for (let i = 0; i < children.length; i++) { + this._handleNode(n, children[i]); + } + } + else { + this.emit("value-change", node); + } + return; + } + + /** + * + * @param {TreeNode} parent + * @param {TreeNode} node + */ + _handleQualifiedNode(parent, node) { + let element = parent.getElementByPath(node.path); + if (element !== null) { + this.emit("value-change", node); + if (element.update(node)) { + element.updateSubscribers(); + } + } + else { + const path = node.path.split("."); + if (path.length === 1) { + this.root.addChild(node); + } + else { + // Let's try to get the parent + path.pop(); + parent = this.root.getElementByPath(path.join(".")); + if (parent === null) { + return; + } + parent.addChild(node); + parent.update(parent); + } + element = node; + } + + const children = node.getChildren(); + if (children !== null) { + for (let i = 0; i < children.length; i++) { + if (children[i].isQualified()) { + this._handleQualifiedNode(element, children[i]); + } + else { + this._handleNode(element, children[i]); + } + } + } + + return; + } + + /** + * + * @param {TreeNode} root + */ + _handleRoot (root) { + winston.debug("handling root", JSON.stringify(root)); + this.root.update(root); + if (root.elements != null) { + const elements = root.getChildren(); + for (let i = 0; i < elements.length; i++) { + if (elements[i].isQualified()) { + this._handleQualifiedNode(this.root, elements[i]); + } + else { + this._handleNode(this.root, elements[i]); + } + } + } + if (this._callback) { + this._callback(null, root); + } + } + + /** + * + * @param {number} timeout + */ + connect(timeout = 2) { + return new Promise((resolve, reject) => { + this._callback = e => { + this._callback = undefined; + if (e === undefined) { + return resolve(); + } + return reject(e); + }; + if ((this._client != null) && (this._client.isConnected())) { + this._client.disconnect(); + } + this._client.connect(timeout); + }); + } + + /** + * @returns {Promise} + */ + disconnect() { + if (this._client != null) { + return this._client.disconnect(); + } + return Promise.resolve(); + } + + /** + * + * @param {TreeNode} qnode + * @param {function} callback=null + * @returns {Promise} + */ + expand(node, callback = null) { + if (node != null && (node.isParameter() || node.isMatrix() || node.isFunction())) { + return this.getDirectory(node); + } + return this.getDirectory(node, callback).then(res => { + const children = node == null ? res == null ? null : res.getChildren() : node.getChildren(); + if ((res == null) || (children == null)) { + winston.debug("No more children for ", node); + return; + } + let directChildren = Promise.resolve(); + for (let child of children) { + if (child.isParameter()) { + // Parameter can only have a single child of type Command. + continue; + } + directChildren = directChildren.then(() => { + return this.expand(child, callback).catch((e) => { + // We had an error on some expansion + // let's save it on the child itthis + child.error = e; + }); + }); + } + return directChildren + }); + } + /** + * + * @param {TreeNode} qnode + * @param {function} callback=null + * @returns {Promise} + */ + getDirectory(qnode, callback = null) { + if (qnode == null) { + this.root.clear(); + qnode = this.root; + } + return new Promise((resolve, reject) => { + this.addRequest({node: qnode, func: error => { + if (error) { + this._finishRequest(); + return reject(error); + } + + this._callback = (error, node) => { + const requestedPath = qnode.getPath(); + if (node == null) { + winston.debug(`received null response for ${requestedPath}`); + // ignore + return; + } + if (error) { + winston.debug("Received getDirectory error", error); + this._finishRequest(); + return reject(error); + } + if (qnode.isRoot()) { + const elements = qnode.getChildren(); + if (elements == null || elements.length === 0) { + winston.debug("getDirectory response", node); + return reject(new Errors.InvalidEmberNode()); + } + + const nodeElements = node == null ? null : node.getChildren(); + + if (nodeElements != null + && nodeElements.every(el => el._parent instanceof ember.Root)) { + winston.debug("Received getDirectory response", node); + this._finishRequest(); + return resolve(node); // make sure the info is treated before going to next request. + } + else { + return this._callback(new Errors.InvalidEmberResponse(`getDirectory ${requestedPath}`)); + } + } + else if (node.getElementByPath(requestedPath) != null) { + this._finishRequest(); + return resolve(node); // make sure the info is treated before going to next request. + } + else { + const nodeElements = node == null ? null : node.getChildren(); + if (nodeElements != null && + ((qnode.isMatrix() && nodeElements.length === 1 && nodeElements[0].getPath() === requestedPath) || + (!qnode.isMatrix() && nodeElements.every(el => isDirectSubPathOf(el.getPath(), requestedPath))))) { + winston.debug("Received getDirectory response", node); + this._finishRequest(); + return resolve(node); // make sure the info is treated before going to next request. + } + else { + winston.debug(node); + winston.error(new Error(requestedPath)); + } + } + }; + try { + this._client.sendBERNode(qnode.getDirectory(callback)); + } + catch(e) { + reject(e); + } + }}); + }); + } + + /** + * + * @param {string} path ie: "path/to/destination" + * @param {function} callback=null + * @returns {Promise} + */ + getElementByPath(path, callback=null) { + const pathError = new Errors.PathDiscoveryFailure(path); + const TYPE_NUM = 1; + const TYPE_ID = 2; + let type = TYPE_NUM; + let pathArray = []; + if (path.indexOf("/") >= 0) { + type = TYPE_ID; + pathArray = path.split("/"); + } + else { + pathArray = path.split("."); + if (pathArray.length === 1) { + if (isNaN(Number(pathArray[0]))) { + type = TYPE_ID; + } + } + } + let pos = 0; + let lastMissingPos = -1; + let currentNode = this.root; + const getNext = () => { + return Promise.resolve() + .then(() => { + let node; + if (type === TYPE_NUM) { + const number = Number(pathArray[pos]); + node = currentNode.getElementByNumber(number); + } + else { + const children = currentNode.getChildren(); + const identifier = pathArray[pos]; + if (children != null) { + let i = 0; + for (i = 0; i < children.length; i++) { + node = children[i]; + if (node.contents != null && node.contents.identifier === identifier) { + break; + } + } + if (i >= children.length) { + node = null; + } + } + } + if (node != null) { + // We have this part already. + pos++; + if (pos >= pathArray.length) { + if (callback) { + node.getDirectory(callback); + } + return node; + } + currentNode = node; + return getNext(); + } + // We do not have that node yet. + if (lastMissingPos === pos) { + throw pathError; + } + lastMissingPos = pos; + return this.getDirectory(currentNode, callback).then(() => getNext()); + }); + } + return getNext(); + } + + /** + * + * @param {TreeNode} fnNode + * @param {FunctionArgument[]} params + */ + invokeFunction(fnNode, params) { + return new Promise((resolve, reject) => { + this.addRequest({node: fnNode, func: (error) => { + if (error) { + reject(error); + this._finishRequest(); + return; + } + const cb = (error, result) => { + this._clearTimeout(); + if (error) { + reject(error); + } + else { + winston.debug("InvocationResult", result); + resolve(result); + } + // cleaning callback and making next request. + this._finishRequest(); + }; + winston.debug("Invocking function", fnNode); + this._callback = cb; + this._client.sendBERNode(fnNode.invoke(params)); + }}); + }) + } + + /** + * @returns {boolean} + */ + isConnected() { + return ((this._client != null) && (this._client.isConnected())); + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + * @param {MatrixOperation} operation + * @returns {Promise} + */ + matrixOPeration(matrixNode, targetID, sources, operation = ember.MatrixOperation.connect) { + return new Promise((resolve, reject) => { + if (!Array.isArray(sources)) { + return reject(new Errors.InvalidSourcesFormat()); + } + try { + matrixNode.validateConnection(targetID, sources); + } + catch(e) { + return reject(e); + } + const connections = {} + const targetConnection = new ember.MatrixConnection(targetID); + targetConnection.operation = operation; + targetConnection.setSources(sources); + connections[targetID] = targetConnection; + + this.addRequest({node: matrixNode, func: (error) => { + if (error) { + this._finishRequest(); + reject(error); + return; + } + + this._callback = (error, node) => { + const requestedPath = matrixNode.getPath(); + if (node == null) { + winston.debug(`received null response for ${requestedPath}`); + return; + } + if (error) { + winston.error("Received getDirectory error", error); + this._clearTimeout(); // clear the timeout now. The resolve below may take a while. + this._finishRequest(); + reject(error); + return; + } + let matrix = null; + if (node != null) { + matrix = node.getElementByPath(requestedPath); + } + if (matrix != null && matrix.isMatrix() && matrix.getPath() === requestedPath) { + this._clearTimeout(); // clear the timeout now. The resolve below may take a while. + this._finishRequest(); + resolve(matrix); + } + else { + winston.debug(`unexpected node response during matrix connect ${requestedPath}`, + matrix == null ? null : JSON.stringify(matrix.toJSON(), null, 4)); + } + } + this._client.sendBERNode(matrixNode.connect(connections)); + }}); + }); + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + * @returns {Promise} + */ + matrixConnect(matrixNode, targetID, sources) { + return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.connect) + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + * @returns {Promise} + */ + matrixDisconnect(matrixNode, targetID, sources) { + return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.disconnect) + } + + /** + * + * @param {Matrix} matrixNode + * @param {number} targetID + * @param {number[]} sources + * @returns {Promise} + */ + matrixSetConnection(matrixNode, targetID, sources) { + return this.matrixOPeration(matrixNode, targetID,sources, ember.MatrixOperation.absolute) + } + + /** + * + * @param {function} f + */ + saveTree(f) { + const writer = new BER.Writer(); + this.root.encode(writer); + f(writer.buffer); + } + + /** + * + * @param {TreeNode} node + * @param {string|number} value + * @returns {Promise} + */ + setValue(node, value) { + return new Promise((resolve, reject) => { + if (!node.isParameter()) { + reject(new Errors.EmberAccessError('not a Parameter')); + } + else { + this.addRequest({node: node, func: error => { + if (error) { + this._finishRequest(); + reject(error); + return; + } + + this._callback = (error, node) => { + this._finishRequest(); + this._callback = null; + if (error) { + reject(error); + } + else { + + resolve(node); + } + }; + winston.debug('setValue sending ...', node.getPath(), value); + this._client.sendBERNode(node.setValue(value)); + }}); + } + }); + } + + /** + * + * @param {TreeNode} node + * @param {string|number} value + * @returns {Promise} + */ + setValueNoAck(node, value) { + // This function immediately finish & resolve so we can't get any timeouts ever + // This is a pretty ugly hack, but it doesn't look to bring + // any negative consequences regarding the execution and resolving of other + // functions. It´s needed this because if the node already has the value we are + // setting it too, it will cause a timeout. + return new Promise((resolve, reject) => { + if (!node.isParameter()) { + reject(new Errors.EmberAccessError('not a Parameter')); + return; + } + this.addRequest({node: node, func: error => { + if (error) { + this._finishRequest(); + reject(error); + return; + } + winston.debug('setValue sending ...', node.getPath(), value); + this._client.sendBERNode(node.setValue(value)); + + this._finishRequest(); + this._callback = null; + return resolve(node) + }}); + }) + } + + /** + * + * @param {TreeNode} qnode + * @param {function} callback + * @returns {Promise} + */ + subscribe(qnode, callback) { + if ((qnode.isParameter() || qnode.isMatrix()) && qnode.isStream()) { + if (qnode == null) { + this.root.clear(); + qnode = this.root; + } + return new Promise((resolve, reject) => { + this.addRequest({node: qnode, func: error => { + if (error != null) { + return reject(error); + } + winston.debug("Sending subscribe", qnode); + this._client.sendBERNode(qnode.subscribe(callback)); + this._finishRequest(); + resolve(); + }}); + }); + } + return Promise.resolve(); + } + + /** + * + * @param {TreeNode} qnode + * @param {function} callback + * @returns {Promise} + */ + unsubscribe(qnode, callback) { + if (qnode.isParameter() && qnode.isStream()) { + return new Promise((resolve, reject) => { + this.addRequest({node: qnode, func: (error) => { + if (error != null) { + return reject(error); + } + winston.debug("Sending subscribe", qnode); + this._client.sendBERNode(qnode.unsubscribe(callback)); + this._finishRequest(); + resolve(); + }}); + }); + } + return Promise.resolve(); + } +} + +function isDirectSubPathOf(path, parent) { + return path === parent || (path.lastIndexOf('.') === parent.length && path.startsWith(parent)); +} + +module.exports = EmberClient; + diff --git a/EmberClient/index.js b/EmberClient/index.js new file mode 100755 index 0000000..964a37d --- /dev/null +++ b/EmberClient/index.js @@ -0,0 +1 @@ +module.exports = require("./EmberClient"); \ No newline at end of file diff --git a/EmberLib/Command.js b/EmberLib/Command.js new file mode 100755 index 0000000..73bf653 --- /dev/null +++ b/EmberLib/Command.js @@ -0,0 +1,145 @@ +"use strict"; +const Enum = require('enum'); +const {COMMAND_GETDIRECTORY, COMMAND_INVOKE} = require("./constants"); +const BER = require('../ber.js'); +const Invocation = require("./Invocation"); +const errors = require("../Errors"); +const ElementInterface = require("./ElementInterface"); + +const FieldFlags = new Enum({ + sparse: -2, + all: -1, + default: 0, + identifier: 1, + description: 2, + tree: 3, + value: 4, + connections: 5 +}); + +class Command extends ElementInterface{ + /** + * + * @param {number} number + */ + constructor(number) { + super(); + this.number = number; + if(number == COMMAND_GETDIRECTORY) { + this.fieldFlags = FieldFlags.all; + } + } + + /** + * @returns {boolean} + */ + isCommand() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(Command.BERID); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.number); + ber.endSequence(); // BER.CONTEXT(0) + + if (this.number === COMMAND_GETDIRECTORY && this.fieldFlags) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeInt(this.fieldFlags.value); + ber.endSequence(); + } + + if (this.number === COMMAND_INVOKE && this.invocation) { + ber.startSequence(BER.CONTEXT(2)); + this.invocation.encode(ber); + ber.endSequence(); + } + // TODO: options + + ber.endSequence(); // BER.APPLICATION(2) + } + + /** + * @returns {number} + */ + getNumber() { + return this.number; + } + + /** + * + */ + toJSON() { + return { + number: this.number, + fieldFlags: this.fieldFlags, + invocation: this.invocation == null ? null : this.invocation.toJSON() + }; + } + + /** + * + * @param {BER} ber + * @returns {Command} + */ + static decode(ber) { + const c = new Command(); + ber = ber.getSequence(Command.BERID); + + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + c.number = seq.readInt(); + } + else if(tag == BER.CONTEXT(1)) { + c.fieldFlags = FieldFlags.get(seq.readInt()); + } + else if(tag == BER.CONTEXT(2)) { + c.invocation = Invocation.decode(seq); + } + else { + // TODO: options + throw new errors.UnimplementedEmberTypeError(tag); + } + } + + return c; + } + + /** + * + * @param {number} cmd + * @param {string} key + * @param {string|value|object} value + */ + static getCommand(cmd, key, value) { + const command = new Command(cmd); + if (key != null) { + command[key] = value; + } + return command; + } + + /** + * + * @param {Invocation} invocation + */ + static getInvocationCommand(invocation) { + return this.getCommand(COMMAND_INVOKE, "invocation", invocation); + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(2); + } +} + +module.exports = Command; \ No newline at end of file diff --git a/EmberLib/Element.js b/EmberLib/Element.js new file mode 100755 index 0000000..00aede0 --- /dev/null +++ b/EmberLib/Element.js @@ -0,0 +1,46 @@ +"use strict"; +const TreeNode = require("./TreeNode"); +const BER = require('../ber.js'); + +class Element extends TreeNode { + /** + * + * @param {number} number + */ + constructor(number) { + super(); + this.number = number; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(this._seqID); + + this.encodeNumber(ber); + + if(this.contents != null) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + this.encodeChildren(ber); + + ber.endSequence(); // BER.APPLICATION(3) + } + + /** + * + * @param {BER} ber + */ + encodeNumber(ber) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.number); + ber.endSequence(); // BER.CONTEXT(0) + } +} + +module.exports = Element; \ No newline at end of file diff --git a/EmberLib/ElementInterface.js b/EmberLib/ElementInterface.js new file mode 100755 index 0000000..0d93c6c --- /dev/null +++ b/EmberLib/ElementInterface.js @@ -0,0 +1,61 @@ +"use strict"; + +class ElementType { + /** + * @returns {boolean} + */ + isCommand() { + return false; + } + /** + * @returns {boolean} + */ + isNode() { + return false; + } + /** + * @returns {boolean} + */ + isMatrix() { + return false; + } + /** + * @returns {boolean} + */ + isParameter() { + return false; + } + /** + * @returns {boolean} + */ + isFunction() { + return false; + } + /** + * @returns {boolean} + */ + isRoot() { + return false; + } + /** + * @returns {boolean} + */ + isQualified() { + return false; + } + /** + * @returns {boolean} + */ + isStream() { + return false; + } + + /** + * @returns {boolean} + */ + isTemplate() { + return false; + } +} + +module.exports = ElementType; \ No newline at end of file diff --git a/EmberLib/Function.js b/EmberLib/Function.js new file mode 100755 index 0000000..54c777b --- /dev/null +++ b/EmberLib/Function.js @@ -0,0 +1,68 @@ +"use strict"; +const Element = require("./Element"); +const QualifiedFunction = require("./QualifiedFunction"); +const BER = require('../ber.js'); +const Command = require("./Command"); +const {COMMAND_INVOKE} = require("./constants"); +const FunctionContent = require("./FunctionContent"); +const Errors = require("../Errors"); + +class Function extends Element { + constructor(number, func) { + super(); + this.number = number; + this.func = func; + this._seqID = Function.BERID; + } + + /** + * @returns {boolean} + */ + isFunction() { + return true; + } + + /** + * @returns {QualifiedFunction} + */ + toQualified() { + const qf = new QualifiedFunction(this.getPath()); + qf.update(this); + return qf; + } + + + /** + * + * @param {BER} ber + * @returns {Function} + */ + static decode(ber) { + const f = new Function(); + ber = ber.getSequence(Function.BERID); + + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + f.number = seq.readInt(); + } else if(tag == BER.CONTEXT(1)) { + f.contents = FunctionContent.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + f.decodeChildren(seq); + } else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return f; + } + + /** + * + */ + static get BERID() { + return BER.APPLICATION(19); + } +} + +module.exports = Function; \ No newline at end of file diff --git a/EmberLib/FunctionArgument.js b/EmberLib/FunctionArgument.js new file mode 100755 index 0000000..de1a792 --- /dev/null +++ b/EmberLib/FunctionArgument.js @@ -0,0 +1,102 @@ +"use strict"; +const BER = require('../ber.js'); +const {ParameterType} = require("./ParameterType"); +const Errors = require("../Errors"); + +/* +TupleDescription ::= + SEQUENCE OF [0] TupleItemDescription +TupleItemDescription ::= + [APPLICATION 21] IMPLICIT + SEQUENCE { + type [0] ParameterType, + name [1] EmberString OPTIONAL + } +Invocation ::= + [APPLICATION 22] IMPLICIT + SEQUENCE { + invocationId [0] Integer32 OPTIONAL, + arguments [1] Tuple OPTIONAL + } +Tuple ::= + SEQUENCE OF [0] Value +*/ + +class FunctionArgument { + /** + * + * @param {ParameterType} type + * @param {number|string|null} value + * @param {string|null} name + */ + constructor (type = null, value = null, name = null) { + /** @type {ParameterType} */ + this.type = type; + this.value = value; + this.name = name; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(FunctionArgument.BERID); + if (this.type == null) { + throw new Errors.InvalidEmberNode("", "FunctionArgument requires a type") + } + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.type.value); + ber.endSequence(); + if (this.name != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.name, BER.EMBER_STRING); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * + */ + toJSON() { + return { + type: this.type, + name: this.name, + value: this.value + }; + } + + /** + * + * @param {BER} ber + * @returns {FunctionArgument} + */ + static decode(ber) { + const tuple = new FunctionArgument(); + ber = ber.getSequence(FunctionArgument.BERID); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if (tag === BER.CONTEXT(0)) { + tuple.type = ParameterType.get(seq.readInt()); + } + else if (tag === BER.CONTEXT(1)) { + tuple.name = seq.readString(BER.EMBER_STRING); + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return tuple; + } + + /** + * + */ + static get BERID() { + return BER.APPLICATION(21); + } +} + +module.exports = FunctionArgument; \ No newline at end of file diff --git a/EmberLib/FunctionContent.js b/EmberLib/FunctionContent.js new file mode 100755 index 0000000..737beae --- /dev/null +++ b/EmberLib/FunctionContent.js @@ -0,0 +1,113 @@ +"use strict"; +const BER = require('../ber.js'); +const FunctionArgument = require("./FunctionArgument"); +const errors = require("../Errors"); + +class FunctionContent { + constructor(identifier=null, description=null) { + this.arguments = []; + this.result = []; + this.identifier = identifier; + this.description = description; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.EMBER_SET); + + if(this.identifier != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeString(this.identifier, BER.EMBER_STRING); + ber.endSequence(); // BER.CONTEXT(0) + } + + if(this.description != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.description, BER.EMBER_STRING); + ber.endSequence(); // BER.CONTEXT(1) + } + + if(this.arguments != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.startSequence(BER.EMBER_SEQUENCE); + for(var i = 0; i < this.arguments.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + this.arguments[i].encode(ber); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); // BER.CONTEXT(2) + } + + if(this.result != null && this.result.length > 0) { + ber.startSequence(BER.CONTEXT(3)); + ber.startSequence(BER.EMBER_SEQUENCE); + for(let i = 0; i < this.result.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + /** @type {FunctionArgument} */ + this.result[i].encode(ber); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); // BER.CONTEXT(3) + } + + if(this.templateReference != null) { + ber.startSequence(BER.CONTEXT(4)); + ber.writeRelativeOID(this.templateReference, BER.EMBER_RELATIVE_OID); + ber.endSequence(); // BER.CONTEXT(3) + } + + ber.endSequence(); // BER.EMBER_SET + } + + /** + * + * @param {BER} ber + * @returns {FunctionContent} + */ + static decode(ber) { + const fc = new FunctionContent(); + ber = ber.getSequence(BER.EMBER_SET); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + fc.identifier = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(1)) { + fc.description = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(2)) { + fc.arguments = []; + let dataSeq = seq.getSequence(BER.EMBER_SEQUENCE); + while(dataSeq.remain > 0) { + seq = dataSeq.getSequence(BER.CONTEXT(0)); + fc.arguments.push(FunctionArgument.decode(seq)); + } + } else if(tag == BER.CONTEXT(3)) { + fc.result = []; + let dataSeq = seq.getSequence(BER.EMBER_SEQUENCE); + while(dataSeq.remain > 0) { + tag = dataSeq.peek(); + if (tag === BER.CONTEXT(0)) { + const fcSeq = dataSeq.getSequence(tag); + fc.result.push(FunctionArgument.decode(fcSeq)); + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + } else if(tag == BER.CONTEXT(4)) { + fc.templateReference = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + + return fc; + } +} + +module.exports = FunctionContent; \ No newline at end of file diff --git a/EmberLib/Invocation.js b/EmberLib/Invocation.js new file mode 100755 index 0000000..945b828 --- /dev/null +++ b/EmberLib/Invocation.js @@ -0,0 +1,105 @@ +"use strict"; +const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); +const BER = require('../ber.js'); +const FunctionArgument = require("./FunctionArgument"); +const errors = require("../Errors"); + +let _id = 1; +class Invocation { + /** + * + * @param {number} id + * @param {FunctionArgument[]} args + */ + constructor(id = null, args = []) { + this.id = id; + this.arguments = args; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(Invocation.BERID); + if (this.id != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.id) + ber.endSequence(); + } + ber.startSequence(BER.CONTEXT(1)); + ber.startSequence(BER.EMBER_SEQUENCE); + for(var i = 0; i < this.arguments.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeValue( + this.arguments[i].value, + ParameterTypetoBERTAG(this.arguments[i].type + )); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); + + ber.endSequence(); // BER.APPLICATION(22) + } + + /** + * + */ + toJSON() { + return { + id: this.id, + arguments: this.arguments == null ? null : this.arguments.map(a => a.toJSON()), + } + } + + /** + * + * @param {BER} ber + * @returns {Invocation} + */ + static decode(ber) { + const invocation = new Invocation(); + ber = ber.getSequence(Invocation.BERID); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + invocation.id = seq.readInt(); + } + else if(tag == BER.CONTEXT(1)) { + invocation.arguments = []; + seq = seq.getSequence(BER.EMBER_SEQUENCE); + while(seq.remain > 0) { + const dataSeq = seq.getSequence(BER.CONTEXT(0)); + tag = dataSeq.peek(); + const val = dataSeq.readValue(); + invocation.arguments.push( + new FunctionArgument(ParameterTypefromBERTAG(tag), val) + ); + } + } + else { + // TODO: options + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return invocation; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(22); + } + + /** + * @returns {number} + */ + static newInvocationID() { + return _id++; + } +} + +module.exports = Invocation; \ No newline at end of file diff --git a/EmberLib/InvocationResult.js b/EmberLib/InvocationResult.js new file mode 100755 index 0000000..b036b32 --- /dev/null +++ b/EmberLib/InvocationResult.js @@ -0,0 +1,128 @@ +"use strict"; + +const BER = require('../ber.js'); +const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("./ParameterType"); +const FunctionArgument = require("./FunctionArgument"); +const Errors = require("../Errors"); + + +class InvocationResult { + /** + * + * @param {number|null} invocationId=null + */ + constructor(invocationId = null) { + this.invocationId = invocationId; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(InvocationResult.BERID); + if (this.invocationId != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.invocationId); + ber.endSequence(); + } + if (this.success != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeBoolean(this.success); + ber.endSequence(); + } + if (this.result != null && this.result.length) { + ber.startSequence(BER.CONTEXT(2)); + ber.startSequence(BER.EMBER_SEQUENCE); + for (let i = 0; i < this.result.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeValue(this.result[i].value, ParameterTypetoBERTAG(this.result[i].type)); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); + } + ber.endSequence(); // BER.APPLICATION(23)} + } + + /** + * + */ + setFailure() { + this.success = false; + } + + /** + * + */ + setSuccess() { + this.success = true; + } + + /** + * + * @param {} result + */ + setResult(result) { + if (!Array.isArray(result)) { + throw new Errors.InvalidResultFormat(); + } + this.result = result; + } + + toQualified() { + return this; + } + + /** + * + * @param {BER} ber + * @returns {InvocationResult} + */ + static decode(ber) { + const invocationResult = new InvocationResult(); + ber = ber.getSequence(InvocationResult.BERID); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { // invocationId + invocationResult.invocationId = seq.readInt(); + } else if(tag == BER.CONTEXT(1)) { // success + invocationResult.success = seq.readBoolean() + }else if(tag == BER.CONTEXT(2)) { + invocationResult.result = []; + let res = seq.getSequence(BER.EMBER_SEQUENCE); + while(res.remain > 0) { + tag = res.peek(); + if (tag === BER.CONTEXT(0)) { + let resTag = res.getSequence(BER.CONTEXT(0)); + tag = resTag.peek(); + invocationResult.result.push( + new FunctionArgument( + ParameterTypefromBERTAG(tag), + resTag.readValue() + )); + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + continue + } else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + + return invocationResult; + } + + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(23); + } +} + +module.exports = InvocationResult; \ No newline at end of file diff --git a/EmberLib/Label.js b/EmberLib/Label.js new file mode 100755 index 0000000..8a41235 --- /dev/null +++ b/EmberLib/Label.js @@ -0,0 +1,69 @@ +"use strict"; +const BER = require('../ber.js'); +const Errors = require("../Errors"); + +class Label { + constructor(path, description) { + if (path) { + this.basePath = path; + } + if (description) { + this.description = description; + } + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(Label.BERID); + if (this.basePath == null) { + throw new Errors.InvalidEmberNode("", "Missing label base path"); + } + ber.startSequence(BER.CONTEXT(0)); + ber.writeRelativeOID(this.basePath, BER.EMBER_RELATIVE_OID); + ber.endSequence(); + if (this.description == null) { + throw new Errors.InvalidEmberNode("", "Missing label description"); + } + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.description, BER.EMBER_STRING); + ber.endSequence(); + ber.endSequence(); + } + + /** + * + * @param {BER} ber + * @returns {Label} + */ + static decode(ber) { + var l = new Label(); + + ber = ber.getSequence(Label.BERID); + + while (ber.remain > 0) { + var tag = ber.peek(); + var seq = ber.getSequence(tag); + if (tag == BER.CONTEXT(0)) { + l.basePath = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); + } else if (tag == BER.CONTEXT(1)) { + l.description = seq.readString(BER.EMBER_STRING); + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return l; + } + + /** + * + */ + static get BERID() { + return BER.APPLICATION(18); + } +} + +module.exports = Label; \ No newline at end of file diff --git a/EmberLib/Matrix.js b/EmberLib/Matrix.js new file mode 100755 index 0000000..9f8ac2f --- /dev/null +++ b/EmberLib/Matrix.js @@ -0,0 +1,460 @@ +"use strict"; +const MatrixConnection = require("./MatrixConnection"); +const TreeNode = require("./TreeNode"); +const BER = require('../ber.js'); +const MatrixMode = require("./MatrixMode"); +const MatrixOperation = require("./MatrixOperation"); +const MatrixType = require("./MatrixType"); +const Errors = require("../Errors"); + +class Matrix extends TreeNode +{ + constructor() { + super(); + this._connectedSources = {}; + this._numConnections = 0; + this.targets = null; + this.sources = null; + this.connections = {}; + } + + isMatrix() { + return true; + } + + /** + * + * @param {number} targetID + * @param {number[]} sources + * @param {Operation} operation + * @returns {boolean} + */ + canConnect(targetID, sources, operation) { + return Matrix.canConnect(this, targetID, sources, operation); + } + + /** + * + * @param {Object} connections + * @returns {root} + */ + connect(connections) { + const r = this.getTreeBranch(); + const m = r.getElementByPath(this.getPath()); + m.connections = connections; + return r; + } + + /** + * + * @param {number} targetID + * @param {number[]} sources + */ + connectSources(targetID, sources) { + return Matrix.connectSources(this, targetID, sources); + } + + /** + * + * @param {number} targetID + * @param {number[]} sources + */ + disconnectSources(targetID, sources) { + return Matrix.disconnectSources(this, targetID, sources); + } + + /** + * + * @param {BER} ber + */ + encodeConnections(ber) { + if (this.connections != null) { + ber.startSequence(BER.CONTEXT(5)); + ber.startSequence(BER.EMBER_SEQUENCE); + + for(let id in this.connections) { + if (this.connections.hasOwnProperty(id)) { + ber.startSequence(BER.CONTEXT(0)); + this.connections[id].encode(ber); + ber.endSequence(); + } + } + ber.endSequence(); + ber.endSequence(); + } + } + + /** + * + * @param {BER} ber + */ + encodeSources(ber) { + if (this.sources != null) { + ber.startSequence(BER.CONTEXT(4)); + ber.startSequence(BER.EMBER_SEQUENCE); + + for(let i=0; i Number(i))); + + if (matrixNode.connections[targetID].isLocked()) { + return false; + } + if (type === MatrixType.oneToN && + matrixNode.contents.maximumTotalConnects == null && + matrixNode.contents.maximumConnectsPerTarget == null) { + return sMap.size < 2; + } + else if (type === MatrixType.oneToN && sMap.size >= 2) { + return false; + } + else if (type === MatrixType.oneToOne) { + if (sMap.size > 1) { + return false; + } + const sourceConnections = matrixNode._connectedSources[sources[0]]; + return sourceConnections == null || sourceConnections.size === 0 || sourceConnections.has(targetID); + } + else { + // N to N + if (matrixNode.contents.maximumConnectsPerTarget != null && + newSources.length > matrixNode.contents.maximumConnectsPerTarget) { + return false; + } + if (matrixNode.contents.maximumTotalConnects != null) { + let count = matrixNode._numConnections - oldSources.length; + if (newSources) { + count += newSources.length; + } + return count <= matrixNode.contents.maximumTotalConnects; + } + return true; + } + } + + /** + * + * @param {MatrixNode} matrixNode + * @param {number} targetID + * @param {number[]} sources + */ + static connectSources(matrix, targetID, sources) { + const target = Number(targetID); + if (matrix.connections == null) { + matrix.connections = {}; + } + if (matrix.connections[target] == null) { + matrix.connections[target] = new MatrixConnection(target); + } + matrix.connections[target].connectSources(sources); + if (sources != null) { + for(let source of sources) { + if (matrix._connectedSources[source] == null) { + matrix._connectedSources[source] = new Set(); + } + if (!matrix._connectedSources[source].has(target)) { + matrix._connectedSources[source].add(target); + matrix._numConnections++; + } + } + } + } + + /** + * + * @param {BER} ber + * @returns {number[]} + */ + static decodeTargets(ber) { + const targets = []; + ber = ber.getSequence(BER.EMBER_SEQUENCE); + while(ber.remain > 0) { + let seq = ber.getSequence(BER.CONTEXT(0)); + seq = seq.getSequence(BER.APPLICATION(14)); + seq = seq.getSequence(BER.CONTEXT(0)); + targets.push(seq.readInt()); + } + return targets; + } + + /** + * + * @param {BER} ber + * @returns {number[]} + */ + static decodeSources(ber) { + const sources = []; + ber = ber.getSequence(BER.EMBER_SEQUENCE); + while(ber.remain > 0) { + let seq = ber.getSequence(BER.CONTEXT(0)); + seq = seq.getSequence(BER.APPLICATION(15)); + seq = seq.getSequence(BER.CONTEXT(0)); + sources.push(seq.readInt()); + } + return sources; + } + + /** + * + * @param {BER} ber + * @returns {Object} + */ + static decodeConnections(ber) { + const connections = {}; + const seq = ber.getSequence(BER.EMBER_SEQUENCE); + while(seq.remain > 0) { + const conSeq = seq.getSequence(BER.CONTEXT(0)); + const con = MatrixConnection.decode(conSeq); + connections[con.target] = (con); + } + return connections; + } + + /** + * + * @param {MatrixNode} matrixNode + * @param {number} targetID + * @param {number[]} sources + */ + static disconnectSources(matrix, targetID, sources) { + const target = Number(targetID); + if (matrix.connections[target] == null) { + matrix.connections[target] = new MatrixConnection(target); + } + matrix.connections[target].disconnectSources(sources); + if (sources != null) { + for(let source of sources) { + if (matrix._connectedSources[source] == null) { + continue; + } + if (matrix._connectedSources[source].has(target)) { + matrix._connectedSources[source].delete(target); + matrix._numConnections--; + } + } + } + } + + /** + * + * @param {MatrixNode} matrix + * @param {number} source + */ + static getSourceConnections(matrix, source) { + const targets = matrix._connectedSources[source]; + if (targets) { + return [...targets]; + } + return []; + } + + /** + * + * @param {QualifiedMatrix|MatrixNode} matrix + * @param {QualifiedMatrix|MatrixNode} newMatrix + * @returns {boolean} - True if something changed + */ + static MatrixUpdate(matrix, newMatrix) { + let modified = false; + if (newMatrix.targets != null) { + matrix.targets = newMatrix.targets; + modified = true; + } + if (newMatrix.sources != null) { + matrix.sources = newMatrix.sources; + modified = true; + } + if (newMatrix.connections != null) { + if (matrix.connections == null) { + matrix.connections = {}; + modified = true; + } + for(let id in newMatrix.connections) { + if (newMatrix.connections.hasOwnProperty(id)) { + const connection = newMatrix.connections[id]; + if ((connection.target < matrix.contents.targetCount) && + (connection.target >= 0)) { + if (matrix.connections[connection.target] == null) { + matrix.connections[connection.target] = new MatrixConnection(connection.target); + modified = true; + } + if (matrix.connections[connection.target].isDifferent(connection.sources)) { + matrix.connections[connection.target].setSources(connection.sources); + modified = true; + } + } + else { + throw new Errors.InvalidMatrixSignal(connection.target, "Invalid target") + } + } + } + } + return modified; + } + + /** + * + * @param {MatrixNode} matrixNode + * @param {number} targetID + * @param {number[]} sources + */ + static setSources(matrix, targetID, sources) { + const currentSource = matrix.connections[targetID] == null || matrix.connections[targetID].sources == null ? + [] : matrix.connections[targetID].sources; + if (currentSource.length > 0) { + this.disconnectSources(matrix, targetID, currentSource) + } + Matrix.connectSources(matrix, targetID, sources); + } + + /** + * + * @param {MatrixNode} matrixNode + * @param {number} targetID + * @param {number[]} sources + */ + static validateConnection(matrixNode, targetID, sources) { + if (targetID < 0) { + throw new Errors.InvalidMatrixSignal(targetID, "target"); + } + if (sources == null) { + throw new Errors.InvalidSourcesFormat(); + } + for(let i = 0; i < sources.length; i++) { + if (sources[i] < 0) { + throw new Errors.InvalidMatrixSignal(sources[i], `Source at index ${i}`); + } + } + if (matrixNode.contents.mode === MatrixMode.linear) { + if (targetID >= matrixNode.contents.targetCount) { + throw new Errors.InvalidMatrixSignal(targetID, `Target higher than max value ${matrixNode.contents.targetCount}`); + } + for(let i = 0; i < sources.length; i++) { + if (sources[i] >= matrixNode.contents.sourceCount) { + throw new Errors.InvalidMatrixSignal(sources[i],`Source at index ${i} higher than max ${matrixNode.contents.sourceCount}`); + } + } + } + else if ((matrixNode.targets == null) || (matrixNode.sources == null)) { + throw new Errors.InvalidEmberNode(matrixNode.getPath(),"Non-Linear matrix should have targets and sources"); + } + else { + let found = false; + for(let i = 0; i < matrixNode.targets.length; i++) { + if (matrixNode.targets[i] === targetID) { + found = true; + break; + } + } + if (!found) { + throw new Errors.InvalidMatrixSignal(targetID, "Not part of existing targets"); + } + found = false; + for(let i = 0; i < sources.length; i++) { + for(let j = 0; j < matrixNode.sources.length; j++) { + if (matrixNode.sources[j] === sources[i]) { + found = true; + break; + } + } + if (!found) { + throw new Errors.InvalidMatrixSignal(sources[i],`Unknown source at index ${i}`); + } + } + } + } +} + +module.exports = Matrix; \ No newline at end of file diff --git a/EmberLib/MatrixConnection.js b/EmberLib/MatrixConnection.js new file mode 100755 index 0000000..45ec689 --- /dev/null +++ b/EmberLib/MatrixConnection.js @@ -0,0 +1,192 @@ +"use strict"; +const BER = require('../ber.js'); +const MatrixOperation = require("./MatrixOperation"); +const MatrixDisposition = require("./MatrixDisposition"); +const Errors = require("../Errors"); + +class MatrixConnection { + /** + * + * @param {number} target + */ + constructor(target) { + if (target) { + let _target = Number(target); + if (isNaN(_target)) { + throw new Errors.InvalidMatrixSignal(target, "Can't create connection with invalid target.") + } + this.target = _target; + } + else { + this.target = 0; + } + this._locked = false; + } + + /** + * + * @param {number[]} sources + */ + connectSources(sources) { + this.sources = this.validateSources(sources); + } + + /** + * + * @param {number[]} sources + */ + disconnectSources(sources) { + if (sources == null) { + return; + } + let s = new Set(this.sources); + for(let item of sources) { + s.delete(item); + } + this.sources = [...s].sort(); + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(MatrixConnection.BERID); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.target); + ber.endSequence(); + + if (this.sources != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeRelativeOID(this.sources.join("."), BER.EMBER_RELATIVE_OID); + ber.endSequence(); + } + if (this.operation != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.writeInt(this.operation.value); + ber.endSequence(); + } + if (this.disposition != null) { + ber.startSequence(BER.CONTEXT(3)); + ber.writeInt(this.disposition.value); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * + * @param {number[]|null} sources + */ + isDifferent(sources) { + const newSources = this.validateSources(sources); + + if (this.sources == null && newSources == null) { + return false; + } + + if ((this.sources == null && newSources != null)|| + (this.sources != null && newSources == null) || + (this.sources.length != newSources.length)) { + return true; + } + // list are ordered, so we can simply parse 1 by 1. + for(let i = 0; i < this.sources.length; i++) { + if (this.sources[i] !== newSources[i]) { + return true; + } + } + return false; + } + + /** + * @returns {boolean} + */ + isLocked() { + return this._locked; + } + + /** + * + */ + lock() { + this._locked = true; + } + + /** + * + * @param {number[]} sources + */ + setSources(sources) { + if (sources == null) { + delete this.sources; + return; + } + this.sources = this.validateSources(sources); + } + + /** + * + * @param {number[]} sources + * @returns {number[]} - uniq and sorted + */ + validateSources(sources) { + if (sources == null) { + return null; + } + const s = new Set(sources.map(i => Number(i))); + return [...s].sort(); + } + + /** + * + */ + unlock() { + this._locked = false; + } + + /** + * + * @param {BER} ber + * @returns {MatrixConnection} + */ + static decode(ber) { + const c = new MatrixConnection(); + ber = ber.getSequence(MatrixConnection.BERID); + while (ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if (tag == BER.CONTEXT(0)) { + c.target = seq.readInt(); + } + else if (tag == BER.CONTEXT(1)) { + const sources = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); + if (sources === "") { + c .sources = []; + } + else { + c.sources = sources.split(".").map(i => Number(i)); + } + } else if (tag == BER.CONTEXT(2)) { + c.operation = MatrixOperation.get(seq.readInt()); + + } else if (tag == BER.CONTEXT(3)) { + c.disposition = MatrixDisposition.get(seq.readInt()); + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return c; + } + + /** + * + */ + static get BERID() { + return BER.APPLICATION(16); + } +} + +module.exports = MatrixConnection; \ No newline at end of file diff --git a/EmberLib/MatrixContents.js b/EmberLib/MatrixContents.js new file mode 100755 index 0000000..1bbcda8 --- /dev/null +++ b/EmberLib/MatrixContents.js @@ -0,0 +1,154 @@ +"use strict"; + +const MatrixType = require("./MatrixType"); +const MatrixMode = require("./MatrixMode"); +const BER = require('../ber.js'); +const Label = require("./Label"); +const errors = require("../Errors"); + +class MatrixContents { + constructor(type = MatrixType.oneToN, mode = MatrixMode.linear) { + this.type = type; + this.mode = mode; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.EMBER_SET); + if (this.identifier != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeString(this.identifier, BER.EMBER_STRING); + ber.endSequence(); + } + if (this.description != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.description, BER.EMBER_STRING); + ber.endSequence(); + } + if (this.type != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.writeInt(this.type.value); + ber.endSequence(); + } + if (this.mode != null) { + ber.startSequence(BER.CONTEXT(3)); + ber.writeInt(this.mode.value); + ber.endSequence(); + } + if (this.targetCount != null) { + ber.startSequence(BER.CONTEXT(4)); + ber.writeInt(this.targetCount); + ber.endSequence(); + } + if (this.sourceCount != null) { + ber.startSequence(BER.CONTEXT(5)); + ber.writeInt(this.sourceCount); + ber.endSequence(); + } + if (this.maximumTotalConnects != null) { + ber.startSequence(BER.CONTEXT(6)); + ber.writeInt(this.maximumTotalConnects); + ber.endSequence(); + } + if (this.maximumConnectsPerTarget != null) { + ber.startSequence(BER.CONTEXT(7)); + ber.writeInt(this.maximumConnectsPerTarget); + ber.endSequence(); + } + if (this.parametersLocation != null) { + ber.startSequence(BER.CONTEXT(8)); + let param = Number(this.parametersLocation) + if (isNaN(param)) { + ber.writeRelativeOID(this.parametersLocation, BER.EMBER_RELATIVE_OID); + } + else { + ber.writeInt(param); + } + ber.endSequence(); + } + if (this.gainParameterNumber != null) { + ber.startSequence(BER.CONTEXT(9)); + ber.writeInt(this.gainParameterNumber); + ber.endSequence(); + } + if (this.labels != null) { + ber.startSequence(BER.CONTEXT(10)); + ber.startSequence(BER.EMBER_SEQUENCE); + for(var i =0; i < this.labels.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + this.labels[i].encode(ber); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); + } + if (this.schemaIdentifiers != null) { + ber.startSequence(BER.CONTEXT(11)); + ber.writeString(this.schemaIdentifiers, BER.EMBER_STRING); + ber.endSequence(); + } + if (this.templateReference != null) { + ber.startSequence(BER.CONTEXT(12)); + ber.writeRelativeOID(this.templateReference, BER.EMBER_RELATIVE_OID); + ber.endSequence(); + } + ber.endSequence(); + } + + static decode(ber) { + const mc = new MatrixContents(); + ber = ber.getSequence(BER.EMBER_SET); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + + if(tag == BER.CONTEXT(0)) { + mc.identifier = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(1)) { + mc.description = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(2)) { + mc.type = MatrixType.get(seq.readInt()); + } else if(tag == BER.CONTEXT(3)) { + mc.mode = MatrixMode.get(seq.readInt()); + } else if(tag == BER.CONTEXT(4)) { + mc.targetCount = seq.readInt(); + } else if(tag == BER.CONTEXT(5)) { + mc.sourceCount = seq.readInt(); + } else if(tag == BER.CONTEXT(6)) { + mc.maximumTotalConnects = seq.readInt(); + } else if(tag == BER.CONTEXT(7)) { + mc.maximumConnectsPerTarget = seq.readInt(); + } else if(tag == BER.CONTEXT(8)) { + tag = seq.peek(); + if (tag === BER.EMBER_RELATIVE_OID) { + mc.parametersLocation = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else { + mc.parametersLocation = seq.readInt(); + } + } else if(tag == BER.CONTEXT(9)) { + mc.gainParameterNumber = seq.readInt(); + } else if(tag == BER.CONTEXT(10)) { + mc.labels = []; + seq = seq.getSequence(BER.EMBER_SEQUENCE); + while(seq.remain > 0) { + let lSeq = seq.getSequence(BER.CONTEXT(0)); + mc.labels.push(Label.decode(lSeq)); + } + } else if(tag == BER.CONTEXT(11)) { + mc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(12)) { + mc.templateReference = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return mc; + } +} + +module.exports = MatrixContents; \ No newline at end of file diff --git a/EmberLib/MatrixDisposition.js b/EmberLib/MatrixDisposition.js new file mode 100755 index 0000000..1cce450 --- /dev/null +++ b/EmberLib/MatrixDisposition.js @@ -0,0 +1,18 @@ +const Enum = require('enum'); + +// ConnectionDisposition ::= +// INTEGER { +// tally (0), -- default +// modified (1), -- sources contains new current state +// pending (2), -- sources contains future state +// locked (3) -- error: target locked. sources contains current state +// -- more tbd. +// } +const MatrixDisposition = new Enum({ + tally: 0, + modified: 1, + pending: 2, + locked: 3 +}); + +module.exports = MatrixDisposition; \ No newline at end of file diff --git a/EmberLib/MatrixMode.js b/EmberLib/MatrixMode.js new file mode 100755 index 0000000..7101df0 --- /dev/null +++ b/EmberLib/MatrixMode.js @@ -0,0 +1,8 @@ +const Enum = require('enum'); + +const MatrixMode = new Enum({ + linear: 0, + nonLinear: 1 +}); + +module.exports = MatrixMode; \ No newline at end of file diff --git a/EmberLib/MatrixNode.js b/EmberLib/MatrixNode.js new file mode 100755 index 0000000..7cf53d4 --- /dev/null +++ b/EmberLib/MatrixNode.js @@ -0,0 +1,117 @@ +"use strict"; + +const Matrix = require("./Matrix"); +const MatrixContents = require("./MatrixContents"); +const QualifiedMatrix = require("./QualifiedMatrix"); +const BER = require('../ber.js'); +const errors = require("../Errors"); + +class MatrixNode extends Matrix { + constructor(number = undefined) { + super(); + this.number = number; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(MatrixNode.BERID); + + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.number); + ber.endSequence(); // BER.CONTEXT(0) + + if(this.contents != null) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + this.encodeChildren(ber); + this.encodeTargets(ber); + this.encodeSources(ber); + this.encodeConnections(ber); + + ber.endSequence(); // BER.APPLICATION(3) + } + + + + /** + * + * @param {boolean} complete + * @returns {MatrixNode} + */ + getMinimal(complete = false) { + const number = this.getNumber(); + const m = new MatrixNode(number); + if (complete) { + if (this.contents != null) { + m.contents = this.contents; + } + if (this.targets != null) { + m.targets = this.targets; + } + if (this.sources != null) { + m.sources = this.sources; + } + if (this.connections != null) { + m.connections = this.connections; + } + } + return m; + } + + /** + * @returns {QualifiedMatrix} + */ + toQualified() { + const qm = new QualifiedMatrix(this.getPath()); + qm.update(this); + return qm; + } + + /** + * + * @param {BER} ber + * @returns {MatrixNode} + */ + static decode(ber) { + const m = new MatrixNode(); + ber = ber.getSequence(MatrixNode.BERID); + while (ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if (tag == BER.CONTEXT(0)) { + m.number = seq.readInt(); + } + else if (tag == BER.CONTEXT(1)) { + m.contents = MatrixContents.decode(seq); + + } else if (tag == BER.CONTEXT(2)) { + m.decodeChildren(seq); + } else if (tag == BER.CONTEXT(3)) { + m.targets = Matrix.decodeTargets(seq); + } else if (tag == BER.CONTEXT(4)) { + m.sources = Matrix.decodeSources(seq); + } else if (tag == BER.CONTEXT(5)) { + m.connections = Matrix.decodeConnections(seq); + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return m; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(13); + } +} + +module.exports = MatrixNode; \ No newline at end of file diff --git a/EmberLib/MatrixOperation.js b/EmberLib/MatrixOperation.js new file mode 100755 index 0000000..f211a0b --- /dev/null +++ b/EmberLib/MatrixOperation.js @@ -0,0 +1,17 @@ +const Enum = require('enum'); + +// ConnectionOperation ::= +// INTEGER { +// absolute (0), -- default. sources contains absolute information +// connect (1), -- nToN only. sources contains sources to add to connection +// disconnect (2) -- nToN only. sources contains sources to remove from +// connection +// } + +const MatrixOperation = new Enum({ + absolute: 0, + connect: 1, + disconnect: 2 +}); + +module.exports = MatrixOperation; \ No newline at end of file diff --git a/EmberLib/MatrixType.js b/EmberLib/MatrixType.js new file mode 100755 index 0000000..8008bd4 --- /dev/null +++ b/EmberLib/MatrixType.js @@ -0,0 +1,10 @@ + +const Enum = require('enum'); + +const MatrixType = new Enum({ + oneToN: 0, + oneToOne: 1, + nToN: 2 +}); + +module.exports = MatrixType; \ No newline at end of file diff --git a/EmberLib/Node.js b/EmberLib/Node.js new file mode 100755 index 0000000..e24c2eb --- /dev/null +++ b/EmberLib/Node.js @@ -0,0 +1,70 @@ +"use strict"; + +const Element = require("./Element"); +const QualifiedNode = require("./QualifiedNode"); +const NodeContents = require("./NodeContents"); +const BER = require('../ber.js'); +const Errors = require("../Errors"); + +class Node extends Element { + /** + * + * @param {number} number + */ + constructor(number) { + super(number); + this._seqID = Node.BERID; + /** @type {NodeContents} */ + this.contents = null; + } + + /** + * @returns {boolean} + */ + isNode() { + return true; + } + + /** + * @returns {QualifiedNode} + */ + toQualified() { + const qn = new QualifiedNode(this.getPath()); + qn.update(this); + return qn; + } + + /** + * + * @param {BER} ber + * @returns {Node} + */ + static decode(ber) { + const n = new Node(); + ber = ber.getSequence(Node.BERID); + + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + n.number = seq.readInt(); + } else if(tag == BER.CONTEXT(1)) { + n.contents = NodeContents.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + n.decodeChildren(seq); + } else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return n; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(3); + } +} + +module.exports = Node; diff --git a/EmberLib/NodeContents.js b/EmberLib/NodeContents.js new file mode 100755 index 0000000..cb3c479 --- /dev/null +++ b/EmberLib/NodeContents.js @@ -0,0 +1,87 @@ +"use strict"; +const BER = require('../ber.js'); +const errors = require("../Errors"); + +class NodeContents{ + /** + * + * @param {string} identifier + * @param {string} description + */ + constructor(identifier=null, description=null) { + this.isOnline = true; + this.identifier = identifier; + this.description = description; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.EMBER_SET); + + if(this.identifier != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeString(this.identifier, BER.EMBER_STRING); + ber.endSequence(); // BER.CONTEXT(0) + } + + if(this.description != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeString(this.description, BER.EMBER_STRING); + ber.endSequence(); // BER.CONTEXT(1) + } + + if(this.isRoot != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.writeBoolean(this.isRoot); + ber.endSequence(); // BER.CONTEXT(2) + } + + if(this.isOnline != null) { + ber.startSequence(BER.CONTEXT(3)); + ber.writeBoolean(this.isOnline); + ber.endSequence(); // BER.CONTEXT(3) + } + + if(this.schemaIdentifiers != null) { + ber.startSequence(BER.CONTEXT(4)); + ber.writeString(this.schemaIdentifiers, BER.EMBER_STRING); + ber.endSequence(); // BER.CONTEXT(4) + } + + ber.endSequence(); // BER.EMBER_SET + } + + /** + * + * @param {BER} ber + * @returns {NodeContents} + */ + static decode(ber) { + var nc = new NodeContents(); + ber = ber.getSequence(BER.EMBER_SET); + while(ber.remain > 0) { + var tag = ber.peek(); + var seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + nc.identifier = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(1)) { + nc.description = seq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(2)) { + nc.isRoot = seq.readBoolean(); + } else if(tag == BER.CONTEXT(3)) { + nc.isOnline = seq.readBoolean(); + } else if(tag == BER.CONTEXT(4)) { + nc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); + } else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + + return nc; + } +} + +module.exports = NodeContents; \ No newline at end of file diff --git a/EmberLib/Parameter.js b/EmberLib/Parameter.js new file mode 100755 index 0000000..a7a07c8 --- /dev/null +++ b/EmberLib/Parameter.js @@ -0,0 +1,82 @@ +"use strict"; + +const Element = require("./Element"); +const QualifiedParameter = require("./QualifiedParameter"); +const BER = require('../ber.js'); +const ParameterContents = require("./ParameterContents"); +const Errors = require("../Errors"); + +class Parameter extends Element { + /** + * + * @param {number} number + */ + constructor(number) { + super(); + this.number = number; + this._seqID = Parameter.BERID; + } + + /** + * @returns {boolean} + */ + isParameter() { + return true; + } + + /** + * Generate a Root of a partial tree containing the Parameter and its new value. + * Should be sent to the Provider to update the value. + * @param {string|number} value + * @returns {Root} + */ + setValue(value) { + return this.getTreeBranch(undefined, (m) => { + m.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); + }); + } + + /** + * @returns {QualifiedParameter} + */ + toQualified() { + const qp = new QualifiedParameter(this.getPath()); + qp.update(this); + return qp; + } + + /** + * + * @param {BER} ber + * @returns {Parameter} + */ + static decode(ber) { + const p = new Parameter(); + ber = ber.getSequence(Parameter.BERID); + + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + p.number = seq.readInt(); + + } else if(tag == BER.CONTEXT(1)) { + p.contents = ParameterContents.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + p.decodeChildren(seq); + } else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return p; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(1); + } +} + +module.exports = Parameter; \ No newline at end of file diff --git a/EmberLib/ParameterAccess.js b/EmberLib/ParameterAccess.js new file mode 100755 index 0000000..e6f7113 --- /dev/null +++ b/EmberLib/ParameterAccess.js @@ -0,0 +1,12 @@ + +const Enum = require('enum'); + +var ParameterAccess = new Enum({ + none: 0, + read: 1, + write: 2, + readWrite: 3 +}); + + +module.exports = ParameterAccess; \ No newline at end of file diff --git a/EmberLib/ParameterContents.js b/EmberLib/ParameterContents.js new file mode 100755 index 0000000..07ad48a --- /dev/null +++ b/EmberLib/ParameterContents.js @@ -0,0 +1,142 @@ +"use strict"; + +const {ParameterType} = require("./ParameterType"); +const ParameterAccess = require("./ParameterAccess"); +const StringIntegerCollection = require("./StringIntegerCollection"); +const StreamDescription = require("./StreamDescription"); +const BER = require('../ber.js'); +const Errors = require("../Errors"); + +class ParameterContents { + /** + * + * @param {string|number} value + * @param {string} type + */ + constructor(value, type) { + if(value != null) { + this.value = value; + } + if(type != null) { + if((type = ParameterType.get(type)) != null){ + this.type = type + } + } + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.EMBER_SET); + + ber.writeIfDefined(this.identifier, ber.writeString, 0, BER.EMBER_STRING); + ber.writeIfDefined(this.description, ber.writeString, 1, BER.EMBER_STRING); + ber.writeIfDefined(this.value, ber.writeValue, 2); + ber.writeIfDefined(this.minimum, ber.writeValue, 3); + ber.writeIfDefined(this.maximum, ber.writeValue, 4); + ber.writeIfDefinedEnum(this.access, ParameterAccess, ber.writeInt, 5); + ber.writeIfDefined(this.format, ber.writeString, 6, BER.EMBER_STRING); + ber.writeIfDefined(this.enumeration, ber.writeString, 7, BER.EMBER_STRING); + ber.writeIfDefined(this.factor, ber.writeInt, 8); + ber.writeIfDefined(this.isOnline, ber.writeBoolean, 9); + ber.writeIfDefined(this.formula, ber.writeString, 10, BER.EMBER_STRING); + ber.writeIfDefined(this.step, ber.writeInt, 11); + ber.writeIfDefined(this.default, ber.writeValue, 12); + ber.writeIfDefinedEnum(this.type, ParameterType, ber.writeInt, 13); + ber.writeIfDefined(this.streamIdentifier, ber.writeInt, 14); + + if(this.enumMap != null) { + ber.startSequence(BER.CONTEXT(15)); + this.enumMap.encode(ber); + ber.endSequence(); + } + + if(this.streamDescriptor != null) { + ber.startSequence(BER.CONTEXT(16)); + this.streamDescriptor.encode(ber); + ber.endSequence(); + } + + ber.writeIfDefined(this.schemaIdentifiers, ber.writeString, 17, BER.EMBER_STRING); + + ber.endSequence(); + } + + /** + * + * @param {BER} ber + * @returns {ParameterContents} + */ + static decode(ber) { + const pc = new ParameterContents(); + ber = ber.getSequence(BER.EMBER_SET); + + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + switch(tag) { + case BER.CONTEXT(0): + pc.identifier = seq.readString(BER.EMBER_STRING); + break; + case BER.CONTEXT(1): + pc.description = seq.readString(BER.EMBER_STRING); + break; + case BER.CONTEXT(2): + pc.value = seq.readValue(); + break; + case BER.CONTEXT(3): + pc.minimum = seq.readValue(); + break; + case BER.CONTEXT(4): + pc.maximum = seq.readValue(); + break; + case BER.CONTEXT(5): + pc.access = ParameterAccess.get(seq.readInt()); + break; + case BER.CONTEXT(6): + pc.format = seq.readString(BER.EMBER_STRING); + break; + case BER.CONTEXT(7): + pc.enumeration = seq.readString(BER.EMBER_STRING); + break; + case BER.CONTEXT(8): + pc.factor = seq.readInt(); + break; + case BER.CONTEXT(9): + pc.isOnline = seq.readBoolean(); + break; + case BER.CONTEXT(10): + pc.formula = seq.readString(BER.EMBER_STRING); + break; + case BER.CONTEXT(11): + pc.step = seq.readInt(); + break; + case BER.CONTEXT(12): + pc.default = seq.readValue(); + break; + case BER.CONTEXT(13): + pc.type = ParameterType.get(seq.readInt()); + break; + case BER.CONTEXT(14): + pc.streamIdentifier = seq.readInt(); + break; + case BER.CONTEXT(15): + pc.enumMap = StringIntegerCollection.decode(seq); + break; + case BER.CONTEXT(16): + pc.streamDescriptor = StreamDescription.decode(seq); + break; + case BER.CONTEXT(17): + pc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); + break; + default: + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return pc; + } +} + +module.exports = ParameterContents; diff --git a/EmberLib/ParameterType.js b/EmberLib/ParameterType.js new file mode 100755 index 0000000..421a1c9 --- /dev/null +++ b/EmberLib/ParameterType.js @@ -0,0 +1,53 @@ +const Enum = require('enum'); +const BER = require('../ber.js'); +const Errors = require("../Errors"); + +function ParameterTypetoBERTAG(type) { + switch (type.value) { + case 1: return BER.EMBER_INTEGER; + case 2: return BER.EMBER_REAL; + case 3: return BER.EMBER_STRING; + case 4: return BER.EMBER_BOOLEAN; + case 7: return BER.EMBER_OCTETSTRING; + default: + throw new Errors.InvalidEmberNode("", `Unhandled ParameterType ${type}`); + } +} + +function ParameterTypefromBERTAG(tag) { + switch (tag) { + case BER.EMBER_INTEGER: return ParameterType.integer; + case BER.EMBER_REAL: return ParameterType.real; + case BER.EMBER_STRING: return ParameterType.string; + case BER.EMBER_BOOLEAN: return ParameterType.boolean; + case BER.EMBER_OCTETSTRING: return ParameterType.octets; + default: + throw new Errors.InvalidBERFormat(`Unhandled BER TAB ${tag}`); + } +} + +/* +BER VAlue +Value ::= + CHOICE { + integer Integer64, + real REAL, + string EmberString, + boolean BOOLEAN, + octets OCTET STRING, + null NULL + }*/ + + var ParameterType = new Enum({ + integer: 1, + real: 2, + string: 3, + boolean: 4, + trigger: 5, + enum: 6, + octets: 7 +}); + +module.exports = { + ParameterType, ParameterTypetoBERTAG, ParameterTypefromBERTAG +}; \ No newline at end of file diff --git a/EmberLib/QualifiedElement.js b/EmberLib/QualifiedElement.js new file mode 100755 index 0000000..98e45f4 --- /dev/null +++ b/EmberLib/QualifiedElement.js @@ -0,0 +1,59 @@ +"use strict"; +const TreeNode = require("./TreeNode"); +const BER = require('../ber.js'); +const Command = require("./Command"); +const {COMMAND_GETDIRECTORY} = require("./constants"); + +class QualifiedElement extends TreeNode { + /** + * + * @param {string} path + */ + constructor(path) { + super(); + this.path = path; + } + + /** + * @returns {boolean} + */ + isQualified() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(this._seqID); + + this.encodePath(ber); + + if(this.contents != null) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + this.encodeChildren(ber); + + ber.endSequence(); // BER.APPLICATION(3) + } + + /** + * + * @param {Command} cmd + * @returns {TreeNode} + */ + getCommand(cmd) { + const r = this.getNewTree(); + const qn = new this.constructor(); + qn.path = this.getPath(); + r.addElement(qn); + qn.addChild(cmd); + return r; + } +} + +module.exports = QualifiedElement; \ No newline at end of file diff --git a/EmberLib/QualifiedFunction.js b/EmberLib/QualifiedFunction.js new file mode 100755 index 0000000..8e4ac84 --- /dev/null +++ b/EmberLib/QualifiedFunction.js @@ -0,0 +1,63 @@ +"use strict"; + +const QualifiedElement = require("./QualifiedElement"); +const FunctionContent = require("./FunctionContent"); +const {COMMAND_GETDIRECTORY} = require("./constants"); +const BER = require('../ber.js'); +const Command = require("./Command"); +const Errors = require("../Errors"); + +class QualifiedFunction extends QualifiedElement { + /** + * + * @param {string} path + * @param {function} func + */ + constructor(path, func) { + super(path); + this.func = func; + this._seqID = QualifiedFunction.BERID; + } + + /** + * @returns {boolean} + */ + isFunction() { + return true; + } + + /** + * + * @param {BER} ber + * @returns {QualifiedFunction} + */ + static decode(ber) { + const qf = new QualifiedFunction(); + ber = ber.getSequence(QualifiedFunction.BERID); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + qf.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else if(tag == BER.CONTEXT(1)) { + qf.contents = FunctionContent.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + qf.decodeChildren(seq); + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return qf; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(20); + } +} + +module.exports = QualifiedFunction; \ No newline at end of file diff --git a/EmberLib/QualifiedMatrix.js b/EmberLib/QualifiedMatrix.js new file mode 100755 index 0000000..3d4f890 --- /dev/null +++ b/EmberLib/QualifiedMatrix.js @@ -0,0 +1,108 @@ +"use strict"; + +const Matrix = require("./Matrix"); +const BER = require('../ber.js'); +const Command = require("./Command"); +const MatrixContents = require("./MatrixContents"); +const MatrixConnection = require("./MatrixConnection"); +const errors = require("../Errors"); + +class QualifiedMatrix extends Matrix { + /** + * + * @param {string} path + */ + constructor(path) { + super(); + this.path = path; + } + + isQualified() { + return true; + } + /** + * + * @param {Object} connections + * @returns {Root} + */ + connect(connections) { + const r = this.getNewTree(); + const qn = new QualifiedMatrix(); + qn.path = this.path; + r.addElement(qn); + qn.connections = connections; + return r; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(QualifiedMatrix.BERID); + + this.encodePath(ber); + + if(this.contents != null) { + ber.startSequence(BER.CONTEXT(1)); + this.contents.encode(ber); + ber.endSequence(); // BER.CONTEXT(1) + } + + this.encodeChildren(ber); + this.encodeTargets(ber); + this.encodeSources(ber); + this.encodeConnections(ber); + + ber.endSequence(); // BER.APPLICATION(3) + } + + /** + * + * @param {BER} ber + * @returns {QualifiedMatrix} + */ + static decode(ber) { + const qm = new QualifiedMatrix(); + ber = ber.getSequence(QualifiedMatrix.BERID); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + qm.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else if(tag == BER.CONTEXT(1)) { + qm.contents = MatrixContents.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + qm.decodeChildren(seq); + } else if (tag == BER.CONTEXT(3)) { + qm.targets = Matrix.decodeTargets(seq); + } else if (tag == BER.CONTEXT(4)) { + qm.sources = Matrix.decodeSources(seq); + } else if (tag == BER.CONTEXT(5)) { + qm.connections = {}; + seq = seq.getSequence(BER.EMBER_SEQUENCE); + while(seq.remain > 0) { + let conSeq = seq.getSequence(BER.CONTEXT(0)); + let con = MatrixConnection.decode(conSeq); + if (con.target != null) { + qm.connections[con.target] = con; + } + } + } + else { + throw new errors.UnimplementedEmberTypeError(tag); + } + } + return qm; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(17); + } +} + +module.exports = QualifiedMatrix; \ No newline at end of file diff --git a/EmberLib/QualifiedNode.js b/EmberLib/QualifiedNode.js new file mode 100755 index 0000000..155e23c --- /dev/null +++ b/EmberLib/QualifiedNode.js @@ -0,0 +1,53 @@ +"user strict"; +const QualifiedElement = require("./QualifiedElement"); +const BER = require('../ber.js'); +const NodeContents = require("./NodeContents"); +const Errors = require("../Errors"); + +class QualifiedNode extends QualifiedElement { + constructor (path) { + super(path); + this._seqID = QualifiedNode.BERID; + } + + /** + * @returns {boolean} + */ + isNode() { + return true; + } + + /** + * + * @param {BER} ber + * @returns {QualifiedNode} + */ + static decode(ber) { + const qn = new QualifiedNode(); + ber = ber.getSequence(QualifiedNode.BERID); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + qn.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else if(tag == BER.CONTEXT(1)) { + qn.contents = NodeContents.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + qn.decodeChildren(seq); + } else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return qn; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(10); + } +} + +module.exports = QualifiedNode; \ No newline at end of file diff --git a/EmberLib/QualifiedParameter.js b/EmberLib/QualifiedParameter.js new file mode 100755 index 0000000..2ff6d8c --- /dev/null +++ b/EmberLib/QualifiedParameter.js @@ -0,0 +1,73 @@ +"use strict"; + +const QualifiedElement = require("./QualifiedElement"); +const ParameterContents = require("./ParameterContents"); +const BER = require('../ber.js'); +const Errors = require("../Errors"); + +class QualifiedParameter extends QualifiedElement { + /** + * + * @param {string} path + */ + constructor(path) { + super(path); + this._seqID = QualifiedParameter.BERID; + } + + /** + * @returns {boolean} + */ + isParameter() { + return true; + } + + /** + * Generate a Root containing a minimal QualifiedParameter and its new value. + * Should be sent to the Provider to update the value. + * @param {number|string} value + * @returns {TreeNode} + */ + setValue(value) { + let r = this.getNewTree(); + let qp = new QualifiedParameter(this.path); + r.addElement(qp); + qp.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); + return r; + } + + + /** + * + * @param {BER} ber + * @returns {QualifiedParameter} + */ + static decode(ber) { + var qp = new QualifiedParameter(); + ber = ber.getSequence(QualifiedParameter.BERID); + while(ber.remain > 0) { + var tag = ber.peek(); + var seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + qp.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else if(tag == BER.CONTEXT(1)) { + qp.contents = ParameterContents.decode(seq); + } else if(tag == BER.CONTEXT(2)) { + qp.decodeChildren(seq); + } else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return qp; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(9); + } +} + +module.exports = QualifiedParameter; \ No newline at end of file diff --git a/EmberLib/QualifiedTemplate.js b/EmberLib/QualifiedTemplate.js new file mode 100755 index 0000000..a5a2d51 --- /dev/null +++ b/EmberLib/QualifiedTemplate.js @@ -0,0 +1,78 @@ +"use strict"; +const TemplateElement = require("./TemplateElement"); +const QualifiedElement = require("./QualifiedElement"); +const Template = require("./Template"); +const BER = require('../ber.js'); +const Errors = require("../Errors"); + +class QualifiedTemplate extends QualifiedElement { + /** + * + * @param {string} path + * @param {Node|Function|MatrixNode|Parameter} element + */ + constructor(path, element) { + super(path); + this.element = element; + this._seqID = QualifiedTemplate.BERID; + } + + /** + * @returns {boolean} + */ + isTemplate() { + return true; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(QualifiedTemplate.BERID); + + this.encodePath(ber); + + TemplateElement.encodeContent(this, ber); + + ber.endSequence(); + } + + /** + * + * @param {Template} other + */ + update(other) { + this.element = other.element; + } + + /** + * + * @param {BER} ber + * @returns {QualifiedTemplate} + */ + static decode(ber) { + const qt = new QualifiedTemplate(); + ber = ber.getSequence(QualifiedTemplate.BERID); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + qt.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID + } + else { + TemplateElement.decodeContent(qt, tag, seq); + } + } + return qt; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(25); + } +} + +module.exports = QualifiedTemplate; \ No newline at end of file diff --git a/EmberLib/StreamCollection.js b/EmberLib/StreamCollection.js new file mode 100755 index 0000000..4576809 --- /dev/null +++ b/EmberLib/StreamCollection.js @@ -0,0 +1,102 @@ +const BER = require("../ber"); +const StreamEntry = require("./StreamEntry"); + +class StreamCollection { + /** + * + */ + constructor() { + /** @type {Map} */ + this.elements = new Map(); + } + /** + * + * @param {StreamEntry} entry + */ + addEntry(entry) { + this.elements.set(entry.identifier, entry); + } + /** + * + * @param {StreamEntry} entry + */ + removeEntry(entry) { + this.elements.delete(entry.identifier); + } + /** + * + * @param {number} identifier + * @returns {StreamEntry} + */ + getEntry(identifier) { + return this.elements.get(identifier); + } + + /** + * @returns {StreamEntry} + */ + [Symbol.iterator]() { + return this.elements.values(); + } + + /** + * @retuns {number} + */ + size() { + return this.elements.size; + } + + /** + * + * @param {BER.Writer} ber + */ + encode(ber) { + ber.startSequence(StreamCollection.BERID); + for(let [, entry] of this.elements) { + ber.startSequence(BER.CONTEXT(0)); + entry.encode(ber); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * @returns { + * {identifier: number, value: string|number|boolean|Buffer}[] + * } + */ + toJSON() { + const js = []; + for(let [, entry] of this.elements) { + js.push(entry.toJSON()); + } + return js; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(5); + } + + /** + * + * @param {BER.ExtendedReader} ber + * @returns {StreamCollection} + */ + static decode(ber) { + const streamCollection = new StreamCollection(); + const seq = ber.getSequence(this.BERID); + while (seq.remain > 0) { + const rootReader = seq.getSequence(BER.CONTEXT(0)); + while (rootReader.remain > 0) { + const entry = StreamEntry.decode(rootReader); + streamCollection.addEntry(entry); + } + } + return streamCollection; + } +} + +module.exports = StreamCollection; \ No newline at end of file diff --git a/EmberLib/StreamDescription.js b/EmberLib/StreamDescription.js new file mode 100755 index 0000000..4915551 --- /dev/null +++ b/EmberLib/StreamDescription.js @@ -0,0 +1,71 @@ +"use strict"; +const BER = require('../ber.js'); +const StreamFormat = require("./StreamFormat"); +const Errors = require("../Errors"); + +class StreamDescription { + /** + * + * @param {number} offset + * @param {StreamFormat} format + */ + constructor(offset, format) { + this.offset = offset; + this.format = format; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(StreamDescription.BERID); + + ber.writeIfDefinedEnum(this.format, StreamFormat, ber.writeInt, 0); + ber.writeIfDefined(this.offset, ber.writeInt, 1); + + ber.endSequence(); + } + + /** + * + */ + toJSON() { + return { + format: this.format == null ? null : this.format.key, + offset: this.offset + }; + } + + /** + * + * @param {BER} ber + * @returns {StreamDescription} + */ + static decode(ber) { + const sd = new StreamDescription(); + ber = ber.getSequence(StreamDescription.BERID); + + while(ber.remain > 0) { + var tag = ber.peek(); + var seq =ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + sd.format = StreamFormat.get(seq.readInt()); + } else if(tag == BER.CONTEXT(1)) { + sd.offset = seq.readInt(); + } else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return sd; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(12); + } +} + +module.exports = StreamDescription; \ No newline at end of file diff --git a/EmberLib/StreamEntry.js b/EmberLib/StreamEntry.js new file mode 100755 index 0000000..2e6b10e --- /dev/null +++ b/EmberLib/StreamEntry.js @@ -0,0 +1,70 @@ +const BER = require("../ber"); +const Errors = require("../Errors"); + +class StreamEntry { + /** + * + * @param {number} identifier + * @param {string|number|boolean|Buffer} value + */ + constructor(identifier, value ) { + this.identifier = identifier; + this.value = value; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(StreamEntry.BERID); + if (this.identifier != null) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeInt(this.identifier); + ber.endSequence(); + } + if (this.value != null) { + ber.startSequence(BER.CONTEXT(1)); + ber.writeValue(this.value); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * @returns {{ + * identifier: number, + * value: string|number + * }} + */ + toJSON() { + return { + identifier: this.identifier, + value: this.value + } + } + + static get BERID() { + return BER.APPLICATION(5); + } + + static decode(ber) { + const entry = new StreamEntry(); + const seq = ber.getSequence(this.BERID); + while(seq.remain > 0) { + const tag = seq.peek(); + const data = seq.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + entry.identifier = data.readInt(); + } else if(tag == BER.CONTEXT(1)) { + entry.value = data.readValue(); + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return entry; + } +} + +module.exports = StreamEntry; \ No newline at end of file diff --git a/EmberLib/StreamFormat.js b/EmberLib/StreamFormat.js new file mode 100755 index 0000000..ed3435d --- /dev/null +++ b/EmberLib/StreamFormat.js @@ -0,0 +1,25 @@ +"use strict"; +const Enum = require('enum'); + +const StreamFormat = new Enum({ + unsignedInt8: 0, + unsignedInt16BigEndian: 2, + unsignedInt16LittleEndian: 3, + unsignedInt32BigEndian: 4, + unsignedInt32LittleEndian: 5, + unsignedInt64BigEndian: 6, + unsignedInt64LittleENdian: 7, + signedInt8: 8, + signedInt16BigEndian: 10, + signedInt16LittleEndian: 11, + signedInt32BigEndian: 12, + signedInt32LittleEndian: 13, + signedInt64BigEndian: 14, + signedInt64LittleEndian: 15, + ieeeFloat32BigEndian: 20, + ieeeFloat32LittleEndian: 21, + ieeeFloat64BigEndian: 22, + ieeeFloat64LittleEndian: 23 +}); + +module.exports = StreamFormat; \ No newline at end of file diff --git a/EmberLib/StringIntegerCollection.js b/EmberLib/StringIntegerCollection.js new file mode 100755 index 0000000..3fd3eb1 --- /dev/null +++ b/EmberLib/StringIntegerCollection.js @@ -0,0 +1,85 @@ +"use strict"; +const BER = require('../ber.js'); +const StringIntegerPair = require("./StringIntegerPair"); +const Errors = require("../Errors"); + +class StringIntegerCollection { + constructor() { + this._collection = new Map(); + } + + /** + * + * @param {string} key + * @param {StringIntegerPair} value + */ + addEntry(key, value) { + if (!(value instanceof StringIntegerPair)) { + throw new Errors.InvalidStringPair(); + } + this._collection.set(key, value); + } + + /** + * + * @param {string} key + * @returns {StringIntegerPair} + */ + get(key) { + return this._collection.get(key); + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(StringIntegerCollection.BERID); + for(let [,sp] of this._collection) { + ber.startSequence(BER.CONTEXT(0)); + sp.encode(ber); + ber.endSequence(); + } + ber.endSequence(); + } + + /** + * @returns {JSON_StringPair[]} + */ + toJSON() { + const collection = []; + for(let [,sp] of this._collection) { + collection.push(sp.toJSON()); + } + return collection; + } + + /** + * + * @param {BER} ber + * @returns {StringIntegerCollection} + */ + static decode(ber) { + const sc = new StringIntegerCollection(); + const seq = ber.getSequence(StringIntegerCollection.BERID); + while(seq.remain > 0) { + const tag = seq.peek(); + if (tag != BER.CONTEXT(0)) { + throw new Errors.UnimplementedEmberTypeError(tag); + } + const data = seq.getSequence(BER.CONTEXT(0)); + const sp = StringIntegerPair.decode(data) + sc.addEntry(sp.key, sp); + } + return sc; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(8); + } +} + +module.exports = StringIntegerCollection; \ No newline at end of file diff --git a/EmberLib/StringIntegerPair.js b/EmberLib/StringIntegerPair.js new file mode 100755 index 0000000..15bbe1c --- /dev/null +++ b/EmberLib/StringIntegerPair.js @@ -0,0 +1,78 @@ +"use strict"; +const BER = require('../ber.js'); +const Errors = require("../Errors"); + +class StringIntegerPair { + constructor(key,value) { + this.key = key; + this.value = value; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + if (this.key == null || this.value == null) { + throw new Errors.InvalidEmberNode("", "Invalid key/value missing"); + } + ber.startSequence(StringIntegerPair.BERID); + ber.startSequence(BER.CONTEXT(0)); + ber.writeString(this.key, BER.EMBER_STRING); + ber.endSequence(); + ber.startSequence(BER.CONTEXT(1)); + ber.writeInt(this.value); + ber.endSequence(); + ber.endSequence(); + } + + /** + * @typedef {{ + * key: string + * value: number + * }} JSON_StringPair + */ + + /** + * @returns {{ + * key: string + * value: number + * }} + */ + toJSON() { + return { + key: this.key, + value: this.value + } + } + /** + * + * @param {BER} ber + * @returns {StringIntegerPair} + */ + static decode(ber) { + const sp = new StringIntegerPair(); + let seq = ber.getSequence(StringIntegerPair.BERID); + while(seq.remain > 0) { + const tag = seq.peek(); + const dataSeq = seq.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + sp.key = dataSeq.readString(BER.EMBER_STRING); + } else if(tag == BER.CONTEXT(1)) { + sp.value = dataSeq.readInt(); + } else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return sp; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(7); + } +} + +module.exports = StringIntegerPair; \ No newline at end of file diff --git a/EmberLib/Template.js b/EmberLib/Template.js new file mode 100755 index 0000000..44a3950 --- /dev/null +++ b/EmberLib/Template.js @@ -0,0 +1,85 @@ +"use strict"; + +const Element = require("./Element"); +const QualifiedTemplate = require("./QualifiedTemplate"); +const BER = require('../ber.js'); +const TemplateElement = require("./TemplateElement"); +const Errors = require("../Errors"); + +class Template extends Element { + /** + * + * @param {number} number + * @param {Node|Function|MatrixNode|Parameter} element + */ + constructor(number, element) { + super(); + this.number = number; + this.element = element; + this._seqID = Template.BERID; + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(Template.BERID); + this.encodeNumber(ber); + TemplateElement.encodeContent(this, ber); + ber.endSequence(); + } + + /** + * @returns {boolean} + */ + isTemplate() { + return true; + } + + /** + * @returns {QualifiedParameter} + */ + toQualified() { + const qp = new QualifiedTemplate(this.getPath()); + qp.update(this); + return qp; + } + + /** + * + * @param {Template} other + */ + update(other) { + this.element = other.element; + } + + /** + * + * @param {BER} ber + * @returns {Template} + */ + static decode(ber) { + const template = new Template(); + ber = ber.getSequence(Template.BERID); + while(ber.remain > 0) { + let tag = ber.peek(); + let seq = ber.getSequence(tag); + if(tag == BER.CONTEXT(0)) { + template.number = seq.readInt(); + } else { + TemplateElement.decodeContent(template, tag, seq); + } + } + return template; + } + + /** + * @returns {number} + */ + static get BERID() { + return BER.APPLICATION(24); + } +} + +module.exports = Template; \ No newline at end of file diff --git a/EmberLib/TemplateElement.js b/EmberLib/TemplateElement.js new file mode 100755 index 0000000..1642ac0 --- /dev/null +++ b/EmberLib/TemplateElement.js @@ -0,0 +1,75 @@ +"use strict"; + +const BER = require('../ber.js'); +const Parameter = require("./Parameter"); +const Node = require("./Node"); +const MatrixNode = require("./MatrixNode"); +const Function = require("./Function"); +const Errors = require("../Errors"); + +/* +TemplateElement ::= + CHOICE { + parameter Parameter, + node Node, + matrix Matrix, + function Function + } +*/ +class TemplateElement { + /** + * + * @param {Node|Function|Parameter|MatrixNode} ber + */ + static decode(ber) { + const tag = ber.peek(); + if (tag == BER.APPLICATION(1)) { + return Parameter.decode(ber); + } else if(tag == BER.APPLICATION(3)) { + return Node.decode(ber); + } else if(tag == BER.APPLICATION(19)) { + return Function.decode(ber); + } else if(tag == BER.APPLICATION(13)) { + return MatrixNode.decode(ber); + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + + /** + * + * @param {Template|QualifiedTemplate} template + * @param {number} tag + * @param {BER} ber + */ + static decodeContent(template, tag, ber) { + if(tag == BER.CONTEXT(1)) { + template.element = TemplateElement.decode(ber); + } else if(tag == BER.CONTEXT(2)) { + template.description = ber.readString(BER.EMBER_STRING); + } else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + /** + * + * @param {Template|QualifiedTemplate} template + * @param {BER} ber + */ + static encodeContent(template, ber) { + if(template.element != null) { + ber.startSequence(BER.CONTEXT(1)); + template.element.encode(ber); + ber.endSequence(); + } + + if (template.description != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.writeString(template.description, BER.EMBER_STRING); + ber.endSequence(); + } + } +} + +module.exports = TemplateElement; \ No newline at end of file diff --git a/EmberLib/TreeNode.js b/EmberLib/TreeNode.js new file mode 100755 index 0000000..69b6ebe --- /dev/null +++ b/EmberLib/TreeNode.js @@ -0,0 +1,560 @@ +"use strict"; +const BER = require('../ber.js'); +const ElementInterface = require("./ElementInterface"); +const Invocation = require("./Invocation"); +const Command = require("./Command"); +const {COMMAND_GETDIRECTORY, COMMAND_SUBSCRIBE, COMMAND_UNSUBSCRIBE, COMMAND_INVOKE} = require("./constants"); +const Errors = require("../Errors"); + +class TreeNode extends ElementInterface { + constructor() { + super(); + /** @type {TreeNode} */ + this._parent = null; + this._subscribers = new Set(); + } + + _isSubscribable(callback) { + return (callback != null && + (this.isParameter() || this.isMatrix())); + } + + _subscribe(callback) { + this._subscribers.add(callback); + } + + _unsubscribe(callback) { + this._subscribers.delete(callback); + } + + /** + * + * @param {TreeNode} child + */ + addChild(child) { + TreeNode.addElement(this, child); + } + + /** + * + * @param {TreeNode} element + */ + addElement(element) { + TreeNode.addElement(this, element); + } + /** + * + */ + addResult(result) { + this.result = result; + } + + /** + * + */ + clear() { + this.elements = undefined; + } + + get children() { + let it = {}; + const self = this; + it[Symbol.iterator] = function*() { + if (self.elements == null) { return null;} + for(let child of self.elements.entries()) { + yield child[1]; + } + } + return it; + } + + /** + * + * @param {BER} ber + */ + decodeChildren(ber) { + const seq = ber.getSequence(BER.APPLICATION(4)); + while(seq.remain > 0) { + const nodeSeq = seq.getSequence(BER.CONTEXT(0)); + this.addChild(TreeNode.decode(nodeSeq)); + } + } + + /** + * + * @param {BER} ber + */ + encode(ber) { + ber.startSequence(BER.APPLICATION(0)); + if(this.elements != null) { + const elements = this.getChildren(); + ber.startSequence(BER.APPLICATION(11)); + for(var i=0; i < elements.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + elements[i].encode(ber); + ber.endSequence(); // BER.CONTEXT(0) + } + ber.endSequence(); + } + if (this.result != null) { + this.result.encode(ber); + } + ber.endSequence(); // BER.APPLICATION(0) + } + + /** + * + * @param {BER} ber + */ + encodeChildren(ber) { + const children = this.getChildren(); + if(children != null) { + ber.startSequence(BER.CONTEXT(2)); + ber.startSequence(BER.APPLICATION(4)); + for(var i = 0; i < children.length; i++) { + ber.startSequence(BER.CONTEXT(0)); + children[i].encode(ber); + ber.endSequence(); + } + ber.endSequence(); + ber.endSequence(); + } + } + + /** + * + * @param {BER} ber + */ + encodePath(ber) { + if (this.isQualified()) { + ber.startSequence(BER.CONTEXT(0)); + ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); + ber.endSequence(); // BER.CONTEXT(0) + } + } + + /** + * @returns {TreeNode} + */ + getNewTree() { + return new TreeNode(); + } + + /** + * @returns {boolean} + */ + hasChildren() { + return this.elements != null && this.elements.size > 0; + } + + /** + * @returns {boolean} + */ + isRoot() { + return this._parent == null; + } + + /** + * @returns {boolean} + */ + isStream() { + return this.contents != null && + this.contents.streamIdentifier != null; + } + + /** + * @returns {TreeNode} + */ + getMinimalContent() { + let obj; + if (this.isQualified()) { + obj = new this.constructor(this.path); + } + else { + obj = new this.constructor(this.number); + } + if (this.contents != null) { + obj.contents= this.contents; + } + return obj; + } + /** + * @returns {TreeNode} + */ + getDuplicate() { + const obj = this.getMinimal(); + obj.update(this); + return obj; + } + + getMinimal() { + if (this.isQualified()) { + return new this.constructor(this.path); + } + else { + return new this.constructor(this.number); + } + } + + getTreeBranch(child, modifier) { + const m = this.getMinimal(); + if(child != null) { + m.addChild(child); + } + + if(modifier != null) { + modifier(m); + } + + if(this._parent === null) { + return m; + } + else { + return this._parent.getTreeBranch(m); + } + } + + getRoot() { + if(this._parent == null) { + return this; + } else { + return this._parent.getRoot(); + } + } + + /** + * + * @param {Command} cmd + */ + getCommand(cmd) { + return this.getTreeBranch(cmd); + } + + /** + * + * @param {function} callback + */ + getDirectory(callback) { + if (this._isSubscribable(callback) && !this.isStream()) { + this._subscribe(callback); + } + return this.getCommand(new Command(COMMAND_GETDIRECTORY)); + } + + + /** + * @returns {TreeNode[]} + */ + getChildren() { + if(this.elements != null) { + return [...this.elements.values()]; + } + return null; + } + + /** + * @returns {number} + */ + getNumber() { + if (this.isQualified()) { + return TreeNode.path2number(this.getPath()); + } + else { + return this.number; + } + } + + /** + * @returns {TreeNode} + */ + getParent() { + return this._parent; + } + + /** + * + * @param {string} path + * @returns {TreeNode} + */ + getElementByPath(path) { + if (this.elements == null || this.elements.size === 0) { + return null; + } + if (this.isRoot()) { + // check if we have QualifiedElement + const node = this.elements.get(path); + if (node != null) { + return node; + } + } + const myPath = this.getPath(); + if (path == myPath) { + return this; + } + const myPathArray = this.isRoot() ? [] : myPath.split("."); + let pathArray = path.split("."); + + if (pathArray.length < myPathArray.length) { + // We are lower in the tree than the requested path + return null; + } + + // Verify that our path matches the beginning of the requested path + for(var i = 0; i < myPathArray.length; i++) { + if (pathArray[i] != myPathArray[i]) { + return null; + } + } + //Now add child by child to get the requested path + let node = this; + while(myPathArray.length != pathArray.length) { + const number = pathArray[myPathArray.length]; + node = node.getElementByNumber(number); + if (node == null) { + return null; + } + if (node.isQualified() && node.path == path) { + return node; + } + myPathArray.push(number); + } + return node; + } + + /** + * + * @param {number} number + * @returns {TreeNode} + */ + getElementByNumber(number) { + const n = Number(number); + if (this.elements != null) { + return this.elements.get(n); + } + return null; + } + /** + * + * @param {string} identifier + * @returns {TreeNode} + */ + getElementByIdentifier(identifier) { + const children = this.getChildren(); + if (children == null) return null; + for(let i = 0; i < children.length; i++) { + if(children[i].contents != null && + children[i].contents.identifier == identifier) { + return children[i]; + } + } + return null; + } + + /** + * + * @param {number|string} id + * @returns {TreeNode} + */ + getElement(id) { + if(Number.isInteger(id)) { + return this.getElementByNumber(id); + } else { + return this.getElementByIdentifier(id); + } + } + + + /** + * @returns {string} + */ + getPath() { + if (this.path != null) { + return this.path; + } + if(this._parent == null) { + if(this.number == null) { + return ""; + } + else { + return this.number.toString(); + } + } else { + let path = this._parent.getPath(); + if(path.length > 0) { + path = path + "."; + } + return path + this.number; + } + } + + /** + * + * @param {FunctionArgument[]} params + * @returns {TreeNode} + */ + invoke(params) { + if (!this.isFunction()) { + throw new Errors.InvalidEmberNode(this.getPath(), "Invoke only for Ember Function"); + } + const invocation = new Invocation(Invocation.newInvocationID()); + invocation.arguments = params; + const req = this.getCommand(Command.getInvocationCommand(invocation)); + return req; + } + + /** + * + */ + toJSON() { + const res = {nodeType: this.constructor.name}; + const node = this; + if (this.isRoot()) { + const elements = this.getChildren(); + return { + elements: elements.map(e => e.toJSON()) + }; + } + res.number = node.getNumber(); + res.path = node.getPath(); + if (node.contents) { + for(let prop in node.contents) { + if (prop[0] == "_" || node.contents[prop] == null) { + continue; + } + if (node.contents.hasOwnProperty(prop)) { + const type = typeof node.contents[prop]; + if ((type === "string") || (type === "number")) { + res[prop] = node.contents[prop]; + } + else if (node.contents[prop].value != null) { + res[prop] = node.contents[prop].value; + } + else { + res[prop] = node.contents[prop]; + } + } + } + } + if (node.isMatrix()) { + if (node.targets) { + res.targets = node.targets.slice(0); + } + if (node.sources) { + res.sources = node.sources.slice(0); + } + if (node.connections) { + res.connections = {}; + for (let target in node.connections) { + if (node.connections.hasOwnProperty(target)) { + res.connections[target] = {target: target, sources: []}; + if (node.connections[target].sources) { + res.connections[target].sources = node.connections[target].sources.slice(0); + } + } + } + + } + } + const children = node.getChildren(); + if (children) { + res.children = []; + for(let child of children) { + res.children.push(child.toJSON()); + } + } + return res; + } + + /** + * + * @param {function} callback + */ + subscribe(callback) { + if (this._isSubscribable(callback) && this.isStream()) { + this._subscribe(callback); + } + return this.getCommand(new Command(COMMAND_SUBSCRIBE)); + } + + /** + * + * @param {*} callback + */ + unsubscribe(callback) { + this._unsubscribe(callback); + return this.getCommand(new Command(COMMAND_UNSUBSCRIBE)); + } + + /** + * + * @param {TreeNode} other + */ + update(other) { + let modified = false; + if ((other != null) && (other.contents != null)) { + if (this.contents == null) { + this.contents = other.contents; + modified = true; + } + else { + for (var key in other.contents) { + if (key[0] === "_") { continue; } + if (other.contents.hasOwnProperty(key) && + this.contents[key] != other.contents[key]) { + this.contents[key] = other.contents[key]; + modified = true; + } + } + } + } + return modified; + } + + updateSubscribers() { + if (this._subscribers != null) { + for(let cb of this._subscribers) { + cb(this); + } + } + } + + /** + * + * @param {TreeNode} parent + * @param {TreeNode} element + */ + static addElement(parent, element) { + /* + Store element hashed by number direct to the parent. + But if QualifiedElement, it could be directly attached to the root. + In this case, use the path instead of number. + However, if the path is a single number, it is equivalent to number. + */ + element._parent = parent; + if(parent.elements == null) { + parent.elements = new Map(); + } + if (parent.isRoot() && element.isQualified()) { + const path = element.getPath().split("."); + if (path.length > 1) { + parent.elements.set(element.getPath(), element); + return; + } + } + parent.elements.set(element.getNumber(), element); + } + + static path2number(path) { + try { + const numbers = path.split("."); + if (numbers.length > 0) { + return Number(numbers[numbers.length - 1]); + } + } + catch(e) { + // ignore + } + } +} + +module.exports = TreeNode; \ No newline at end of file diff --git a/EmberLib/constants.js b/EmberLib/constants.js new file mode 100755 index 0000000..e49d071 --- /dev/null +++ b/EmberLib/constants.js @@ -0,0 +1,23 @@ +const COMMAND_SUBSCRIBE = 30; +const COMMAND_UNSUBSCRIBE = 31; +const COMMAND_GETDIRECTORY = 32; +const COMMAND_INVOKE = 33; + +const COMMAND_STRINGS = { + [COMMAND_SUBSCRIBE]: "subscribe", + [COMMAND_UNSUBSCRIBE]: "unsubscribe", + [COMMAND_GETDIRECTORY]: "getdirectory", + [COMMAND_INVOKE]: "invoke" +}; + +module.exports = { + COMMAND_SUBSCRIBE, + COMMAND_UNSUBSCRIBE, + COMMAND_GETDIRECTORY, + COMMAND_INVOKE, + Subscribe: COMMAND_SUBSCRIBE, + Unsubscribe: COMMAND_UNSUBSCRIBE, + GetDirectory: COMMAND_GETDIRECTORY, + Invoke: COMMAND_INVOKE, + COMMAND_STRINGS +}; \ No newline at end of file diff --git a/EmberLib/index.js b/EmberLib/index.js new file mode 100755 index 0000000..9dc3813 --- /dev/null +++ b/EmberLib/index.js @@ -0,0 +1,164 @@ +const {Subscribe,COMMAND_SUBSCRIBE,Unsubscribe,COMMAND_UNSUBSCRIBE, + GetDirectory,COMMAND_GETDIRECTORY,Invoke,COMMAND_INVOKE, COMMAND_STRINGS} = require("./constants"); +const BER = require('../ber.js'); +const Errors = require("../Errors"); +const TreeNode = require("./TreeNode"); +const Command = require("./Command"); +const Function = require("./Function"); +const FunctionArgument = require("./FunctionArgument"); +const FunctionContent = require("./FunctionContent"); +const Invocation = require("./Invocation"); +const InvocationResult = require("./InvocationResult"); +const Label = require("./Label"); +const Matrix = require("./Matrix"); +const MatrixNode = require("./MatrixNode"); +const MatrixMode = require("./MatrixMode"); +const MatrixType = require("./MatrixType"); +const MatrixContents = require("./MatrixContents"); +const MatrixConnection = require("./MatrixConnection"); +const MatrixOperation = require("./MatrixOperation"); +const MatrixDisposition = require("./MatrixDisposition"); +const Node = require("./Node"); +const NodeContents = require("./NodeContents"); +const Parameter = require("./Parameter"); +const ParameterContents = require("./ParameterContents"); +const ParameterAccess = require("./ParameterAccess"); +const ParameterType = require("./ParameterType").ParameterType; +const QualifiedFunction = require("./QualifiedFunction"); +const QualifiedMatrix = require("./QualifiedMatrix"); +const QualifiedNode = require("./QualifiedNode"); +const QualifiedParameter = require("./QualifiedParameter"); +const StringIntegerPair = require("./StringIntegerPair"); +const StringIntegerCollection = require("./StringIntegerCollection"); +const StreamFormat = require("./StreamFormat"); +const StreamDescription = require("./StreamDescription"); +const StreamCollection = require("./StreamCollection"); +const Template = require("./Template"); +const TemplateElement = require("./TemplateElement"); +const QualifiedTemplate = require("./QualifiedTemplate"); + +const rootDecode = function(ber) { + const r = new TreeNode(); + let tag = undefined; + while(ber.remain > 0) { + tag = ber.peek(); + if (tag === BER.APPLICATION(0)) { + ber = ber.getSequence(BER.APPLICATION(0)); + tag = ber.peek(); + + if (tag === BER.APPLICATION(11)) { + const seq = ber.getSequence(BER.APPLICATION(11)); + while (seq.remain > 0) { + try { + const rootReader = seq.getSequence(BER.CONTEXT(0)); + while (rootReader.remain > 0) { + r.addElement(childDecode(rootReader)); + } + } + catch (e) { + throw e; + } + } + } + else if (tag === BER.APPLICATION(23)) { // InvocationResult BER.APPLICATION(23) + return InvocationResult.decode(ber) + } + else { + // StreamCollection BER.APPLICATION(6) + // InvocationResult BER.APPLICATION(23) + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + else if (tag === BER.CONTEXT(0)) { + // continuation of previous message + try { + var rootReader = ber.getSequence(BER.CONTEXT(0)); + return childDecode(rootReader) + } + catch (e) { + return r; + } + } + else { + throw new Errors.UnimplementedEmberTypeError(tag); + } + } + return r; +} + +const TreeNodeDecoders = { + [Parameter.BERID]: Parameter.decode, + [Node.BERID]: Node.decode, + [Command.BERID]: Command.decode, + [QualifiedParameter.BERID]: QualifiedParameter.decode, + [QualifiedNode.BERID]: QualifiedNode.decode, + [MatrixNode.BERID]: MatrixNode.decode, + [QualifiedMatrix.BERID]: QualifiedMatrix.decode, + [Function.BERID]: Function.decode, + [QualifiedFunction.BERID]: QualifiedFunction.decode, + [Template.BERID]: Template.decode, + [QualifiedTemplate.BERID]: QualifiedTemplate +}; + +const childDecode = function(ber) { + const tag = ber.peek(); + const decode = TreeNodeDecoders[tag]; + if (decode == null) { + throw new Errors.UnimplementedEmberTypeError(tag); + } + else { + return decode(ber); + } +} + +TreeNode.decode = childDecode; + +const DecodeBuffer = function (packet) { + const ber = new BER.Reader(packet); + return rootDecode(ber); +}; + +module.exports = { + Command, + COMMAND_STRINGS, + childDecode: childDecode, + rootDecode: rootDecode, + DecodeBuffer, + Root: TreeNode, + Function, + FunctionArgument, + FunctionContent, + Invocation, + InvocationResult, + Label, + Matrix, + MatrixNode, + MatrixMode, + MatrixType, + MatrixContents, + MatrixConnection, + MatrixDisposition, + MatrixOperation, + Node, + NodeContents, + Parameter, + ParameterContents, + ParameterAccess, + ParameterType, + QualifiedFunction , + QualifiedMatrix, + QualifiedNode, + QualifiedParameter, + QualifiedTemplate, + StreamFormat, + StreamDescription, + StreamCollection, + StringIntegerPair, + StringIntegerCollection, + Template, + TemplateElement, + Subscribe,COMMAND_SUBSCRIBE, + Unsubscribe,COMMAND_UNSUBSCRIBE, + GetDirectory,COMMAND_GETDIRECTORY, + Invoke,COMMAND_INVOKE +} \ No newline at end of file diff --git a/EmberServer/ElementHandlers.js b/EmberServer/ElementHandlers.js new file mode 100755 index 0000000..e48d60f --- /dev/null +++ b/EmberServer/ElementHandlers.js @@ -0,0 +1,195 @@ +"use strict"; +const QualifiedHandlers = require("./QualifiedHandlers"); +const EmberLib = require('../EmberLib'); +const ServerEvents = require("./ServerEvents"); +const Errors = require("../Errors"); +const winston = require("winston"); + +class ElementHandlers extends QualifiedHandlers{ + /** + * + * @param {EmberServer} server + */ + constructor(server) { + super(server); + } + /** + * + * @param {S101Client} client + * @param {TreeNode} root + * @param {Command} cmd + */ + handleCommand(client, element, cmd) { + let identifier = "root" + if (!element.isRoot()) { + const node = this.server.tree.getElementByPath(element.getPath()); + identifier = node == null || node.contents == null || node.contents.identifier == null ? "unknown" : node.contents.identifier; + } + const src = client == null ? "local" : `${client.socket.remoteAddress}:${client.socket.remotePort}`; + switch(cmd.number) { + case EmberLib.COMMAND_GETDIRECTORY: + this.server.emit("event", ServerEvents.GETDIRECTORY(identifier, element.getPath(), src)); + this.handleGetDirectory(client, element); + break; + case EmberLib.COMMAND_SUBSCRIBE: + this.server.emit("event", ServerEvents.SUBSCRIBE(identifier, element.getPath(), src)); + this.handleSubscribe(client, element); + break; + case EmberLib.COMMAND_UNSUBSCRIBE: + this.server.emit("event", ServerEvents.UNSUBSCRIBE(identifier, element.getPath(), src)); + this.handleUnSubscribe(client, element); + break; + case EmberLib.COMMAND_INVOKE: + this.server.emit("event", ServerEvents.INVOKE(identifier, element.getPath(), src)); + this.handleInvoke(client, cmd.invocation, element); + break; + default: + this.server.emit("error", new Errors.InvalidCommand(cmd.number)); + return; + } + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleGetDirectory(client, element) { + if (client != null) { + if ((element.isMatrix() || element.isParameter()) && + (!element.isStream())) { + // ember spec: parameter without streamIdentifier should + // report their value changes automatically. + this.server.subscribe(client, element); + } + else if (element.isNode()) { + const children = element.getChildren(); + if (children != null) { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if ((child.isMatrix() || child.isParameter()) && + (!child.isStream())) { + this.server.subscribe(client, child); + } + } + } + } + + const res = this.server.getQualifiedResponse(element); + winston.debug("getDirectory response", res); + client.sendBERNode(res); + } + } + + /** + * + * @param {S101Client} client + * @param {Invocation} invocation + * @param {TreeNode} element + */ + handleInvoke(client, invocation, element) { + const result = new EmberLib.InvocationResult(); + result.invocationId = invocation.id; + if (element == null || !element.isFunction()) { + result.setFailure(); + } + else { + try { + result.setResult(element.func(invocation.arguments)); + } + catch(e){ + this.server.emit("error", e); + result.setFailure(); + } + } + const res = new EmberLib.Root(); + res.addResult(result); + client.sendBERNode(res); + } + + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleNode(client, node) { + // traverse the tree + let element = node; + let path = []; + while(element != null) { + if (element.isCommand()) { + break; + } + if (element.number == null) { + this.server.emit("error", new Errors.MissingElementNumber()); + return; + } + + path.push(element.number); + + const children = element.getChildren(); + if ((! children) || (children.length === 0)) { + break; + } + element = children[0]; + } + let cmd = element; + + if (cmd == null) { + this.server.emit("error", new Errors.InvalidRequest()); + this.server.handleError(client); + return path; + } + + element = this.server.tree.getElementByPath(path.join(".")); + + if (element == null) { + this.server.emit("error", new Errors.UnknownElement(path.join("."))); + return this.server.handleError(client); + } + if (cmd.isCommand()) { + this.handleCommand(client, element, cmd); + return path; + } else if ((cmd.isMatrix()) && (cmd.connections != null)) { + this.handleMatrixConnections(client, element, cmd.connections); + } + else if ((cmd.isParameter()) && + (cmd.contents != null) && (cmd.contents.value != null)) { + winston.debug(`setValue for element at path ${path} with value ${cmd.contents.value}`); + this.server.setValue(element, cmd.contents.value, client); + const res = this.server.getResponse(element); + client.sendBERNode(res) + this.server.updateSubscribers(element.getPath(), res, client); + } + else { + this.server.emit("error", new Errors.InvalidRequesrFormat(path.join("."))); + winston.debug("invalid request format"); + return this.server.handleError(client, element.getTreeBranch()); + } + // for logging purpose, return the path. + return path; + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleSubscribe(client, element) { + winston.debug("subscribe", element); + this.server.subscribe(client, element); + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleUnSubscribe(client, element) { + winston.debug("unsubscribe", element); + this.server.unsubscribe(client, element); + } +} + +module.exports = ElementHandlers; \ No newline at end of file diff --git a/EmberServer/EmberServer.js b/EmberServer/EmberServer.js new file mode 100755 index 0000000..741f045 --- /dev/null +++ b/EmberServer/EmberServer.js @@ -0,0 +1,519 @@ +const EventEmitter = require('events').EventEmitter; +const S101Server = require('../EmberSocket').S101Server; +const EmberLib = require('../EmberLib'); +const JSONParser = require("./JSONParser"); +const ElementHandlers = require("./ElementHandlers"); +const ServerEvents = require("./ServerEvents"); +const Errors = require("../Errors"); +const winston = require("winston"); + +class TreeServer extends EventEmitter{ + /** + * + * @param {string} host + * @param {number} port + * @param {TreeNode} tree + */ + constructor(host, port, tree) { + super(); + this._debug = true; + this.timeoutValue = 2000; + this.server = new S101Server(host, port); + this.tree = tree; + this.clients = new Set(); + this.subscribers = {}; + this._handlers = new ElementHandlers(this); + + this.server.on('listening', () => { + winston.debug("listening"); + this.emit('listening'); + }); + + this.server.on('connection', client => { + winston.debug("ember new connection from", client.remoteAddress()); + this.clients.add(client); + client.on("emberTree", (root) => { + winston.debug("ember new request from", client.remoteAddress(), root); + // Queue the action to make sure responses are sent in order. + client.addRequest(() => { + try { + const path = this.handleRoot(client, root); + this.emit("request", {client: client.remoteAddress(), root: root, path: path}); + } + catch(e) { + winston.debug(e.stack) + this.emit("error", e); + } + }); + }); + client.on("disconnected", () => { + this.clients.delete(client); + this.emit('disconnect', client.remoteAddress()); + }); + client.on("error", error => { + this.emit('clientError', { remoteAddress: client.remoteAddress(), error }); + }); + this.emit('connection', client.remoteAddress()); + }); + + this.server.on('disconnected', () => { + this.clients.clear(); + this.emit('disconnected'); + }); + + this.server.on("error", (e) => { + this.emit("error", e); + }); + } + + /** + * @returns {Promise} + */ + close() { + return new Promise((resolve, reject) => { + const cb = e => { + if (e == null) { + return resolve(); + } + return reject(e); + }; + if (this.server.server != null) { + this.server.server.close(cb); + } + else { + cb(); + } + this.clients.clear(); + }); + } + + /** + * + * @param {Matrix} matrix + * @param {number} targetID + * @returns {number} + */ + getDisconnectSource(matrix, targetID) { + return this._handlers.getDisconnectSource(matrix, targetID); + } + + /** + * + * @param {TreeNode} element + * @returns {TreeNode} + */ + getResponse(element) { + return element.getTreeBranch(undefined, node => { + node.update(element); + const children = element.getChildren(); + if (children != null) { + for (let i = 0; i < children.length; i++) { + node.addChild(children[i].getDuplicate()); + } + } + }); + } + + /** + * + * @param {TreeNode} element + */ + getQualifiedResponse(element) { + const res = new EmberLib.Root(); + let dup; + if (element.isRoot() === false) { + dup = element.toQualified(); + } + const children = element.getChildren(); + if (children != null) { + for (let i = 0; i < children.length; i++) { + res.addChild(children[i].toQualified().getMinimalContent()); + } + } + else { + res.addChild(dup); + } + return res; + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + handleError(client, node) { + if (client != null) { + const res = node == null ? this.tree.getMinimal() : node; + client.sendBERNode(res); + } + } + + /** + * + * @param {S101Socket} client + * @param {TreeNode} root + */ + handleRoot(client, root) { + if ((root == null) || (root.elements == null) || (root.elements.size < 1)) { + // ignore empty requests. + return; + } + + const node = root.getChildren()[0]; + client.request = node; + + if (node.path != null) { + return this._handlers.handleQualifiedNode(client, node); + } + else if (node.isCommand()) { + // Command on root element + this._handlers.handleCommand(client, this.tree, node); + return "root"; + } + else { + return this._handlers.handleNode(client, node); + } + } + + /** + * @returns {Promise} + */ + listen() { + return this.server.listen(); + } + + /** + * + * @param {string} path + * @param {number} target + * @param {number[]} sources + */ + matrixConnect(path, target, sources) { + doMatrixOperation(this, path, target, sources, EmberLib.MatrixOperation.connect); + } + + /** + * + * @param {string} path + * @param {number} target + * @param {number[]} sources + */ + matrixDisconnect(path, target, sources) { + doMatrixOperation(this, path, target, sources, EmberLib.MatrixOperation.disconnect); + } + + /** + * + * @param {string} path + * @param {number} target + * @param {number[]} sources + */ + matrixSet(path, target, sources) { + doMatrixOperation(this, path, target, sources, EmberLib.MatrixOperation.absolute); + } + + /** + * + * @param {Matrix} matrix + * @param {number} target + * @param {number[]} sources + * @param {S101Socket} client + * @param {boolean} response + */ + disconnectMatrixTarget(matrix, target, sources, client, response) { + const disconnect = new EmberLib.MatrixConnection(target); + disconnect.setSources([]); + disconnect.disposition = EmberLib.MatrixDisposition.modified; + matrix.setSources(target, []); + if (response) { + this.emit("matrix-disconnect", { + target: target, + sources: sources, + client: client == null ? null : client.remoteAddress() + }); + } + return disconnect; + } + + /** + * + * @param {Matrix} matrix + * @param {number} target + * @param {number[]} sources + * @param {S101Socket} client + * @param {boolean} response + */ + disconnectSources(matrix, target, sources, client, response) { + const disconnect = new EmberLib.MatrixConnection(target); + disconnect.disposition = EmberLib.MatrixDisposition.modified; + matrix.disconnectSources(target, sources); + if (response) { + this.emit("matrix-disconnect", { + target: target, + sources: sources, + client: client == null ? null : client.remoteAddress() + }); + } + return disconnect; + } + + /** + * + * @param {Matrix} matrix + * @param {MatrixConnection} connection + * @param {Matrix} res - result + * @param {S101Socket} client + * @param {boolean} response + */ + preMatrixConnect(matrix, connection, res, client, response) { + const conResult = res.connections[connection.target]; + + if (matrix.contents.type !== EmberLib.MatrixType.nToN && + connection.operation !== EmberLib.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length === 1) { + if (matrix.contents.type === EmberLib.MatrixType.oneToOne) { + // if the source is being used already, disconnect it from current target. + const currentTargets = matrix.getSourceConnections(connection.sources[0]); + if (currentTargets.length === 1 && currentTargets[0] !== connection.target) { + res.connections[currentTargets[0]] = + this.disconnectMatrixTarget(matrix, currentTargets[0], connection.sources, client, response); + } + } + // if the target is connected already, disconnect it + if (matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length === 1) { + if (matrix.contents.type === EmberLib.MatrixType.oneToN) { + const disconnectSource = this.getDisconnectSource(matrix, connection.target); + if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { + if (disconnectSource >= 0 && disconnectSource != connection.sources[0]) { + connection.sources = [disconnectSource]; + } + else { + // do nothing => set disposition to bypass further processing + conResult.disposition = EmberLib.MatrixDisposition.tally; + } + } + } + if (matrix.connections[connection.target].sources[0] !== connection.sources[0]) { + this.disconnectMatrixTarget(matrix, connection.target, matrix.connections[connection.target].sources, client, response) + } + else if (matrix.contents.type === EmberLib.MatrixType.oneToOne) { + // let's change the request into a disconnect + connection.operation = EmberLib.MatrixOperation.disconnect; + } + } + } + } + + applyMatrixConnect(matrix, connection, conResult, client, response) { + // Apply changes + let emitType; + if ((connection.operation == null) || + (connection.operation.value == EmberLib.MatrixOperation.absolute)) { + matrix.setSources(connection.target, connection.sources); + emitType = "matrix-change"; + } + else if (connection.operation == EmberLib.MatrixOperation.connect) { + matrix.connectSources(connection.target, connection.sources); + emitType = "matrix-connect"; + } + conResult.disposition = EmberLib.MatrixDisposition.modified; + if (response && emitType != null) { + // We got a request so emit something. + this.emit(emitType, { + target: connection.target, + sources: connection.sources, + client: client == null ? null : client.remoteAddress() + }); + } + } + + /** + * + * @param {Matrix} matrix + * @param {MatrixConnection} connection + * @param {Matrix} res - result + * @param {S101Socket} client + * @param {boolean} response + */ + applyMatrixOneToNDisconnect(matrix, connection, res, client, response) { + const disconnectSource = this.getDisconnectSource(matrix, connection.target); + if (matrix.connections[connection.target].sources[0] == connection.sources[0]) { + const conResult = res.connections[connection.target]; + if (disconnectSource >= 0 && disconnectSource != connection.sources[0]) { + if (response) { + this.server.emit("matrix-disconnect", { + target: connection.target, + sources: matrix.connections[connection.target].sources, + client: client == null ? null : client.remoteAddress() + }); + } + matrix.setSources(connection.target, [disconnectSource]); + conResult.disposition = EmberLib.MatrixDisposition.modified; + } + else { + // do nothing + conResult.disposition = EmberLib.MatrixDisposition.tally; + } + } + } + + /** + * + * @param {TreeNode} element + */ + replaceElement(element) { + const path = element.getPath(); + const existingElement = this.tree.getElementByPath(path); + if (existingElement == null) { + throw new Errors.UnknownElement(path); + } + const parent = existingElement._parent; + if (parent == null) { + throw new Errors.InvalidEmberNode(path, "No parent. Can't execute replaceElement"); + } + // Replace the element at the parent + parent.elements.set(existingElement.getNumber(), element); + // point the new element to parent + element._parent = parent; + const res = this.getResponse(element); + this.updateSubscribers(path,res); + } + + /** + * + * @param {TreeNode} element + * @param {string|number} value + * @param {S101Socket} origin + * @param {string} key + */ + setValue(element, value, origin, key) { + return new Promise(resolve => { + // Change the element value if write access permitted. + winston.debug("New Setvalue request"); + if (element.contents == null) { + return resolve(); + } + if (element.isParameter() || element.isMatrix()) { + if (element.isParameter() && + (element.contents.access != null) && + (element.contents.access.value > 1)) { + element.contents.value = value; + winston.debug("New value ", value, "path", element.getPath()); + const res = this.getResponse(element); + this.updateSubscribers(element.getPath(),res, origin); + } + else if ((key != null) && (element.contents.hasOwnProperty(key))) { + element.contents[key] = value; + const res = this.getResponse(element); + this.updateSubscribers(element.getPath(),res, origin); + } + const src = origin == null ? "local" : `${origin.socket.remoteAddress}:${origin.socket.remotePort}`; + this.emit("value-change", element); + this.emit("event", ServerEvents.SETVALUE(element.contents.identifier,element.getPath(),src)); + } + return resolve(); + }); + } + + /** + * + * @param {S101Socket} client + * @param {TreeNode} root + */ + subscribe(client, element) { + const path = element.getPath(); + if (this.subscribers[path] == null) { + this.subscribers[path] = new Set(); + } + this.subscribers[path].add(client); + } + + /** + * @returns {Object} + */ + toJSON() { + if (this.tree == null) { + return []; + } + const elements = this.tree.getChildren(); + + return elements.map(element => element.toJSON()); + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} root + */ + unsubscribe(client, element) { + const path = element.getPath(); + if (this.subscribers[path] == null) { + return; + } + this.subscribers[path].delete(client); + } + + /** + * + * @param {string} path + * @param {TreeNode} response + * @param {S101Socket} origin + */ + updateSubscribers(path, response, origin) { + if (this.subscribers[path] == null) { + winston.debug("No subscribers for", path); + return; + } + + for (let client of this.subscribers[path]) { + if (client === origin) { + continue; // already sent the response to origin + } + if (this.clients.has(client)) { + winston.debug("Sending new value to", client.remoteAddress()); + client.queueMessage(response); + } + else { + // clean up subscribers - client is gone + winston.debug("deleting client"); + this.subscribers[path].delete(client); + } + } + } + + /** + * + * @param {object} obj + * @returns {TreeNode} + */ + static JSONtoTree(obj) { + const tree = new EmberLib.Root(); + JSONParser.parseObj(tree, obj); + return tree; + } +} + + +const validateMatrixOperation = function(matrix, target, sources) { + if (matrix == null) { + throw new Errors.UnknownElement(`matrix not found`); + } + if (matrix.contents == null) { + throw new Errors.MissingElementContents(matrix.getPath()); + } + matrix.validateConnection(target, sources); +} + +const doMatrixOperation = function(server, path, target, sources, operation) { + const matrix = server.tree.getElementByPath(path); + + validateMatrixOperation(matrix, target, sources); + + const connection = new EmberLib.MatrixConnection(target); + connection.sources = sources; + connection.operation = operation; + server._handlers.handleMatrixConnections(undefined, matrix, [connection], false); +} + +module.exports = TreeServer; diff --git a/EmberServer/JSONParser.js b/EmberServer/JSONParser.js new file mode 100755 index 0000000..dfa5015 --- /dev/null +++ b/EmberServer/JSONParser.js @@ -0,0 +1,170 @@ +"use strict"; +const ember = require('../EmberLib'); +const Errors = require("../Errors"); + +class JSONParser { + /** + * + * @param {MatrixContent} matrixContent + * @param {object} content + */ + static parseMatrixContent(matrixContent, content) { + if (content.labels) { + matrixContent.labels = []; + for(let l = 0; l < content.labels.length; l++) { + if (typeof (content.labels[l]) === "object") { + matrixContent.labels.push( + new ember.Label( + content.labels[l].basePath, + content.labels[l].description + ) + ); + } + else { + // for backward compatibility... Remove in the future + matrixContent.labels.push( + new ember.Label(content.labels[l]) + ); + } + } + delete content.labels; + } + if (content.type != null) { + if (content.type == "oneToN") { + matrixContent.type = ember.MatrixType.oneToN; + } + else if (content.type == "oneToOne") { + matrixContent.type = ember.MatrixType.oneToOne; + } + else if (content.type == "nToN") { + matrixContent.type = ember.MatrixType.nToN; + matrixContent.maximumTotalConnects = content.maximumTotalConnects == null ? + Number(content.targetCount) * Number(content.sourceCount) : Number(content.maximumTotalConnects); + matrixContent.maximumConnectsPerTarget = content.maximumConnectsPerTarget == null ? + Number(content.sourceCount) : Number(content.maximumConnectsPerTarget); + } + else { + throw new Errors.InvalidEmberNode("", `Invalid matrix type ${content.type}`); + } + delete content.type; + } + if (content.mode != null) { + if (content.mode == "linear") { + matrixContent.mode = ember.MatrixMode.linear; + } + else if (content.mode == "nonLinear") { + matrixContent.mode = ember.MatrixMode.nonLinear; + } + else { + throw new Errors.InvalidEmberNode("",`Invalid matrix mode ${content.mode}`); + } + delete content.mode; + } + } + + /** + * + * @param {TreeNode} parent + * @param {object} obj + */ + static parseObj(parent, obj) { + for(let i = 0; i < obj.length; i++) { + let emberElement; + let content = obj[i]; + let number = content.number != null ? content.number : i; + delete content.number; + if (content.value != null) { + emberElement = new ember.Parameter(number); + emberElement.contents = new ember.ParameterContents(content.value); + if (content.type) { + emberElement.contents.type = ember.ParameterType.get(content.type); + delete content.type; + } + else { + emberElement.contents.type = ember.ParameterType.string; + } + if (content.access) { + emberElement.contents.access = ember.ParameterAccess.get(content.access); + delete content.access; + } + else { + emberElement.contents.access = ember.ParameterAccess.read; + } + if (content.streamDescriptor != null) { + if (content.streamDescriptor.offset == null || content.streamDescriptor.format == null) { + throw new Error("Missing offset or format for streamDescriptor"); + } + emberElement.contents.streamDescriptor = new ember.StreamDescription(); + emberElement.contents.streamDescriptor.offset = content.streamDescriptor.offset; + emberElement.contents.streamDescriptor.format = ember.StreamFormat.get(content.streamDescriptor.format); + delete content.streamDescriptor; + } + } + else if (content.func != null) { + emberElement = new ember.Function(number, content.func); + emberElement.contents = new ember.FunctionContent(); + if (content.arguments != null) { + for(let argument of content.arguments) { + emberElement.contents.arguments.push(new ember.FunctionArgument( + argument.type, + argument.value, + argument.name + )); + } + } + if (content.result != null) { + for(let argument of content.result) { + emberElement.contents.result.push(new ember.FunctionArgument( + argument.type, + argument.value, + argument.name + )); + } + } + delete content.result; + } + else if (content.targetCount != null) { + emberElement = new ember.MatrixNode(number); + emberElement.contents = new ember.MatrixContents(); + this.parseMatrixContent(emberElement.contents, content); + if (content.connections) { + emberElement.connections = {}; + for (let c in content.connections) { + if (! content.connections.hasOwnProperty(c)) { + continue; + } + const t = content.connections[c].target != null ? content.connections[c].target : 0; + emberElement.setSources(t, content.connections[c].sources); + } + delete content.connections; + } + else { + emberElement.connections = {}; + for (let t = 0; t < content.targetCount; t++) { + let connection = new ember.MatrixConnection(t); + emberElement.connections[t] = connection; + } + } + } + else { + emberElement = new ember.Node(number); + emberElement.contents = new ember.NodeContents(); + } + for(let id in content) { + if (emberElement.isFunction() && id === "arguments") { + // we did it already. + continue; + } + if ((id !== "children") && (content.hasOwnProperty(id))) { + emberElement.contents[id] = content[id]; + } + else { + this.parseObj(emberElement, content.children); + } + } + parent.addChild(emberElement); + } + } +} + +module.exports = JSONParser; \ No newline at end of file diff --git a/EmberServer/MatrixHandlers.js b/EmberServer/MatrixHandlers.js new file mode 100755 index 0000000..0bb9f9c --- /dev/null +++ b/EmberServer/MatrixHandlers.js @@ -0,0 +1,135 @@ +"use strict"; +const EmberLib = require('../EmberLib'); +const ServerEvents = require("./ServerEvents"); +const winston = require("winston"); + +class MatrixHandlers { + /** + * + * @param {EmberServer} server + */ + constructor(server) { + this.server = server; + } + + /** + * + * @param {Matrix} matrix + * @param {number} targetID + * @return {number} + */ + getDisconnectSource(matrix, targetID) { + if (matrix.defaultSources) { + return matrix.defaultSources[targetID].contents.value; + } + if (matrix.contents.labels == null || matrix.contents.labels.length == 0) { + return null; + } + const basePath = matrix.contents.labels[0].basePath; + const labels = this.server.tree.getElementByPath(basePath); + const number = labels.getNumber() + 1; + const parent = labels.getParent(); + const children = parent.getChildren(); + for(let child of children) { + if (child.getNumber() === number) { + matrix.defaultSources = child.getChildren(); + return matrix.defaultSources[targetID].contents.value; + } + } + return -1; + } + + /** + * + * @param {S101Client} client + * @param {Matrix} matrix + * @param {Object} connections + * @param {boolean} response=true + */ + handleMatrixConnections(client, matrix, connections, response = true) { + let res; // response + let conResult; + let root; // ember message root + winston.debug("Handling Matrix Connection"); + if (client != null && client.request.isQualified()) { + root = new EmberLib.Root(); + res = new EmberLib.QualifiedMatrix(matrix.getPath()); + //root.elements = [res]; // do not use addchild or the element will get removed from the tree. + root.addElement(res); + } + else { + res = new EmberLib.MatrixNode(matrix.number); + root = matrix._parent.getTreeBranch(res); + } + res.connections = {}; + for(let id in connections) { + if (!connections.hasOwnProperty(id)) { + continue; + } + const connection = connections[id]; + const src = client == null ? "local" : `${client.socket.remoteAddress}:${client.socket.remotePort}`; + this.server.emit("event", ServerEvents.MATRIX_CONNECTION( + matrix.contents.identifier,matrix.getPath(),src,id,connection.sources + )); + conResult = new EmberLib.MatrixConnection(connection.target); + res.connections[connection.target] = conResult; + + if (matrix.connections[connection.target].isLocked()) { + conResult.disposition = EmberLib.MatrixDisposition.locked; + } + else { + // Call pre-processing function + this.server.preMatrixConnect(matrix, connection, res, client, response); + } + + if (conResult.disposition == null) { + // No decision made yet + if (connection.operation !== EmberLib.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length > 0 && + matrix.canConnect(connection.target,connection.sources,connection.operation)) { + this.server.applyMatrixConnect(matrix, connection, conResult, client, response); + } + else if (connection.operation !== EmberLib.MatrixOperation.disconnect && + connection.sources != null && connection.sources.length === 0 && + matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length > 0) { + // let's disconnect + conResult = this.server.disconnectMatrixTarget( + matrix, connection.target, + matrix.connections[connection.target].sources, + client, + response); + } + else if (connection.operation === EmberLib.MatrixOperation.disconnect && + matrix.connections[connection.target].sources != null && + matrix.connections[connection.target].sources.length > 0) { + // Disconnect + if (matrix.contents.type === EmberLib.MatrixType.oneToN) { + this.server.applyMatrixOneToNDisconnect(matrix, connection, res, client, response); + } + else { + conResult = this.server.disconnectSources(matrix, connection.target, connection.sources, client, response); + } + } + } + if (conResult.disposition == null){ + winston.debug(`Invalid Matrix operation ${connection.operation} on target ${connection.target} with sources ${JSON.stringify(connection.sources)}`); + conResult.disposition = EmberLib.MatrixDisposition.tally; + } + + // Send response or update subscribers. + conResult.sources = matrix.connections[connection.target].sources; + } + if (client != null) { + client.sendBERNode(root); + } + + if (conResult != null && conResult.disposition !== EmberLib.MatrixDisposition.tally) { + winston.debug("Updating subscribers for matrix change"); + this.server.updateSubscribers(matrix.getPath(), root, client); + } + } + +} + +module.exports = MatrixHandlers; diff --git a/EmberServer/QualifiedHandlers.js b/EmberServer/QualifiedHandlers.js new file mode 100755 index 0000000..43755ff --- /dev/null +++ b/EmberServer/QualifiedHandlers.js @@ -0,0 +1,75 @@ +"use strict"; +const MatrixHandlers = require("./MatrixHandlers"); +const Errors = require("../Errors"); + +class QualifiedHandlers extends MatrixHandlers { + /** + * + * @param {EmberServer} server + */ + constructor(server) { + super(server); + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} element + * @param {QualifiedMatrix} matrix + */ + handleQualifiedMatrix(client, element, matrix) + { + this.handleMatrixConnections(client, element, matrix.connections); + } + + /** + * + * @param {S101Client} client + * @param {QualifiedNode} root + */ + handleQualifiedNode(client, node) { + const path = node.path; + // Find this element in our tree + const element = this.server.tree.getElementByPath(path); + + if (element == null) { + this.server.emit("error", new Errors.UnknownElement(path)); + return this.server.handleError(client); + } + + if (node.hasChildren()) { + for(let child of node.children) { + if (child.isCommand()) { + this.server._handlers.handleCommand(client, element, child); + } + break; + } + } + else { + if (node.isMatrix()) { + this.handleQualifiedMatrix(client, element, node); + } + else if (node.isParameter()) { + this.handleQualifiedParameter(client, element, node); + } + } + return path; + } + + /** + * + * @param {S101Client} client + * @param {TreeNode} element + * @param {QualifiedParameter} parameter + */ + handleQualifiedParameter(client, element, parameter) + { + if (parameter.contents.value != null) { + this.server.setValue(element, parameter.contents.value, client); + let res = this.server.getQualifiedResponse(element); + client.sendBERNode(res) + } + } +} + +module.exports = QualifiedHandlers; diff --git a/EmberServer/ServerEvents.js b/EmberServer/ServerEvents.js new file mode 100755 index 0000000..62b19a5 --- /dev/null +++ b/EmberServer/ServerEvents.js @@ -0,0 +1,124 @@ +"use strict"; + +const Enum = require('enum'); + +const Types = new Enum({ + UNKNOWN: 0, + SETVALUE: 1, + GETDIRECTORY: 2, + SUBSCRIBE: 3, + UNSUBSCRIBE: 4, + INVOKE: 5, + MATRIX_CONNECTION: 6 +}); + +class ServerEvents { + /** + * + * @param {string} txt + * @param {number} type=0 + */ + constructor(txt, type=Types.UNKNOWN) { + /** @type {string} */ + this._txt = txt; + /** @type {number} */ + this._type = type; + this._timestamp = Date.now(); + } + + /** + * @returns {number} + */ + get type() { + return this._type; + } + + /** + * @returns {number} + */ + get timestamp() { + return this._timestamp; + } + + /** + * @returns {string} + */ + toString() { + return this._txt; + } + + /** + * @returns {Enum} + */ + static get Types() { + return Types; + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + */ + static SETVALUE(identifier,path, src) { + return new ServerEvents(`set value for ${identifier}(path: ${path}) from ${src}`, Types.SETVALUE); + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + */ + static GETDIRECTORY(identifier,path, src) { + return new ServerEvents(`getdirectory to ${identifier}(path: ${path}) from ${src}`, Types.GETDIRECTORY); + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + */ + static SUBSCRIBE(identifier,path, src) { + return new ServerEvents(`subscribe to ${identifier}(path: ${path}) from ${src}`, Types.SUBSCRIBE); + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + */ + static UNSUBSCRIBE(identifier,path, src) { + return new ServerEvents(`unsubscribe to ${identifier}(path: ${path}) from ${src}`, Types.UNSUBSCRIBE); + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + */ + static INVOKE(identifier,path, src) { + return new ServerEvents(`invoke to ${identifier}(path: ${path}) from ${src}`, Types.INVOKE); + } + + /** + * + * @param {number} identifier + * @param {string} path + * @param {string} src + * @param {number} target + * @param {number[]} sources + */ + static MATRIX_CONNECTION(identifier, path, src, target, sources) { + const sourcesInfo = sources == null || sources.length === 0 ? "empty" : sources.toString(); + return new ServerEvents( + `Matrix connection to ${identifier}(path: ${path}) target ${target} connections: ${sourcesInfo} from ${src}`, + Types.MATRIX_CONNECTION + ); + } +} + +module.exports = ServerEvents; \ No newline at end of file diff --git a/EmberServer/index.js b/EmberServer/index.js new file mode 100755 index 0000000..2a6ba5c --- /dev/null +++ b/EmberServer/index.js @@ -0,0 +1,4 @@ +module.exports = { + EmberServer: require("./EmberServer"), + ServerEvents: require("./ServerEvents") +}; \ No newline at end of file diff --git a/EmberSocket/S101Client.js b/EmberSocket/S101Client.js new file mode 100755 index 0000000..d1d2527 --- /dev/null +++ b/EmberSocket/S101Client.js @@ -0,0 +1,49 @@ +"use strict"; +const net = require('net'); +const S101Socket = require("./S101Socket"); + +const DEFAULT_PORT = 9000; +class S101Client extends S101Socket { + /** + * + * @param {string} address + * @param {number} port=9000 + */ + constructor(address, port=DEFAULT_PORT) { + super(); + this.address = address; + this.port = port; + } + + /** + * + * @param {number} timeout + */ + connect(timeout = 2) { + if (this.status !== "disconnected") { + return; + } + this.emit('connecting'); + const connectTimeoutListener = () => { + this.socket.destroy(); + this.emit("error", new Error(`Could not connect to ${this.address}:${this.port} after a timeout of ${timeout} seconds`)); + }; + + this.socket = net.createConnection({ + port: this.port, + host: this.address, + timeout: 1000 * timeout + }, + () => { + // Disable connect timeout to hand-over to keepalive mechanism + this.socket.removeListener("timeout", connectTimeoutListener); + this.socket.setTimeout(0); + this.startKeepAlive(); + this.emit('connected'); + }) + .once("timeout", connectTimeoutListener); + this._initSocket(); + } +} + +module.exports = S101Client; diff --git a/EmberSocket/S101Server.js b/EmberSocket/S101Server.js new file mode 100755 index 0000000..d4e80ec --- /dev/null +++ b/EmberSocket/S101Server.js @@ -0,0 +1,55 @@ +"use strict"; +const EventEmitter = require('events').EventEmitter; +const S101Socket = require("./S101Socket"); +const net = require('net'); +const Errors = require("../Errors"); + +class S101Server extends EventEmitter { + /** + * + * @param {string} address + * @param {number} port + */ + constructor(address, port) { + super(); + this.address = address; + this.port = Number(port); + this.server = null; + this.status = "disconnected"; + } + /** + * + * @param {Socket} socket - tcp socket + */ + addClient(socket) { + // Wrap the tcp socket into an S101Socket. + const client = new S101Socket(socket); + this.emit("connection", client); + } + /** + * @returns {Promise} + */ + listen() { + return new Promise((resolve, reject) => { + if (this.status !== "disconnected") { + return reject(new Errors.S101SocketError("Already listening")); + } + this.server = net.createServer((socket) => { + this.addClient(socket); + }).on("error", (e) => { + this.emit("error", e); + if (this.status === "disconnected") { + return reject(e); + } + }).on("listening", () => { + this.emit("listening"); + this.status = "listening"; + resolve(); + }); + this.server.listen(this.port, this.address); + }); + } + +} + +module.exports = S101Server; \ No newline at end of file diff --git a/EmberSocket/S101Socket.js b/EmberSocket/S101Socket.js new file mode 100755 index 0000000..9035be7 --- /dev/null +++ b/EmberSocket/S101Socket.js @@ -0,0 +1,218 @@ +"use strict"; + +const EventEmitter = require('events').EventEmitter; +const BER = require('../ber.js'); +const ember = require('../EmberLib'); +const S101Codec = require('../s101.js'); + + +class S101Socket extends EventEmitter{ + /** + * + * @param {Socket} socket + */ + constructor(socket = null) { + super(); + this.socket = socket; + this.keepaliveInterval = 10; + this.keepaliveIntervalTimer = null; + this.pendingRequests = []; + this.activeRequest = null; + this.status = this.isConnected() ? "connected" : "disconnected"; + + this.codec = new S101Codec(); + this.codec.on('keepaliveReq', () => { + this.sendKeepaliveResponse(); + }); + + this.codec.on('emberPacket', (packet) => { + this.emit('emberPacket', packet); + const ber = new BER.Reader(packet); + try { + const root = ember.rootDecode(ber); + if (root != null) { + this.emit('emberTree', root); + } + } catch (e) { + this.emit("error", e); + } + }); + + this._initSocket(); + } + + _initSocket() { + if (this.socket != null) { + this.socket.on('data', (data) => { + this.codec.dataIn(data); + }); + + this.socket.on('close', () => { + this.emit('disconnected'); + this.status = "disconnected"; + this.socket = null; + }); + + this.socket.on('error', (e) => { + this.emit("error", e); + }); + } + } + + /** + * + * @param {function} cb + */ + addRequest(cb) { + this.pendingRequests.push(cb); + this._makeRequest(); + } + + _makeRequest() { + if (this.activeRequest === null && this.pendingRequests.length > 0) { + this.activeRequest = this.pendingRequests.shift(); + this.activeRequest(); + this.activeRequest = null; + } + } + + /** + * + * @param {TreeNode} node + */ + queueMessage(node) { + this.addRequest(() => this.sendBERNode(node)); + } + + /** + * @returns {string} - ie: "10.1.1.1:9000" + */ + remoteAddress() { + if (this.socket == null) { + return "not connected"; + } + return `${this.socket.remoteAddress}:${this.socket.remotePort}` + } + + /** + * @param {number} timeout=2 + */ + disconnect(timeout = 2) { + if (!this.isConnected()) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + if (this.keepaliveIntervalTimer != null) { + clearInterval(this.keepaliveIntervalTimer); + this.keepaliveIntervalTimer = null; + } + let done = false; + const cb = (data, error) => { + if (done) { return; } + done = true; + if (timer != null) { + clearTimeout(timer); + timer = null; + } + if (error == null) { + resolve(); + } + else { + reject(error); + } + }; + let timer; + if (timeout != null && (!isNaN(timeout)) && timeout > 0) { + timer = setTimeout(cb, 100 * timeout); + } + this.socket.end(cb); + this.status = "disconnected"; + }); + } + + /** + * + */ + handleClose() { + this.socket = null; + clearInterval(this.keepaliveIntervalTimer); + this.status = "disconnected"; + this.emit('disconnected'); + } + + /** + * @returns {boolean} + */ + isConnected() { + return ((this.socket !== null) && (this.socket != null)); + } + + /** + * + * @param {Buffer} data + */ + sendBER(data) { + if (this.isConnected()) { + try { + const frames = this.codec.encodeBER(data); + for (let i = 0; i < frames.length; i++) { + this.socket.write(frames[i]); + } + } + catch(e){ + this.handleClose(); + } + } + } + + /** + * + */ + sendKeepaliveRequest() { + if (this.isConnected()) { + try { + this.socket.write(this.codec.keepAliveRequest()); + } + catch(e){ + this.handleClose(); + } + } + } + + /** + * + */ + sendKeepaliveResponse() { + if (this.isConnected()) { + try { + this.socket.write(this.codec.keepAliveResponse()); + } + catch(e){ + this.handleClose(); + } + } + } + + /** + * + * @param {TreeNode} node + */ + sendBERNode(node) { + if (!node) return; + const writer = new BER.Writer(); + node.encode(writer); + this.sendBER(writer.buffer); + } + + startKeepAlive() { + this.keepaliveIntervalTimer = setInterval(() => { + try { + this.sendKeepaliveRequest(); + } catch (e) { + this.emit("error", e); + } + }, 1000 * this.keepaliveInterval); + } +} + +module.exports = S101Socket; \ No newline at end of file diff --git a/EmberSocket/index.js b/EmberSocket/index.js new file mode 100755 index 0000000..d755a5b --- /dev/null +++ b/EmberSocket/index.js @@ -0,0 +1,7 @@ +const S101Socket = require("./S101Socket"); +const S101Server = require("./S101Server"); +const S101Client = require("./S101Client"); + +module.exports = { + S101Client, S101Server, S101Socket +}; \ No newline at end of file diff --git a/Errors.js b/Errors.js new file mode 100755 index 0000000..dcffe41 --- /dev/null +++ b/Errors.js @@ -0,0 +1,228 @@ + +/**************************************************************************** + * UnimplementedEmberType error + ***************************************************************************/ + +class UnimplementedEmberTypeError extends Error { + constructor(tag) { + super(); + this.name = this.constructor.name; + var identifier = (tag & 0xC0) >> 6; + var value = (tag & 0x1F).toString(); + var tagStr = tag.toString(); + if(identifier == 0) { + tagStr = "[UNIVERSAL " + value + "]"; + } else if(identifier == 1) { + tagStr = "[APPLICATION " + value + "]"; + } else if(identifier == 2) { + tagStr = "[CONTEXT " + value + "]"; + } else { + tagStr = "[PRIVATE " + value + "]"; + } + this.message = "Unimplemented EmBER type " + tagStr; + } +} + +module.exports.UnimplementedEmberTypeError = UnimplementedEmberTypeError; + +class S101SocketError extends Error { + constructor(message) { + super(message); + } +} +module.exports.S101SocketError = S101SocketError; + +class ASN1Error extends Error { + constructor(message) { + super(message); + } +} + +module.exports.ASN1Error = ASN1Error; + +class EmberAccessError extends Error { + constructor(message) { + super(message); + } +} + +module.exports.EmberAccessError = EmberAccessError; + +class EmberTimeoutError extends Error { + constructor(message) { + super(message); + } +} + +module.exports.EmberTimeoutError = EmberTimeoutError; + +class InvalidCommand extends Error { + /** + * + * @param {number} number + */ + constructor(number) { + super(`Invalid command ${number}`); + } +} + +module.exports.InvalidCommand = InvalidCommand; + +class MissingElementNumber extends Error { + constructor() { + super("Missing element number"); + } +} + +module.exports.MissingElementNumber = MissingElementNumber; + +class MissingElementContents extends Error { + /** + * + * @param {string} path + */ + constructor(path) { + super(`Missing element contents at ${path}`); + } +} + +module.exports.MissingElementContents = MissingElementContents; + +class UnknownElement extends Error { + /** + * + * @param {string} path + */ + constructor(path) { + super(`No element at path ${path}`); + } +} + +module.exports.UnknownElement = UnknownElement; + +class InvalidRequest extends Error { + constructor() { + super("Invalid Request"); + } +} + +module.exports.InvalidRequest = InvalidRequest; + +class InvalidRequestFormat extends Error { + /** + * + * @param {string} path + */ + constructor(path) { + super(`Invalid Request Format with path ${path}`); + } +} + +module.exports.InvalidRequestFormat = InvalidRequestFormat; + +class InvalidEmberNode extends Error { + /** + * + * @param {string} path + * @param {string} info + */ + constructor(path="unknown", info="") { + super(`Invalid Ember Node at ${path}: ${info}`); + } +} +module.exports.InvalidEmberNode = InvalidEmberNode; + +class InvalidEmberResponse extends Error { + /** + * + * @param {string} req + */ + constructor(req) { + super(`Invalid Ember Response to ${req}`); + } +} +module.exports.InvalidEmberResponse = InvalidEmberResponse; + +class PathDiscoveryFailure extends Error { + /** + * + * @param {string} path + */ + constructor(path) { + super(PathDiscoveryFailure.getMessage(path)); + } + + /** + * + * @param {string} path + */ + setPath(path) { + this.message = PathDiscoveryFailure.getMessage(path); + } + + static getMessage(path) { + return `Failed path discovery at ${path}`; + } +} +module.exports.PathDiscoveryFailure = PathDiscoveryFailure; + +class InvalidSourcesFormat extends Error { + constructor() { + super("Sources should be an array"); + } +} +module.exports.InvalidSourcesFormat = InvalidSourcesFormat; + +class InvalidBERFormat extends Error { + /** + * + * @param {string} info + */ + constructor(info="") { + super(`Invalid BER format: ${info}`); + } +} +module.exports.InvalidBERFormat = InvalidBERFormat; + +class InvalidResultFormat extends Error { + /** + * + * @param {string} info + */ + constructor(info="") { + super(`Invalid Result format: ${info}`); + } +} +module.exports.InvalidResultFormat = InvalidResultFormat; + +class InvalidMatrixSignal extends Error { + /** + * + * @param {number} value + * @param {string} info + */ + constructor(value, info) { + super(`Invalid Matrix Signal ${value}: ${info}`); + } +} +module.exports.InvalidMatrixSignal = InvalidMatrixSignal; + +class InvalidStringPair extends Error { + /** + * + */ + constructor() { + super("Invalid StringPair Value"); + } +} +module.exports.InvalidStringPair = InvalidStringPair; + +class InvalidRequesrFormat extends Error { + /** + * @param {string} path + */ + constructor(path) { + super(`Can't process request for node ${path}`); + } +} +module.exports.InvalidRequesrFormat = InvalidRequesrFormat; \ No newline at end of file diff --git a/README.md b/README.md index 2464cc9..e2c77ae 100755 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # node-emberplus -This is an implementation of [Lawo's -Ember+](https://github.com/Lawo/ember-plus) control protocol for Node. One of -Node's great strengths is the ready availability of frameworks for various +This is version 2 of emberplus library. +An implementation of [Lawo's Ember+](https://github.com/Lawo/ember-plus) control protocol for Node. +One of Node's great strengths is the ready availability of frameworks for various communication protocols and user interfaces; this module allows those to be integrated with Ember+ somewhat more easily than the reference libember C++ implementation. -This version support following ember objects : Node, Parameter, Matrix, QualifiedNode, +This version support following ember objects : Node, Parameter, Matrix, Function, QualifiedNode, QualifiedParameter, QualifiedMatrix, QualifiedFunction. It has been tested with EVS XT4k and Embrionix IP solutions. @@ -17,37 +17,288 @@ the way the lib is used so that now it uses Promise Server has been added in version 1.6.0. +Support for StreamCollection, UDP, packet stats/rate, support for tree with size higher than 8M +is available in a private branch. +If you want access to full version and/or would like support for new features or integration +with your projects, please contact Gilles Dufour - dufour.gilles@gmail.com + ## Example usage +### Client + +Get Full tree: + ```javascript -const DeviceTree = require('emberplus').DeviceTree; -var root; -var tree = new DeviceTree("10.9.8.7", 9000); -tree.connect() -.then(() => { - return tree.getDirectory(); -}) -.then((r) => { - root = r ; - return tree.expand(r.elements[0]); -}) -.then(() => { - console.log("done"); -}) -.catch((e) => { - console.log(e.stack); +const EmberClient = require('node-emberplus').EmberClient; +const client = new EmberClient("10.9.8.7", 9000); +client.on("error", e => { + console.log(e); }); +client.connect() + // Get Root info + .then(() => client.getDirectory()) + // Get a Specific Node + .then(() => client.getElementByPath("0.0.2")) + .then(node => { + console.log(node); + }) + // Get a node by its path identifiers + .then(() => client.getElementByPath("path/to/node")) + .then(node => { + console.log(node); + }) + // Expand entire tree under node 0 + .then(() => client.expand(client.root.getElementByNumber(0))) + .catch((e) => { + console.log(e.stack); + }); +``` + +Subsribe to changes + +```javascript +const {EmberClient, EmberLib} = require('node-emberplus'); + +const client = new EmberClient(HOST, PORT); +client.connect()) + .then(() => client.getDirectory()) + .then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) + .then(() => client.getElementByPath("scoreMaster/router/labels/group 1")) + .then(node => { + // For streams, use subscribe + return client.subscribe(node, update => { + console.log(udpate); + }); + }) + .then(() => client.getElementByPath("0.2")) + .then(node => { + // For non-streams a getDirectory will automatically subscribe for update + return client.getDirectory(node, update => { + console.log(udpate); + }); + }) + // You can also provide a callback to the getElementByPath + // Be carefull that subscription will be done for all elements in the path + .then(() => client.getElementByPath("0.3", update => {console.log(update);})) + ; +``` + +### Setting new value + +```javascript +client = new EmberClient(LOCALHOST, PORT); +await client.connect() +await client.getDirectory(); +await client.getElementByPath("0.0.1"); +await client.setValue(client.root.getElementByPath("0.0.1"), "gdnet"); +console.log("result", server.tree.getElementByPath("0.0.1").contents.value) +return client.disconnect().then(() => { console.log("disconnected")}); +``` + +### Invoking Function + +```javascript +const {EmberClient, EmberLib} = require('node-emberplus'); + +const client = new EmberClient(HOST, PORT); +client.connect()) + .then(() => client.getDirectory()) + .then(() => {console.log(JSON.stringify(client.root.toJSON(), null, 4));}) + .then(() => client.expand(client.root.getElementByNumber(0))) + .then(() => { + console.log(JSON.stringify(client.root.getElementByNumber(0).toJSON(), null, 4)); + const funcNode = client.root.getElementByNumber(0).getElementByNumber(5).getElementByNumber(0); + return client.invokeFunction(funcNode, [ + new ember.FunctionArgument(EmberLib.ParameterType.integer, 1), + new ember.FunctionArgument(EmberLib.ParameterType.integer, 7) + ]); + }); +``` +### Matrix Connection +```javascript +const {EmberClient, EmberLib} = require('node-emberplus'); + + +const client = new EmberClient(HOST, PORT); +client.connect() + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("0.1.0")) + .then(matrix => { + console.log("Connecting source 1 to target 0); + return client.matrixConnect(matrix, 0, [1]); + }) + .then(() => client.matrixDisconnect(matrix, 0, [1])) + .then(() => client.matrixSetConnection(matrix, 0, [0,1])) + .then(matrix => client.getElementByPath(matrix.getPath())) + .then(() => client.disconnect()); + +``` + +### Packet decoder + +```javascript // Simple packet decoder -const Decoder = require('emberplus').Decoder; +const Decoder = require('node-emberplus').Decoder; const fs = require("fs"); fs.readFile("tree.ember", (e,data) => { var root = Decoder(data); }); +``` +### Server + +```javascript // Server -const TreeServer = require("emberplus").TreeServer; -const server = new TreeServer("127.0.0.1", 9000, root); +const EmberServer = require("node-emberplus").EmberServer; +const server = new EmberServer("127.0.0.1", 9000, root); +server.on("error", e => { + console.log("Server Error", e); +}); +server.on("clientError", info => { + console.log("clientError", info); +}); +server.on("matrix-disconnect", info => { + console.log(`Client ${info.client} disconnected ${info.target} and ${info.sources}`); +} +server.on("matrix-connect", info => { + console.log(`Client ${info.client} connected ${info.target} and ${info.sources}`); +} +server.on("matrix-change", info => { + console.log(`Client ${info.client} changed ${info.target} and ${info.sources}`); +} +server.on("event", txt => { + console.log("event: " + txt); +}) server.listen().then(() => { console.log("listening"); }).catch((e) => { console.log(e.stack); }); +``` + +### Construct Tree + +```javascript +const EmberServer = require("node-emberplus").EmberServer; +const {ParameterType, FunctionArgument} = require("node-emberplus").EmberLib; + +const targets = [ "tgt1", "tgt2", "tgt3" ]; +const sources = [ "src1", "src2", "src3" ]; +const defaultSources = [ + {identifier: "t-0", value: -1, access: "readWrite" }, + {identifier: "t-1", value: 0, access: "readWrite"}, + {identifier: "t-2", value: 0, access: "readWrite"} +]; +const labels = function(endpoints, type) { + let labels = []; + for (let i = 0; i < endpoints.length; i++) { + let endpoint = endpoints[i]; + let l = { identifier: `${type}-${i}` }; + if (endpoint) { + l.value = endpoint; + } + labels.push(l); + } + return labels; +}; + +const buildConnections = function(s, t) { + let connections = []; + for (let i = 0; i < t.length; i++) { + connections.push({target: `${i}`}); + } + return connections; +}; +const jsonTree = [ + { + // path "0" + identifier: "GDNet Tree", + children: [ + { + // path "0.0" + identifier: "identity", + children: [ + {identifier: "product", value: "gdnet core"}, + {identifier: "company", value: "GDNet", access: "readWrite"}, + {identifier: "version", value: "1.2.0"}, + {identifier: "author", value: "dufour.gilles@gmail.com"}, + ] + }, + { + // path "0.1" + identifier: "router", + children: [ + { + // path 0.1.0 + identifier: "matrix", + type: "oneToN", + mode: "linear", + targetCount: targets.length, + sourceCount: sources.length, + connections: buildConnections(sources, targets), + labels: [{basePath: "0.1.1000", description: "primary"}] + }, + { + identifier: "labels", + // path "0.1.1000" + number: 1000, + children: [ + { + identifier: "targets", + // Must be 1 + number: 1, + children: labels(targets, "t") + }, + { + identifier: "sources", + // Must be 2 + number: 2, + children: labels(sources, "s") + }, + { + identifier: "group 1", + children: [ {identifier: "sdp A", value: "A"}, {identifier: "sdp B", value: "B"}] + } + ] + }, + { + identifier: "disconnect sources", + // must be labels + 1 + number: 1001, + children: defaultSources + } + ] + }, + { + // path "0.2" + identifier: "addFunction", + func: args => { + const res = new FunctionArgument(); + res.type = ParameterType.integer; + res.value = args[0].value + args[1].value; + return [res]; + }, + arguments: [ + { + type: ParameterType.integer, + value: null, + name: "arg1" + }, + { + type: ParameterType.integer, + value: null, + name: "arg2" + } + ], + result: [ + { + type: ParameterType.integer, + value: null, + name: "changeCount" + } + ] + } + ] + } +]; +const root = EmberServer.JSONtoTree(jsonTree); +``` diff --git a/ber.js b/ber.js index 8518c39..72ae011 100755 --- a/ber.js +++ b/ber.js @@ -22,7 +22,7 @@ ***************************************************************************/ const BER = require('asn1').Ber; -const errors = require('./errors.js'); +const errors = require('./Errors.js'); const util = require('util'); const Long = require('long'); @@ -74,17 +74,21 @@ function ExtendedReader(data) { util.inherits(ExtendedReader, BER.Reader); module.exports.Reader = ExtendedReader; - -readBlock = function(ber) { - -} - - ExtendedReader.prototype.getSequence = function(tag) { var buf = this.readString(tag, true); return new ExtendedReader(buf); } +/** +Value ::= + CHOICE { + integer Integer64, + real REAL, + string EmberString, + boolean BOOLEAN, + octets OCTET STRING, + null NULL + } */ ExtendedReader.prototype.readValue = function() { var tag = this.peek(); @@ -108,7 +112,7 @@ ExtendedReader.prototype.readValue = function() { ExtendedReader.prototype.readReal = function(tag) { - if(tag !== undefined) { + if(tag != null) { tag = UNIVERSAL(9); } @@ -313,7 +317,7 @@ ExtendedWriter.prototype.writeValue = function(value, tag) { } ExtendedWriter.prototype.writeIfDefined = function(property, writer, outer, inner) { - if(property !== undefined) { + if(property != null) { this.startSequence(CONTEXT(outer)); writer.call(this, property, inner); this.endSequence(); @@ -321,9 +325,9 @@ ExtendedWriter.prototype.writeIfDefined = function(property, writer, outer, inne } ExtendedWriter.prototype.writeIfDefinedEnum = function(property, type, writer, outer, inner) { - if(property !== undefined) { + if(property != null) { this.startSequence(CONTEXT(outer)); - if(property.value !== undefined) { + if(property.value != null) { writer.call(this, property.value, inner); } else { writer.call(this, type.get(property), inner); diff --git a/client.js b/client.js deleted file mode 100755 index a60916a..0000000 --- a/client.js +++ /dev/null @@ -1,291 +0,0 @@ -const EventEmitter = require('events').EventEmitter; -const util = require('util'); -const winston = require('winston'); -const net = require('net'); -const BER = require('./ber.js'); -const ember = require('./ember.js'); - -const S101Codec = require('./s101.js'); - - -function S101Socket(address, port) { - var self = this; - S101Socket.super_.call(this); - - self.address = address; - self.port = port; - self.socket = null; - self.keepaliveInterval = 10; - self.codec = null; - self.status = "disconnected"; - -} - -util.inherits(S101Socket, EventEmitter); - - -function S101Client(socket, server) { - var self = this; - S101Client.super_.call(this); - - self.request = null; - self.server = server; - self.socket = socket; - - self.pendingRequests = []; - self.activeRequest = null; - - self.status = "connected"; - - self.codec = new S101Codec(); - self.codec.on('keepaliveReq', () => { - self.sendKeepaliveResponse(); - }); - - self.codec.on('emberPacket', (packet) => { - self.emit('emberPacket', packet); - var ber = new BER.Reader(packet); - try { - var root = ember.Root.decode(ber); - if (root !== undefined) { - self.emit('emberTree', root); - } - } catch (e) { - self.emit("error", e); - } - }); - - if (socket !== undefined) { - self.socket.on('data', (data) => { - self.codec.dataIn(data); - }); - - self.socket.on('close', () => { - self.emit('disconnected'); - self.status = "disconnected"; - self.socket = null; - }); - - self.socket.on('error', (e) => { - self.emit("error", e); - }); - } -} - -util.inherits(S101Client, S101Socket); - - -/********************************************** - * SERVER - **********************************************/ - -function S101Server(address, port) { - var self = this; - S101Server.super_.call(this); - - self.address = address; - self.port = port; - self.server = null; - self.status = "disconnected"; -} - -util.inherits(S101Server, EventEmitter); - -S101Server.prototype.listen = function () { - var self = this; - if (self.status !== "disconnected") { - return; - } - - self.server = net.createServer((socket) => { - self.addClient(socket); - }); - - self.server.on("error", (e) => { - self.emit("error", e); - }); - - self.server.on("listening", () => { - self.emit("listening"); - self.status = "listening"; - }); - - self.server.listen(self.port, self.address); -} - - -S101Server.prototype.addClient = function (socket) { - var client = new S101Client(socket, this); - this.emit("connection", client); -} - - -/***************************************************** - * Client - *****************************************************/ - -S101Client.prototype.remoteAddress = function () { - if (this.socket === undefined) { - return; - } - return `${this.socket.remoteAddress}:${this.socket.remotePort}` -} - -S101Client.prototype.queueMessage = function (node) { - const self = this; - this.addRequest(() => { - self.sendBERNode(node); - }); -} - -S101Client.prototype.makeRequest = function () { - if (this.activeRequest === null && this.pendingRequests.length > 0) { - this.activeRequest = this.pendingRequests.shift(); - this.activeRequest(); - this.activeRequest = null; - } -}; - -S101Client.prototype.addRequest = function (cb) { - this.pendingRequests.push(cb); - this.makeRequest(); -} - - -/***************************************************** - * Socket - *****************************************************/ - -S101Socket.prototype.connect = function (timeout = 2) { - var self = this; - if (self.status !== "disconnected") { - return; - } - - self.emit('connecting'); - - self.codec = new S101Codec(); - - const connectTimeoutListener = () => { - self.socket.destroy(); - self.emit("error", new Error(`Could not connect to ${self.address}:${self.port} after a timeout of ${timeout} seconds`)); - }; - - self.socket = net.createConnection({ - port: self.port, - host: self.address, - timeout: 1000 * timeout - }, - () => { - winston.debug('socket connected'); - - // Disable connect timeout to hand-over to keepalive mechanism - self.socket.removeListener("timeout", connectTimeoutListener); - self.socket.setTimeout(0); - - self.keepaliveIntervalTimer = setInterval(() => { - try { - self.sendKeepaliveRequest(); - } catch (e) { - self.emit("error", e); - } - }, 1000 * self.keepaliveInterval); - - self.codec.on('keepaliveReq', () => { - self.sendKeepaliveResponse(); - }); - - self.codec.on('emberPacket', (packet) => { - self.emit('emberPacket', packet); - - var ber = new BER.Reader(packet); - try { - var root = ember.Root.decode(ber); - if (root !== undefined) { - self.emit('emberTree', root); - } - } catch (e) { - self.emit("error", e); - } - }); - - self.emit('connected'); - }) - .on('error', (e) => { - self.emit("error", e); - }) - .once("timeout", connectTimeoutListener) - .on('data', (data) => { - if (self.isConnected()) { - self.codec.dataIn(data); - } - }) - .on('close', () => { - clearInterval(self.keepaliveIntervalTimer); - self.emit('disconnected'); - self.status = "disconnected"; - self.socket = null; - }); -} - -S101Socket.prototype.isConnected = function () { - return ((this.socket !== null) && (this.socket !== undefined)); -} - -S101Socket.prototype.disconnect = function () { - var self = this; - - if (!self.isConnected()) { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - self.socket.once('close', () => { - self.codec = null; - self.socket = null; - resolve(); - }); - self.socket.once('error', reject); - clearInterval(self.keepaliveIntervalTimer); - self.socket.end(); - self.status = "disconnected"; - } - ); -}; - -S101Socket.prototype.sendKeepaliveRequest = function () { - var self = this; - if (self.isConnected()) { - self.socket.write(self.codec.keepAliveRequest()); - winston.debug('sent keepalive request'); - } -} - -S101Socket.prototype.sendKeepaliveResponse = function () { - var self = this; - if (self.isConnected()) { - self.socket.write(self.codec.keepAliveResponse()); - winston.debug('sent keepalive response'); - } -} - -S101Socket.prototype.sendBER = function (data) { - var self = this; - if (self.isConnected()) { - var frames = self.codec.encodeBER(data); - for (var i = 0; i < frames.length; i++) { - self.socket.write(frames[i]); - } - } -} - -S101Socket.prototype.sendBERNode = function (node) { - var self = this; - if (!node) return; - var writer = new BER.Writer(); - node.encode(writer); - self.sendBER(writer.buffer); -} - - -module.exports = { S101Socket, S101Server, S101Client }; - diff --git a/device.js b/device.js deleted file mode 100755 index a238066..0000000 --- a/device.js +++ /dev/null @@ -1,518 +0,0 @@ -const EventEmitter = require('events').EventEmitter; -const util = require('util'); -const S101Client = require('./client.js').S101Socket; -const ember = require('./ember.js'); -const BER = require('./ber.js'); -const errors = require('./errors.js'); - - -function DeviceTree(host, port = 9000) { - DeviceTree.super_.call(this); - var self = this; - self._debug = false; - self.timeoutValue = 3000; - self.client = new S101Client(host, port); - self.root = new ember.Root(); - self.pendingRequests = []; - self.activeRequest = null; - self.timeout = null; - self.callback = undefined; - self.requestID = 0; - - self.client.on('connecting', () => { - self.emit('connecting'); - }); - - self.client.on('connected', () => { - self.emit('connected'); - if (self.callback !== undefined) { - self.callback(); - } - }); - - self.client.on('disconnected', () => { - self.emit('disconnected'); - }); - - self.client.on("error", (e) => { - if (self.callback !== undefined) { - self.callback(e); - } - self.emit("error", e); - }); - - self.client.on('emberTree', (root) => { - try { - if (root instanceof ember.InvocationResult) { - self.emit('invocationResult', root); - if (self._debug) { - console.log("Received InvocationResult", root); - } - } else { - self.handleRoot(root); - if (self._debug) { - console.log("Received root", root); - } - } - if (self.callback) { - self.callback(undefined, root); - } - } - catch(e) { - if (self._debug) { - console.log(e, root); - } - if (self.callback) { - self.callback(e); - } - } - }); -} - -util.inherits(DeviceTree, EventEmitter); - - -var DecodeBuffer = function (packet) { - var ber = new BER.Reader(packet); - return ember.Root.decode(ber); -}; - -DeviceTree.prototype.saveTree = function (f) { - var writer = new BER.Writer(); - this.root.encode(writer); - f(writer.buffer); -}; - -DeviceTree.prototype.isConnected = function () { - return ((this.client !== undefined) && (this.client.isConnected())); -}; - -DeviceTree.prototype.connect = function (timeout = 2) { - return new Promise((resolve, reject) => { - this.callback = (e) => { - this.callback = undefined; - if (e === undefined) { - return resolve(); - } - return reject(e); - }; - if ((this.client !== undefined) && (this.client.isConnected())) { - this.client.disconnect(); - } - this.client.connect(timeout); - }); -}; - -DeviceTree.prototype.expand = function (node) { - let self = this; - if (node == null) { - return Promise.reject(new Error("Invalid null node")); - } - if (node.isParameter() || node.isMatrix() || node.isFunction()) { - return self.getDirectory(node); - } - return self.getDirectory(node).then((res) => { - let children = node.getChildren(); - if ((res === undefined) || (children === undefined) || (children === null)) { - if (self._debug) { - console.log("No more children for ", node); - } - return; - } - let p = Promise.resolve(); - for (let child of children) { - if (child.isParameter()) { - // Parameter can only have a single child of type Command. - continue; - } - if (self._debug) { - console.log("Expanding child", child); - } - p = p.then(() => { - return self.expand(child).catch((e) => { - // We had an error on some expansion - // let's save it on the child itself - child.error = e; - }); - }); - } - return p; - }); -}; - -function isDirectSubPathOf(path, parent) { - return path.lastIndexOf('.') === parent.length && path.startsWith(parent) -} - -DeviceTree.prototype.getDirectory = function (qnode) { - var self = this; - if (qnode == null) { - self.root.clear(); - qnode = self.root; - } - return new Promise((resolve, reject) => { - self.addRequest({node: qnode, func: (error) => { - if (error) { - self.finishRequest(); - reject(error); - return; - } - - self.callback = (error, node) => { - const requestedPath = qnode.getPath(); - if (node == null) { - if (self._debug) { - console.log(`received null response for ${requestedPath}`); - } - return; - } - if (error) { - if (self._debug) { - console.log("Received getDirectory error", error); - } - self.clearTimeout(); // clear the timeout now. The resolve below may take a while. - self.finishRequest(); - reject(error); - return; - } - if (qnode instanceof ember.Root) { - if (qnode.elements == null || qnode.elements.length === 0) { - if (self._debug) { - console.log("getDirectory response", node); - } - return self.callback(new Error("Invalid qnode for getDirectory")); - } - - const nodeElements = node == null ? null : node.elements; - - if (nodeElements != null - && nodeElements.every(el => el._parent instanceof ember.Root)) { - if (self._debug) { - console.log("Received getDirectory response", node); - } - self.clearTimeout(); // clear the timeout now. The resolve below may take a while. - self.finishRequest(); - resolve(node); // make sure the info is treated before going to next request. - } - else { - return self.callback(new Error(`Invalid response for getDirectory ${requestedPath}`)); - } - } else { - const nodeElements = node == null ? null : node.elements; - if (nodeElements != null && - ((qnode.isMatrix() && nodeElements.length === 1 && nodeElements[0].getPath() === requestedPath) || - (!qnode.isMatrix() && nodeElements.every(el => isDirectSubPathOf(el.getPath(), requestedPath))))) { - if (self._debug) { - console.log("Received getDirectory response", node); - } - self.clearTimeout(); // clear the timeout now. The resolve below may take a while. - self.finishRequest(); - resolve(node); // make sure the info is treated before going to next request. - } - else if (self._debug) { - console.log(node); - console.log(new Error(requestedPath)); - } - } - }; - - if (self._debug) { - console.log("Sending getDirectory", qnode); - } - self.client.sendBERNode(qnode.getDirectory()); - }}); - }); -}; - -DeviceTree.prototype.invokeFunction = function (fnNode, params) { - var self = this; - return new Promise((resolve, reject) => { - self.addRequest({node: fnNode, func: (error) => { - if (error) { - reject(error); - self.finishRequest(); - return; - } - - let cb = (error, result) => { - self.clearTimeout(); - if (error) { - reject(error); - } - else { - if (self._debug) { - console.log("InvocationResult", result); - } - resolve(result); - } - self.finishRequest(); - }; - - if (self._debug) { - console.log("Invocking function", fnNode); - } - self.callback = cb; - self.client.sendBERNode(fnNode.invoke(params)); - }}); - }) -}; - -DeviceTree.prototype.disconnect = function () { - if (this.client !== undefined) { - return this.client.disconnect(); - } -}; - -DeviceTree.prototype.makeRequest = function () { - var self = this; - if (self.activeRequest === null && self.pendingRequests.length > 0) { - self.activeRequest = self.pendingRequests.shift(); - - const t = function (id) { - var path = self.activeRequest.path == null ? - self.activeRequest.node.getPath() : - self.activeRequest.path; - var req = `${id} - ${path}`; - if (self._debug) { - console.log(`Making request ${req}`, Date.now()); - } - self.timeout = setTimeout(() => { - self.timeoutRequest(req); - }, self.timeoutValue); - }; - - t(self.requestID++); - self.activeRequest.func(); - } -}; - -DeviceTree.prototype.addRequest = function (req) { - var self = this; - self.pendingRequests.push(req); - self.makeRequest(); -}; - -DeviceTree.prototype.clearTimeout = function () { - if (this.timeout != null) { - clearTimeout(this.timeout); - this.timeout = null; - } -}; - -DeviceTree.prototype.finishRequest = function () { - var self = this; - self.callback = undefined; - self.clearTimeout(); - self.activeRequest = null; - try { - self.makeRequest(); - } catch(e) { - console.log("warn:" + e.message) - } -}; - -DeviceTree.prototype.timeoutRequest = function (id) { - var self = this; - self.root.cancelCallbacks(); - self.activeRequest.func(new errors.EmberTimeoutError(`Request ${id !== undefined ? id : ""} timed out`)); -}; - -DeviceTree.prototype.handleRoot = function (root) { - var self = this; - - if (self._debug) { - console.log("handling root", JSON.stringify(root)); - } - var callbacks = self.root.update(root); - if (root.elements !== undefined) { - for (var i = 0; i < root.elements.length; i++) { - if (root.elements[i].isQualified()) { - callbacks = callbacks.concat(this.handleQualifiedNode(this.root, root.elements[i])); - } - else { - callbacks = callbacks.concat(this.handleNode(this.root, root.elements[i])); - } - } - - // Fire callbacks once entire tree has been updated - for (var j = 0; j < callbacks.length; j++) { - //console.log('hr cb'); - callbacks[j](); - } - } -}; - -DeviceTree.prototype.handleQualifiedNode = function (parent, node) { - var self = this; - var callbacks = []; - var element = parent.getElementByPath(node.path); - if (element !== null) { - self.emit("value-change", node); - callbacks = element.update(node); - } - else { - var path = node.path.split("."); - if (path.length === 1) { - this.root.addChild(node); - } - else { - // Let's try to get the parent - path.pop(); - parent = this.root.getElementByPath(path.join(".")); - if (parent === null) { - return callbacks; - } - parent.addChild(node); - callbacks = parent.update(parent); - } - element = node; - } - - var children = node.getChildren(); - if (children !== null) { - for (var i = 0; i < children.length; i++) { - if (children[i].isQualified()) { - callbacks = callbacks.concat(this.handleQualifiedNode(element, children[i])); - } - else { - callbacks = callbacks.concat(this.handleNode(element, children[i])); - } - } - } - - return callbacks; -}; - -DeviceTree.prototype.handleNode = function (parent, node) { - var self = this; - var callbacks = []; - - var n = parent.getElementByNumber(node.getNumber()); - if (n === null) { - parent.addChild(node); - n = node; - } else { - callbacks = n.update(node); - } - - var children = node.getChildren(); - if (children !== null) { - for (var i = 0; i < children.length; i++) { - callbacks = callbacks.concat(this.handleNode(n, children[i])); - } - } - else { - self.emit("value-change", node); - } - - return callbacks; -}; - -DeviceTree.prototype.getNodeByPath = function (path, timeout = 2) { - var self = this; - if (typeof path === 'string') { - path = path.split('/'); - } - var timeoutError = new Error("Request timeout"); - return new Promise((resolve, reject) => { - self.addRequest({path: path, func: (error) => { - if (error) { - reject(error); - self.finishRequest(); - return; - } - var timedOut = false; - var cb = (error, node) => { - if (timer) { - clearTimeout(timer); - } - if (timedOut) { return; } - if (error) { - reject(error); - } else { - resolve(node); - } - self.finishRequest(); - }; - var cbTimeout = () => { - timedOut = true; - reject(timeoutError); - } - var timer = timeout === 0 ? null : setTimeout(cbTimeout, timeout * 1000); - self.root.getNodeByPath(self.client, path, cb); - }}); - }); -}; - -DeviceTree.prototype.subscribe = function (node, callback) { - if (node instanceof ember.Parameter && node.isStream()) { - // TODO: implement - } else { - node.addCallback(callback); - } -}; - -DeviceTree.prototype.unsubscribe = function (node, callback) { - if (node instanceof ember.Parameter && node.isStream()) { - // TODO: implement - } else { - node.addCallback(callback); - } -}; - -DeviceTree.prototype.setValue = function (node, value) { - var self = this; - return new Promise((resolve, reject) => { - if ((!(node instanceof ember.Parameter)) && - (!(node instanceof ember.QualifiedParameter))) { - reject(new errors.EmberAccessError('not a property')); - } - else { - // if (this._debug) { console.log('setValue', node.getPath(), value); } - self.addRequest({node: node, func: (error) => { - if (error) { - self.finishRequest(); - reject(error); - return; - } - - let cb = (error, node) => { - //console.log('setValue complete...', node.getPath(), value); - self.finishRequest(); - if (error) { - reject(error); - } - else { - resolve(node); - } - }; - - self.callback = cb; - if (this._debug) { - console.log('setValue sending ...', node.getPath(), value); - } - self.client.sendBERNode(node.setValue(value)); - }}); - } - }); -}; - -function TreePath(path) { - this.identifiers = []; - this.numbers = []; - - if (path !== undefined) { - for (var i = 0; i < path.length; i++) { - if (Number.isInteger(path[i])) { - this.numbers.push(path[i]); - this.identifiers.push(null); - } else { - this.identifiers.push(path[i]); - this.numbers.push(null); - } - } - } -} - - -module.exports = { DeviceTree, DecodeBuffer }; diff --git a/ember.js b/ember.js deleted file mode 100755 index 3e594bb..0000000 --- a/ember.js +++ /dev/null @@ -1,2708 +0,0 @@ -const BER = require('./ber.js'); -const errors = require('./errors.js'); -const util = require('util'); -const Enum = require('enum'); - -const COMMAND_SUBSCRIBE = 30; -const COMMAND_UNSUBSCRIBE = 31; -const COMMAND_GETDIRECTORY = 32; -const COMMAND_INVOKE = 33; -module.exports.Subscribe = COMMAND_SUBSCRIBE; -module.exports.Unsubscribe = COMMAND_UNSUBSCRIBE; -module.exports.GetDirectory = COMMAND_GETDIRECTORY; -module.exports.Invoke = COMMAND_INVOKE; - -DEBUG = false; - -module.exports.DEBUG = function(d) { - DEBUG = d; -}; - -/**************************************************************************** - * Root - ***************************************************************************/ - -function Root() { - Root.super_.call(this); - - //Object.defineProperty(this, '_parent', {value: null, enumerable: false}); -}; - -util.inherits(Root, TreeNode); - - -Root.decode = function(ber) { - let r = new Root(); - let tag = undefined; - - while(ber.remain > 0) { - if (DEBUG) { console.log("Reading root"); } - tag = ber.peek(); - if (tag === BER.APPLICATION(0)) { - ber = ber.getSequence(BER.APPLICATION(0)); - tag = ber.peek(); - if (DEBUG) { - console.log("Application 0 start"); - } - - if (tag === BER.APPLICATION(11)) { - if (DEBUG) { - console.log("Application 11 start"); - } - var seq = ber.getSequence(BER.APPLICATION(11)); - r.elements = []; - while (seq.remain > 0) { - try { - var rootReader = seq.getSequence(BER.CONTEXT(0)); - while (rootReader.remain > 0) { - r.addElement(RootElement.decode(rootReader)); - } - } - catch (e) { - if (DEBUG) { - console.log("Decode ERROR", e.stack); - return r; - } - throw e; - } - } - } - else if (tag === BER.APPLICATION(23)) { // InvocationResult BER.APPLICATION(23) - return InvocationResult.decode(ber) - } - else { - // StreamCollection BER.APPLICATION(6) - // InvocationResult BER.APPLICATION(23) - throw new errors.UnimplementedEmberTypeError(tag); - } - } - else if (tag === BER.CONTEXT(0)) { - // continuation of previous message - try { - return RootElement.decode(rootReader) - } - catch (e) { - console.log(e.stack); - return r; - } - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return r; -} - -Root.prototype.addElement = function(ele) { - ele._parent = this; - if(this.elements === undefined) { - this.elements = []; - } - this.elements.push(ele); -} - - - -Root.prototype.addChild = function(child) { - this.addElement(child); -} - -Root.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(0)); - if(this.elements !== undefined) { - ber.startSequence(BER.APPLICATION(11)); - for(var i=0; i { callback(error, node) }); - } - return this.getTreeBranch(new Command(COMMAND_GETDIRECTORY)); -} - -TreeNode.prototype.subscribe = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - return this.getTreeBranch(new Command(COMMAND_SUBSCRIBE)); -} - -TreeNode.prototype.unsubscribe = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - return this.getTreeBranch(new Command(COMMAND_UNSUBSCRIBE)); -} - -TreeNode.prototype.getChildren = function() { - if(this.children !== undefined) { - return this.children; - } - return null; -} - -function path2number(path) { - try { - let numbers = path.split("."); - if (numbers.length > 0) { - return Number(numbers[numbers.length - 1]); - } - } - catch(e) { - // ignore - } -} - -TreeNode.prototype.getNumber = function() { - if (this.isQualified()) { - return path2number(this.getPath()); - } - else { - return this.number; - } -} - -_getElementByPath = function(children, pathArray, path) { - if ((children === null)||(children === undefined)||(pathArray.length < 1)) { - return null; - } - var currPath = pathArray.join("."); - var number = pathArray[pathArray.length - 1]; - - for (var i = 0; i < children.length; i++) { - if ((children[i].path == currPath)|| - (children[i].number == number)) { - if (path.length === 0) { - return children[i]; - } - pathArray.push(path.splice(0,1)); - return _getElementByPath(children[i].getChildren(), pathArray, path); - } - } - - return null; -} - -TreeNode.prototype.toJSON = function() { - let res = {}; - const node = this; - if (node.number) { - res.number = node.number - } - if (node.path) { - res.path = node.path; - } - if (node.contents) { - for(let prop in node.contents) { - if (node.contents.hasOwnProperty(prop)) { - let type = typeof node.contents[prop]; - if ((type === "string") || (type === "number")) { - res[prop] = node.contents[prop]; - } - else if (node.contents[prop].value !== undefined) { - res[prop] = node.contents[prop].value; - } - else { - res[prop] = node.contents[prop]; - } - } - } - } - if (node.isMatrix()) { - if (node.targets) { - res.targets = node.targets.slice(0); - } - if (node.sources) { - res.sources = node.sources.slice(0); - } - if (node.connections) { - res.connections = {}; - for (let target in node.connections) { - if (node.connections.hasOwnProperty(target)) { - res.connections[target] = {target: target, sources: []}; - if (node.connections[target].sources) { - res.connections[target].sources = node.connections[target].sources.slice(0); - } - } - } - - } - } - let children = node.getChildren(); - if (children) { - res.children = []; - for(let child of children) { - res.children.push(child.toJSON()); - } - } - return res; -}; - -TreeNode.prototype.getElementByPath = function(path) { - var children = this.getChildren(); - if ((children === null)||(children === undefined)) { - return null; - } - - var myPath = this.getPath(); - if (path == myPath) { - return this; - } - var myPathArray = []; - if (this._parent) { - myPathArray = myPath.split("."); - } - path = path.split("."); - - if (path.length > myPathArray.length) { - pathArray = path.splice(0, myPath.length + 1); - for(var i = 0; i < pathArray.length - 1; i++) { - if (pathArray[i] != myPathArray[i]) { - return null; - } - } - } - else { - return null; - } - return _getElementByPath(children, pathArray, path); -} - -TreeNode.prototype.getElementByNumber = function(index) { - var children = this.getChildren(); - if(children === null) return null; - for(var i=0; i 0) { - (function(cb) { - callbacks.push(() => { - cb(null, self) - }); - })(self._directoryCallbacks.shift()); - } - - for(var i=0; i { - cb(self) - }); - })(self._callbacks[i]); - } - - return callbacks; -} - -TreeNode.prototype.getNodeByPath = function(client, path, callback) { - var self=this; - - if(path.length == 0) { - callback(null, self); - return; - } - - - var child = self.getElement(path[0]); - if(child !== null) { - child.getNodeByPath(client, path.slice(1), callback); - } else { - var cmd = self.getDirectory((error, node) => { - if(error) { - callback(error); - } - child = node.getElement(path[0]); - if(child === null) { - //let e = new Error('invalid path: "' + path[0] + '"'); - // console.log(e.message ,"Self:", self, "Node:", node); - //callback(e); - //DO NOT REJECT !!!! We could still be updating the tree. - return; - } else { - child.getNodeByPath(client, path.slice(1), callback); - } - }); - if(cmd !== null) { - client.sendBERNode(cmd); - } - } -} - -TreeNode.prototype.getPath = function() { - if (this.path !== undefined) { - return this.path; - } - if(this._parent === null) { - if(this.number === undefined) { - return ""; - } else { - return this.number.toString(); - } - } else { - var path = this._parent.getPath(); - if(path.length > 0) { - path = path + "."; - } - return path + this.number; - } -} - -/**************************************************************************** - * RootElement - ***************************************************************************/ - -function RootElement() {}; - -RootElement.decode = function(ber) { - return Element.decode(ber); - - // TODO: handle qualified types -} - -/**************************************************************************** - * Element - ***************************************************************************/ - -function Element() {}; - -Element.decode = function(ber) { - var tag = ber.peek(); - if(tag == BER.APPLICATION(1)) { - if (DEBUG) { console.log("Parameter decode");} - return Parameter.decode(ber); - } else if(tag == BER.APPLICATION(3)) { - if (DEBUG) { console.log("Node decode");} - return Node.decode(ber); - } else if(tag == BER.APPLICATION(2)) { - if (DEBUG) { console.log("Command decode");} - return Command.decode(ber); - } else if(tag == BER.APPLICATION(9)) { - if (DEBUG) { console.log("QualifiedParameter decode");} - return QualifiedParameter.decode(ber); - } else if(tag == BER.APPLICATION(10)) { - if (DEBUG) { console.log("QualifiedNode decode");} - return QualifiedNode.decode(ber); - } else if(tag == BER.APPLICATION(13)) { - if (DEBUG) { console.log("MatrixNode decode");} - return MatrixNode.decode(ber); - } - else if(tag == BER.APPLICATION(17)) { - if (DEBUG) { console.log("QualifiedMatrix decode");} - return QualifiedMatrix.decode(ber); - } - else if(tag == BER.APPLICATION(19)) { - if (DEBUG) { console.log("Function decode");} - return Function.decode(ber); - } else if (tag == BER.APPLICATION(20)) { - if (DEBUG) { console.log("QualifiedFunction decode");} - return QualifiedFunction.decode(ber); - } - else if(tag == BER.APPLICATION(24)) { - // Template - throw new errors.UnimplementedEmberTypeError(tag); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } -} - -/**************************************************************************** - * ElementCollection - ***************************************************************************/ - - - -/**************************************************************************** - * QualifiedNode - ***************************************************************************/ - -function QualifiedNode(path) { - QualifiedNode.super_.call(this); - if (path != undefined) { - this.path = path; - } -} - -util.inherits(QualifiedNode, TreeNode); - - -QualifiedNode.decode = function(ber) { - var qn = new QualifiedNode(); - ber = ber.getSequence(BER.APPLICATION(10)); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - qn.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID - } - else if(tag == BER.CONTEXT(1)) { - qn.contents = NodeContents.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - qn.children = []; - seq = seq.getSequence(BER.APPLICATION(4)); - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - qn.addChild(Element.decode(nodeSeq)); - } - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("QualifiedNode", qn); } - return qn; -} - -QualifiedNode.prototype.getMinimal = function(complete = false) { - const number = this.getNumber(); - const n = new Node(number); - if (complete && (this.contents != null)) { - n.contents = this.contents; - } - return n; -} - -QualifiedNode.prototype.update = function(other) { - callbacks = QualifiedNode.super_.prototype.update.apply(this); - if((other !== undefined) && (other.contents !== undefined)) { - if (this.contents == null) { - this.contents = other.contents; - } - else { - for(var key in other.contents) { - if(other.contents.hasOwnProperty(key)) { - this.contents[key] = other.contents[key]; - } - } - } - } - return callbacks; -} - -function QualifiedNodeCommand(self, cmd, callback) { - var r = new Root(); - var qn = new QualifiedNode(); - qn.path = self.path; - r.addElement(qn); - qn.addChild(new Command(cmd)); - if(callback !== undefined) { - self._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - return r; -} - -QualifiedNode.prototype.getDirectory = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedNodeCommand(this, COMMAND_GETDIRECTORY, callback) -} - -QualifiedNode.prototype.subscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedNodeCommand(this, COMMAND_SUBSCRIBE, callback) -} - -QualifiedNode.prototype.unsubscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedNodeCommand(this, COMMAND_UNSUBSCRIBE, callback) -} - -QualifiedNode.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(10)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - n.number = seq.readInt(); - } else if(tag == BER.CONTEXT(1)) { - n.contents = NodeContents.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - seq = seq.getSequence(BER.APPLICATION(4)); - n.children = []; - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - n.addChild(Element.decode(nodeSeq)); - } - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("Node", n); } - return n; -} - -Node.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(3)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.number); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if (tag == BER.CONTEXT(0)) { - m.number = seq.readInt(); - } - else if (tag == BER.CONTEXT(1)) { - m.contents = MatrixContents.decode(seq); - - } else if (tag == BER.CONTEXT(2)) { - m.children = []; - seq = seq.getSequence(BER.APPLICATION(4)); - while (seq.remain > 0) { - var childSeq = seq.getSequence(BER.CONTEXT(0)); - m.addChild(Element.decode(childSeq)); - } - } else if (tag == BER.CONTEXT(3)) { - m.targets = decodeTargets(seq); - } else if (tag == BER.CONTEXT(4)) { - m.sources = decodeSources(seq); - } else if (tag == BER.CONTEXT(5)) { - m.connections = decodeConnections(seq); - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("MatrixNode", m); } - return m; -}; - - -MatrixNode.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(13)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.number); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - - if(tag == BER.CONTEXT(0)) { - mc.identifier = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - mc.description = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(2)) { - mc.type = MatrixType.get(seq.readInt()); - } else if(tag == BER.CONTEXT(3)) { - mc.mode = MatrixMode.get(seq.readInt()); - } else if(tag == BER.CONTEXT(4)) { - mc.targetCount = seq.readInt(); - } else if(tag == BER.CONTEXT(5)) { - mc.sourceCount = seq.readInt(); - } else if(tag == BER.CONTEXT(6)) { - mc.maximumTotalConnects = seq.readInt(); - } else if(tag == BER.CONTEXT(7)) { - mc.maximumConnectsPerTarget = seq.readInt(); - } else if(tag == BER.CONTEXT(8)) { - tag = seq.peek(); - if (tag === BER.EMBER_RELATIVE_OID) { - mc.parametersLocation = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID - } - else { - mc.parametersLocation = seq.readInt(); - } - } else if(tag == BER.CONTEXT(9)) { - mc.gainParameterNumber = seq.readInt(); - } else if(tag == BER.CONTEXT(10)) { - mc.labels = []; - seq = seq.getSequence(BER.EMBER_SEQUENCE); - while(seq.remain > 0) { - var lSeq = seq.getSequence(BER.CONTEXT(0)); - mc.labels.push(Label.decode(lSeq)); - } - } else if(tag == BER.CONTEXT(11)) { - mc.schemaIdentifiers = seq.readInt(); - } else if(tag == BER.CONTEXT(12)) { - mc.templateReference = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return mc; -}; - -MatrixContents.prototype.encode = function(ber) { - ber.startSequence(BER.EMBER_SET); - if (this.identifier !== undefined) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeString(this.identifier, BER.EMBER_STRING); - ber.endSequence(); - } - if (this.description !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeString(this.description, BER.EMBER_STRING); - ber.endSequence(); - } - if (this.type !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.writeInt(this.type.value); - ber.endSequence(); - } - if (this.mode !== undefined) { - ber.startSequence(BER.CONTEXT(3)); - ber.writeInt(this.mode.value); - ber.endSequence(); - } - if (this.targetCount !== undefined) { - ber.startSequence(BER.CONTEXT(4)); - ber.writeInt(this.targetCount); - ber.endSequence(); - } - if (this.sourceCount !== undefined) { - ber.startSequence(BER.CONTEXT(5)); - ber.writeInt(this.sourceCount); - ber.endSequence(); - } - if (this.maximumTotalConnects !== undefined) { - ber.startSequence(BER.CONTEXT(6)); - ber.writeInt(this.maximumTotalConnects); - ber.endSequence(); - } - if (this.maximumConnectsPerTarget !== undefined) { - ber.startSequence(BER.CONTEXT(7)); - ber.writeInt(this.maximumConnectsPerTarget); - ber.endSequence(); - } - if (this.parametersLocation !== undefined) { - ber.startSequence(BER.CONTEXT(8)); - let param = Number(this.parametersLocation) - if (isNaN(param)) { - ber.writeRelativeOID(this.parametersLocation, BER.EMBER_RELATIVE_OID); - } - else { - ber.writeInt(param); - } - ber.endSequence(); - } - if (this.gainParameterNumber !== undefined) { - ber.startSequence(BER.CONTEXT(9)); - ber.writeInt(this.gainParameterNumber); - ber.endSequence(); - } - if (this.labels !== undefined) { - ber.startSequence(BER.CONTEXT(10)); - ber.startSequence(BER.EMBER_SEQUENCE); - for(var i =0; i < this.labels.length; i++) { - ber.startSequence(BER.CONTEXT(0)); - this.labels[i].encode(ber); - ber.endSequence(); - } - ber.endSequence(); - ber.endSequence(); - } - if (this.schemaIdentifiers !== undefined) { - ber.startSequence(BER.CONTEXT(11)); - ber.writeInt(this.schemaIdentifiers, BER.EMBER_STRING); - ber.endSequence(); - } - if (this.templateReference !== undefined) { - ber.startSequence(BER.CONTEXT(12)); - ber.writeRelativeOID(this.templateReference, BER.EMBER_RELATIVE_OID); - ber.endSequence(); - } - ber.endSequence(); -} - -decodeTargets = function(ber) { - let targets = []; - - ber = ber.getSequence(BER.EMBER_SEQUENCE); - - - while(ber.remain > 0) { - var seq = ber.getSequence(BER.CONTEXT(0)); - seq = seq.getSequence(BER.APPLICATION(14)); - seq = seq.getSequence(BER.CONTEXT(0)); - targets.push(seq.readInt()); - } - - return targets; -} - -decodeSources = function(ber) { - let sources = []; - - ber = ber.getSequence(BER.EMBER_SEQUENCE); - - while(ber.remain > 0) { - var seq = ber.getSequence(BER.CONTEXT(0)); - seq = seq.getSequence(BER.APPLICATION(15)); - seq = seq.getSequence(BER.CONTEXT(0)); - sources.push(seq.readInt()); - } - - return sources; -}; - -decodeConnections = function(ber) { - let connections = {}; - - let seq = ber.getSequence(BER.EMBER_SEQUENCE); - while(seq.remain > 0) { - var conSeq = seq.getSequence(BER.CONTEXT(0)); - var con = MatrixConnection.decode(conSeq); - if (con.target !== undefined) { - connections[con.target] = (con); - } - } - return connections; -} - -module.exports.MatrixContents = MatrixContents; - - - -function MatrixConnection(target) { - if (target) { - target = Number(target); - if (isNaN(target)) { target = 0; } - this.target = target; - } - else { - this.target = 0; - } -} - -// ConnectionOperation ::= -// INTEGER { -// absolute (0), -- default. sources contains absolute information -// connect (1), -- nToN only. sources contains sources to add to connection -// disconnect (2) -- nToN only. sources contains sources to remove from -// connection -// } -var MatrixOperation = new Enum({ - absolute: 0, - connect: 1, - disconnect: 2 -}); - -// ConnectionDisposition ::= -// INTEGER { -// tally (0), -- default -// modified (1), -- sources contains new current state -// pending (2), -- sources contains future state -// locked (3) -- error: target locked. sources contains current state -// -- more tbd. -// } -var MatrixDisposition = new Enum({ - tally: 0, - modified: 1, - pending: 2, - locked: 3 -}); - -module.exports.MatrixOperation = MatrixOperation; -module.exports.MatrixDisposition = MatrixDisposition; - -MatrixConnection.prototype.setSources = function(sources) { - if (sources === undefined) { - delete this.sources; - return; - } - let s = new Set(sources.map(i => Number(i))); - this.sources = [...s].sort(); // sources should be an array -} - -MatrixConnection.prototype.connectSources = function(sources) { - if (sources === undefined) { - return; - } - let s = new Set(this.sources); - for(let item of sources) { - s.add(item); - } - this.sources = [...s].sort(); -} - -MatrixConnection.prototype.disconnectSources = function(sources) { - if (sources === undefined) { - return; - } - let s = new Set(this.sources); - for(let item of sources) { - s.delete(item); - } - this.sources = [...s].sort(); -} - -MatrixConnection.decode = function(ber) { - var c = new MatrixConnection(); - ber = ber.getSequence(BER.APPLICATION(16)); - while (ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if (tag == BER.CONTEXT(0)) { - c.target = seq.readInt(); - } - else if (tag == BER.CONTEXT(1)) { - c.sources = seq.readRelativeOID(BER.EMBER_RELATIVE_OID).split(".").map(i => Number(i)); - } else if (tag == BER.CONTEXT(2)) { - c.operation = MatrixOperation.get(seq.readInt()); - - } else if (tag == BER.CONTEXT(3)) { - c.disposition = MatrixDisposition.get(seq.readInt()); - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return c; -} - -MatrixConnection.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(16)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.target); - ber.endSequence(); - - if ((this.sources !== undefined)&& (this.sources.length > 0)) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeRelativeOID(this.sources.join("."), BER.EMBER_RELATIVE_OID); - ber.endSequence(); - } - if (this.operation !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.writeInt(this.operation.value); - ber.endSequence(); - } - if (this.disposition !== undefined) { - ber.startSequence(BER.CONTEXT(3)); - ber.writeInt(this.disposition.value); - ber.endSequence(); - } - ber.endSequence(); -} - -module.exports.MatrixConnection = MatrixConnection; - -function Label(path) { - if (path) { - this.basePath = path; - } -} - -Label.decode = function(ber) { - var l = new Label(); - - ber = ber.getSequence(BER.APPLICATION(18)); - - while (ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if (tag == BER.CONTEXT(0)) { - l.basePath = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); - } else if (tag == BER.CONTEXT(1)) { - l.description = seq.readString(BER.EMBER_STRING); - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return l; -}; - -Label.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(18)); - if (this.basePath !== undefined) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.basePath, BER.EMBER_RELATIVE_OID); - ber.endSequence(); - } - if (this.description !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeString(this.description, BER.EMBER_STRING); - ber.endSequence(); - } - ber.endSequence(); -} - -module.exports.Label = Label; - - -function ParametersLocation() { -} - -ParametersLocation.decode = function(ber) { - var tag = ber.peek(); - ber = ber.getSequence(tag); - this.value = ber.readValue(); -} - -module.exports.ParametersLocation = ParametersLocation; - - - -var MatrixType = new Enum({ - oneToN: 0, - oneToOne: 1, - nToN: 2 -}); - - -module.exports.MatrixType = MatrixType; - - -var MatrixMode = new Enum({ - linear: 0, - nonLinear: 1 -}); - - -module.exports.MatrixMode = MatrixMode; - - -/**************************************************************************** - * QualifiedMatrix - ***************************************************************************/ - -function QualifiedMatrix(path) { - QualifiedMatrix.super_.call(this); - if (path != undefined) { - this.path = path; - } -} - -util.inherits(QualifiedMatrix, TreeNode); - -QualifiedMatrix.prototype.getMinimal = function(complete = false) { - const number = this.getNumber(); - const m = new MatrixNode(number); - if (complete) { - if (this.contents != null) { - m.contents = this.contents; - } - if (this.targets != null) { - m.targets = this.targets; - } - if (this.sources != null) { - m.sources = this.sources; - } - if (this.connections != null) { - m.connections = this.connections; - } - } - return m; -} - - -QualifiedMatrix.decode = function(ber) { - var qm = new QualifiedMatrix(); - ber = ber.getSequence(BER.APPLICATION(17)); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - qm.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID - } - else if(tag == BER.CONTEXT(1)) { - qm.contents = MatrixContents.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - qm.children = []; - seq = seq.getSequence(BER.APPLICATION(4)); - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - qm.addChild(Element.decode(nodeSeq)); - } - } else if (tag == BER.CONTEXT(3)) { - qm.targets = decodeTargets(seq); - } else if (tag == BER.CONTEXT(4)) { - qm.sources = decodeSources(seq); - } else if (tag == BER.CONTEXT(5)) { - qm.connections = {}; - seq = seq.getSequence(BER.EMBER_SEQUENCE); - while(seq.remain > 0) { - var conSeq = seq.getSequence(BER.CONTEXT(0)); - var con = MatrixConnection.decode(conSeq); - if (con.target !== undefined) { - qm.connections[con.target] = con; - } - } - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("QualifiedMatrix", qm); } - return qm; -} - -function MatrixUpdate(matrix, newMatrix) { - if (newMatrix != null) { - if (newMatrix.contents != null) { - if (matrix.contents == null) { - matrix.contents = newMatrix.contents; - } - else { - for (var key in newMatrix.contents) { - if (newMatrix.contents.hasOwnProperty(key)) { - matrix.contents[key] = newMatrix.contents[key]; - } - } - } - } - if (newMatrix.targets != null) { - matrix.targets = newMatrix.targets; - } - if (newMatrix.sources != null) { - matrix.sources = newMatrix.sources; - } - if (newMatrix.connections != null) { - if (matrix.connections == null) { - matrix.connections = {}; - } - for(let id in newMatrix.connections) { - if (newMatrix.connections.hasOwnProperty(id)) { - let connection = newMatrix.connections[id]; - if ((connection.target < matrix.contents.targetCount) && - (connection.target >= 0)) { - if (matrix.connections[connection.target] == null) { - matrix.connections[connection.target] = new MatrixConnection(connection.target); - } - matrix.connections[connection.target].setSources(connection.sources); - } - } - } - } - } -} - -QualifiedMatrix.prototype.update = function(other) { - callbacks = QualifiedMatrix.super_.prototype.update.apply(this); - MatrixUpdate(this, other); - return callbacks; -} - -function QualifiedMatrixCommand(self, cmd, callback) { - var r = new Root(); - var qn = new QualifiedMatrix(); - qn.path = self.path; - r.addElement(qn); - qn.addChild(new Command(cmd)); - if(callback !== undefined) { - self._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - return r; -} - -QualifiedMatrix.prototype.getDirectory = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedMatrixCommand(this, COMMAND_GETDIRECTORY, callback); -} - -QualifiedMatrix.prototype.subscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedMatrixCommand(this, COMMAND_SUBSCRIBE, callback); -} - -QualifiedMatrix.prototype.unsubscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedMatrixCommand(this, COMMAND_UNSUBSCRIBE, callback); -} - -QualifiedMatrix.prototype.connect = function(connections) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - var r = new Root(); - var qn = new QualifiedMatrix(); - qn.path = this.path; - r.addElement(qn); - qn.connections = connections; - return r; -} - -QualifiedMatrix.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(17)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i 0) { - tag = ber.peek(); - var seq = ber.getSequence(tag); - if (tag === BER.CONTEXT(0)) { - tuple.type = ParameterType.get(seq.readInt()); - } - else if (tag === BER.CONTEXT(1)) { - tuple.name = seq.readString(BER.EMBER_STRING); - } - } - return tuple; -} - -module.exports.FunctionArgument = FunctionArgument; - -/**************************************************************************** - * FunctionContent - ***************************************************************************/ - -function FunctionContent() { -} - - -FunctionContent.decode = function(ber) { - var fc = new FunctionContent(); - ber = ber.getSequence(BER.EMBER_SET); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - fc.identifier = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - fc.description = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(2)) { - fc.arguments = []; - seq = seq.getSequence(BER.EMBER_SEQUENCE); - while(seq.remain > 0) { - tag = seq.peek(); - var dataSeq = seq.getSequence(BER.CONTEXT(0)); - if (tag === BER.CONTEXT(0)) { - fc.arguments.push(FunctionArgument.decode(dataSeq)); - } - } - } else if(tag == BER.CONTEXT(3)) { - fc.result = []; - while(seq.remain > 0) { - tag = seq.peek(); - var dataSeq = seq.getSequence(tag); - if (tag === BER.CONTEXT(0)) { - fc.result.push(FunctionArgument.decode(dataSeq)); - } - } - } else if(tag == BER.CONTEXT(4)) { - fc.templateReference = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return fc; -} - -FunctionContent.prototype.encode = function(ber) { - ber.startSequence(BER.EMBER_SET); - - if(this.identifier != null) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeString(this.identifier, BER.EMBER_STRING); - ber.endSequence(); // BER.CONTEXT(0) - } - - if(this.description != null) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeString(this.description, BER.EMBER_STRING); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.arguments != null) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.EMBER_SEQUENCE); - for(var i =0; i < this.arguments.length; i++) { - this.arguments[i].encode(ber); - } - ber.endSequence(); - ber.endSequence(); // BER.CONTEXT(2) - } - - if(this.result != null) { - ber.startSequence(BER.CONTEXT(3)); - ber.startSequence(BER.EMBER_SEQUENCE); - for(var i =0; i < this.result; i++) { - ber.startSequence(BER.CONTEXT(0)); - this.result[i].encode(ber); - ber.endSequence(); - } - ber.endSequence(); - ber.endSequence(); // BER.CONTEXT(3) - } - - ber.endSequence(); // BER.EMBER_SET -} - -module.exports.FunctionContent = FunctionContent; - -/**************************************************************************** - * QualifiedFunction - ***************************************************************************/ - -function QualifiedFunction(path) { - QualifiedFunction.super_.call(this); - if (path != undefined) { - this.path = path; - } -} - -util.inherits(QualifiedFunction, TreeNode); - - -QualifiedFunction.decode = function(ber) { - var qf = new QualifiedFunction(); - ber = ber.getSequence(BER.APPLICATION(20)); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - qf.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID - } - else if(tag == BER.CONTEXT(1)) { - qf.contents = FunctionContent.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - qf.children = []; - seq = seq.getSequence(BER.APPLICATION(4)); - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - qf.addChild(Element.decode(nodeSeq)); - } - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return qf; -} - -QualifiedFunction.prototype.update = function(other) { - callbacks = QualifiedFunction.super_.prototype.update.apply(this); - if ((other !== undefined) && (other.contents !== undefined)) { - if (this.contents == null) { - this.contents = other.contents; - } - else { - for (var key in other.contents) { - if (other.contents.hasOwnProperty(key)) { - this.contents[key] = other.contents[key]; - } - } - } - } - return callbacks; -} - -function QualifiedFunctionCommand(self, cmd, callback) { - var r = new Root(); - var qf = new QualifiedFunction(); - qf.path = self.path; - r.addElement(qf); - qf.addChild(new Command(cmd)); - if(callback !== undefined) { - self._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - return r; -} - -QualifiedFunction.prototype.invoke = function(params, callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - var QualifiedFunctionNode = QualifiedFunctionCommand(this, COMMAND_INVOKE, callback); - var invocation = new Invocation() - invocation.arguments = params - QualifiedFunctionNode.elements[0].children[0].invocation = invocation - return QualifiedFunctionNode -} - -QualifiedFunction.prototype.getDirectory = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedFunctionCommand(this, COMMAND_GETDIRECTORY, callback); -} - -QualifiedFunction.prototype.subscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedFunctionCommand(this, COMMAND_SUBSCRIBE, callback); -} - -QualifiedFunction.prototype.unsubscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedFunctionCommand(this, COMMAND_UNSUBSCRIBE, callback); -} - -QualifiedFunction.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(20)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - f.number = seq.readInt(); - } else if(tag == BER.CONTEXT(1)) { - f.contents = FunctionContent.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - seq = seq.getSequence(BER.APPLICATION(4)); - f.children = []; - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - f.addChild(Element.decode(nodeSeq)); - } - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("Function", f); } - return f; -} - -Function.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(19)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.number); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i { callback(error, node) }); - } - - return this.getTreeBranch(undefined, (m) => { - m.addChild(new Command(COMMAND_INVOKE)) - }); -} - - - -/**************************************************************************** - * NodeContents - ***************************************************************************/ - -function NodeContents() { - this.isOnline = true; -}; - - - -NodeContents.decode = function(ber) { - var nc = new NodeContents(); - ber = ber.getSequence(BER.EMBER_SET); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - nc.identifier = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - nc.description = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(2)) { - nc.isRoot = seq.readBoolean(); - } else if(tag == BER.CONTEXT(3)) { - nc.isOnline = seq.readBoolean(); - } else if(tag == BER.CONTEXT(4)) { - nc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return nc; -} - -NodeContents.prototype.encode = function(ber) { - ber.startSequence(BER.EMBER_SET); - - if(this.identifier !== undefined) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeString(this.identifier, BER.EMBER_STRING); - ber.endSequence(); // BER.CONTEXT(0) - } - - if(this.description !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeString(this.description, BER.EMBER_STRING); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.isRoot !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.writeBoolean(this.isRoot); - ber.endSequence(); // BER.CONTEXT(2) - } - - if(this.isOnline !== undefined) { - ber.startSequence(BER.CONTEXT(3)); - ber.writeBoolean(this.isOnline); - ber.endSequence(); // BER.CONTEXT(3) - } - - if(this.schemaIdentifiers !== undefined) { - ber.startSequence(BER.CONTEXT(4)); - ber.writeString(this.schemaIdentifiers, BER.EMBER_STRING); - ber.endSequence(); // BER.CONTEXT(4) - } - - ber.endSequence(); // BER.EMBER_SET -} - -module.exports.NodeContents = NodeContents; - -/**************************************************************************** - * Command - ***************************************************************************/ - -function Command(number) { - if(number !== undefined) - this.number = number; - if(number == COMMAND_GETDIRECTORY) { - this.fieldFlags = FieldFlags.all; - } -} - -var FieldFlags = new Enum({ - sparse: -2, - all: -1, - default: 0, - identifier: 1, - description: 2, - tree: 3, - value: 4, - connections: 5 -}); - -Command.decode = function(ber) { - var c = new Command(); - ber = ber.getSequence(BER.APPLICATION(2)); - - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - c.number = seq.readInt(); - } - else if(tag == BER.CONTEXT(1)) { - c.fieldFlags = FieldFlags.get(seq.readInt()); - } - else if(tag == BER.CONTEXT(2)) { - c.invocation = Invocation.decode(ber); - } - else { - // TODO: options - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return c; -} - -Command.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(2)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.number); - ber.endSequence(); // BER.CONTEXT(0) - - if (this.number === COMMAND_GETDIRECTORY && this.fieldFlags) { - ber.startSequence(BER.CONTEXT(1)); - ber.writeInt(this.fieldFlags.value); - ber.endSequence(); - } - - if (this.number === COMMAND_INVOKE && this.invocation) { - ber.startSequence(BER.CONTEXT(2)); - this.invocation.encode(ber); - ber.endSequence(); - } - // TODO: options - - ber.endSequence(); // BER.APPLICATION(2) -} - -module.exports.Command = Command; - -/**************************************************************************** - * Invocation - ***************************************************************************/ -function Invocation(id = null) { - this.id = id == null ? Invocation._id++ : id; - this.arguments = []; -} - -Invocation._id = 1 - -Invocation.prototype.decode = function(ber) { - let invocation = new Invocation(); - ber = ber.getSequence(BER.APPLICATION(22)); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - invocation.invocationId = seq.readInt(); - } - if(tag == BER.CONTEXT(1)) { - invocation.arguments = []; - const seq = ber.getSequence(BER.EMBER_SEQUENCE); - while(seq.remain > 0) { - invocation.arguments.push(FunctionArgument.decode(seq)); - } - } - else { - // TODO: options - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return invocation; -} - - -Invocation.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(22)); - // ber.startSequence(BER.EMBER_SEQUENCE); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeInt(this.id) - ber.endSequence(); - - ber.startSequence(BER.CONTEXT(1)); - ber.startSequence(BER.EMBER_SEQUENCE); - for(var i = 0; i < this.arguments.length; i++) { - ber.startSequence(BER.CONTEXT(0)); - ber.writeValue( - this.arguments[i].value, - ParameterTypetoBERTAG(this.arguments[i].type - )); - ber.endSequence(); - } - ber.endSequence(); - ber.endSequence(); - - ber.endSequence(); // BER.APPLICATION(22) - -} -/**************************************************************************** - * InvocationResult - ***************************************************************************/ -function InvocationResult() { -} -module.exports.InvocationResult = InvocationResult; - -InvocationResult.decode = function(ber) { - let invocationResult = new InvocationResult(); - ber = ber.getSequence(BER.APPLICATION(23)); - while(ber.remain > 0) { - tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { // invocationId - invocationResult.invocationId = seq.readInt(); - } else if(tag == BER.CONTEXT(1)) { // success - invocationResult.success = seq.readBoolean() - }else if(tag == BER.CONTEXT(2)) { - invocationResult.result = []; - let res = seq.getSequence(BER.EMBER_SEQUENCE); - while(res.remain > 0) { - tag = res.peek(); - var resTag = res.getSequence(BER.CONTEXT(0)); - if (tag === BER.CONTEXT(0)) { - invocationResult.result.push(resTag.readValue()); - } - } - continue - } else { - // TODO: options - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return invocationResult; -} -/**************************************************************************** - * QualifiedParameter - ***************************************************************************/ - -function QualifiedParameter(path) { - QualifiedParameter.super_.call(this); - if(path !== undefined) - this.path = path; -} - -util.inherits(QualifiedParameter, TreeNode); -module.exports.QualifiedParameter = QualifiedParameter; - -QualifiedParameter.prototype.getMinimal = function(complete = false) { - const number = this.getNumber(); - const p = new Parameter(number); - if (complete) { - if (this.contents != null) { - p = this.contents; - } - } - return p; -} - - -QualifiedParameter.decode = function(ber) { - var qp = new QualifiedParameter(); - ber = ber.getSequence(BER.APPLICATION(9)); - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - qp.path = seq.readRelativeOID(BER.EMBER_RELATIVE_OID); // 13 => relative OID - } - else if(tag == BER.CONTEXT(1)) { - qp.contents = ParameterContents.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - qp.children = []; - seq = seq.getSequence(BER.APPLICATION(4)); - while(seq.remain > 0) { - var nodeSeq = seq.getSequence(BER.CONTEXT(0)); - qp.addChild(Element.decode(nodeSeq)); - } - } else { - return qp; - } - } - if (DEBUG) { console.log("QualifiedParameter", qp); } - return qp; -} - -QualifiedParameter.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(9)); - - ber.startSequence(BER.CONTEXT(0)); - ber.writeRelativeOID(this.path, BER.EMBER_RELATIVE_OID); - ber.endSequence(); // BER.CONTEXT(0) - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); // BER.CONTEXT(1) - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i { callback(error, node) }); - } - return r; -} - -QualifiedParameter.prototype.getDirectory = function(callback) { - if(callback !== undefined) { - this._directoryCallbacks.push((error, node) => { callback(error, node) }); - } - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedParameterCommand(this, COMMAND_GETDIRECTORY, callback); -} - -QualifiedParameter.prototype.subscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedParameterCommand(this, COMMAND_SUBSCRIBE, callback); -} - -QualifiedParameter.prototype.unsubscribe = function(callback) { - if (this.path === undefined) { - throw new Error("Invalid path"); - } - return QualifiedParameterCommand(this, COMMAND_UNSUBSCRIBE, callback); -} - -QualifiedParameter.prototype.setValue = function(value, callback) { - if(callback !== undefined) { - this._directoryCallbacks.push(callback); - } - - let r = new Root(); - let qp = new QualifiedParameter(this.path); - r.addElement(qp); - qp.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); - return r; -} - -/**************************************************************************** - * Parameter - ***************************************************************************/ - -function Parameter(number) { - Parameter.super_.call(this); - if(number !== undefined) - this.number = number; -} - -util.inherits(Parameter, TreeNode); -module.exports.Parameter = Parameter; - -Parameter.decode = function(ber) { - var p = new Parameter(); - ber = ber.getSequence(BER.APPLICATION(1)); - - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - p.number = seq.readInt(); - - } else if(tag == BER.CONTEXT(1)) { - p.contents = ParameterContents.decode(seq); - } else if(tag == BER.CONTEXT(2)) { - seq = seq.getSequence(BER.APPLICATION(4)); - p.children = []; - while(seq.remain > 0) { - var paramSeq = seq.getSequence(BER.CONTEXT(0)); - p.addChild(Element.decode(paramSeq)); - } - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - if (DEBUG) { console.log("Parameter", p); } - return p; -} - -Parameter.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(1)); - - ber.writeIfDefined(this.number, ber.writeInt, 0); - - if(this.contents !== undefined) { - ber.startSequence(BER.CONTEXT(1)); - this.contents.encode(ber); - ber.endSequence(); - } - - if(this.children !== undefined) { - ber.startSequence(BER.CONTEXT(2)); - ber.startSequence(BER.APPLICATION(4)); - for(var i=0; i { - m.contents = (value instanceof ParameterContents) ? value : new ParameterContents(value); - }); -} - -Parameter.prototype.toQualified = function() { - let qp = new QualifiedParameter(this.getPath()); - qp.update(this); - return qp; -} - -Parameter.prototype.update = function(other) { - callbacks = Parameter.super_.prototype.update.apply(this); - if ((other !== undefined) && (other.contents !== undefined)) { - if (this.contents == null) { - this.contents = other.contents; - } - else { - for (var key in other.contents) { - if (other.contents.hasOwnProperty(key)) { - this.contents[key] = other.contents[key]; - } - } - } - } - return callbacks; -} - - -var ParameterAccess = new Enum({ - none: 0, - read: 1, - write: 2, - readWrite: 3 -}); - - -/* -BER VAlue -Value ::= - CHOICE { - integer Integer64, - real REAL, - string EmberString, - boolean BOOLEAN, - octets OCTET STRING, - null NULL - }*/ - -var ParameterType = new Enum({ - integer: 1, - real: 2, - string: 3, - boolean: 4, - trigger: 5, - enum: 6, - octets: 7 -}); - -function ParameterTypetoBERTAG(type) { - switch (type.value) { - case 1: return BER.EMBER_INTEGER; - case 2: return BER.EMBER_REAL; - case 3: return BER.EMBER_STRING; - case 4: return BER.EMBER_BOOLEAN; - case 7: return BER.EMBER_OCTETSTRING; - default: - throw new Error(`Unhandled ParameterType ${type}`); - } -} - -module.exports.ParameterAccess = ParameterAccess; -module.exports.ParameterType = ParameterType; - -function ParameterContents(value, type) { - if(value !== undefined) { - this.value = value; - } - if(type !== undefined) { - if((type = ParameterType.get(type)) !== undefined){ - this.type = type - } - } -}; - -module.exports.ParameterContents = ParameterContents; - -ParameterContents.decode = function(ber) { - var pc = new ParameterContents(); - ber = ber.getSequence(BER.EMBER_SET); - - while(ber.remain > 0) { - var tag = ber.peek(); - var seq = ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - pc.identifier = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - pc.description = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(2)) { - pc.value = seq.readValue(); - } else if(tag == BER.CONTEXT(3)) { - pc.minimum = seq.readValue(); - } else if(tag == BER.CONTEXT(4)) { - pc.maximum = seq.readValue(); - } else if(tag == BER.CONTEXT(5)) { - pc.access = ParameterAccess.get(seq.readInt()); - } else if(tag == BER.CONTEXT(6)) { - pc.format = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(7)) { - pc.enumeration = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(8)) { - pc.factor = seq.readInt(); - } else if(tag == BER.CONTEXT(9)) { - pc.isOnline = seq.readBoolean(); - } else if(tag == BER.CONTEXT(10)) { - pc.formula = seq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(11)) { - pc.step = seq.readInt(); - } else if(tag == BER.CONTEXT(12)) { - pc.default = seq.readValue(); - } else if(tag == BER.CONTEXT(13)) { - pc.type = ParameterType.get(seq.readInt()); - } else if(tag == BER.CONTEXT(14)) { - pc.streamIdentifier = seq.readInt(); - } else if(tag == BER.CONTEXT(15)) { - pc.enumMap = StringIntegerCollection.decode(seq); - } else if(tag == BER.CONTEXT(16)) { - pc.streamDescriptor = StreamDescription.decode(seq); - } else if(tag == BER.CONTEXT(17)) { - pc.schemaIdentifiers = seq.readString(BER.EMBER_STRING); - } else if (tag == null) { - break; - } - else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - return pc; -} - -ParameterContents.prototype.encode = function(ber) { - ber.startSequence(BER.EMBER_SET); - - ber.writeIfDefined(this.identifier, ber.writeString, 0, BER.EMBER_STRING); - ber.writeIfDefined(this.description, ber.writeString, 1, BER.EMBER_STRING); - ber.writeIfDefined(this.value, ber.writeValue, 2); - ber.writeIfDefined(this.minimum, ber.writeValue, 3); - ber.writeIfDefined(this.maximum, ber.writeValue, 4); - ber.writeIfDefinedEnum(this.access, ParameterAccess, ber.writeInt, 5); - ber.writeIfDefined(this.format, ber.writeString, 6, BER.EMBER_STRING); - ber.writeIfDefined(this.enumeration, ber.writeString, 7, BER.EMBER_STRING); - ber.writeIfDefined(this.factor, ber.writeInt, 8); - ber.writeIfDefined(this.isOnline, ber.writeBoolean, 9); - ber.writeIfDefined(this.formula, ber.writeString, 10, BER.EMBER_STRING); - ber.writeIfDefined(this.step, ber.writeInt, 11); - ber.writeIfDefined(this.default, ber.writeValue, 12); - ber.writeIfDefinedEnum(this.type, ParameterType, ber.writeInt, 13); - ber.writeIfDefined(this.streamIdentifier, ber.writeInt, 14); - - if(this.emumMap !== undefined) { - ber.startSequence(BER.CONTEXT(15)); - StringIntegerCollection.encode(ber, this.enumMap); - ber.endSequence(); - } - - if(this.streamDescriptor !== undefined) { - ber.startSequence(BER.CONTEXT(16)); - this.streamDescriptor.encode(ber); - ber.endSequence(); - } - - ber.writeIfDefined(this.schemaIdentifiers, ber.writeString, 17, BER.EMBER_STRING); - - ber.endSequence(); -} - -/**************************************************************************** - * StringIntegerCollection - ***************************************************************************/ - -// This is untested, VPB doesn't seem to use this that I've seen so far - -function StringIntegerCollection() {}; - -StringIntegerCollection.decode = function(ber) { - var enumMap = {}; - ber = ber.getSequence(BER.APPLICATION(8)); - while(ber.remain > 0) { - var seq = ber.getSequence(BER.CONTEXT(0)); - seq = seq.getSequence(BER.APPLICATION(7)); - var entryString, entryInteger; - while(seq.remain > 0) { - var tag = seq.peek(); - var dataSeq = seq.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - entryString = dataSeq.readString(BER.EMBER_STRING); - } else if(tag == BER.CONTEXT(1)) { - entryInteger = dataSeq.readInt(); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - enumMap[entryString] = entryInteger; - } - - return new Enum(enumMap); -} - -StringIntegerCollection.encode = function(ber, e) { - ber.startSequence(BER.APPLICATION(8)); - ber.startSequence(BER.CONTEXT(0)); - e.enums.forEach((item) => { - ber.startSequence(BER.APPLICATION(7)); - ber.startSequence(BER.CONTEXT(0)); - ber.writeString(item.key, BER.EMBER_STRING); - ber.endSequence(); - ber.startSequence(BER.CONTEXT(1)); - ber.writeInt(item.value); - ber.endSequence(); - ber.endSequence(); - }); - ber.endSequence(); - ber.endSequence(); -} - -/**************************************************************************** - * StreamDescription - ***************************************************************************/ - -var StreamFormat = new Enum({ - unsignedInt8: 0, - unsignedInt16BigEndian: 2, - unsignedInt16LittleEndian: 3, - unsignedInt32BigEndian: 4, - unsignedInt32LittleEndian: 5, - unsignedInt64BigEndian: 6, - unsignedInt64LittleENdian: 7, - signedInt8: 8, - signedInt16BigEndian: 10, - signedInt16LittleEndian: 11, - signedInt32BigEndian: 12, - signedInt32LittleEndian: 13, - signedInt64BigEndian: 14, - signedInt64LittleEndian: 15, - ieeeFloat32BigEndian: 20, - ieeeFloat32LittleEndian: 21, - ieeeFloat64BigEndian: 22, - ieeeFloat64LittleEndian: 23 -}); - -function StreamDescription() {}; - -StreamDescription.decode = function(ber) { - var sd = new StreamDescription(); - ber = ber.getSequence(BER.APPLICATION(12)); - - while(ber.remain > 0) { - var tag = ber.peek(); - var seq =ber.getSequence(tag); - if(tag == BER.CONTEXT(0)) { - sd.format = StreamFormat.get(seq.readInt()); - } else if(tag == BER.CONTEXT(1)) { - sd.offset = seq.readInt(); - } else { - throw new errors.UnimplementedEmberTypeError(tag); - } - } - - return sd; -} - -StreamDescription.prototype.encode = function(ber) { - ber.startSequence(BER.APPLICATION(12)); - - ber.writeIfDefinedEnum(this.format, StreamFormat, ber.writeInt, 0); - ber.writeIfDefined(this.offset, ber.writeInt, 1); - - ber.endSequence(); -} - - diff --git a/errors.js b/errors.js deleted file mode 100755 index 9aad982..0000000 --- a/errors.js +++ /dev/null @@ -1,59 +0,0 @@ -const util = require('util'); - -/**************************************************************************** - * UnimplementedEmberType error - ***************************************************************************/ - -function UnimplementedEmberTypeError(tag) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - var identifier = (tag & 0xC0) >> 6; - var value = (tag & 0x1F).toString(); - var tagStr = tag.toString(); - if(identifier == 0) { - tagStr = "[UNIVERSAL " + value + "]"; - } else if(identifier == 1) { - tagStr = "[APPLICATION " + value + "]"; - } else if(identifier == 2) { - tagStr = "[CONTEXT " + value + "]"; - } else { - tagStr = "[PRIVATE " + value + "]"; - } - this.message = "Unimplemented EmBER type " + tagStr; -} - -util.inherits(UnimplementedEmberTypeError, Error); -module.exports.UnimplementedEmberTypeError = UnimplementedEmberTypeError; - - -function ASN1Error(message) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.message = message; -} - -util.inherits(ASN1Error, Error); -module.exports.ASN1Error = ASN1Error; - -function EmberAccessError(message) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - if(this.message !== undefined) { - this.message = message; - } else { - this.message("Parameter access error"); - } -} - -util.inherits(EmberAccessError, Error); -module.exports.EmberAccessError = EmberAccessError; - -function EmberTimeoutError(message) { - Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; - this.message = message; -} - -util.inherits(EmberTimeoutError, Error); -module.exports.EmberTimeoutError = EmberTimeoutError; - diff --git a/index.js b/index.js index 924f192..6727f4f 100755 --- a/index.js +++ b/index.js @@ -1,7 +1,8 @@ -const DeviceTree = require('./device.js').DeviceTree; -const Decoder = require('./device.js').DecodeBuffer; -const Ember = require("./ember.js"); +const EmberClient = require('./EmberClient'); +const EmberLib = require("./EmberLib"); +const Decoder = EmberLib.DecodeBuffer; const S101 = require("./s101"); -const TreeServer = require("./server"); -const {S101Client} = require("./client"); -module.exports = {DeviceTree, Decoder, Ember, TreeServer, S101, S101Client}; +const {EmberServer,ServerEvents} = require("./EmberServer"); +const {S101Client} = require("./EmberSocket"); +const BER = require('./ber.js') +module.exports = {EmberClient, Decoder, EmberLib, EmberServer,ServerEvents, S101, S101Client, BER}; diff --git a/package.json b/package.json index 270514d..2d5206e 100755 --- a/package.json +++ b/package.json @@ -1,36 +1,47 @@ { - "name": "emberplus", - "version": "1.8.0", + "name": "node-emberplus", + "version": "2.5.9", "description": "Javascript implementation of the Ember+ automation protocol", "main": "index.js", "scripts": { - "test": "jest test", - "eslint": "eslint ./" + "test": "jest test --coverage", + "eslint": "eslint ./", + "start": "node server.js" }, - "author": "Brian Mayton (http://bdm.cc)", + "author": "Gilles Dufour (www.gdnet.be)", "contributors": [ - { - "name": "Gilles Dufour", - "email": "dufour.gilles@gmail.com" - } + "Gilles Dufour (www.gdnet.be)", + "Brian Mayton (http://bdm.cc)" ], "repository": { "type": "git", - "url": "https://github.com/bmayton/node-emberplus" + "url": "git+https://github.com/dufourgilles/node-emberplus.git" }, "license": "MIT", "dependencies": { - "asn1": "evs-broadcast/node-asn1#date_2018_01_02", + "asn1": "github:evs-broadcast/node-asn1", "enum": "^2.4.0", "long": "^3.2.0", "smart-buffer": "^3.0.3", "winston": "^2.1.1", - "winston-color": "^1.0.0" + "winston-color": "^1.0.0", + "yargs": "^15.1.0" }, "devDependencies": { "eslint": "^5.5.0", "jest": "^23.5.0", "jest-cli": "^24.9.0", "sinon": "^7.4.1" - } + }, + "bugs": { + "url": "https://github.com/dufourgilles/node-emberplus/issues" + }, + "homepage": "https://github.com/dufourgilles/node-emberplus#readme", + "directories": { + "test": "test" + }, + "keywords": [ + "emberplus", + "lawo" + ] } diff --git a/serve.js b/serve.js new file mode 100755 index 0000000..9447250 --- /dev/null +++ b/serve.js @@ -0,0 +1,55 @@ +const yargs = require('yargs'); +const { EmberServer, Decoder } = require('./index'); +const { readFileSync } = require('fs'); + +const argv = yargs.options({ + host: { + alias: 'h', + description: 'host name|ip', + default: '0.0.0.0' + }, + + port: { + alias: 'p', + default: 9000, + type: 'number', + description: 'port', + demandOption: true + }, + + file: { + alias: 'f', + description: 'file containing the ber (default) or json tree', + demandOption: true + }, + + json: { + alias: 'j', + type: 'boolean', + description: 'file format is json' + }, + debug: { + alias: 'd', + type: 'boolean', + description: 'debug' + } + +}).help().argv; + +const main = async () => { + const data = readFileSync(argv.file); + const tree = argv.json ? EmberServer.JSONtoTree(JSON.parse(data.toString())) : Decoder(data); + const server = new EmberServer(argv.host, argv.port, tree); + server._debug = true; + console.log(Date.now(), 'starting server'); + if (argv.debug) { + server._debug = true; + } + try { + server.listen(); + } catch (e) { + console.log(e); + } +}; + +main(); diff --git a/server.js b/server.js deleted file mode 100755 index 8bdfee5..0000000 --- a/server.js +++ /dev/null @@ -1,611 +0,0 @@ -const EventEmitter = require('events').EventEmitter; -const util = require('util'); -const S101Server = require('./client.js').S101Server; -const ember = require('./ember.js'); - -function TreeServer(host, port, tree) { - TreeServer.super_.call(this); - var self = this; - self._debug = false; - - self.callback = undefined; - self.timeoutValue = 2000; - self.server = new S101Server(host, port); - self.tree = tree; - self.clients = new Set(); - self.subscribers = {}; - - self.server.on('listening', () => { - if (self._debug) { console.log("listening"); } - self.emit('listening'); - if (self.callback !== undefined) { - self.callback(); - self.callback = undefined; - } - }); - - self.server.on('connection', (client) => { - if (self._debug) { console.log("ember new connection from", client.remoteAddress()); } - self.clients.add(client); - client.on("emberTree", (root) => { - if (self._debug) { console.log("ember new request from", client.remoteAddress(), root); } - // Queue the action to make sure responses are sent in order. - client.addRequest(() => { - try { - let path = self.handleRoot(client, root); - self.emit("request", {client: client.remoteAddress(), root: root, path: path}); - } - catch(e) { - if (self._debug) { console.log(e.stack); } - self.emit("error", e); - } - }); - }); - client.on("disconnected", () => { - self.clients.delete(client); - self.emit('disconnect', client.remoteAddress()); - }); - client.on("error", error => { - self.emit('clientError', { remoteAddress: client.remoteAddress(), error }); - }); - self.emit('connection', client.remoteAddress()); - }); - - self.server.on('disconnected', () => { - self.emit('disconnected', client.remoteAddress()); - }); - - self.server.on("error", (e) => { - self.emit("error", e); - if (self.callback !== undefined) { - self.callback(e); - } - }); - -} - -util.inherits(TreeServer, EventEmitter); - - -TreeServer.prototype.listen = function() { - return new Promise((resolve, reject) => { - this.callback = (e) => { - if (e === undefined) { - return resolve(); - } - return reject(e); - }; - this.server.listen(); - }); -}; - -TreeServer.prototype.close = function () { - return new Promise((resolve, reject) => { - this.callback = (e) => { - if (e === undefined) { - return resolve(); - } - return reject(e); - }; - this.server.server.close(); - }); -}; - -TreeServer.prototype.handleRoot = function(client, root) { - if ((root === undefined) || (root.elements === undefined) || (root.elements < 1)) { - this.emit("error", new Error("invalid request")); - return; - } - - - const node = root.elements[0]; - client.request = node; - - if (node.path !== undefined) { - return this.handleQualifiedNode(client, node); - } - else if (node instanceof ember.Command) { - // Command on root element - this.handleCommand(client, this.tree, node.number); - return "root"; - } - else { - return this.handleNode(client, node); - } -} - -TreeServer.prototype.handleError = function(client, node) { - if (client !== undefined) { - let res = node == null ? this.tree._root.getMinimal() : node; - client.sendBERNode(res); - } -} - - -TreeServer.prototype.handleQualifiedNode = function(client, node) { - const path = node.path; - // Find this element in our tree - const element = this.tree.getElementByPath(path); - - if ((element === null) || (element === undefined)) { - this.emit("error", new Error(`unknown element at path ${path}`)); - return this.handleError(client); - } - - if ((node.children !== undefined) && (node.children.length === 1) && - (node.children[0] instanceof ember.Command)) { - this.handleCommand(client, element, node.children[0].number); - } - else { - if (node instanceof ember.QualifiedMatrix) { - this.handleQualifiedMatrix(client, element, node); - } - else if (node instanceof ember.QualifiedParameter) { - this.handleQualifiedParameter(client, element, node); - } - } - return path; -} - - -TreeServer.prototype.handleNode = function(client, node) { - // traverse the tree - let element = node; - let path = []; - while(element !== undefined) { - if (element.number === undefined) { - this.emit("error", "invalid request"); - return; - } - if (element instanceof ember.Command) { - break; - } - path.push(element.number); - - let children = element.getChildren(); - if ((! children) || (children.length === 0)) { - break; - } - element = element.children[0]; - } - let cmd = element; - - if (cmd === undefined) { - this.emit("error", "invalid request"); - return this.handleError(client); - } - - element = this.tree.getElementByPath(path.join(".")); - - if (element == null) { - this.emit("error", new Error(`unknown element at path ${path}`)); - return this.handleError(client); - } - - if (cmd instanceof ember.Command) { - this.handleCommand(client, element, cmd.number); - } - else if ((cmd instanceof ember.MatrixNode) && (cmd.connections !== undefined)) { - this.handleMatrixConnections(client, element, cmd.connections); - } - else if ((cmd instanceof ember.Parameter) && - (cmd.contents !== undefined) && (cmd.contents.value !== undefined)) { - if (this._debug) { console.log(`setValue for element at path ${path} with value ${cmd.contents.value}`); } - this.setValue(element, cmd.contents.value, client); - let res = this.getResponse(element); - client.sendBERNode(res) - this.updateSubscribers(element.getPath(), res, client); - } - else { - this.emit("error", new Error("invalid request format")); - if (this._debug) { console.log("invalid request format"); } - return this.handleError(client, element.getTreeBranch()); - } - return path; -} - -TreeServer.prototype.handleQualifiedMatrix = function(client, element, matrix) -{ - this.handleMatrixConnections(client, element, matrix.connections); -} - -TreeServer.prototype.handleQualifiedParameter = function(client, element, parameter) -{ - if (parameter.contents.value !== undefined) { - this.setValue(element, parameter.contents.value, client); - let res = this.getQualifiedResponse(element); - client.sendBERNode(res) - this.updateSubscribers(element.getPath(), res, client); - } -} - - -TreeServer.prototype.handleMatrixConnections = function(client, matrix, connections, response = true) { - var res; - var root; // ember message root - if (matrix.isQualified()) { - root = new ember.Root(); - res = new ember.QualifiedMatrix(matrix.getPath()); - root.elements = [res]; // do not use addchild or the element will get removed from the tree. - } - else { - res = new ember.MatrixNode(matrix.number); - root = matrix._parent.getTreeBranch(res); - } - res.connections = {}; - for(let id in connections) { - if (!connections.hasOwnProperty(id)) { - continue; - } - let connection = connections[id]; - let conResult = new ember.MatrixConnection(connection.target); - let emitType; - res.connections[connection.target] = conResult; - - - // Apply changes - - if ((connection.operation === undefined) || - (connection.operation.value == ember.MatrixOperation.absolute)) { - matrix.connections[connection.target].setSources(connection.sources); - emitType = "matrix-change"; - } - else if (connection.operation == ember.MatrixOperation.connect) { - matrix.connections[connection.target].connectSources(connection.sources); - emitType = "matrix-connect"; - } - else { // Disconnect - matrix.connections[connection.target].disconnectSources(connection.sources); - emitType = "matrix-disconnect"; - } - - // Send response or update subscribers. - - if (response) { - conResult.sources = matrix.connections[connection.target].sources; - conResult.disposition = ember.MatrixDisposition.modified; - // We got a request so emit something. - this.emit(emitType, { - target: connection.target, - sources: connection.sources, - client: client.remoteAddress() - }); - } - else { - // the action has been applied. So we should either send the current state (absolute) - // or send the action itself (connection.sources) - conResult.sources = matrix.connections[connection.target].sources; - conResult.operation = ember.MatrixOperation.absolute; - } - } - if (client !== undefined) { - client.sendBERNode(root); - } - if (this._debug) { console.log("Updating subscribers for matrix change"); } - this.updateSubscribers(matrix.getPath(), root, client); -} - -const validateMatrixOperation = function(matrix, target, sources) { - if (matrix === undefined) { - throw new Error(`matrix not found with path ${path}`); - } - if (matrix.contents === undefined) { - throw new Error(`invalid matrix at ${path} : no contents`); - } - if (matrix.contents.targetCount === undefined) { - throw new Error(`invalid matrix at ${path} : no targetCount`); - } - if ((target < 0) || (target >= matrix.contents.targetCount)) { - throw new Error(`target id ${target} out of range 0 - ${matrix.contents.targetCount}`); - } - if (sources.length === undefined) { - throw new Error("invalid sources format"); - } -} - -const doMatrixOperation = function(server, path, target, sources, operation) { - let matrix = server.tree.getElementByPath(path); - - validateMatrixOperation(matrix, target, sources); - - let connection = new ember.MatrixConnection(target); - connection.sources = sources; - connection.operation = operation; - server.handleMatrixConnections(undefined, matrix, [connection], false); -} - -TreeServer.prototype.matrixConnect = function(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.connect); -} - -TreeServer.prototype.matrixDisconnect = function(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.disconnect); -} - -TreeServer.prototype.matrixSet = function(path, target, sources) { - doMatrixOperation(this, path, target, sources, ember.MatrixOperation.absolute); -} - -TreeServer.prototype.handleCommand = function(client, element, cmd) { - if (cmd === ember.GetDirectory) { - this.handleGetDirectory(client, element); - } - else if (cmd === ember.Subscribe) { - this.handleSubscribe(client, element); - } - else if (cmd === ember.Unsubscribe) { - this.handleUnSubscribe(client, element); - } - else { - this.emit("error", new Error(`invalid command ${cmd}`)); - } -} - -TreeServer.prototype.getResponse = function(element) { - return element.getTreeBranch(undefined, function(node) { - node.update(element); - let children = element.getChildren(); - if (children != null) { - for (let i = 0; i < children.length; i++) { - node.addChild(children[i].getDuplicate()); - } - } - else if (this._debug) { - console.log("getResponse","no children"); - } - }); -} - -TreeServer.prototype.getQualifiedResponse = function(element) { - let res = new ember.Root(); - let dup; - if (element.isRoot() === false) { - dup = element.toQualified(); - } - let children = element.getChildren(); - if (children != null) { - for (let i = 0; i < children.length; i++) { - res.addChild(children[i].toQualified().getMinimalContent()); - } - } - else { - res.addChild(dup); - } - return res; -} - -TreeServer.prototype.handleGetDirectory = function(client, element) { - if (client !== undefined) { - if ((element.isMatrix() || element.isParameter()) && - (!element.isStream())) { - // ember spec: parameter without streamIdentifier should - // report their value changes automatically. - this.subscribe(client, element); - } - else if (element.isNode()) { - const children = element.getChildren(); - if (children != null) { - for (let i = 0; i < children.length; i++) { - const child = children[i]; - if ((child.isMatrix() || child.isParameter()) && - (!child.isStream())) { - this.subscribe(client, child); - } - } - } - } - - let res = this.getQualifiedResponse(element); - if (this._debug) { - console.log("getDirectory response", res); - } - client.sendBERNode(res); - } -} - -TreeServer.prototype.handleSubscribe = function(client, element) { - this.subscribe(client, element); -} - -TreeServer.prototype.handleUnSubscribe = function(client, element) { - this.unsubscribe(client, element); -} - - -TreeServer.prototype.subscribe = function(client, element) { - const path = element.getPath(); - if (this.subscribers[path] === undefined) { - this.subscribers[path] = new Set(); - } - this.subscribers[path].add(client); -} - -TreeServer.prototype.unsubscribe = function(client, element) { - const path = element.getPath(); - if (this.subscribers[path] === undefined) { - return; - } - this.subscribers[path].delete(client); -} - -TreeServer.prototype.setValue = function(element, value, origin, key) { - return new Promise((resolve, reject) => { - // Change the element value if write access permitted. - if (element.contents !== undefined) { - if (element.isParameter()) { - if ((element.contents.access !== undefined) && - (element.contents.access.value > 1)) { - element.contents.value = value; - this.emit("value-change", element); - } - } - else if (element.isMatrix()) { - if ((key !== undefined) && (element.contents.hasOwnProperty(key))) { - element.contents[key] = value; - this.emit("value-change", element); - } - } - } - }); -} - -TreeServer.prototype.replaceElement = function(element) { - let path = element.getPath(); - let parent = this.tree.getElementByPath(path); - if ((parent === undefined)||(parent._parent === undefined)) { - throw new Error(`Could not find element at path ${path}`); - } - parent = parent._parent; - let children = parent.getChildren(); - let newList = []; - for(let i = 0; i <= children.length; i++) { - if (children[i] && children[i].getPath() == path) { - element._parent = parent; // move it to new tree. - children[i] = element; - let res = this.getResponse(element); - this.updateSubscribers(path,res); - return; - } - } -} - - -TreeServer.prototype.updateSubscribers = function(path, response, origin) { - if (this.subscribers[path] === undefined) { - return; - } - - for (let client of this.subscribers[path]) { - if (client === origin) { - continue; // already sent the response to origin - } - if (this.clients.has(client)) { - client.queueMessage(response); - } - else { - // clean up subscribers - client is gone - this.subscribers[path].delete(client); - } - } -} - -const parseMatrixContent = function(matrixContent, content) { - if (content.labels) { - matrixContent.labels = []; - for(let l = 0; l < content.labels.length; l++) { - matrixContent.labels.push( - new ember.Label(content.labels[l]) - ); - } - delete content.labels; - } - if (content.type != null) { - if (content.type == "oneToN") { - matrixContent.type = ember.MatrixType.oneToN; - } - else if (content.type == "oneToOne") { - matrixContent.type = ember.MatrixType.oneToOne; - } - else if (content.type == "nToN") { - matrixContent.type = ember.MatrixType.nToN; - } - else { - throw new Error(`Invalid matrix type ${content.type}`); - } - delete content.type; - } - if (content.mode != null) { - if (content.mode == "linear") { - matrixContent.mode = ember.MatrixMode.linear; - } - else if (content.mode == "nonLinear") { - matrixContent.mode = ember.MatrixMode.nonLinear; - } - else { - throw new Error(`Invalid matrix mode ${content.mode}`); - } - delete content.mode; - } -} - -const parseObj = function(parent, obj) { - let path = parent.getPath(); - for(let i = 0; i < obj.length; i++) { - let emberElement; - let content = obj[i]; - let number = content.number !== undefined ? content.number : i; - delete content.number; - if (content.value !== undefined) { - emberElement = new ember.Parameter(number); - emberElement.contents = new ember.ParameterContents(content.value); - if (content.type) { - emberElement.contents.type = ember.ParameterType.get(content.type); - delete content.type; - } - else { - emberElement.contents.type = ember.ParameterType.string; - } - if (content.access) { - emberElement.contents.access = ember.ParameterAccess.get(content.access); - delete content.access; - } - else { - emberElement.contents.access = ember.ParameterAccess.read; - } - } - else if (content.targetCount !== undefined) { - emberElement = new ember.MatrixNode(number); - emberElement.contents = new ember.MatrixContents(); - parseMatrixContent(emberElement.contents, content); - if (content.connections) { - emberElement.connections = {}; - for (let c in content.connections) { - if (! content.connections.hasOwnProperty(c)) { - continue; - } - let t = content.connections[c].target !== undefined ? content.connections[c].target : 0; - let connection = new ember.MatrixConnection(t); - connection.setSources(content.connections[c].sources); - emberElement.connections[t] = connection; - } - delete content.connections; - } - else { - emberElement.connections = {}; - for (let t = 0; t < content.targetCount; t++) { - let connection = new ember.MatrixConnection(t); - emberElement.connections[t] = connection; - } - } - } - else { - emberElement = new ember.Node(number); - emberElement.contents = new ember.NodeContents(); - } - for(let id in content) { - if ((id !== "children") && (content.hasOwnProperty(id))) { - emberElement.contents[id] = content[id]; - } - else { - parseObj(emberElement, content.children); - } - } - parent.addChild(emberElement); - } -} - -TreeServer.JSONtoTree = function(obj) { - let tree = new ember.Root(); - parseObj(tree, obj); - return tree; -} - - -TreeServer.prototype.toJSON = function() { - if ((!this.tree) || (!this.tree.elements) || (this.tree.elements.length == 0)) { - return []; - } - return [].push(this.tree.elements[0].toJSON()); -}; - -module.exports = TreeServer; diff --git a/test/DeviceTree.test.js b/test/DeviceTree.test.js index 85a0037..a1c645e 100755 --- a/test/DeviceTree.test.js +++ b/test/DeviceTree.test.js @@ -1,21 +1,21 @@ const fs = require("fs"); const sinon = require("sinon"); -const Decoder = require('../').Decoder; -const DeviceTree = require("../").DeviceTree; -const TreeServer = require("../").TreeServer; +const Decoder = require('../EmberLib').DecodeBuffer; +const EmberClient = require("../EmberClient"); +const {EmberServer} = require("../EmberServer"); const LOCALHOST = "127.0.0.1"; const UNKNOWN_HOST = "192.168.99.99"; const PORT = 9008; -describe("DeviceTree", () => { +describe("EmberClient", () => { describe("With server", () => { - /** @type {TreeServer} */ + /** @type {EmberServer} */ let server; beforeAll(() => { return Promise.resolve() .then(() => new Promise((resolve, reject) => { - fs.readFile("./embrionix.ember", (e, data) => { + fs.readFile("./test/embrionix.ember", (e, data) => { if (e) { reject(e); } @@ -23,17 +23,15 @@ describe("DeviceTree", () => { }); })) .then(root => { - server = new TreeServer(LOCALHOST, PORT, root); + server = new EmberServer(LOCALHOST, PORT, root); return server.listen(); }); }); - afterAll(() => server.close()); - }); it("should gracefully connect and disconnect", () => { return Promise.resolve() .then(() => { - let tree = new DeviceTree(LOCALHOST, PORT); + let tree = new EmberClient(LOCALHOST, PORT); return Promise.resolve() .then(() => tree.connect()) .then(() => tree.getDirectory()) @@ -41,12 +39,50 @@ describe("DeviceTree", () => { .then(() => tree.connect()) .then(() => tree.getDirectory()) .then(() => tree.disconnect()) + }); + }); + + it("should not disconnect after 5 seconds of inactivity", () => { + return Promise.resolve() + .then(() => { + let tree = new EmberClient(LOCALHOST, PORT); + + tree.on("error", error => { + throw error; + }); + + return Promise.resolve() + .then(() => tree.connect()) + .then(() => new Promise(resolve => setTimeout(resolve, 5000))) + .then(() => tree.disconnect()) }) - }); - + }, 7000); + + it("timeout should be taken into account when connecting to unknown host", () => { + let tree = new EmberClient(UNKNOWN_HOST, PORT); + tree.on("error", () => { + }); + const expectedTimeoutInSec = 2; + const initialTime = Date.now(); + return tree.connect(expectedTimeoutInSec) + .then(() => { + throw new Error("Should have thrown"); + }, + error => { + const durationMs = Date.now() - initialTime; + const deltaMs = Math.abs(expectedTimeoutInSec * 1000 - durationMs); + expect(deltaMs).toBeLessThan(10); + expect(error.message).toBe(`Could not connect to ${UNKNOWN_HOST}:${PORT} after a timeout of ${expectedTimeoutInSec} seconds`) + }); + }); it("should gracefully connect and getDirectory", () => { - let tree = new DeviceTree(LOCALHOST, PORT); - let stub = sinon.stub(tree.client, "sendBER"); + let tree = new EmberClient(LOCALHOST, PORT); + tree.on("error", () => { + // ignore + }); + let stub = sinon.stub(tree._client, "sendBER"); + tree._debug = true; + server._debug = true; stub.onFirstCall().returns(); stub.onSecondCall().throws(new Error("blah")); stub.callThrough(); @@ -57,45 +93,11 @@ describe("DeviceTree", () => { .then(() => { stub.restore(); tree.disconnect(); - }, error => { + }, () => { stub.restore(); tree.disconnect(); - throw error; - }) - - }, 10000); - it("should not disconnect after 5 seconds of inactivity", () => { - return Promise.resolve() - .then(() => { - let tree = new DeviceTree(LOCALHOST, PORT); - - tree.on("error", error => { - throw error; - }); - - return Promise.resolve() - .then(() => tree.connect()) - .then(() => new Promise(resolve => setTimeout(resolve, 5000))) - .then(() => tree.disconnect()) - }) - }, 7000); - }); - - it("timeout should be taken into account when connecting to unknown host", () => { - let tree = new DeviceTree(UNKNOWN_HOST, PORT); - tree.on("error", () => { - }); - const expectedTimeoutInSec = 2; - const initialTime = performance.now(); - return tree.connect(expectedTimeoutInSec) - .then(() => { - throw new Error("Should have thrown"); - }, - error => { - const durationMs = performance.now() - initialTime; - const deltaMs = Math.abs(expectedTimeoutInSec * 1000 - durationMs); - expect(deltaMs).toBeLessThan(10); - expect(error.message).toBe(`Could not connect to ${UNKNOWN_HOST}:${PORT} after a timeout of ${expectedTimeoutInSec} seconds`) + // do nothinf }); - }); + }, 10000); + }); }); diff --git a/test/Ember.test.js b/test/Ember.test.js index e1c6e23..f96b136 100755 --- a/test/Ember.test.js +++ b/test/Ember.test.js @@ -1,32 +1,1628 @@ const expect = require("expect"); -const { S101Client } = require("../index"); +const { S101Client } = require("../EmberSocket"); const s101Buffer = Buffer.from("fe000e0001c001021f026082008d6b820089a0176a15a0050d03010201a10c310aa0080c066c6162656c73a01b6a19a0050d03010202a110310ea00c0c0a706172616d6574657273a051714fa0050d03010203a1463144a0080c066d6174726978a403020104a503020104aa183016a0147212a0050d03010201a1090c075072696d617279a203020102a303020101a8050d03010202a903020101f24cff", "hex"); const errorBuffer = Buffer.from("76fe000e0001c001021f026082008d6b820089a0176a15a0050d03010201a10c310aa0080c066c6162656c73a01b6a19a0050d03010202a110310ea00c0c0a706172616d6574657273a051714fa0050d03010203a1463144a0080c066d6174726978a403020104a503020104aa183016a0147212a0050d03010201a1090c075072696d617279a203020102a303020101a8050d03010202a903020101f24cff", "hex"); -const ember = require("../ember"); +const ember = require("../EmberLib"); const BER = require('../ber.js'); -const errors = require('../errors.js'); - +const Errors = require('../Errors.js'); +const EmberLib = require("../EmberLib"); +const {ParameterTypefromBERTAG, ParameterTypetoBERTAG} = require("../EmberLib/ParameterType"); +const identifier = "node_identifier"; +const description = "node_description"; describe("Ember", () => { - let client; + describe("generic", () => { + let client; + + beforeEach(() => { + client = new S101Client(); + }); + + it("should parse S101 message without error", (done) => { + client.on("emberPacket", () => { + done(); + }); + client.on("error", e => { + // eslint-disable-next-line no-console + console.log(e); + expect(e).toBeUndefined(); + done(); + }); + client.codec.dataIn(s101Buffer); + }); + + it("should handle Errors in message", () => { + var ber = new BER.Reader(errorBuffer); + expect(() => ember.Root.decode(ber)).toThrow(Errors.UnimplementedEmberTypeError); + }); + it("Should have a toJSON()", () => { + const node = new EmberLib.Node(); + node.addChild(new EmberLib.Node(0)); + node.getElementByNumber(0).addChild(new EmberLib.Parameter(1)); + const matrix = new EmberLib.MatrixNode(2); + matrix.targets = [0,3,6,7]; + matrix.sources = [2,6,8]; + matrix.contents = new EmberLib.MatrixContents(EmberLib.MatrixType.oneToN, EmberLib.MatrixMode.nonLinear); + node.getElementByNumber(0).addChild(matrix); + const js = node.toJSON(); + expect(js).toBeDefined(); + expect(js.elements.length).toBe(1); + expect(js.elements[0].number).toBe(0); + expect(js.elements[0].children[0].number).toBe(1); + expect(js.elements[0].children[1].number).toBe(2); + expect(js.elements[0].children[1].targets.length).toBe(matrix.targets.length); + }); + it("should have a getElement()", () => { + const node = new EmberLib.Node(); + node.addChild(new EmberLib.Node(0)); + let res = node.getElement(0); + expect(res).toBeDefined(); + }); + it("should have a isCommand(), isRoot() ... functions", () => { + const root = new EmberLib.Root(); + const node = new EmberLib.Node(); + root.addElement(node); + expect(node.isCommand()).toBeFalsy(); + expect(node.isRoot()).toBeFalsy(); + expect(node.isStream()).toBeFalsy(); + expect(node.isTemplate()).toBeFalsy(); + }); + it("should have function getElement", () => { + const node = new EmberLib.Node(0); + const identifier = "node_identifier"; + const description = "node_description"; + node.contents = new EmberLib.NodeContents(identifier, description); + const root = new EmberLib.Root(); + root.addElement(node); + let res = root.getElement(identifier); + expect(res).toBeDefined(); + expect(res.contents.identifier).toBe(identifier); + }); + + it("should throw error if function getElement called from a node with longer parh", () => { + const root = new EmberLib.Root(); + root.addChild(new EmberLib.Node(0)); + root.getElement(0).addChild(new EmberLib.Node(1)); + root.getElementByPath("0.1").addChild(new EmberLib.Node(1)); + const node = new EmberLib.Node(0); + root.getElementByPath("0.1.1").addChild(node); + const identifier = "node_identifier"; + const description = "node_description"; + node.contents = new EmberLib.NodeContents(identifier, description); + let res = root.getElementByPath("0.1").getElementByPath("0"); + expect(res).toBe(null); + + res = root.getElementByPath("0.1").getElementByPath("0.2.0"); + expect(res).toBe(null); + + res = root.getElementByPath("0.1").getElementByPath("0.1"); + expect(res).toBeDefined(); + }); + it("should have a getRoot function", () => { + const root = new EmberLib.Root(); + root.addChild(new EmberLib.Node(0)); + root.getElement(0).addChild(new EmberLib.Node(1)); + root.getElementByPath("0.1").addChild(new EmberLib.Node(1)); + const node = new EmberLib.Node(0); + root.getElementByPath("0.1.1").addChild(node); + let res = node.getRoot(); + expect(res).toBe(root); + }); + it("should have a getDirectory() and accept a callback for subscribers", () => { + const parameter = new EmberLib.Parameter(0); + parameter.contents = new EmberLib.ParameterContents(7, "integer"); + parameter.contents.streamIdentifier = 12345; + let res = parameter.getDirectory(0, () => {}); + expect(res).toBeDefined(); + expect(parameter._subscribers.size).toBe(1); + }); + it("should have a getDuplicate function", () => { + const parameter = new EmberLib.Parameter(0); + parameter.contents = new EmberLib.ParameterContents("test", "string"); + let res = parameter.getDuplicate(); + expect(res).toBeDefined(); + + const qp = new EmberLib.QualifiedParameter("0.1"); + qp.contents = parameter.contents; + res = qp.getDuplicate(); + expect(res).toBeDefined(); + }); + it("should decode continuation messages", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.CONTEXT(0)); + const qp = new EmberLib.QualifiedParameter("0.1"); + qp.encode(writer); + writer.endSequence(); + const res = EmberLib.rootDecode(new BER.Reader(writer.buffer)); + expect(res).toBeDefined(); + expect(res.getElementByPath("0.1")).toBeDefined(); + }); + it("should throw an error if not able to decode root", () => { + let writer = new BER.Writer(); + writer.startSequence(BER.CONTEXT(0)); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.rootDecode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(error){ + expect(error instanceof Errors.UnimplementedEmberTypeError); + } + + writer = new BER.Writer(); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + try { + EmberLib.rootDecode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(error){ + expect(error instanceof Errors.UnimplementedEmberTypeError); + } + + writer = new BER.Writer(); + writer.startSequence(BER.APPLICATION(0)); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.rootDecode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(error){ + expect(error instanceof Errors.UnimplementedEmberTypeError); + } + }); + }); + describe("Command", () => { + it("should throw error if unknown context found", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.APPLICATION(2)); + writer.startSequence(BER.CONTEXT(0)); + writer.writeInt(EmberLib.COMMAND_GETDIRECTORY); + writer.endSequence(); // BER.CONTEXT(0) + writer.startSequence(BER.CONTEXT(1)); + writer.writeInt(0); + writer.endSequence(); + writer.startSequence(BER.CONTEXT(3)); + writer.writeInt(0); + writer.endSequence(); + writer.endSequence(); // BER.APPLICATION(2) + try { + EmberLib.Command.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e).not.toBe("Should not succeed"); + } + }); + it("should have a toJSON", () => { + const command = new EmberLib.Command(EmberLib.COMMAND_GETDIRECTORY); + let jsonCommand = command.toJSON(); + expect(jsonCommand.number).toBe(EmberLib.COMMAND_GETDIRECTORY); + command.invocation = new EmberLib.Invocation(1, [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 1, "arg1") + ]); + jsonCommand = command.toJSON(); + expect(jsonCommand.invocation.arguments.length).toBe(1); + }); + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.Command.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Command.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should have a getElementByIdentifier", () => { + const node = new EmberLib.Node(0); + const identifier = "node_identifier"; + const description = "node_description"; + node.contents = new EmberLib.NodeContents(identifier, description); + const root = new EmberLib.Root(); + root.addElement(node); + let res = root.getElementByIdentifier(identifier); + expect(res).toBeDefined(); + expect(res.contents.identifier).toBe(identifier); + + res = root.getElementByIdentifier("unknown"); + expect(res).toBe(null); + }); + }); + describe("Node", () => { + it("should have an encoder", () => { + const node = new EmberLib.Node(0); + const identifier = "node_identifier"; + const description = "node_description"; + node.contents = new EmberLib.NodeContents(identifier, description); + node.contents.isRoot = true; + node.contents.isOnline = true; + node.contents.schemaIdentifiers = "schema1"; + const root = new EmberLib.Node(0); + root.addChild(node); + let writer = new BER.Writer(); + root.encode(writer); + expect(writer.buffer.size).not.toBe(0); + node.contents.isOnline = null; + node.contents.identifier = null; + writer = new BER.Writer(); + root.encode(writer); + expect(writer.buffer.size).not.toBe(0); + }); + it("should have a decoder", () => { + const node = new EmberLib.Node(0); + node.contents = new EmberLib.NodeContents(identifier, description); + node.contents.isRoot = true; + node.contents.isOnline = true; + node.contents.schemaIdentifiers = "schema1"; + const writer = new BER.Writer(); + node.encode(writer); + const n = EmberLib.Node.decode(new BER.Reader(writer.buffer)); + expect(n.number).toBe(node.number); + expect(n.contents.identifier).toBe(identifier); + expect(n.contents.description).toBe(description); + }); + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.Node.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Node.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should throw an error if unable to decode content", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.EMBER_SET); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.NodeContents.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + }); + describe("Function", () => { + let func; + beforeEach(() => { + func = new EmberLib.Function(0, args => { + const res = new EmberLib.FunctionArgument(); + res.type = EmberLib.ParameterType.integer; + res.value = args[0].value + args[1].value; + return [res]; + }); + }); + it("should be able to encode FunctionArgument with no name", () => { + const res = new EmberLib.FunctionArgument(); + res.type = EmberLib.ParameterType.integer; + const writer = new BER.Writer(); + res.encode(writer); + expect(writer.buffer.length > 0).toBeTruthy(); + }); + it("should throw an Error if encoding FunctionArgument with no type", () => { + const res = new EmberLib.FunctionArgument(); + const writer = new BER.Writer(); + try { + res.encode(writer); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidEmberNode); + } + }); + it("should throw an Error if unable to decode", () => { + const writer = new BER.Writer(); + try { + writer.startSequence(EmberLib.Function.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); // BER.CONTEXT(0) + writer.endSequence(); // BER.CONTEXT(0) + EmberLib.Function.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError); + } + }); + it("should return true when calling isFunction", () => { + expect(func.isFunction()).toBeTruthy(); + }); + it("should have an invoke function", () => { + const invoke = func.invoke(); + const children = invoke.getChildren(); + expect(children.length).toBe(1); + expect(children[0].isCommand()).toBeTruthy(); + }); + it("should have a encoder/decoder", () => { + func.contents = new EmberLib.FunctionContent(identifier, description); + func.contents.templateReference = "1.2.3"; + func.addChild(new EmberLib.Node(1)); + func.contents.arguments = [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer,null, "arg1"), + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer,null, "arg2") + ]; + func.contents.result = [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer,null, "result") + ]; + let writer = new BER.Writer(); + func.encode(writer); + let f = EmberLib.Function.decode(new BER.Reader(writer.buffer)); + expect(f.number).toBe(func.number); + expect(f.contents.identifier).toBe(identifier); + expect(f.contents.description).toBe(description); + expect(f.contents.result.length).toBe(1); + expect(f.contents.templateReference).toBe(func.contents.templateReference); - beforeAll(() => { - client = new S101Client(); + writer = new BER.Writer(); + func.contents.identifier = null; + func.contents.arguments = null; + func.contents.result = null; + func.encode(writer); + f = EmberLib.Function.decode(new BER.Reader(writer.buffer)); + expect(f.number).toBe(func.number); + expect(f.contents.identifier == null).toBeTruthy(); + expect(f.contents.result == null || f.contents.result.length == 0).toBeTruthy(); + }); + it("should throw an error if unable to decode result", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.EMBER_SET); + writer.startSequence(BER.CONTEXT(3)); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.FunctionContent.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should throw an error if unable to decode content", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.EMBER_SET); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.FunctionContent.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should throw an error if unable to decode FunctionArgument", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.FunctionArgument.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.FunctionArgument.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); }); + describe("Parameter", () => { + it("should through an error if decoding unknown parameter type", () => { + try { + ParameterTypefromBERTAG(99); + } + catch(e) { + expect(e instanceof Errors.InvalidBERFormat).toBeTruthy(); + } + try { + ParameterTypetoBERTAG(99); + } + catch(e) { + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); + it("should have an update function", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE = 1; + parameter.contents = new EmberLib.ParameterContents(VALUE, "integer"); + const newParameter = new EmberLib.Parameter(0); + const NEW_VALUE = VALUE + 1; + newParameter.contents = new EmberLib.ParameterContents(NEW_VALUE, "integer"); + parameter.update(newParameter); + expect(parameter.contents.value).toBe(NEW_VALUE); + }); + it("should have setValue function", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE = 1; + parameter.contents = new EmberLib.ParameterContents(VALUE, "integer"); + let NEW_VALUE = VALUE + 1; + let setVal = parameter.setValue(NEW_VALUE); + expect(setVal.contents.value).toBe(NEW_VALUE); + NEW_VALUE = NEW_VALUE + 1; + setVal = parameter.setValue(new EmberLib.ParameterContents(NEW_VALUE)); + expect(setVal.contents.value).toBe(NEW_VALUE); + }); + it("should have decoder function", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE = 1; + parameter.contents = new EmberLib.ParameterContents(VALUE, "integer"); + parameter.contents.minimum = 0; + parameter.contents.maximum = 100; + parameter.contents.access = EmberLib.ParameterAccess.readWrite; + parameter.contents.format = "db"; + parameter.contents.factor = 10; + parameter.contents.isOnline = true; + parameter.contents.formula = "x10"; + const STEP = 2; + parameter.contents.step = STEP; + const DEFAULT = 0; + parameter.contents.default = DEFAULT; + parameter.contents.type = EmberLib.ParameterType.integer; + parameter.contents.enumeration = "enumeration"; + parameter.contents.description = "description"; + parameter.contents.enumMap = new EmberLib.StringIntegerCollection(); + const KEY = "one"; + const KEY_VAL = 1; + parameter.contents.enumMap.addEntry(KEY, new EmberLib.StringIntegerPair(KEY, KEY_VAL)); + parameter.contents.streamDescriptor = new EmberLib.StreamDescription(); + parameter.contents.streamDescriptor.format = EmberLib.StreamFormat.signedInt8; + const OFFSET = 4; + parameter.contents.streamDescriptor.offset = OFFSET; - it("should parse S101 message without error", (done) => { - client.on("emberPacket", () => { - done(); + const SCHEMA = "schema"; + parameter.contents.schemaIdentifiers = SCHEMA; + const node = new EmberLib.Node(0); + parameter.addChild(node); + const writer = new BER.Writer(); + parameter.encode(writer); + const newParameter = EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); + expect(newParameter.getChildren().length).toBe(1); + expect(newParameter.contents.streamDescriptor.offset).toBe(OFFSET); + expect(newParameter.contents.step).toBe(STEP); + expect(newParameter.contents.default).toBe(DEFAULT); + expect(newParameter.contents.enumMap.get(KEY).value).toBe(KEY_VAL); + expect(newParameter.contents.schemaIdentifiers).toBe(SCHEMA); + }); + it("should support type real", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE = 1.1; + parameter.contents = new EmberLib.ParameterContents(VALUE, "real"); + const writer = new BER.Writer(); + parameter.encode(writer); + const newParameter = EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); + expect(newParameter.contents.value).toBe(VALUE); + }); + it("should support type string", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE ="string support"; + parameter.contents = new EmberLib.ParameterContents(VALUE, "string"); + const writer = new BER.Writer(); + parameter.encode(writer); + const newParameter = EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); + expect(newParameter.contents.value).toBe(VALUE); }); - client.on("error", e => { - console.log(e); - expect(e).toBeUndefined(); - done(); + it("should support type boolean", () => { + const parameter = new EmberLib.Parameter(0); + const VALUE = true; + parameter.contents = new EmberLib.ParameterContents(VALUE, "boolean"); + const writer = new BER.Writer(); + parameter.encode(writer); + const newParameter = EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); + expect(newParameter.contents.value).toBe(VALUE); + }); + it("should throw an error if fails to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.Parameter.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Parameter.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should throw an error if fails to decode StringIntegerPair", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.StringIntegerPair.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.StringIntegerPair.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should throw an error if fails to decode StringIntegerCollection", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.StringIntegerCollection.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.StringIntegerCollection.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should throw an error if fails to decode ParameterContents", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.EMBER_SET); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.ParameterContents.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } }); - client.codec.dataIn(s101Buffer); }); + describe("Matrix", () => { + describe("validateConnection", () => { + const PATH = "0.0.0"; + let matrixNode; + let qMatrixNode; + const TARGETCOUNT = 5; + const SOURCECOUNT = 5; + beforeEach(() => { + matrixNode = new EmberLib.MatrixNode(0); + qMatrixNode = new EmberLib.QualifiedMatrix(PATH); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.contents.targetCount = TARGETCOUNT; + matrixNode.contents.sourceCount = SOURCECOUNT; + qMatrixNode.contents = matrixNode.contents; + }); + it("should have encoder/decoder", () => { + matrixNode.addChild(new EmberLib.Node(0)); + let writer = new BER.Writer(); + matrixNode.encode(writer); + let newMatrixNode = EmberLib.MatrixNode.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.getChildren().length).toBe(1); + + writer = new BER.Writer(); + qMatrixNode.encode(writer); + newMatrixNode = EmberLib.QualifiedMatrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.path).toBe(PATH); + + matrixNode.contents.identifier = null; + matrixNode.contents.type = null; + matrixNode.contents.mode = null; + writer = new BER.Writer(); + matrixNode.encode(writer); + newMatrixNode = EmberLib.MatrixNode.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.contents.identifier == null).toBeTruthy(); + + writer = new BER.Writer(); + qMatrixNode.encode(writer); + newMatrixNode = EmberLib.QualifiedMatrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.contents.identifier == null).toBeTruthy(); + + matrixNode.contents = null; + writer = new BER.Writer(); + matrixNode.encode(writer); + newMatrixNode = EmberLib.MatrixNode.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.contents == null).toBeTruthy(); + + qMatrixNode.contents = null; + writer = new BER.Writer(); + qMatrixNode.encode(writer); + newMatrixNode = EmberLib.QualifiedMatrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.contents == null).toBeTruthy(); + }); + it("should throw an error if target is negative", () => { + try { + EmberLib.Matrix.validateConnection(matrixNode, -1, []); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should throw an error if source is negative", () => { + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [-1]); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should throw an error if target higher than max target", () => { + try { + EmberLib.Matrix.validateConnection(matrixNode, TARGETCOUNT, [0]); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should throw an error if target higher than max target", () => { + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [SOURCECOUNT]); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should throw an error if non-Linear Matrix without targets", () => { + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [0]); + throw new Error("Should not succeed"); + } + catch(e) { + matrixNode.contents.mode = EmberLib.MatrixMode.linear; + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); + it("should throw an error if non-Linear Matrix without sources", () => { + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + matrixNode.targets = [0, 3]; + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [0]); + throw new Error("Should not succeed"); + } + catch(e) { + matrixNode.contents.mode = EmberLib.MatrixMode.linear; + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); + it("should throw an error if non-Linear Matrix and not valid target", () => { + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + matrixNode.targets = [0, 3]; + matrixNode.sources = [0, 3]; + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + } + const min = matrixNode.getMinimal(true); + expect(min.sources).toBeDefined(); + try { + EmberLib.Matrix.validateConnection(matrixNode, 1, [0]); + throw new Error("Should not succeed"); + } + catch(e) { + matrixNode.contents.mode = EmberLib.MatrixMode.linear; + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should have getMinimal function", () => { + matrixNode.contents = null; + matrixNode.connections = null; + const min = matrixNode.getMinimal(true); + expect(min.number).toBe(matrixNode.getNumber()); + }); + it("should throw an error if non-Linear Matrix and not valid source", () => { + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + matrixNode.targets = [0, 3]; + matrixNode.sources = [0, 3]; + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [1]); + throw new Error("Should not succeed"); + } + catch(e) { + matrixNode.contents.mode = EmberLib.MatrixMode.linear; + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should not throw an error on valid non-linear connect", () => { + let error = null; + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + matrixNode.targets = [0, 3]; + matrixNode.sources = [0, 3]; + try { + EmberLib.Matrix.validateConnection(matrixNode, 0, [0]); + } + catch(e) { + error = e; + } + expect(error == null).toBeTruthy(); + }); + it("should not throw an error if can't decode MatrixContent", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.EMBER_SET); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.MatrixContents.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + }); + describe("MatrixUpdate", () => { + let matrixNode; + const TARGETCOUNT = 5; + const SOURCECOUNT = 5; + beforeEach(() => { + matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.contents.targetCount = TARGETCOUNT; + matrixNode.contents.sourceCount = SOURCECOUNT; + }); + it("should update connections", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; - it("should handle errors in message", () => { - var ber = new BER.Reader(errorBuffer); - expect(() => ember.Root.decode(ber)).toThrow(errors.UnimplementedEmberTypeError); - }) + const newMatrixNode = new EmberLib.MatrixNode(0); + newMatrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + newMatrixNode.contents.identifier = "matrix"; + newMatrixNode.contents.description = "matrix"; + matrixNode.connections[0].sources = [1]; + newMatrixNode.connections = { + 0: matrixNode.connections[0], + 1: new EmberLib.MatrixConnection(1) + }; + EmberLib.Matrix.MatrixUpdate(matrixNode, newMatrixNode); + expect(matrixNode.connections[1]).toBeDefined(); + matrixNode.connections = null; + EmberLib.Matrix.MatrixUpdate(matrixNode, newMatrixNode); + expect(matrixNode.connections[1]).toBeDefined(); + }); + it("should ignore empty connections request", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; + + const newMatrixNode = new EmberLib.MatrixNode(0); + newMatrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + newMatrixNode.contents.identifier = "matrix"; + newMatrixNode.contents.description = "matrix"; + newMatrixNode.connections = null; + EmberLib.Matrix.MatrixUpdate(matrixNode, newMatrixNode); + expect(matrixNode.connections[0]).toBeDefined(); + }); + it("should throw error if invalid target inside new connections", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; + + const newMatrixNode = new EmberLib.MatrixNode(0); + newMatrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + newMatrixNode.contents.identifier = "matrix"; + newMatrixNode.contents.description = "matrix"; + newMatrixNode.connections = { + 7: new EmberLib.MatrixConnection(7) + }; + try { + EmberLib.Matrix.MatrixUpdate(matrixNode, newMatrixNode); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + it("should not throw an error on valid non-linear connect", () => { + let error = null; + const newMatrixNode = new EmberLib.MatrixNode(0); + newMatrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + newMatrixNode.targets = [0, 3]; + newMatrixNode.sources = [0, 3]; + newMatrixNode.contents.identifier = "matrix"; + newMatrixNode.contents.description = "matrix"; + + + matrixNode.contents.mode = EmberLib.MatrixMode.nonLinear; + matrixNode.connections = null; + try { + EmberLib.Matrix.MatrixUpdate(matrixNode, newMatrixNode); + } + catch(e) { + error = e; + } + expect(error == null).toBeTruthy(); + expect(matrixNode.targets).toBeDefined(); + expect(matrixNode.targets.length).toBe(newMatrixNode.targets.length); + expect(matrixNode.sources.length).toBe(newMatrixNode.sources.length); + }); + }); + describe("disconnectSources", () => { + let matrixNode; + const TARGETCOUNT = 5; + const SOURCECOUNT = 5; + beforeEach(() => { + matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.contents.targetCount = TARGETCOUNT; + matrixNode.contents.sourceCount = SOURCECOUNT; + }); + it("should generate the connection structure if not existent", () => { + EmberLib.Matrix.disconnectSources(matrixNode, 0, [1]); + }); + it("should disconnect existing connection", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; + EmberLib.Matrix.connectSources(matrixNode, 0, [1]); + EmberLib.Matrix.connectSources(matrixNode, 1, [1]); + expect(matrixNode._numConnections).toBe(2); + EmberLib.Matrix.disconnectSources(matrixNode, 0, [1]); + expect(matrixNode.connections[0]).toBeDefined(); + expect(matrixNode.connections[0].sources.length).toBe(0); + expect(matrixNode._numConnections).toBe(1); + }); + it("should ignore disconnect with no source", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; + EmberLib.Matrix.connectSources(matrixNode, 0, [1]); + expect(matrixNode._numConnections).toBe(1); + EmberLib.Matrix.disconnectSources(matrixNode, 0, null); + expect(matrixNode.connections[0]).toBeDefined(); + expect(matrixNode.connections[0].sources.length).toBe(1); + }); + it("should ignore disconnect with not connected source", () => { + matrixNode.connections = { + 0: new EmberLib.MatrixConnection(0) + }; + EmberLib.Matrix.connectSources(matrixNode, 0, [1]); + EmberLib.Matrix.connectSources(matrixNode, 1, [0]); + expect(matrixNode._numConnections).toBe(2); + EmberLib.Matrix.disconnectSources(matrixNode, 0, [0]); + expect(matrixNode.connections[0]).toBeDefined(); + expect(matrixNode.connections[0].sources.length).toBe(1); + expect(matrixNode._numConnections).toBe(2); + }); + }); + describe("decodeConnections", () => { + let matrixNode; + const TARGETCOUNT = 5; + const SOURCECOUNT = 5; + beforeEach(() => { + matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.contents.targetCount = TARGETCOUNT; + matrixNode.contents.sourceCount = SOURCECOUNT; + }); + it("should generate the connection structure if not existent", () => { + const SOURCEID = 0; + EmberLib.Matrix.connectSources(matrixNode, 0, [SOURCEID]); + const writer = new BER.Writer(); + matrixNode.encodeConnections(writer); + const ber = new BER.Reader(writer.buffer); + const seq = ber.getSequence(BER.CONTEXT(5)); + const connections = EmberLib.Matrix.decodeConnections(seq); + expect(connections[0].sources).toBeDefined(); + expect(connections[0].sources.length).toBe(1); + expect(connections[0].sources[0]).toBe(SOURCEID); + }); + }); + describe("encodeConnections", () => { + it ("should ignore empty/null connections", () => { + const matrixNode = new EmberLib.MatrixNode(0); + matrixNode.connections = null; + const writer = new BER.Writer(); + matrixNode.encodeConnections(writer); + expect(writer.buffer.length).toBe(0); + }); + }); + describe("canConnect", () => { + let matrixNode; + const TARGETCOUNT = 5; + const SOURCECOUNT = 5; + beforeEach(() => { + matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.contents.targetCount = TARGETCOUNT; + matrixNode.contents.sourceCount = SOURCECOUNT; + }); + it("should consider default type as 1toN", () => { + matrixNode.connections = null; + matrixNode.contents.type = null; + matrixNode.contents.maximumTotalConnects = 1; + const res = EmberLib.Matrix.canConnect(matrixNode, 0, [0,3]); + expect(res).toBeFalsy(); + }); + it("should return false if more than 1 source in 1toN", () => { + matrixNode.connections = null; + matrixNode.contents.maximumTotalConnects = 1; + const res = EmberLib.Matrix.canConnect(matrixNode, 0, [0,3]); + expect(res).toBeFalsy(); + }); + it("should always return true if NtoN and no limits", () => { + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.nToN, + EmberLib.MatrixMode.linear + ); + matrixNode.connections = null; + matrixNode.contents.maximumConnectsPerTarget = null; + matrixNode.contents.maximumTotalConnects = null; + const res = EmberLib.Matrix.canConnect(matrixNode, 0, [0,3]); + expect(res).toBeTruthy(); + }); + it("should check maximumTotalConnects in NtoN and reject on limit pass", () => { + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.nToN, + EmberLib.MatrixMode.linear + ); + matrixNode.contents.maximumConnectsPerTarget = null; + matrixNode.contents.maximumTotalConnects = 2; + EmberLib.Matrix.connectSources(matrixNode, 0, [1,2]); + const res = EmberLib.Matrix.canConnect(matrixNode, 1, [3]); + expect(res).toBeFalsy(); + }); + it("should check maximumTotalConnects in NtoN and accept if below limit", () => { + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.nToN, + EmberLib.MatrixMode.linear + ); + matrixNode.connections = null; + matrixNode.contents.maximumConnectsPerTarget = null; + matrixNode.contents.maximumTotalConnects = 2; + const res = EmberLib.Matrix.canConnect(matrixNode, 1, [3]); + expect(res).toBeTruthy(); + }); + it("should check locked connection", () => { + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.nToN, + EmberLib.MatrixMode.linear + ); + matrixNode.connections = null; + matrixNode.contents.maximumConnectsPerTarget = null; + matrixNode.contents.maximumTotalConnects = 2; + EmberLib.Matrix.connectSources(matrixNode, 0, [1]); + matrixNode.connections[0].lock(); + let res = EmberLib.Matrix.canConnect(matrixNode, 0, [3]); + expect(res).toBeFalsy(); + matrixNode.connections[0].unlock(); + res = EmberLib.Matrix.canConnect(matrixNode, 0, [3]); + expect(res).toBeTruthy(); + }); + }); + describe("Matrix Non-Linear", () => { + it("should have encoder / decoder", () => { + const PATH = "0.1.2"; + const matrixNode = new EmberLib.MatrixNode(0); + const qMatrixNode = new EmberLib.QualifiedMatrix(PATH); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + matrixNode.contents.gainParameterNumber = 4; + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.contents.maximumTotalConnects = 5; + matrixNode.contents.maximumConnectsPerTarget = 1; + matrixNode.contents.parametersLocation = "1.2.3"; + matrixNode.contents.schemaIdentifiers = "de.l-s-b.emberplus.schema1"; + matrixNode.contents.templateReference = "0.1.2.3"; + qMatrixNode.contents = matrixNode.contents; + matrixNode.targets = [0,3]; + qMatrixNode.targets = matrixNode.targets; + matrixNode.sources = [1,2]; + qMatrixNode.sources = matrixNode.sources; + let writer = new BER.Writer(); + matrixNode.encode(writer); + let newMatrixNode = EmberLib.Matrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.targets).toBeDefined(); + + writer = new BER.Writer(); + qMatrixNode.encode(writer); + newMatrixNode = EmberLib.QualifiedMatrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.targets).toBeDefined(); + + + // Should support int + matrixNode.contents.parametersLocation = 123; + writer = new BER.Writer(); + matrixNode.encode(writer); + newMatrixNode = EmberLib.Matrix.decode(new BER.Reader(writer.buffer)); + expect(newMatrixNode.targets).toBeDefined(); + expect(newMatrixNode.contents.parametersLocation).toBe(matrixNode.contents.parametersLocation); + }); + it("should have connect function", () => { + const root = new EmberLib.Root(); + const matrixNode = new EmberLib.MatrixNode(0); + matrixNode.contents = new EmberLib.MatrixContents( + EmberLib.MatrixType.onetoN, + EmberLib.MatrixMode.nonLinear + ); + matrixNode.contents.identifier = "matrix"; + matrixNode.contents.description = "matrix"; + matrixNode.targets = [0,3]; + matrixNode.sources = [1,2]; + root.addChild(matrixNode); + const connect = matrixNode.connect({0: new EmberLib.MatrixConnection(0)}); + expect(connect).toBeDefined(); + }); + it("should throw an error if can't decode", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.APPLICATION(13)); + writer.startSequence(BER.CONTEXT(0)); + writer.writeInt(1); + writer.endSequence(); // BER.CONTEXT(0) + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.MatrixNode.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + }); + describe("Label", () => { + it ("should throw an error if it fails to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.APPLICATION(18)); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Label.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it ("should throw an error if no basePath", () => { + const label = new EmberLib.Label(null, "test"); + const writer = new BER.Writer(); + try { + label.encode(writer); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); + it ("should throw an error if no description", () => { + const label = new EmberLib.Label("1.2.3", null); + const writer = new BER.Writer(); + try { + label.encode(writer); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); + it ("should be able to encode/decode a valid label", () => { + const label = new EmberLib.Label("1.2.3", "primary"); + const writer = new BER.Writer(); + label.encode(writer); + const reader = new BER.Reader(writer.buffer); + const newLabel = EmberLib.Label.decode(reader); + expect(newLabel.description).toBe(label.description); + expect(newLabel.basePath).toBe(label.basePath); + }); + }) + }); + describe("Invocation", () => { + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.Invocation.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Invocation.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("Should be able to encode even if no id", () => { + const invocation = new EmberLib.Invocation(); + const writer = new BER.Writer(); + invocation.encode(writer); + const i = EmberLib.Invocation.decode(new BER.Reader(writer.buffer)); + expect(i.id == null).toBeTruthy(); + }); + it("Should have a toJSON", () => { + const invocation = new EmberLib.Invocation(1, [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 10, "arg1" ) + ]); + let js = invocation.toJSON(); + expect(js.id).toBe(invocation.id); + expect(js.arguments.length).toBe(invocation.arguments.length); + invocation.arguments = null; + js = invocation.toJSON(); + expect(js.id).toBe(invocation.id); + expect(js.arguments).toBe(null); + }); + }); + describe("InvocationResult", () => { + it("should support all types of result", () => { + const invocationResult = new EmberLib.InvocationResult(); + invocationResult.invocationId = 100; + const valBuf = [0xa, 0x1, 0x2]; + invocationResult.setFailure(); + expect(invocationResult.success).toBeFalsy(); + invocationResult.setSuccess(); + expect(invocationResult.success).toBeTruthy(); + try { + invocationResult.setResult(new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 1)); + throw new Error("should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidResultFormat).toBeTruthy(); + } + expect(invocationResult.toQualified()).toBe(invocationResult); + invocationResult.setResult([ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 1), + new EmberLib.FunctionArgument(EmberLib.ParameterType.real, 1.1), + new EmberLib.FunctionArgument(EmberLib.ParameterType.string, "one"), + new EmberLib.FunctionArgument(EmberLib.ParameterType.boolean, false), + new EmberLib.FunctionArgument(EmberLib.ParameterType.octets, Buffer.from(valBuf)) + ]); + const writer = new BER.Writer(); + invocationResult.encode(writer); + const newInvocationRes = EmberLib.InvocationResult.decode(new BER.Reader(writer.buffer)); + expect(newInvocationRes.success).toBe(invocationResult.success); + expect(newInvocationRes.invocationId).toBe(invocationResult.invocationId); + expect(newInvocationRes.result.length).toBe(invocationResult.result.length); + expect(newInvocationRes.result[4].value.length).toBe(valBuf.length); + expect(newInvocationRes.result[4].value[0]).toBe(valBuf[0]); + }); + it("should be able to encode with not result, no success", () => { + const invocationResult = new EmberLib.InvocationResult(); + invocationResult.invocationId = 100; + invocationResult.result = null; + invocationResult.sucess = null; + const writer = new BER.Writer(); + invocationResult.encode(writer); + const newInvocationRes = EmberLib.InvocationResult.decode(new BER.Reader(writer.buffer)); + expect(newInvocationRes.result).not.toBeDefined(); + }); + it("should be able to encode with no invocationId", () => { + const invocationResult = new EmberLib.InvocationResult(); + invocationResult.invocationId = null; + invocationResult.result = null; + invocationResult.sucess = null; + const writer = new BER.Writer(); + invocationResult.encode(writer); + const newInvocationRes = EmberLib.InvocationResult.decode(new BER.Reader(writer.buffer)); + expect(newInvocationRes.invocationId == null).toBeTruthy(); + }); + it("should throw an error if can't decode", () => { + let writer = new BER.Writer(); + writer.startSequence(EmberLib.InvocationResult.BERID); + writer.startSequence(BER.CONTEXT(3)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.InvocationResult.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + writer = new BER.Writer(); + writer.startSequence(EmberLib.InvocationResult.BERID); + writer.startSequence(BER.CONTEXT(2)); + writer.startSequence(BER.EMBER_SEQUENCE); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.InvocationResult.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + }); + describe("MatrixConnection", () => { + it("should have a decoder and throw error if can't decode", () => { + let writer = new BER.Writer(); + writer.startSequence(EmberLib.MatrixConnection.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.MatrixConnection.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should decode connection with no source", () => { + const matrixConnection = new EmberLib.MatrixConnection(0); + matrixConnection.sources = []; + const writer = new BER.Writer(); + matrixConnection.encode(writer); + const newMC = EmberLib.MatrixConnection.decode(new BER.Reader(writer.buffer)); + expect(newMC.sources).toBeDefined(); + expect(newMC.sources.length).toBe(0); + }); + it("should throw an error if invalid target", () => { + try { + new EmberLib.MatrixConnection("zero"); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidMatrixSignal).toBeTruthy(); + } + }); + }); + describe("QualifiedFunction", () => { + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.QualifiedFunction.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.QualifiedFunction.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + }); + describe("QualifiedMatrix", () => { + const PATH = "1.2.3"; + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.QualifiedMatrix.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.QualifiedMatrix.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("Should have a subscribe/unsubscribe function", () => { + const qMatrixNode = new EmberLib.QualifiedMatrix(PATH); + qMatrixNode.contents = new EmberLib.MatrixContents(); + const cb = function() {}; + let cmd = qMatrixNode.subscribe(cb); + expect(cmd).toBeDefined(); + expect(cmd instanceof EmberLib.Root).toBeTruthy(); + }); + }); + describe("QualifiedNode", () => { + const PATH = "0.1.2"; + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.QualifiedNode.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.QualifiedNode.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("Should return true to isNode() call", () => { + const qNode = new EmberLib.QualifiedNode(PATH); + expect(qNode.isNode()).toBeTruthy(); + }); + }); + describe("QualifiedParameter", () => { + const PATH = "0.1.2"; + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.QualifiedParameter.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.QualifiedParameter.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should update and ignore key starting with _", () => { + const NEW_VAL = 15; + const qp = new EmberLib.QualifiedParameter(PATH); + qp.contents = new EmberLib.ParameterContents(5, "integer"); + const dup = new EmberLib.QualifiedParameter(PATH); + dup.contents = new EmberLib.ParameterContents(NEW_VAL, "integer"); + dup.contents["_ignore"] = "test"; + qp.update(dup); + expect(qp.contents._ignore).not.toBeDefined(); + expect(qp.contents.value).toBe(NEW_VAL); + }); + it("Should return true to isParameter() call", () => { + const qNode = new EmberLib.QualifiedParameter(PATH); + expect(qNode.isParameter()).toBeTruthy(); + }); + it("should have setValue function", () => { + const qp = new EmberLib.QualifiedParameter(PATH); + const VALUE = 1; + qp.contents = new EmberLib.ParameterContents(VALUE, "integer"); + let NEW_VALUE = VALUE + 1; + let setVal = qp.setValue(NEW_VALUE); + let dup = setVal.getElementByPath(PATH); + expect(dup).toBeDefined(); + expect(dup.contents.value).toBe(NEW_VALUE); + NEW_VALUE = NEW_VALUE + 1; + setVal = qp.setValue(new EmberLib.ParameterContents(NEW_VALUE)); + expect(setVal.getElementByPath(PATH).contents.value).toBe(NEW_VALUE); + }); + it("should accept subscribers and have a function to update them", () => { + const qp = new EmberLib.QualifiedParameter(PATH); + const VALUE = 1; + qp.contents = new EmberLib.ParameterContents(VALUE, "integer"); + qp.contents.streamIdentifier = 12345; + let updatedValue = null; + const handleUpdate = function(param) { + updatedValue = param.contents.value; + } + qp.subscribe(handleUpdate); + qp.updateSubscribers(); + expect(updatedValue).toBe(VALUE); + }); + }); + describe("StreamDescription", () => { + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.StreamDescription.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.StreamDescription.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("Should have a toJSON", () => { + const streamDescriptor = new EmberLib.StreamDescription(); + streamDescriptor.format = EmberLib.StreamFormat.signedInt8; + const OFFSET = 4; + streamDescriptor.offset = OFFSET; + + let js = streamDescriptor.toJSON(); + expect(js).toBeDefined(); + expect(js.format).toBeDefined(); + expect(js.offset).toBe(OFFSET); + + streamDescriptor.format = null; + js = streamDescriptor.toJSON(); + expect(js).toBeDefined(); + expect(js.format).toBe(null); + expect(js.offset).toBe(OFFSET); + }); + }); + describe("StringIntegerCollection", () => { + it("should reject invalid value", () => { + const sic = new EmberLib.StringIntegerCollection(); + try { + sic.addEntry("test", 4); + } + catch(e) { + expect(e instanceof Errors.InvalidStringPair).toBeTruthy(); + } + }); + it("should have a toJSON", () => { + const KEY = "test"; + const VAL = 4; + const sic = new EmberLib.StringIntegerCollection(); + sic.addEntry("test", new EmberLib.StringIntegerPair(KEY, VAL)); + const js = sic.toJSON(); + expect(js).toBeDefined(); + expect(js.length).toBe(1); + }); + }); + describe("StringIntegerPair", () => { + it("should throw an error if trying to encode invalid key/value", () => { + const sp = new EmberLib.StringIntegerPair(); + const writer = new BER.Writer(); + try { + sp.encode(writer); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.InvalidEmberNode).toBeTruthy(); + } + }); + }); + describe("rootDecode", () => { + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + try { + EmberLib.rootDecode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + }); + describe("QualifiedTemplate", () => { + const PATH = "0.1.2"; + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.QualifiedTemplate.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.QualifiedTemplate.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should have encoder/decoder", () => { + const qp = new EmberLib.QualifiedTemplate(PATH, new EmberLib.Node(0)); + let writer = new BER.Writer(); + qp.encode(writer); + let dup = EmberLib.QualifiedTemplate.decode(new BER.Reader(writer.buffer)); + expect(dup).toBeDefined(); + expect(dup.getPath()).toBe(PATH); + expect(dup.element instanceof EmberLib.Node).toBeTruthy(); + + const DESCRIPTION = "description"; + qp.description = DESCRIPTION; + writer = new BER.Writer(); + qp.encode(writer); + dup = EmberLib.QualifiedTemplate.decode(new BER.Reader(writer.buffer)); + expect(dup).toBeDefined(); + expect(dup.getPath()).toBe(PATH); + expect(dup.element instanceof EmberLib.Node).toBeTruthy(); + expect(dup.description).toBe(DESCRIPTION); + }); + + it("Should return true to isTemplate() call", () => { + const qp = new EmberLib.QualifiedTemplate(PATH, new EmberLib.Node(0)); + expect(qp.isTemplate()).toBeTruthy(); + }); + }); + describe("Template", () => { + it("Should throw an error if unable to decode", () => { + const writer = new BER.Writer(); + writer.startSequence(EmberLib.Template.BERID); + writer.startSequence(BER.CONTEXT(99)); + writer.endSequence(); + writer.endSequence(); + try { + EmberLib.Template.decode(new BER.Reader(writer.buffer)); + throw new Error("Should not succeed"); + } + catch(e) { + expect(e instanceof Errors.UnimplementedEmberTypeError).toBeTruthy(); + } + }); + it("should have encoder/decoder", () => { + const qp = new EmberLib.Template(10, new EmberLib.Node(0)); + let writer = new BER.Writer(); + qp.encode(writer); + let dup = EmberLib.Template.decode(new BER.Reader(writer.buffer)); + expect(dup).toBeDefined(); + expect(dup.getNumber()).toBe(10); + + const DESCRIPTION = "description"; + qp.description = DESCRIPTION; + writer = new BER.Writer(); + qp.encode(writer); + dup = EmberLib.Template.decode(new BER.Reader(writer.buffer)); + expect(dup).toBeDefined(); + expect(dup.element instanceof EmberLib.Node).toBeTruthy(); + expect(dup.description).toBe(DESCRIPTION); + + writer = new BER.Writer(); + qp.element = new EmberLib.Function(0, null); + qp.encode(writer); + dup = EmberLib.Template.decode(new BER.Reader(writer.buffer)); + expect(dup.element instanceof EmberLib.Function).toBeTruthy(); + + writer = new BER.Writer(); + qp.element = new EmberLib.Parameter(0); + qp.encode(writer); + dup = EmberLib.Template.decode(new BER.Reader(writer.buffer)); + expect(dup.element instanceof EmberLib.Parameter).toBeTruthy(); + + writer = new BER.Writer(); + qp.element = new EmberLib.MatrixNode(0); + qp.encode(writer); + dup = EmberLib.Template.decode(new BER.Reader(writer.buffer)); + expect(dup.element instanceof EmberLib.MatrixNode).toBeTruthy(); + + }); + + it("Should return true to isTemplate() call", () => { + const qp = new EmberLib.Template(10, new EmberLib.Node(0)); + expect(qp.isTemplate()).toBeTruthy(); + }); + + it("Should have toQualified function", () => { + const template = new EmberLib.Template(10, new EmberLib.Node(0)); + const qp = template.toQualified(); + expect(qp.isTemplate()).toBeTruthy(); + }); + + it("Should have update function", () => { + const template = new EmberLib.Template(10, new EmberLib.Node(0)); + const DUP_NUM = 5; + const dup = new EmberLib.Template(10, new EmberLib.Node(DUP_NUM)); + template.update(dup); + expect(template.element.getNumber()).toBe(DUP_NUM); + }); + }); }); diff --git a/test/EmberClient.test.js b/test/EmberClient.test.js new file mode 100755 index 0000000..66f66cd --- /dev/null +++ b/test/EmberClient.test.js @@ -0,0 +1,66 @@ +const fs = require("fs"); +const {EmberServer} = require("../EmberServer"); +const Decoder = require('../EmberLib').DecodeBuffer; +const EmberClient = require("../EmberClient"); + +const HOST = "127.0.0.1"; +const PORT = 9010; + +function getRoot() { + return new Promise((resolve, reject) => { + fs.readFile("test/embrionix.ember", (e, data) => { + if (e) { + reject(e); + } + try { + resolve(Decoder(data)); + } + catch(error) { + reject(error); + } + }); + }); +} + +let server; +describe("EmberClient", () => { + + beforeEach(() => { + return getRoot() + .then(root => { + server = new EmberServer(HOST, PORT, root); + //server._debug = true; + server.on("error", e => { + console.log("Server Error", e); + }); + server.on("clientError", info => { + console.log("clientError", info.error); + }); + server.on("event", event => { + console.log("Event: " + event); + }); + return server.listen() + }); + }); + + afterEach(() => { + return server.close(); + }); + + it("should be able to fetch a specific node", () => { + const client = new EmberClient(HOST, PORT); + const PATH = "0.1"; + client.on("error", () => { + // ignore + }); + return Promise.resolve() + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => client.getElementByPath(PATH)) + .then(node => { + expect(node).toBeDefined(); + expect(node.getPath()).toBe(PATH); + return client.disconnect(); + }); + }); +}); diff --git a/test/Server.test.js b/test/Server.test.js index ed44416..0ec6105 100755 --- a/test/Server.test.js +++ b/test/Server.test.js @@ -1,211 +1,1051 @@ const expect = require("expect"); -const TreeServer = require("../server"); -const DeviceTree = require("../").DeviceTree; -const ember = require("../ember"); +const {EmberServer, ServerEvents} = require("../EmberServer"); +const EmberClient = require("../EmberClient"); +const EmberLib = require("../EmberLib"); const {jsonRoot} = require("./utils"); +const MatrixHandlers = require("../EmberServer/MatrixHandlers"); +const Errors = require("../Errors"); const LOCALHOST = "127.0.0.1"; -const PORT = 9009; - -const wait = function(t) { - return new Promise(resolve => { - setTimeout(resolve, t); - }); -} +let PORT = 9009; describe("server", function() { - describe("JSONtoTree", function() { let jsonTree; - beforeAll(function() { + beforeEach(function() { jsonTree = jsonRoot(); }); it("should generate an ember tree from json", function() { - const root = TreeServer.JSONtoTree(jsonTree); + const root = EmberServer.JSONtoTree(jsonTree); + // JSONtoTree will modify the json object. + jsonTree = jsonRoot(); expect(root).toBeDefined(); expect(root.elements).toBeDefined(); - expect(root.elements.length).toBe(1); - console.log("root", root.elements[0].contents); - expect(root.elements[0].contents.identifier).toBe("scoreMaster"); - expect(root.elements[0].children.length).toBe(2); + expect(root.elements.size).toBe(jsonTree.length); + expect(root.getElementByNumber(0).contents.identifier).toBe("scoreMaster"); + expect(root.getElementByNumber(0).elements.size).toBe(jsonTree[0].children.length); + expect(root.getElementByNumber(1).contents.streamDescriptor instanceof EmberLib.StreamDescription).toBeTruthy(); + expect(root.getElementByNumber(1).contents.streamDescriptor.offset).toBe(jsonTree[1].streamDescriptor.offset); + }); + it("should throw an error if invalid matrix mode", function() { + jsonTree[0].children[1].children[0].mode = "invalid"; + let error; + try { + const root = EmberServer.JSONtoTree(jsonTree); + } + catch(e) { + error = e; + } + expect(error).toBeDefined(); + expect(error instanceof Errors.InvalidEmberNode).toBeDefined(); + }); + it("should support matrix type nToN nonLinear", function() { + jsonTree[0].children[1].children[0].type = "nToN"; + jsonTree[0].children[1].children[0].mode = "nonLinear"; + jsonTree[0].children[1].children[0].maximumConnectsPerTarget = 10; + jsonTree[0].children[1].children[0].maximumTotalConnects = 100; + const root = EmberServer.JSONtoTree(jsonTree); + const matrix = root.getElementByPath("0.1.0"); + expect(matrix).toBeDefined(); + expect(matrix.contents.maximumConnectsPerTarget).toBe(jsonTree[0].children[1].children[0].maximumConnectsPerTarget); + expect(matrix.contents.maximumTotalConnects).toBe(jsonTree[0].children[1].children[0].maximumTotalConnects); + expect(matrix.contents.type).toBe(EmberLib.MatrixType.nToN); + const jMatrix = matrix.toJSON(); + expect(jMatrix.type).toBeDefined(); + expect(jMatrix.mode).toBeDefined(); + }); + it("should support matrix type oneToOne", function() { + jsonTree[0].children[1].children[0].type = "oneToOne"; + const root = EmberServer.JSONtoTree(jsonTree); + const matrix = root.getElementByPath("0.1.0"); + expect(matrix).toBeDefined(); + expect(matrix.contents.type).toBe(EmberLib.MatrixType.oneToOne); + }); + it("should throw an error if invalid matrix type", function() { + jsonTree[0].children[1].children[0].type = "invalid"; + let error; + try { + const root = EmberServer.JSONtoTree(jsonTree); + } + catch(e) { + error = e; + } + expect(error).toBeDefined(); + expect(error instanceof Errors.InvalidEmberNode).toBeDefined(); + }); + it("should generate a matrix with a valid toJSON", function() { + const root = EmberServer.JSONtoTree(jsonTree); + const matrix = root.getElementByPath("0.1.0"); + matrix.connectSources(0, [0]); + matrix.connectSources(1, [1]); + const jMatrix = matrix.toJSON(); + expect(jMatrix.type).toBeDefined(); + expect(jMatrix.type).toBe(matrix.contents.type.value); + expect(jMatrix.mode).toBeDefined(); + expect(jMatrix.mode).toBe(matrix.contents.mode.value); + expect(jMatrix.connections[0].sources.length).toBe(1); + expect(jMatrix.connections[0].sources[0]).toBe(0); }); }); describe("Server - Client communication", function() { - let server,client; - beforeAll(function() { + let server,client,jsonTree; + beforeEach(() => { jsonTree = jsonRoot(); - const root = TreeServer.JSONtoTree(jsonTree); - server = new TreeServer(LOCALHOST, PORT, root); - //server._debug = true; - return server.listen().then(() => { - console.log("server listening"); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + server.on("error", e => { + // ignore + }); + server.on("clientError", e => { + // ignore }); + //server._debug = true; + return server.listen(); + }); + afterEach(() => { + return server.close(); }); - afterAll(function() { - client.disconnect(); - server.close(); - }) - it("should receive and decode the full tree", function () { - client = new DeviceTree(LOCALHOST, PORT); + it("should receive and decode the full tree", () => { + client = new EmberClient(LOCALHOST, PORT); //client._debug = true; return Promise.resolve() .then(() => client.connect()) - .then(() => { - console.log("client connected"); - return client.getDirectory(); - }) + .then(() => client.getDirectory()) .then(() => { expect(client.root).toBeDefined(); expect(client.root.elements).toBeDefined(); - expect(client.root.elements.length).toBe(1); - expect(client.root.elements[0].contents.identifier).toBe("scoreMaster"); - return client.getDirectory(client.root.elements[0]); + expect(client.root.elements.size).toBe(jsonTree.length); + expect(client.root.getElementByNumber(0).contents.identifier).toBe("scoreMaster"); + return client.getDirectory(client.root.getElementByNumber(0)); }) .then(() => { - expect(client.root.elements[0].children.length).toBe(2); - return client.getDirectory(client.root.elements[0].children[0]); + expect(client.root.getElementByNumber(0).elements.size).toBe(jsonTree[0].children.length); + return client.getDirectory(client.root.getElementByPath("0.0")); }) .then(() => { - expect(client.root.elements[0].children[0].children.length).toBe(4); - expect(client.root.elements[0].children[0].children[3].contents.identifier).toBe("author"); - // Issue #33 TreeServer.handleGetDirectory does not subscribe to child parameters + expect(client.root.getElementByPath("0.0").elements.size).toBe(4); + expect(client.root.getElementByPath("0.0.3").contents.identifier).toBe("author"); + // Issue #33 EmberServer.handleGetDirectory does not subscribe to child parameters expect(server.subscribers["0.0.0"]).toBeDefined(); - // Keepalive - return client.disconnect(); + return client.disconnect(); }); }); - it("should be able to modify a parameter", () => { - client = new DeviceTree(LOCALHOST, PORT); + it("should be able to modify a parameter", async () => { + client = new EmberClient(LOCALHOST, PORT); + await client.connect() + await client.getDirectory(); + await client.getElementByPath("0.0.1"); + expect(server.tree.getElementByPath("0.0.1").contents.value).not.toBe("gdnet"); + await client.setValue(client.root.getElementByPath("0.0.1"), "gdnet"); + expect(server.tree.getElementByPath("0.0.1").contents.value).toBe("gdnet"); + console.log("result", server.tree.getElementByPath("0.0.1").contents.value) + return client.disconnect().then(() => { console.log("disconnected")}); + }); + + it("should be able to call a function with parameters", () => { + client = new EmberClient(LOCALHOST, PORT); //client._debug = true; - return Promise.resolve() - .then(() => client.connect()) + return client.connect() + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("0.2")) .then(() => { - return client.getDirectory(); + const func = client.root.getElementByPath("0.2"); + return client.invokeFunction(func, [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 1), + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 7) + ]); }) - .then(() => client.expand(client.root.elements[0])) + .then(result => { + expect(result).toBeDefined(); + expect(result.result).toBeDefined(); + expect(result.result.length).toBe(1); + expect(result.result[0].value).toBe(8); + return client.disconnect(); + }); + }); + + it("should be able to get child with client.getElement", function() { + client = new EmberClient(LOCALHOST, PORT); + return Promise.resolve() + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("scoreMaster/identity/product")) + .then(() => client.getElementByPath("scoreMaster/router/labels/group 1")) + .then(() => client.disconnect()); + }); + it("should be able to get child with getElementByPath", function() { + client = new EmberClient(LOCALHOST, PORT); + return Promise.resolve() + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("scoreMaster/identity/product")) + .then(() => client.getElementByPath("scoreMaster/router/labels/group 1")) + .then(() => client.disconnect()); + }); + it("should throw an error if getElementByPath for unknown path", function() { + client = new EmberClient(LOCALHOST, PORT); + return Promise.resolve() + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("scoreMaster/router/labels/group")) .then(() => { - expect(server.tree.elements[0].children[0].children[1].contents.value).not.toBe("gdnet"); - return client.setValue(client.root.elements[0].children[0].children[1], "gdnet"); + throw new Error("Should not succeed"); }) - .then(() => { - expect(server.tree.elements[0].children[0].children[1].contents.value).toBe("gdnet"); - return client.disconnect(); + .catch(e => { + expect(e.message).toMatch(/Failed path discovery/); + return client.disconnect(); }); }); it("should be able to make a matrix connection", () => { - client = new DeviceTree(LOCALHOST, PORT); - //client._debug = true; + client = new EmberClient(LOCALHOST, PORT); + return Promise.resolve() + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("0.1.0")) + .then(matrix => client.matrixConnect(matrix, 0, [1])) + .then(matrix => client.getElementByPath(matrix.getPath())) + .then(matrix => { + expect(matrix.connections['0'].sources).toBeDefined(); + expect(matrix.connections['0'].sources.length).toBe(1); + expect(matrix.connections['0'].sources[0]).toBe(1); + }) + .then(() => client.disconnect()); + }); + it("should generate events on command and matrix connection", () => { + client = new EmberClient(LOCALHOST, PORT); + let count = 0; + let receivedEvent = null; + const eventHandler = event => { + count++; + receivedEvent = event; + } return Promise.resolve() .then(() => client.connect()) .then(() => { + count = 0; + server.on("event", eventHandler); return client.getDirectory(); }) - .then(() => client.expand(client.root.elements[0])) .then(() => { - console.log(client.root.elements[0].children[1].children[0]); - const matrix = client.root.elements[0].children[1].children[0]; - const connections = {} - const target0Connection = new ember.MatrixConnection(0); - target0Connection.operation = ember.MatrixOperation.connect; - target0Connection.setSources([1]); // connect with src 1 - connections[0] = target0Connection; - const p = new Promise(resolve => { - client.on("value-change", node => { - resolve(node); - }); - }); - client.client.sendBERNode(matrix.connect(connections)); - return p; - }) - .then(node => { - console.log(client.root.elements[0].children[1].children[0]); - expect(client.root.elements[0].children[1].children[0].connections['0'].sources).toBeDefined(); - expect(client.root.elements[0].children[1].children[0].connections['0'].sources.length).toBe(1); - expect(client.root.elements[0].children[1].children[0].connections['0'].sources[0]).toBe(1); - return client.disconnect(); - }); - }); - - it("should be able to get child with getNodeByPath", function() { - //server._debug = true; - client = new DeviceTree(LOCALHOST, PORT); - //client._debug = true; - //client._debug = true; - return Promise.resolve() - .then(() => client.connect()) + expect(count).toBe(1); + expect(receivedEvent.type).toBe(ServerEvents.Types.GETDIRECTORY); + return client.getElementByPath("0.1.0"); + }) + .then(matrix => { + count = 0; + return client.matrixConnect(matrix, 0, [1]); + }) .then(() => { - console.log("client connected"); - return client.getDirectory(); - }) - .then(() => { - return new Promise((resolve, reject) => { - client.root.getNodeByPath(client.client, ["scoreMaster", "identity", "product"], (err, child) => { - if (err) { reject(err) } - else { - resolve(child); - } - }); - }); + expect(count).toBe(1); + expect(receivedEvent.type).toBe(ServerEvents.Types.MATRIX_CONNECTION); + }) + .then(() => { + count = 0; + const func = client.root.getElementByPath("0.2"); + return client.invokeFunction(func, [ + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 1), + new EmberLib.FunctionArgument(EmberLib.ParameterType.integer, 7) + ]); + }) + .then(() => { + expect(count).toBe(1); + expect(receivedEvent.type).toBe(ServerEvents.Types.INVOKE); }) - .then(child => { - console.log(child); - }) - .then(() => { - return new Promise((resolve, reject) => { - client.root.getNodeByPath(client.client, ["scoreMaster", "router", "labels"], (err, child) => { - if (err) { reject(err) } - else { - resolve(child); - } - }); + .then(() => client.getElementByPath("0.0.2")) + .then(parameter => { + server._subscribe = server.subscribe; + let _resolve; + const p = new Promise((resolve) => { + _resolve = resolve; }); + server.subscribe = (c,e) => { + server._subscribe(c,e); + _resolve(); + }; + count = 0; + return client.subscribe(parameter).then(() => (p)) }) - .then(child => { - console.log(child); - client.disconnect(); + .then(() => { + expect(count).toBe(1); + expect(receivedEvent.type).toBe(ServerEvents.Types.SUBSCRIBE); + }) + .then(() => { + server.off("event", eventHandler); + }) + .then(() => client.disconnect()); + }); + }); + describe("Matrix Connect", function() { + let jsonTree; + let server; + beforeEach(function() { + jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + }); + afterEach(() => { + return server.close(); + }); + it("should verify if connection allowed in 1-to-N", function() { + let disconnectCount = 0; + const handleDisconnect = () => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); + const matrix = server.tree.getElementByPath("0.1.0"); + let connection = new EmberLib.MatrixConnection(0); + connection.setSources([1]); + connection.operation = EmberLib.MatrixOperation.connect; + let res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + matrix.setSources(0, [0]); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + connection.operation = EmberLib.MatrixOperation.absolute; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + // We can't connect. But server will disconnect existing source and connect new one. + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); + expect(matrix.connections[0].sources[0]).toBe(1); + expect(disconnectCount).toBe(1); + // But if connecting same source and dest this is a disconnect. But not possible in 1toN. + // instead connect with defaultSource or do nothing + const matrixHandlers = new MatrixHandlers(server); + matrixHandlers.getDisconnectSource(matrix, 0); + matrix.defaultSources[0].contents.value = 222; + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); + expect(disconnectCount).toBe(2); + expect(matrix.connections[0].sources[0]).toBe(222); + matrix.setSources(0, [0]); + connection = new EmberLib.MatrixConnection(1); + connection.operation = EmberLib.MatrixOperation.absolute; + connection.setSources([1]); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + }); + it("should verify if connection allowed in 1-to-1", function() { + const matrix = server.tree.getElementByPath("0.1.0"); + let disconnectCount = 0; + const handleDisconnect = () => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); + matrix.contents.type = EmberLib.MatrixType.oneToOne; + const connection = new EmberLib.MatrixConnection(0); + connection.setSources([1]); + connection.operation = EmberLib.MatrixOperation.connect; + let res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + matrix.setSources(0, [0]); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + // We can't connect but in 1-on-1 server should disconnect existing source and connect new one. + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); + expect(matrix.connections[0].sources[0]).toBe(1); + expect(disconnectCount).toBe(1); + // But if connecting same source and dest. just disconnect and do not reconnect. + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); + expect(disconnectCount).toBe(2); + connection.operation = EmberLib.MatrixOperation.absolute; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + matrix.setSources(0, []); + matrix.setSources(1, [1]); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + server.off("matrix-disconnect", handleDisconnect); + }); + it("should disconnect if trying to connect same source and target in 1-to-1", function() { + const matrix = server.tree.getElementByPath("0.1.0"); + let disconnectCount = 0; + const handleDisconnect = () => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); + matrix.contents.type = EmberLib.MatrixType.oneToOne; + matrix.setSources(0, [1]); + const connection = new EmberLib.MatrixConnection(0); + connection.setSources([1]); + connection.operation = EmberLib.MatrixOperation.connect; + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); + expect(matrix.connections[0].sources.length).toBe(0); + expect(disconnectCount).toBe(1); + }); + it("should be able to lock a connection", function() { + const matrix = server.tree.getElementByPath("0.1.0"); + let disconnectCount = 0; + const handleDisconnect = () => { + disconnectCount++; + } + server.on("matrix-disconnect", handleDisconnect.bind(this)); + matrix.contents.type = EmberLib.MatrixType.oneToOne; + matrix.setSources(0, [1]); + matrix.connections[0].lock(); + const connection = new EmberLib.MatrixConnection(0); + connection.setSources([0]); + connection.operation = EmberLib.MatrixOperation.connect; + server._handlers.handleMatrixConnections(null, matrix, {0: connection}); + expect(matrix.connections[0].sources.length).toBe(1); + expect(matrix.connections[0].sources[0]).toBe(1); + expect(disconnectCount).toBe(0); + }); + it("should verify if connection allowed in N-to-N", function() { + const matrix = server.tree.getElementByPath("0.1.0"); + matrix.contents.type = EmberLib.MatrixType.nToN; + matrix.contents.maximumTotalConnects = 2; + matrix.setSources(0, [0,1]); + + const connection = new EmberLib.MatrixConnection(0); + connection.setSources([2]); + connection.operation = EmberLib.MatrixOperation.connect; + let res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + + matrix.setSources(2, [2]); + matrix.setSources(1, [1]); + matrix.setSources(0, []); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + + matrix.setSources(1, []); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + + matrix.setSources(0, [1,2]); + matrix.setSources(1, []); + connection.operation = EmberLib.MatrixOperation.absolute; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + + + matrix.contents.maximumTotalConnects = 20; + matrix.contents.maximumConnectsPerTarget = 1; + + matrix.setSources(2, [2]); + matrix.setSources(1, [1]); + matrix.setSources(0, [0]); + connection.setSources([2]); + connection.operation = EmberLib.MatrixOperation.connect; + + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeFalsy(); + + matrix.setSources(0, []); + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + + matrix.setSources(0, [0]); + connection.operation = EmberLib.MatrixOperation.absolute; + res = matrix.canConnect(connection.target,connection.sources,connection.operation); + expect(res).toBeTruthy(); + + }); + it("should return modified answer on absolute connect", function() { + let client; + server.on("error", () => { + // ignore + }); + server.on("clientError", () => { + // ignore + }); + //server._debug = true; + return server.listen() + .then(() => { + client = new EmberClient(LOCALHOST, PORT); + return Promise.resolve() + }) + .then(() => client.connect()) + .then(() => client.getDirectory()) + .then(() => client.getElementByPath("0.1.0")) + .then(matrix => client.matrixSetConnection(matrix, 0, [1])) + .then(result => { + expect(result).toBeDefined(); + expect(result.connections).toBeDefined(); + expect(result.connections[0]).toBeDefined(); + expect(result.connections[0].disposition).toBe(EmberLib.MatrixDisposition.modified); + return client.disconnect(); }); - }); - it("should be able to get child with tree.getNodeByPath", function() { - //server._debug = true; - client = new DeviceTree(LOCALHOST, PORT); - //client._debug = true; + }); + }); + describe("Parameters subscribe/unsubscribe", function( ){ + let jsonTree; + let server; + beforeEach(function() { + jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + server.on("error", () => { + // ignore + }); + server.on("clientError", () => { + // ignore + }); + return server.listen(); + }); + afterEach(function() { + return server.close(); + }); + it("should not auto subscribe stream parameter", function() { + const parameter = server.tree.getElementByPath("0.0.2"); + expect(parameter.isStream()).toBeTruthy(); + expect(server.subscribers["0.0.2"]).not.toBeDefined(); + }); + it("should be able subscribe to parameter changes", function() { + const client = new EmberClient(LOCALHOST, PORT); + const cb = () => { + return "updated"; + } //client._debug = true; return Promise.resolve() .then(() => client.connect()) .then(() => { - console.log("client connected"); return client.getDirectory(); }) - .then(() => client.getNodeByPath("scoreMaster/identity/product")) - .then(child => { - console.log(child); - return client.getNodeByPath("scoreMaster/router/labels/group 1"); - }) - .then(child => { - console.log("router/labels", child); - client.disconnect(); - }); - }); - it("should throw an erro if getNodeByPath for unknown path", function() { - //server._debug = true; - client = new DeviceTree(LOCALHOST, PORT); - return Promise.resolve() - .then(() => client.connect()) - .then(() => { - console.log("client connected"); - return client.getDirectory(); + .then(() => client.getElementByPath("0.0.2")) + .then(parameter => { + expect(server.subscribers["0.0.2"]).not.toBeDefined(); + expect(parameter._subscribers).toBeDefined(); + expect(parameter._subscribers.size).toBe(0); + server._subscribe = server.subscribe; + let _resolve; + const p = new Promise(resolve => { + _resolve = resolve; + }); + server.subscribe = (c,e) => { + server._subscribe(c,e); + _resolve(); + }; + return client.subscribe(parameter, cb).then(() => (p)) + }) + .then(() => { + expect(server.subscribers["0.0.2"]).toBeDefined(); + expect(server.subscribers["0.0.2"].size).toBe(1); + return client.getElementByPath("0.0.2"); }) - .then(() => client.getNodeByPath("scoreMaster/router/labels/group 1")) - .then(child => { - console.log("router/labels", child); - throw new Error("Should not succeed"); - }) - .catch(e => { - client.disconnect(); - console.log(e); - expect(e.message).toMatch(/timeout/); - }); - }); + .then(parameter => { + expect(parameter._subscribers).toBeDefined(); + expect(parameter._subscribers.size).toBe(1); + server._unsubscribe = server.unsubscribe; + let _resolve; + const p = new Promise(resolve => { + _resolve = resolve; + }); + server.unsubscribe = (c,e) => { + server._unsubscribe(c,e); + _resolve(); + }; + return client.unsubscribe(parameter, cb).then(() => (p)) + }) + .then(() => { + expect(server.subscribers["0.0.2"]).toBeDefined(); + return client.getElementByPath("0.0.2"); + }) + .then(parameter => { + expect(server.subscribers["0.0.2"]).toBeDefined(); + expect(server.subscribers["0.0.2"].size).toBe(0); + expect(parameter._subscribers).toBeDefined(); + expect(parameter._subscribers.size).toBe(0); + }) + .then(() => client.disconnect()); + }); + }); + describe("Handlers", () => { + let jsonTree; + let server; + beforeEach(function() { + jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + server.on("error", () => { + //ignore + }); + server.on("clientError", () => { + //ignore + }); + return server.listen(); + }); + afterEach(function() { + return server.close(); + }); + it("Should through an error if can't process request", () => { + const root = new EmberLib.Root(); + root.addElement(new EmberLib.Node(0)); + let error; + const errorHandler = function(e) { + error = e; + } + server.on("error", errorHandler); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + server.handleRoot(client._client, root); + expect(error instanceof Errors.InvalidRequesrFormat); + client.disconnect(); + }); + }); + it("should ignore empty or null tree", () => { + const root = new EmberLib.Root(); + let error; + try { + server.handleRoot(null, root); + } + catch(e) { + error = e; + } + expect(error).not.toBeDefined(); + }); + it("should generate responses which include children", () => { + const node = server.tree.getElementByNumber(0); + server.getResponse(node); + expect(node.getChildren().length > 0).toBeTruthy(); + }); + it("Should update parameter value if new parameter value received", () => { + const root = new EmberLib.Root(); + const parameter = new EmberLib.Parameter(2); + const VALUE = "3.4.5"; + parameter.contents = new EmberLib.ParameterContents(VALUE, "string"); + root.addElement(new EmberLib.Node(0)); + root.getElement(0).addChild(new EmberLib.Node(0)); + root.getElementByPath("0.0").addChild(parameter); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + server.handleRoot(client._client, root); + const res = server.tree.getElementByPath("0.0.2"); + expect(res.contents.value).toBe(VALUE); + return client.disconnect(); + }); + }); + it("Should throw an error if element not found during request process", () => { + const root = new EmberLib.Root(); + const parameter = new EmberLib.Parameter(99); + const VALUE = "3.4.5"; + parameter.contents = new EmberLib.ParameterContents(VALUE, "string"); + root.addElement(new EmberLib.Node(0)); + root.getElement(0).addChild(new EmberLib.Node(0)); + root.getElementByPath("0.0").addChild(parameter); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + let count = 0; + server.handleError = () => { + count++; + } + server.handleRoot(client._client, root); + expect(count).toBe(1); + return client.disconnect(); + }); + }); + it("Should throw an error if element contains null child", () => { + const root = new EmberLib.Root(); + const node = new EmberLib.Node(0); + root.addElement(node); + node.elements = new Map(); + node.elements.set(0, null); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + let count = 0; + server.handleError = () => { + count++; + } + server.handleRoot(client._client, root); + expect(count).toBe(1); + return client.disconnect(); + }); + }); + it("should handle commands embedded in Node", () => { + const root = new EmberLib.Root(); + const node = new EmberLib.Node(0); + root.addElement(node); + node.elements = new Map(); + node.elements.set(EmberLib.COMMAND_GETDIRECTORYGETDIRECTORY, new EmberLib.Command(EmberLib.COMMAND_GETDIRECTORYGETDIRECTORY)); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + let count = 0; + server._handlers.handleCommand = () => { + count++; + } + server.handleRoot(client._client, root); + expect(count).toBe(1); + return client.disconnect(); + }); + }); + it("should catch unknown commands", () => { + const command = new EmberLib.Command(99); + let count = 0; + server.on("error", e => { + expect(e instanceof Errors.InvalidCommand); + count++; + }); + server._handlers.handleCommand(null, new EmberLib.Root(), command); + expect(count).toBe(1); + }); + it("should catch invalid node with no number", () => { + const node = new EmberLib.Node(99); + node.number = null; + let count = 0; + server.on("error", e => { + expect(e instanceof Errors.MissingElementNumber); + count++; + }); + server._handlers.handleNode(null, node); + expect(count).toBe(1); + }); + it("should handle matrix connections embedded in Node", () => { + const root = new EmberLib.Root(); + const node = new EmberLib.Node(0); + root.addElement(node); + const matrix = new EmberLib.MatrixNode(0); + matrix.connections = [ + new EmberLib.MatrixConnection(0) + ]; + node.elements = new Map(); + node.elements.set(0, matrix); + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + let count = 0; + server._handlers.handleMatrixConnections = () => { + count++; + } + server.handleRoot(client._client, root); + expect(count).toBe(1); + return client.disconnect(); + }); + }); + it("should catch function invocation errors and set success to false", () => { + const client = new EmberClient(LOCALHOST, PORT); + return client.connect() + .then(() => { + const root = new EmberLib.Root(); + const func = new EmberLib.Function(0, () => { throw Error("function error")}); + root.addElement(func); + server.tree = root; + return client.invokeFunction(func, []); + }) + .then(result => { + expect(result).toBeDefined(); + expect(result.success).toBeFalsy(); + }) + .then(() => client.disconnect()); + }); + it("should catch invoke to non function", () => { + const client = new EmberClient(LOCALHOST, PORT); + let result; + client.sendBERNode = function(res) { + result = res; + } + const root = new EmberLib.Root(); + const func = new EmberLib.Node(0); + root.addElement(func); + server.tree = root; + const command = EmberLib.Command.getInvocationCommand(new EmberLib.Invocation(1, [])); + server._handlers.handleInvoke(client, func, command); + expect(result).toBeDefined(); + expect(result.success).toBeFalsy(); + return client.disconnect(); + }); + }); + describe("Matrix", () => { + let jsonTree; + const MATRIX_PATH = "0.1.0"; + /** @type {EmberServer} */ + let server; + beforeEach(function() { + jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + server.on("error", () => { + //ignore + }); + server.on("clientError", () => { + //ignore + }); + return server.listen(); + }); + afterEach(function() { + return server.close(); + }); + it("should generate connections structure if none provided when calling JSONtoStree", () => { + const js = jsonRoot(); + js[0].children[1].children[0].connections = null; + const tree = EmberServer.JSONtoTree(js); + const matrix = tree.getElementByPath(MATRIX_PATH); + expect(matrix.connections).toBeDefined(); + for(let i = 0; i < matrix.contents.targetCount; i++) { + expect(matrix.connections[i]).toBeDefined(); + expect(matrix.connections[i].target).toBe(i); + } + }); + it("should have a matrixConnect function", () => { + const matrix = server.tree.getElementByPath(MATRIX_PATH); + matrix.connections[0].setSources([]); + server.matrixConnect(MATRIX_PATH, 0, [1]); + expect(matrix.connections[0].sources).toBeDefined(); + expect(matrix.connections[0].sources.length).toBe(1); + expect(matrix.connections[0].sources[0]).toBe(1); + }); + it("should throw an error if can't find matrix", () => { + try { + server.matrixConnect("0.99.0", 0, [1]); + throw new Error("Should not succeed"); + } + catch(error) { + expect(error instanceof Errors.UnknownElement); + } + }); + it("should throw an error if invalid matrix", () => { + const matrix = server.tree.getElementByPath(MATRIX_PATH); + matrix.contents = null; + try { + server.matrixConnect(MATRIX_PATH, 0, [1]); + throw new Error("Should not succeed"); + } + catch(error) { + expect(error instanceof Errors.MissingElementContents); + } + }); + it("should have a matrixSet operation on matrix", () => { + const matrix = server.tree.getElementByPath(MATRIX_PATH); + matrix.connections[0].setSources([0]); + server.matrixSet(MATRIX_PATH, 0, [1]); + expect(matrix.connections[0].sources).toBeDefined(); + expect(matrix.connections[0].sources.length).toBe(1); + expect(matrix.connections[0].sources[0]).toBe(1); + }); + it("should have a matrixDisconnect operation on matrix", () => { + const matrix = server.tree.getElementByPath(MATRIX_PATH); + matrix.contents.type = EmberLib.MatrixType.nToN; + matrix.connections[0].setSources([1]); + server.matrixDisconnect(MATRIX_PATH, 0, [1]); + expect(matrix.connections[0].sources).toBeDefined(); + expect(matrix.connections[0].sources.length).toBe(0); + }); + }); + describe("subscribers", () => { + let jsonTree; + const PARAMETER_PATH = "0.0.1"; + const MATRIX_PATH = "0.1.0"; + /** @type {EmberServer} */ + let server; + beforeEach(function() { + jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + server = new EmberServer(LOCALHOST, PORT, root); + server.on("error", () => { + //ignore + }); + server.on("clientError", () => { + //ignore + }); + return server.listen(); + }); + afterEach(function() { + return server.close(); + }); + it("should accept client to subscribe to parameter and update those who subscribed", () => { + const client = new EmberClient(LOCALHOST, PORT); + const VALUE = "The new Value"; + return client.connect() + .then(() => client.getDirectory()) + .then(() => client.getElementByPath(PARAMETER_PATH)) + .then(() => { + // A get directory on non stream is auto subscribe + expect(server.subscribers).toBeDefined(); + expect(server.subscribers[PARAMETER_PATH]).toBeDefined(); + expect(server.subscribers[PARAMETER_PATH].size).toBe(1); + let res; + for(let c of server.subscribers[PARAMETER_PATH]) { + c.queueMessage = message => { + res = message; + } + } + server.setValue(server.tree.getElementByPath(PARAMETER_PATH), VALUE, null, null); + expect(res).toBeDefined(); + const resParam = res.getElementByPath(PARAMETER_PATH); + expect(resParam).toBeDefined(); + expect(resParam.getPath()).toBe(PARAMETER_PATH); + expect(resParam.contents).toBeDefined(); + expect(resParam.contents.value).toBe(VALUE); + return client.disconnect(); + }); + }); + it("should accept client to subscribe to matrix and update those who subscribed", () => { + const client = new EmberClient(LOCALHOST, PORT); + const VALUE = 17; + const MatrixParamName = "maximumTotalConnects"; + server.tree.getElementByPath(MATRIX_PATH).contents[MatrixParamName] = 0; + return client.connect() + .then(() => client.getDirectory()) + .then(() => client.getElementByPath(MATRIX_PATH)) + .then(() => { + // A get directory on non stream is auto subscribe + expect(server.subscribers).toBeDefined(); + expect(server.subscribers[MATRIX_PATH]).toBeDefined(); + expect(server.subscribers[MATRIX_PATH].size).toBe(1); + let res; + for(let c of server.subscribers[MATRIX_PATH]) { + c.queueMessage = message => { + res = message; + } + } + server.setValue(server.tree.getElementByPath(MATRIX_PATH), VALUE, null, MatrixParamName); + expect(res).toBeDefined(); + const resParam = res.getElementByPath(MATRIX_PATH); + expect(resParam).toBeDefined(); + expect(resParam.getPath()).toBe(MATRIX_PATH); + expect(resParam.contents).toBeDefined(); + expect(resParam.contents[MatrixParamName]).toBe(VALUE); + return client.disconnect(); + }); + }); + it("should clean up subscribers if client not connected anymore", () => { + const client = new EmberClient(LOCALHOST, PORT); + const VALUE = "The new Value"; + server.subscribers[PARAMETER_PATH] = new Set(); + server.subscribers[PARAMETER_PATH].add(client); + expect(server.subscribers[PARAMETER_PATH].size).toBe(1); + let res; + for(let c of server.subscribers[PARAMETER_PATH]) { + c.queueMessage = message => { + res = message; + } + } + server.setValue(server.tree.getElementByPath(PARAMETER_PATH), VALUE, null, null); + expect(res).not.toBeDefined(); + expect(server.subscribers[PARAMETER_PATH].has(client)).toBeFalsy(); + }); + it("should ignore unsubscribe if no subcribers", () => { + const client = new EmberClient(LOCALHOST, PORT); + let error; + try { + server.unsubscribe(client, server.tree.getElementByPath(PARAMETER_PATH)); + } + catch(e) { + error =e ; + } + expect(error).not.toBeDefined(); + }); + it("should ignore serValue on element with no contents", () => { + const param = server.tree.getElementByPath(PARAMETER_PATH); + const VALUE = "The new Value"; + param.contents = null; + let error; + try { + server.setValue(param, VALUE, null, null); + } + catch(e) { + error =e ; + } + expect(error).not.toBeDefined(); + }); + }); + describe("EmberServer toJSON", () => { + it("should have a toJSON", () => { + const PARAMETER_PATH = "0.0.1"; + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + const server = new EmberServer(LOCALHOST, PORT, root); + const js = server.toJSON(); + expect(js[0].children[0].children[1].path).toBe(PARAMETER_PATH); + }); + it("should have a toJSON and return empty array if no tree", () => { + const server = new EmberServer(LOCALHOST, PORT, null); + const js = server.toJSON(); + expect(js).toBeDefined(); + expect(js.length).toBe(0); + }); + }); + describe("replaceElement", () => { + it("should replace existing element with new one", () => { + const PARAMETER_PATH = "0.0.1"; + const VALUE = "Gilles Dufour" + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + const server = new EmberServer(LOCALHOST, PORT, root); + const newParam = new EmberLib.Parameter(1); + newParam.contents = new EmberLib.ParameterContents(VALUE); + newParam.path = PARAMETER_PATH; + server.replaceElement(newParam); + expect(server.tree.getElementByPath(PARAMETER_PATH).contents.value).toBe(VALUE); + }); + it("should throw an error if unknown element path", () => { + const VALUE = "Gilles Dufour" + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + const server = new EmberServer(LOCALHOST, PORT, root); + const newParam = new EmberLib.Parameter(1000); + newParam.contents = new EmberLib.ParameterContents(VALUE); + let error; + try { + server.replaceElement(newParam); + } + catch(e) { + error = e; + } + expect(error).toBeDefined(); + expect(error instanceof Errors.UnknownElement).toBeTruthy(); + }); + it("should throw an error if trying to replace root or unattached element", () => { + const PARAMETER_PATH = "0.0.1"; + const VALUE = "Gilles Dufour" + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + const server = new EmberServer(LOCALHOST, PORT, root); + server.tree.getElementByPath(PARAMETER_PATH)._parent = null; + const newParam = new EmberLib.Parameter(1); + newParam.contents = new EmberLib.ParameterContents(VALUE); + newParam.path = PARAMETER_PATH; + let error; + try { + server.replaceElement(newParam); + } + catch(e) { + error = e; + } + expect(error).toBeDefined(); + expect(error instanceof Errors.InvalidEmberNode).toBeTruthy(); + }); + }); + describe("Events", () => { + it("should catch error emitted by internal tcp server", () => { + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + const ERROR_MESSAGE = "gdnet internal error"; + server = new EmberServer(LOCALHOST, PORT, root); + let error; + server.on("error", e => {error = e;}); + server.server.emit("error", new Error(ERROR_MESSAGE)); + expect(error).toBeDefined(); + expect(error.message).toBe(ERROR_MESSAGE); + }); + it("should catch tcp server disconnected message, and clean up clients", () => { + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + const server = new EmberServer(LOCALHOST, PORT, root); + server.clients.add(new EmberClient(LOCALHOST, PORT)); + let count = 0; + server.on("disconnected", () => {count++;}); + server.server.emit("disconnected"); + expect(count).toBe(1); + expect(server.clients.size).toBe(0); + }); + it("should catch error from connection to clients", () => { + const jsonTree = jsonRoot(); + const root = EmberServer.JSONtoTree(jsonTree); + const ERROR_MESSAGE = "gdnet internal error"; + const server = new EmberServer(LOCALHOST, PORT, root); + const client = new EmberClient(LOCALHOST, PORT); + client.remoteAddress = () => {return "address";} + let info; + server.on("clientError", data => {info = data;}); + server.server.emit("connection", client); + client.emit("error", new Error(ERROR_MESSAGE)); + expect(info).toBeDefined(); + expect(info.error.message).toBe(ERROR_MESSAGE); + }); }); }); diff --git a/test/client.js b/test/client.js deleted file mode 100755 index 880e0cc..0000000 --- a/test/client.js +++ /dev/null @@ -1,21 +0,0 @@ -const DeviceTree = require("../").DeviceTree; -const ember = require("../ember"); - -const HOST = "192.168.4.4"; -const PORT = 9092; - -client = new DeviceTree(HOST, PORT); -client.connect() -.then(() => client.getDirectory()) -.then(() => client.expand(client.root.elements[0])) -.then(() => { - //console.log(client.root.elements[0].children[4].children[2].toJSON()); - console.log(JSON.stringify(client.root.elements[0].children[4].toJSON(), null, 4)); - return client.invokeFunction(client.root.elements[0].children[4].children[0], [ - new ember.FunctionArgument(ember.ParameterType.integer, 1), - new ember.FunctionArgument(ember.ParameterType.integer, 7) - ]); -}) -.then(result => { - console.log(result); -}); diff --git a/embrionix.ember b/test/embrionix.ember similarity index 100% rename from embrionix.ember rename to test/embrionix.ember diff --git a/test/utils.js b/test/utils.js index 74b6eb9..9cca4a4 100755 --- a/test/utils.js +++ b/test/utils.js @@ -1,13 +1,19 @@ -const {ParameterAccess} = require("../ember"); +const {ParameterType, FunctionArgument} = require("../EmberLib"); + const init = function(_src,_tgt) { const targets = _tgt === undefined ? [ "tgt1", "tgt2", "tgt3" ] : _tgt; const sources = _src === undefined ? [ "src1", "src2", "src3" ] : _src; - const labels = function(endpoints) { + const defaultSources = [ + {identifier: "t-0", value: -1, access: "readWrite" }, + {identifier: "t-1", value: 0, access: "readWrite"}, + {identifier: "t-2", value: 0, access: "readWrite"} + ]; + const labels = function(endpoints, type) { let labels = []; for (let i = 0; i < endpoints.length; i++) { let endpoint = endpoints[i]; - let l = { identifier: `Label-${i}` }; + let l = { identifier: `${type}-${i}` }; if (endpoint) { l.value = endpoint; } @@ -33,9 +39,9 @@ const init = function(_src,_tgt) { // path "0.0" identifier: "identity", children: [ - {identifier: "product", value: "S-CORE Master"}, + {identifier: "product", value: "S-CORE Master", type: "string"}, {identifier: "company", value: "EVS", access: "readWrite"}, - {identifier: "version", value: "1.2.0"}, + {identifier: "version", value: "1.2.0", access: "readWrite", streamIdentifier: 1234567}, {identifier: "author", value: "g.dufour@evs.com"}, ] }, @@ -49,9 +55,9 @@ const init = function(_src,_tgt) { type: "oneToN", mode: "linear", targetCount: targets.length, - sourceCount: sources.length, + sourceCount: sources.length, connections: buildConnections(sources, targets), - labels: ["0.1.1000"] + labels: [{basePath: "0.1.1000", description: "primary"}], }, { identifier: "labels", @@ -62,23 +68,70 @@ const init = function(_src,_tgt) { identifier: "targets", // Must be 1 number: 1, - children: labels(targets) + children: labels(targets, "t") }, { identifier: "sources", // Must be 2 number: 2, - children: labels(sources) + children: labels(sources, "s") }, { identifier: "group 1", children: [ {identifier: "sdp A", value: "A"}, {identifier: "sdp B", value: "B"}] } ] + }, + { + identifier: "disconnect sources", + number: 1001, + children: defaultSources + } + ] + }, + { + // path "0.2" + identifier: "addFunction", + func: args => { + const res = new FunctionArgument(); + res.type = ParameterType.integer; + res.value = args[0].value + args[1].value; + return [res]; + }, + arguments: [ + { + type: ParameterType.integer, + value: null, + name: "arg1" + }, + { + type: ParameterType.integer, + value: null, + name: "arg2" + } + ], + result: [ + { + type: ParameterType.integer, + value: null, + name: "changeCount" } ] } ] + }, + { + identifier: "PeakValue_2", + type: 2, + streamIdentifier: 4, + streamDescriptor: { + format: "ieeeFloat32LittleEndian", + offset: 4 + }, + access: 1, + maximum: 20, + minimum: -200, + value: -200 } ]; }