UNPKG

node-vscp

Version:
1,926 lines (1,558 loc) 61.7 kB
// VSCP common javascript library // // Copyright © 2012-2024 Ake Hedman, Grodans Paradis AB // <akhe@grodansparadis.com> // Copyright © 2015-2020 Andreas Merkle // <vscp@blue-andi.de> // // Licence: // The MIT License (MIT) // [OSI Approved License] // // The MIT License (MIT) // // Copyright © 2015-2024 Åke Hedman, Grodans Paradis AB (Paradise of the Frog) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // // Alternative licenses for VSCP & Friends may be arranged by contacting // Grodans Paradis AB at info@grodansparadis.com, http://www.grodansparadis.com // // This code requires node.js > 10.4 as bigint support is needed. // 'use strict'; const vscp_class = require('node-vscp-class'); /** * VSCP core javascript library version * @property {number} major - Major version number * @property {number} minor - Minor version number * @property {number} release - Sub-minor version number */ const version = { major: 1, minor: 1, release: 20 }; /* !!! VSCP classes and types see in the autogenerated files vscp_class.js and * vscp_type.js !!! */ /** * VSCP class priorities * @enum {number} * @const */ const priority = { PRIORITY_0: 0, PRIORITY_HIGH: 0, PRIORITY_1: 1, PRIORITY_2: 2, PRIORITY_3: 3, PRIORITY_NORMAL: 3, PRIORITY_4: 4, PRIORITY_5: 5, PRIORITY_6: 6, PRIORITY_7: 7, PRIORITY_LOW: 7 }; const guidtype = { GUIDTYPE_0: 0, // Standard GUID GUIDTYPE_STANDARD: 0, GUIDTYPE_1: 1, // GUID is IP.v6 address GUIDTYPE_IPV6: 1, GUIDTYPE_2: 2, // GUID is RFC 4122 Version 1 GUIDTYPE_RFC4122_1: 2, GUIDTYPE_3: 3, // GUID is RFC 4122 Version 4 GUIDTYPE_RFC4122_4: 3 } /** * VSCP host capabilities (wcyd - What Can You Do) * * Due to Javascripts incapability to handle 64-bit numbers * values are bit positions instead of proper constants. That is * the constant is gotten with 2^bit. The capabilitues 64-bit * integer can them be divided into two 32-bit integers and with * that be handle also in Javascript * * @enum {number} * @const */ const hostCapability = { REMOTE_VARIABLE: 63, DECISION_MATRIX: 62, INTERFACE: 61, TCPIP: 15, UDP: 14, MULTICAST_ANNOUNCE: 13, RAWETH: 12, WEB: 11, WEBSOCKET: 10, REST: 9, MULTICAST_CHANNEL: 8, IP6: 6, IP4: 5, SSL: 4, TWO_CONNECTIONS: 3, AES256: 2, AES192: 1, AES128: 0 }; /* Measurement data format masks */ const measurementDataCodingMask = { MASK_DATACODING_TYPE: 0xE0, /* Bits 5,6,7 */ MASK_DATACODING_UNIT: 0x18, /* Bits 3,4 */ MASK_DATACODING_INDEX: 0x07 /* Bits 0,1,2 */ }; /* These bits are coded in the three MSB bits of the first data byte of measurement data and tells the type of the data that follows. */ const measurementDataCoding = { DATACODING_BIT: 0x00, DATACODING_BYTE: 0x20, DATACODING_STRING: 0x40, DATACODING_INTEGER: 0x60, DATACODING_NORMALIZED: 0x80, DATACODING_SINGLE: 0xA0, /* single precision float */ DATACODING_DOUBLE: 0xC0, /* double precision float */ DATACODING_RESERVED2: 0xE0 }; /* ---------------------------------------------------------------------- */ /** * VSCP event. * @class * * @param {object} options - Options * @param {number} options.vscpHead - Event head * @param {boolean} options.guidIsIpV6Addr - GUID is a IPv6 address * @param {boolean} options.dumbNode - Node is a dumb node * @param {number} options.vscpPriority - Priority * @param {number} options.vscpGuidType - GUID Type * @param {boolean} options.vscpHardCoded - Hard coded node id * @param {boolean} options.vscpCalcCRC - Calculate CRC * @param {number} options.vscpClass - VSCP class * @param {number} options.vscpType - VSCP type * @param {number} options.vscpObId - Object id * @param {string} options.vscpDateTime - ISO UTC Date + time * @param {number} options.vscpTimeStamp - Timestamp * @param {string} options.vscpGuid - GUID string * @param {(number[]|string)} options.vscpData - Event data * @param {string} options.text - Event on text form */ class Event { constructor(options) { /** * VSCP event head * @member {number} */ this.vscpHead = 0; /** * VSCP class * @member {number} */ this.vscpClass = 0; /** * VSCP type * @member {number} */ this.vscpType = 0; /** * VSCP object id used by driver for channel info and etc. * @member {number} */ this.vscpObId = 0; /** * Relative timestamp for package in us * @member {number} */ this.vscpTimeStamp = 0; /** * Date/Time for package * @member {date} */ this.vscpDateTime = new Date(); this.vscpDateTime = Date.UTC( this.vscpDateTime.getUTCFullYear(), this.vscpDateTime.getUTCMonth(), this.vscpDateTime.getUTCDate(), this.vscpDateTime.getUTCHours(), this.vscpDateTime.getUTCMinutes(), this.vscpDateTime.getUTCSeconds()); this.vscpDateTime = new Date(this.vscpDateTime); /** * Node global unique id LSB(15) -> MSB(0) * @member {string} */ this.vscpGuid = '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00'; /** * Data array or string * @member {(number[]|string)} */ this.vscpData = []; if ('undefined' !== typeof options) { if ('string' === typeof options) { this.setFromString(options); } else { if ('number' === typeof options.vscpHead) { this.vscpHead = options.vscpHead; } else if ('string' === typeof options.vscpHead) { this.vscpHead = parseInt(options.vscpHead); } if ('boolean' === typeof options.guidIsIpV6Addr) { if (false === options.guidIsIpV6Addr) { this.vscpHead &= 0xefff; } else { this.setIPV6Addr(); } } if ('boolean' === typeof options.dumbNode) { if (false === options.dumbNode) { this.vscpHead &= 0x7fff; } else { this.vscpHead |= 0x8000; } } // 0 - 7 if ('number' === typeof options.vscpPriority) { if ((0 <= options.vscpPriority) && (7 >= options.vscpPriority)) { this.vscpHead &= 0xff1f; this.vscpHead |= (options.vscpPriority << 5); } } else if ('string' === typeof options.vscpPriority) { let n = parseInt(options.vscpPriority); this.vscpHead &= 0xff1f; this.vscpHead |= (n << 5); } // 0 - 7 if ('number' === typeof options.vscpGuidType) { if ((0 <= options.vscpGuidType) && (7 >= options.vscpGuidType)) { this.vscpHead &= 0x8fff; this.vscpHead |= (options.vscpGuidType << 12); } } else if ('string' === typeof options.vscpGuidType) { let n = parseInt(options.vscpGuidType); this.vscpHead &= 0xff1f; this.vscpHead |= (n << 5); } if ('boolean' === typeof options.vscpHardCoded) { if (false === options.vscpHardCoded) { this.vscpHead &= 0xffef; } else { this.vscpHead |= 0x0010; } } if ('boolean' === typeof options.vscpCalcCRC) { if (false === options.vscpCalcCRC) { this.vscpHead &= 0xfff7; } else { this.vscpHead |= 0x0008; } } if ('number' === typeof options.vscpClass) { this.vscpClass = options.vscpClass; } else if ('string' === typeof options.vscpClass) { this.vscpClass = parseInt(options.vscpClass); } if ('number' === typeof options.vscpType) { this.vscpType = options.vscpType; } else if ('string' === typeof options.vscpType) { this.vscpType = parseInt(options.vscpType); } if ('number' === typeof options.vscpObId) { this.vscpObId = options.vscpObId; } else if ('string' === typeof options.vscpObId) { this.vscpObId = parseInt(options.vscpObId); } if ('number' === typeof options.vscpTimeStamp) { this.vscpTimeStamp = options.vscpTimeStamp; } else if ('string' === typeof options.vscpTimeStamp) { this.vscpTimeStamp = parseInt(options.vscpTimeStamp); } if ('string' === typeof options.vscpDateTime) { // Time in UTC for events but conversion // is done in send routine this.vscpDateTime = new Date(options.vscpDateTime); } else if (true === (options.vscpDateTime instanceof Date)) { // Time should be GMT this.vscpDateTime = options.vscpDateTime; } // GUID if ('string' === typeof options.vscpGuid) { this.vscpGuid = options.vscpGuid; } // VSCP data if (Array.isArray(options.vscpData)) { this.vscpData = options.vscpData; } else if (('string' === typeof options.vscpData) ) { this.vscpData = options.vscpData.split(','); // Make data numeric for ( var n in this.vscpData ) { this.vscpData[n] = readValue(this.vscpData[n]); } } // 'text' to init from string form if ('string' === typeof options.text) { this.setFromString(options.text); } } } } /** * Set bit in header that mark GUID as IP v6 address */ setIPV6Addr() { this.vscpHead &= 0x8FFF; this.vscpHead |= 0x1000; } /** * Check if GUID for this event is a IP v6 address or not? * * @return {boolean} If the GUID is a IP v6 address, it will return true, * otherwise false. */ isIPV6Addr() { var result = false; if ( 0x1000 === (this.vscpHead & 0x7000)) { result = true; } return result; } /** * Set bit that mark this event as coming from a dumb node (No MDF, registers, nothing). */ setDumbNode() { this.vscpHead |= 0x8000; } /** * Check if this event is marked as coming from a dumb node. * Dumb node means no MDF, registers, nothing. * * @return {boolean} If the node is a dumb node, it will return true, otherwise * false. */ isDumbNode() { var result = false; if (0 < (this.vscpHead & 0x8000)) { result = true; } return result; } /** * Set the VSCP event priority (0-7). Lower value is higher priority. * * @param {number} priority - Priority */ setPriority(priority) { if ((0 <= priority) && (7 >= priority)) { this.vscpHead &= 0xff1f; this.vscpHead |= (priority << 5); } } /** * Get the VSCP event priority (0-7). Lower value is higher priority. * * @return {number} Priority of the event. */ getPriority() { return (this.vscpHead >> 5) & 0x0007; } /** * Set the VSCP GUID type (0-7). * * @param {number} type - Priority */ setGuidType(type) { if ((0 <= type) && (7 >= type)) { this.vscpHead &= 0x8fff; this.vscpHead |= (type << 12); } } /** * Get the VSCP event GUID type (0-7). * * @return {number} Priority of the event. */ getGuidType() { return (this.vscpHead >> 12) & 0x0007; } /** * Set the node id of the event sender as hard coded? */ setHardCodedAddr() { this.vscpHead |= 0x0010; } /** * Is the node id of the event sender hard coded or not? * * @return {boolean} If the node id is hard coded, it will return true, * otherwise false. */ isHardCodedAddr() { var result = false; if (0 < (this.vscpHead & 0x0010)) { result = true; } return result; } /** * Set flag for no CRC calculation? */ setDoNotCalcCRC() { this.vscpHead |= 0x0008; } /** * Is CRC calculated or not? * * @return {boolean} If nor CRC should be calculated true is returned. */ isDoNotCalcCRC() { var result = false; if (0 < (this.vscpHead & 0x0008)) { result = true; } return result; } /*! getRollingIndex Some nodes keep a rolling index of there frames (typically wireless nodes). This function get the index. @return {number} Rolling index 0-7. */ getRollingIndex() { return (this.vscpHead & 7); } /*! setRollingIndex Set rolling index (0-7) @param rindex Rolling index to set (0-7) */ setRollingIndex(rindex) { rindex &= 7; this.vscpHead &= 0xfff8; this.vscpHead += rindex; } // --------------------------------------------------- /** * Get event as string. * @return {string} Event as string with the following format * vscpHead,vscpClass,vscpType,vscpObId,vscpDateTime,vscpTimeStamp,vscpGuid,vspData */ getAsString() { var index = 0; var str = ''; str += this.vscpHead.toString() + ','; str += this.vscpClass.toString() + ','; str += this.vscpType.toString() + ','; str += this.vscpObId.toString() + ','; str += this.vscpDateTime.toISOString() + ','; str += this.vscpTimeStamp.toString() + ','; str += this.vscpGuid; if ( Array.isArray(this.vscpData)) { if (0 < this.vscpData.length) { str += ','; } for (index = 0; index < this.vscpData.length; ++index) { str += this.vscpData[index].toString(); if ((this.vscpData.length - 1) > index) { str += ','; } } } else if ('string' === typeof this.vscpData) { if (0 < this.vscpData.length) { str += ','; } str += this.vscpData; } else { console.error(getTime() + ' Invalid VSCP event data.'); } return str; } /** * Get event as string. * @return {string} Event as string with the following format * vscpHead,vscpClass,vscpType,vscpObId,vscpDateTime,vscpTimeStamp,vscpGuid,vspData */ toString() { return this.getAsString(); } /** * Set event from string. * @return {string} Event as string */ setFromString(str) { if ('string' !== typeof str) { console.error('VSCP event is not in string form.'); throw('VSCP event is not in string form.'); } var ea = str.split(','); // Get head if (ea.length) { this.vscpHead = readValue(ea[0]); } // Get VSCP class if (ea.length > 1) { this.vscpClass = readValue(ea[1]); } // Get VSCP type if (ea.length > 2) { this.vscpType = readValue(ea[2]); } // Get VSCP obid if (ea.length > 3) { // If left empty set default if (0 == ea[3]) { ea[3] = '0'; } this.vscpObId = readValue(ea[3]); } // Get VSCP datetime // If left empty set default if (0 == ea[4].length) { this.vscpDateTime = new Date(); this.vscpDateTime = Date.UTC( this.vscpDateTime.getUTCFullYear(), this.vscpDateTime.getUTCMonth(), this.vscpDateTime.getUTCDate(), this.vscpDateTime.getUTCHours(), this.vscpDateTime.getUTCMinutes(), this.vscpDateTime.getUTCSeconds()); this.vscpDateTime = new Date(this.vscpDateTime); } else if ((ea.length > 4) && (0 !== ea[4].length)) { this.vscpDateTime = new Date(ea[4]); } // Timestamp this.vscpTimeStamp = 0; // If left empty set default if (0 == ea[5]) { ea[5] = '0'; } if (ea.length > 5) { this.vscpTimeStamp = parseInt(ea[5]); } // Get VSCP GUID // If left empty set default if (0 == ea[6]) { ea[6] = '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00'; } this.vscpGuid = '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00'; if (ea.length > 6) { this.vscpGuid = ea[6]; } // Get VSCP data this.vscpData = []; if (ea.length > 7) { for (let i = 7; i < ea.length; i++) { this.vscpData.push(readValue(ea[i])); } } } /** * return JSON object representation of event * @return {object} Event as JSON object */ toJSONObj() { var ev = {}; ev.vscpHead = this.vscpHead & 0xffff; ev.vscpClass = this.vscpClass & 0xffff; ev.vscpType = this.vscpType & 0xffff; ev.vscpGuid = this.vscpGuid; ev.vscpObId = this.vscpObId; ev.vscpTimeStamp = this.vscpTimeStamp; ev.vscpDateTime = this.vscpDateTime; ev.vscpData = this.vscpData; return ev; } } // Event /* ---------------------------------------------------------------------- */ /** * Read a hex, binary, octal or decimal value and return as * an integer. * @param {string} input - Hex or decimal value as string * @return {number} Value */ var readValue = function(input) { var txtvalue = input.toLowerCase(); var poshex = txtvalue.indexOf('0x'); var posbin = txtvalue.indexOf('0b'); var posoct = txtvalue.indexOf('0o'); if ((-1 == poshex) && (-1 == posbin) && (-1 == posoct)) { return parseInt(txtvalue); } else if (-1 != poshex) { txtvalue = txtvalue.substring(poshex + 2); return parseInt(txtvalue, 16); } else if (-1 != posbin) { txtvalue = txtvalue.substring(posbin + 2); return parseInt(txtvalue, 2); } else if (-1 != posoct) { txtvalue = txtvalue.substring(posoct + 2); return parseInt(txtvalue, 8); } else { return NaN; } }; /** * Utility function which returns the current time in the following format: * hh:mm:ss.us * * @return {string} Current time in the format * hh:mm:ss.us */ var getTime = function() { var now = new Date(); var paddingHead = function(num, size) { var str = num + ''; while (str.length < size) { str = '0' + str; } return str; }; var paddingTail = function(num, size) { var str = num + ''; while (str.length < size) { str = str + '0'; } return str; }; return '' + paddingHead(now.getHours(), 2) + ':' + paddingHead(now.getMinutes(), 2) + ':' + paddingHead(now.getSeconds(), 2) + '.' + paddingTail(now.getMilliseconds(), 3); }; /** * Converts a GUID number array to a GUID string. * * @param {number[]} guid - GUID number array * @return {string} GUID string, e.g. * 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 */ var guidToStr = function(guid) { var guidStr = ''; var index = 0; var hexValue = ''; // If buffer . convert to array if ( Buffer.isBuffer(guid) ) { var arr = Array.prototype.slice.call(guid, 0); guid = arr; } if ( !Array.isArray(guid) ) { throw(new Error("Argument must be array or buffer")); } for (index = 0; index < guid.length; ++index) { hexValue = guid[index].toString(16).toUpperCase(); if (2 > hexValue.length) { hexValue = '0' + hexValue; } guidStr += hexValue; if (index < (guid.length - 1)) { guidStr += ':'; } } return guidStr; }; /** * Converts a GUID string to a GUID number array. * * @param {string} guid - GUID string, e.g. * 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 * @return {number[]} GUID number array and array with length != 16 for invalid * GUID */ var strToGuid = function(str) { var guid = []; var items = []; var index = 0; if ('undefined' === typeof str) { throw(new Error("Parameter error: Missing argument")); } if ('string' !== typeof str) { throw(new Error("Parameter error: Argument should be string")); } // If GUID is "-" use interface GUID if ('-' === str.trim()) { str = '00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00'; } items = str.split(':'); if (16 !== items.length) { throw 'Parameter error: A VSCP GUID consist of 16 items'; } for (index = 0; index < items.length; ++index) { guid.push(parseInt(items[index], 16)); } return guid; }; /** * Check for all null GUID * * @param {string|array} guid - GUID string/array * @return {boolean} True if guid is all nills */ var isGuidZero = function(guid) { var guidArray = []; if ('undefined' === typeof guid) { throw(new Error("Parameter error: Missing argument")); } if ('string' === typeof guid) { guidArray = strToGuid(guid); } else if ( Array.isArray(guid) ) { guidArray = guid; } // If buffer . convert to array else if ( Buffer.isBuffer(guid) ) { guidArray = Array.prototype.slice.call(guid, 0); } else { throw(new Error("Parameter error: Argument must be of type string, array or buffer")); } for (let i = 0; i < 16; i++) { if (guidArray[i]) return false; } return true; }; /** * getNodeId * * Get node id from a node GUID string. * * @param {string|array|buffer} guid - GUID string, e.g. * 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 * @return {number} Node id */ var getNodeId = function(guid) { if ('undefined' === typeof guid) { throw new Error("Parameter error: GUID is undefined."); } if ('string' === typeof guid) { // Short for all nulls? if (('-' === guid) || ('' === guid) ) { return 0; } return ( (parseInt(guid.split(':')[14], 16) << 8) + parseInt(guid.split(':')[15], 16)); } else if ( Array.isArray(guid) ) { return ((guid[14] << 8) + guid[15]); } // If buffer . convert to array else if ( Buffer.isBuffer(guid) ) { var guidArray = Array.prototype.slice.call(guid, 0); return ((guidArray[14] << 8) + guidArray[15]); } else { throw("Parameter error: Argument must be of type string, array or buffer"); } }; /** * getNickName * * Get node id from a node GUID string. * * @param {string|array|buffer} guid - GUID string, e.g. * 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 * @return {number} Node id */ var getNickName = function(guid) { return getNodeId(guid); }; /** * setNodeId * * Set node to a node GUID string. TODO should be 16-bit! * * @param {string} guid - GUID string, e.g. * 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 * @param {number} nodeid - Node is to set (16-bit). * @return {string} guid with LSB set to node id, or null * on error. */ var setNodeId = function(guid, nodeid) { var guidArray = []; var rtype = 0; // Return type 0=string,1=array,2=buffer if ( ('undefined' === typeof guid) || ('undefined' === typeof nodeid) ) { throw(new Error("Missing argument")); } if ('number' !== typeof nodeid) { throw("nodeid argument should be a 16-bit number."); } if ('string' === typeof guid) { rtype = 0; // Return string guidArray = guid.split(':'); } else if ( Array.isArray(guid) ) { rtype = 1; // Return array guidArray = guid; } // If buffer . convert to array else if ( Buffer.isBuffer(guid) ) { rtype = 2; // Return buffer guidArray = Array.prototype.slice.call(guid, 0); } else { throw("guid argument should be a string,array or buffer"); } guidArray[14] = ((nodeid >> 8) & 0xff); guidArray[15] = nodeid & 0xff; switch (rtype) { case 0: // String return guidToStr(guidArray); break; case 1: // Array return guidArray; break; case 2: // Buffer return Buffer.from(guidArray); break; } }; /** * setNickName * * Set node to a node GUID string. TODO should be 16-bit! * * @param {string} guid - GUID string, e.g. * 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 * @param {number} nodeid - Node is to set (16-bit). * @return {string} guid with LSB set to node id, or null * on error. */ var setNickName = function(guid, nodeid) { return setNodeId(guid, nodeid); }; // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem // Since DOMStrings are 16-bit-encoded strings, in most browsers // calling window.btoa on a Unicode string will cause a Character // Out Of Range exception if a character exceeds the range of a // 8-bit ASCII-encoded character. /** * Encode base64 unicode safe. * https://stackabuse.com/encoding-and-decoding-base64-strings-in-node-js/ * * @param {string} str - Unicode string * @return {string} Base64 */ var b64EncodeUnicode = function(str) { var rv = Buffer.from(str, 'utf8'); return rv.toString('base64'); }; /** * Decode base64 unicode safe. * @param {string} str - Base64 * @return {string} Unicode string * Note: prior to Node v4, use new Buffer rather than Buffer.from. */ var b64DecodeUnicode = function(str) { return Buffer.from(str, 'base64').toString('utf8'); }; // ---------------------------------------------------------------------------- // Header helpers /*! isIPV6Addr A node that use an IPv6 address can use this address as its's GUID and then should set this bit to indicate this. @param {number} head VSCP head (16-bit) @return {boolean} true if this is a Ipv6 GUID. */ var isIPV6Addr = function(head) { var result = false; if ( 'number' !== typeof head ) { throw(new Error("Parameter error: 'head' should be a number.")) } if ( 0x1000 === (head & 0x7000)) { result = true; } return result; }; /*! isDumbNode A Dumb node have no registers etc and can only send events. This function check if it is. @param {number} head VSCP head (16-bit) @return {boolean} true if this is a dumb node. */ var isDumbNode = function(head) { if ( 'number' !== typeof head ) { throw(new Error("Parameter error: 'head' should be a number.")) } return (head & (1 << 15) ? true : false ); }; /*! getPriority @param {number} head VSCP head (16-bit or 8-bit) @return {number} VSCP priority 0-7 where 0 is highest priority. */ var getPriority = function(head) { if ( 'number' !== typeof head ) { throw(new Error("Parameter error: 'head' should be a number.")) } head = (head & 0xff); // In case 16-bit head return ((head >> 5) & 7); }; /*! Get the VSCP event GUID type (0-7). @return {number} Priority of the event. */ var getGuidType = function(vscpHead) { return (vscpHead >> 12) & 0x0007; }; /*! isHardCodedAddr A hardcoded node is a node where the address is set and can not be changed. This is important for CAN4VSCP and RS-485 systems where the nickname id is dynamic but the GUID for the node is not. @param {number} head VSCP head (16-bit or 8-bit) @return {boolean} true if this is a hardcoded address node. */ var isHardCodedAddr = function(head) { var result = false; if ( 'number' !== typeof head ) { throw("Parameter error: 'head' should be a number.") } if (0 < (head & 0x0010)) { result = true; } return result; }; /*! isDoNotCalcCRC Check if the don't calculate CRC bit is set. This is present for wireless devices and similar. @param {number} head VSCP head (16-bit or 8-bit) @return {boolean} true if CRC should noe be calculated */ var isDoNotCalcCRC = function(head) { var result = false; if ( 'number' !== typeof head ) { throw(new Error("Parameter error: 'head' should be a number.")) } if (0 < (head & 0x0008)) { result = true; } return result; }; /*! getRollingIndex Some nodes keep a rolling index of there frames (typically wireless nodes). This function get the index. @param {number} head VSCP head (16-bit or 8-bit) @return {number} Rolling index 0-7. */ var getRollingIndex = function(head) { if ( 'number' !== typeof head ) { throw(new Error("Parameter error: 'head' should be a number.")) } return (head & 7); }; /* ---------------------------------------------------------------------- */ /*! toFixed Round value to a fixed precision. @param {number} value - Value @param {number} precision - Precision @return {string} Rounded value */ var toFixed = function(value, precision) { if ( ('number' !== typeof value) || ('number' !== typeof precision) ) { throw(new Error("Parameter error: 'value' and precision' should be numbers.")) } var power = Math.pow(10, precision || 0); return String((Math.round(value * power) / power).toFixed(precision)); }; /*! varInt2BigInt Convert VSCP data to a BigInt value. The byte that make up the BigInt is stored in a byte array with MSB to LSB storage order. @param {array[]|buffer[]} data - Byte array/buffer @return {bigint} BigInt value */ var varInt2BigInt = function(data) { var rval = 0.0; var work = 0n; var bNegative = false; var i = 0; // If argument is array convert to buffer if ( Array.isArray(data) ) { data = Buffer.from(data); } // We must have a buffer if ( !Buffer.isBuffer(data) ) { throw(new Error("Parameter error: 'data' should be a numeric array or buffer.")); } if (0 !== (data[0] & 0x80)) { bNegative = true; for (i = 0; i < data.length; i++) { data[i] = ~data[i] & 0xff; } } for (i = 0; i < data.length; i++) { work = work << 8n; work += BigInt(data[i]); } if (true === bNegative) { work = -1n * (work + 1n); } return work; }; /*! Get the data coding for all measurements even for measurements where the data coding is just implied Note! unit and sensor index is not valid for level II measurement events.For level II they are both frull bytes and must be read from the data. { datacoding: unit: sensorindex: } @param vscpClass {number} One of the valid measurement classes @param vscpData {array | buffer } Event data. @return Data coding byte. See measurementDataCoding above. */ var getMeasurementDataCoding = function(vscpClass,vscpData) { // -1 means not defined. var rvobj = { datacoding: 0, unit: 0, sensorindex: 0, index: -1, zone: -1, subzone: -1 }; // Check parameters if ( vscpClass !== number ) { throw(new Error("Parameter error: 'vscpClass' should be a numeric.")); } if ( !Array.isArray(vscpData) && !Buffer.isBuffer(vscpData)) { throw(new Error("Parameter error: 'vscpData' should be a numeric array or buffer.")); } if ( ( (vscpClass >= vscp_class.VSCP_CLASS1_MEASUREMENT ) && (vscpClass <= vscp_class.VSCP_CLASS1_MEASUREMENTX4 ) ) ) { rvobj.datacoding = getDataCoding(vscpData[0]); rvobj.unit = getUnit(vscpData[0]); rvobj.sensorindex = getSensorIndex(vscpData[0]); } else if ( vscpClass == vscp_class.VSCP_CLASS1_DATA ) { rvobj.datacoding = getDataCoding(vscpData[0]); rvobj.unit = getUnit(vscpData[0]); rvobj.sensorindex = getSensorIndex(vscpData[0]); } else if ( (vscpClass >= vscp_class.VSCP_CLASS1_MEASUREMENT64 ) && (vscpClass <= vscp_class.VSCP_CLASS1_MEASUREMENT64X4 ) ) { // Always double, unit=0,sensorindex=0 rvobj.datacoding = measurementDataCoding.DATACODING_DOUBLE; rvobj.unit = 0; rvobj.sensorindex = 0; } else if ( (vscpClass >= vscp_class.VSCP_CLASS1_MEASUREZONE ) && (vscpClass <= vscp_class.VSCP_CLASS1_MEASUREZONEX4 ) ) { rvobj.datacoding = getDataCoding(vscpData[3]); rvobj.unit = getUnit(vscpData[3]); rvobj.sensorindex = getSensorIndex(vscpData[3]); } else if ( (vscpClass >= vscp_class.VSCP_CLASS1_MEASUREMENT32 ) && (vscpClass <= vscp_class.VSCP_CLASS1_MEASUREMENT32X4 ) ) { // Always single, unit=0,sensorindex=0 rvobj.datacoding = measurementDataCoding.DATACODING_SINGLE; rvobj.unit = 0; rvobj.sensorindex = 0; } else if ( (vscpClass >= vscp_class.VSCP_CLASS1_SETVALUEZONE ) && (vscpClass <= vscp_class.VSCP_CLASS1_SETVALUEZONEX4 ) ) { rvobj.datacoding = getDataCoding(vscpData[3]); rvobj.unit = getUnit(vscpData[3]); rvobj.sensorindex = getSensorIndex(vscpData[3]); rvobj.index = vscpData[0]; rvobj.zone = vscpData[1]; rvobj.subzone = vscpData[2]; } else if ( (vscpClass >= (512 + vscp_class.VSCP_CLASS1_MEASUREMENT) ) && (vscpClass <= (512 + vscp_class.VSCP_CLASS1_MEASUREMENTX4) ) ) { // At offset 16 rvobj.datacoding = getDataCoding(vscpData[16]); rvobj.unit = getUnit(vscpData[16]); rvobj.sensorindex = getSensorIndex(vscpData[16]); } else if ( vscpClass == (512 + vscp_class.VSCP_CLASS1_DATA ) ) { // At offset 16 rvobj.datacoding = getDataCoding(vscpData[16]); rvobj.unit = getUnit(vscpData[16]); rvobj.sensorindex = getSensorIndex(vscpData[16]); } else if ( (vscpClass >= (512 + vscp_class.VSCP_CLASS1_MEASUREMENT64) ) && (vscpClass <= (512 + vscp_class.VSCP_CLASS1_MEASUREMENT64X4) ) ) { // Offset 16, Always double, unit=0,sensorindex=0 rvobj.datacoding = measurementDataCoding.DATACODING_DOUBLE; rvobj.unit = 0; rvobj.sensorindex = 0; } else if ( (vscpClass >= (512 + vscp_class.VSCP_CLASS1_MEASUREZONE) ) && (vscpClass <= (512 + vscp_class.VSCP_CLASS1_MEASUREZONEX4) ) ) { // At offset 16 rvobj.datacoding = getDataCoding(vscpData[16+3]); rvobj.unit = getUnit(vscpData[16+3]); rvobj.sensorindex = getSensorIndex(vscpData[16+3]); rvobj.index = vscpData[0]; rvobj.zone = vscpData[1]; rvobj.subzone = vscpData[2]; } else if ( (vscpClass >= (512 + vscp_class.VSCP_CLASS1_MEASUREMENT32) ) && (vscpClass <= (512 + vscp_class.VSCP_CLASS1_MEASUREMENT32X4) ) ) { // Offset 16, Always double, unit=0,sensorindex=0 rvobj.datacoding = measurementDataCoding.DATACODING_SINGLE; rvobj.unit = 0; rvobj.sensorindex = 0; } else if ( (vscpClass >= (512 + vscp_class.VSCP_CLASS1_SETVALUEZONE) ) && (vscpClass <= (512 + vscp_class.VSCP_CLASS1_SETVALUEZONEX4) ) ) { rvobj.datacoding = vscpData[16+3]; rvobj.unit = getUnit(vscpData[16+3]); rvobj.sensorindex = getSensorIndex(vscpData[16+3]); rvobj.index = vscpData[16]; rvobj.zone = vscpData[16+1]; rvobj.subzone = vscpData[16+2]; } else if ( (vscp_class.VSCP_CLASS2_MEASUREMENT_STR == vscpClass) ) { rv = measurementDataCoding.DATACODING_STRING; // Always string, index=0 rvobj.datacoding = measurementDataCoding.DATACODING_STRING; rvobj.sensorindex = vscpData[0]; rvobj.index = 0; rvobj.zone = vscpData[1]; rvobj.subzone = vscpData[2]; rvobj.unit = vscpData[3]; } else if ( (vscp_class.VSCP_CLASS2_MEASUREMENT_FLOAT == vscpClass) ) { // Always double, index=0 rvobj.datacoding = measurementDataCoding.DATACODING_DOUBLE; rvobj.sensorindex = vscpData[0]; rvobj.index = 0; rvobj.zone = vscpData[1]; rvobj.subzone = vscpData[2]; rvobj.unit = vscpData[3]; } return rvobj; } /*! getDataCoding Get data coding. @param {number} data - Data @return {number} Coding */ var getDataCoding = function(datacoding) { if ( 'number' !== typeof datacoding ) { throw("Parameter error: 'datacoding' should be a number.") } return (datacoding & measurementDataCodingMask.MASK_DATACODING_TYPE); }; /*! getDataCodingStr Get unit descriptive string from data coding. @param {number} data - Data coding @return {string} Unit string */ var getDataCodingStr = function(datacoding) { var datacodingtxt = ""; if ( 'number' !== typeof datacoding ) { throw("Parameter error: 'datacoding' should be a number.") } switch (datacoding) { case measurementDataCoding.DATACODING_BIT: datacodingtxt = "Bits"; break; case measurementDataCoding.DATACODING_BYTE: datacodingtxt = "Bytes"; break; case measurementDataCoding.DATACODING_INTEGER: datacodingtxt = "Integer"; break; case measurementDataCoding.DATACODING_NORMALIZED: datacodingtxt = "Normalized integer"; break; case measurementDataCoding.DATACODING_STRING: datacodingtxt = "String"; break; case measurementDataCoding.DATACODING_SINGLE: datacodingtxt = "Floating point (single)"; break; default: datacodingtxt = "Unknown data coding"; break; } return datacodingtxt; } /*! getUnit Get unit from data coding. @param {number} data - Data coding @return {number} Unit */ var getUnit = function(datacoding) { if ( 'number' !== typeof datacoding ) { throw("Parameter error: 'datacoding' should be a number.") } return ((datacoding & measurementDataCodingMask.MASK_DATACODING_UNIT) >> 3); }; /*! getSensorIndex Get sensor index from data coding. @param {number} data - Data coding @return {number} Sensor index */ var getSensorIndex = function(datacoding) { if ( 'number' !== typeof datacoding ) { throw("Parameter error: 'datacoding' should be a number.") } return (datacoding & measurementDataCodingMask.MASK_DATACODING_INDEX); }; /*! isMeasurement Returns true if vscpClass is a measurement class @param {number} vscpClass - VSCP class to check @return {boolean True if vscpClass is a measurement class, false otherwise */ var isMeasurement = function(vscpClass) { let rv = false; // Allow for event object // if ( typeof vscpClass !== 'object') { // vscpClass = vscpClass.vscpClass; // } if (( (vscpClass >= vscp_class.VSCP_CLASS1_MEASUREMENT ) && (vscpClass <= vscp_class.VSCP_CLASS1_MEASUREMENTX4 ) ) || (vscpClass == vscp_class.VSCP_CLASS1_DATA ) || ( (vscpClass >= vscp_class.VSCP_CLASS1_MEASUREMENT64 ) && (vscpClass <= vscp_class.VSCP_CLASS1_MEASUREMENT64X4 ) ) || ( (vscpClass >= vscp_class.VSCP_CLASS1_MEASUREZONE ) && (vscpClass <= vscp_class.VSCP_CLASS1_MEASUREZONEX4 ) ) || ( (vscpClass >= vscp_class.VSCP_CLASS1_MEASUREMENT32 ) && (vscpClass <= vscp_class.VSCP_CLASS1_MEASUREMENT32X4 ) ) || ( (vscpClass >= vscp_class.VSCP_CLASS1_SETVALUEZONE ) && (vscpClass <= vscp_class.VSCP_CLASS1_SETVALUEZONEX4 ) ) || ( (vscpClass >= (512 + vscp_class.VSCP_CLASS1_MEASUREMENT) ) && (vscpClass <= (512 + vscp_class.VSCP_CLASS1_MEASUREMENTX4) ) ) || (vscpClass == (512 + vscp_class.VSCP_CLASS1_DATA ) ) || ( (vscpClass >= (512 + vscp_class.VSCP_CLASS1_MEASUREMENT64) ) && (vscpClass <= (512 + vscp_class.VSCP_CLASS1_MEASUREMENT64X4) ) ) || ( (vscpClass >= (512 + vscp_class.VSCP_CLASS1_MEASUREZONE) ) && (vscpClass <= (512 + vscp_class.VSCP_CLASS1_MEASUREZONEX4) ) ) || ( (vscpClass >= (512 + vscp_class.VSCP_CLASS1_MEASUREMENT32) ) && (vscpClass <= (512 + vscp_class.VSCP_CLASS1_MEASUREMENT32X4) ) ) || ( (vscpClass >= (512 + vscp_class.VSCP_CLASS1_SETVALUEZONE) ) && (vscpClass <= (512 + vscp_class.VSCP_CLASS1_SETVALUEZONEX4) ) ) || (vscp_class.VSCP_CLASS2_MEASUREMENT_STR == vscpClass) || (vscp_class.VSCP_CLASS2_MEASUREMENT_FLOAT == vscpClass)) { rv = true; } return rv; }; /*! decodeMeasurementClass10 Decode a class 10 measurement. CLASS1.MEASUREMENT @param {number[]} data - Data (event data array/buffer where first data byte is the VSCP data coding) @return bits - {logical[]} Array of bits bytes - {number[]} Array of bytes integer . {bigint} Integer as bigint string - {number} String value as number. float - {number} Floating point value as number. */ var decodeMeasurementClass10 = function(data) { var rval; var newData = []; var sign = 0; var exp = 0; var mantissa = 0; var str = ''; var i = 0; var j = 0; // If argument is array convert to buffer if ( Array.isArray(data) ) { data = Buffer.from(data); } // We must have a buffer if ( !Buffer.isBuffer(data) ) { throw(new Error("Parameter error: 'data' should be a numeric array or buffer.")) } // We must have size that fit the expected data if ( data.length < 2 ) { throw(new Error("Parameter error: 'data' should have a length >= 2.")) } switch (getDataCoding( data[0] & measurementDataCodingMask.MASK_DATACODING_TYPE ) ) { case measurementDataCoding.DATACODING_BIT: // Bits rval = []; for (i=1; i<data.length; i++) { for (j=0;j<8;j++) { rval.push((data[i] & (1<<(7-j))) ? true : false); } } break; case measurementDataCoding.DATACODING_BYTE: // Bytes rval = []; for (i=1; i<data.length; i++) { rval.push(data[i]); } break; case measurementDataCoding.DATACODING_INTEGER: // Integer rval = varInt2BigInt(data.slice(1)); break; case measurementDataCoding.DATACODING_STRING: // String for (i = 1; i < data.length; i++) { str += String.fromCharCode(data[i]); } rval = parseFloat(str); break; case measurementDataCoding.DATACODING_NORMALIZED: // Normalized integer exp = data[1]; rval = Number(varInt2BigInt(data.slice(2))); // Handle mantissa if (0 !== (exp & 0x80)) { exp &= 0x7f; rval = rval / Math.pow(10, exp); } else { exp &= 0x7f; rval = rval * Math.pow(10, exp); } break; case measurementDataCoding.DATACODING_SINGLE: // Floating point if (5 === data.length) { rval = data.readFloatBE(1); } break; case measurementDataCoding.DATACODING_DOUBLE: if (8 === data.length) { rval = data.readDoubleBE(1); } break; case measurementDataCoding.DATACODING_RESERVED2: // Reserved break; default: break; } return rval; }; /*! decodeMeasurementClass60 CLASS1.MEASUREMENT64 Decode a class 60 measurement. Data is a 64-bit double floating point number. @param {number[]} data - Data array/buffer @return {number} Value as float */ var decodeMeasurementClass60 = function(data) { // If argument is array convert to buffer if ( Array.isArray(data) ) { data = Buffer.from(data); } // We must have a buffer if ( !Buffer.isBuffer(data) ) { throw(new Error("Parameter error: 'data' should be a numeric array or buffer.")) } // We must have size that fit the expected data if ( data.length < 8 ) { throw(new Error("Parameter error: 'data' should have a length >= 8.")) } return data.readDoubleBE(0); }; /*! decodeMeasurementClass65 Decode a class 65 measurement. CLASS1.MEASUREZONE 0 - Index (Not sensor index) 1 - Zone 2 - subzone 3 - data coding 4-7 - Data with format defined by data coding byte. @param {number[]} data - Data array/buffer @return {number} Value as float */ var decodeMeasurementClass65 = function(data) { // If argument is array convert to buffer if ( Array.isArray(data) ) { data = Buffer.from(data); } // We must have a buffer if ( !Buffer.isBuffer(data) ) { throw(new Error("Parameter error: 'data' should be a numeric array or buffer.")) } // We must have size that fit the expected data if ( data.length < 5 ) { throw(new Error("Parameter error: 'data' should have a length >= 5.")) } var b = data.slice(3); return decodeMeasurementClass10(b); }; /*! decodeMeasurementClass70 CLASS1.MEASUREMENT32 Decode a class 70 measurement. Data is a 32-bit floating point value. @param {number[]} data - Data array/buffer @return {number} Value as float */ var decodeMeasurementClass70 = function(data) { // If argument is array convert to buffer if ( Array.isArray(data) ) { data = Buffer.from(data); } // We must have a buffer if ( !Buffer.isBuffer(data) ) { throw(new Error("Parameter error: 'data' should be a numeric array or buffer.")) } // We must have size that fit the expected data if ( data.length < 4 ) { throw(new Error("Parameter error: 'data' should have a length >= 4.")) } return data.readFloatBE(0); }; /*! decodeMeasurementClass85 CLASS1.SETVALUEZONE Decode a class 85 measurement (setvalue) Data is 0 - Sensor index 1 - Zone 2 - Subzone 3 - Data coding 4-7 - Data Value @param {number[]} data - Data array/buffer @return {number} Value as float */ var decodeMeasurementClass85 = function(data) { return decodeMeasurementClass65(data); }; /*! decodeMeasurementClass1040 Decode a class 1040 measurement CLASS2.MEASUREMENT_STR Data is measurement in string form. 0 - Sensor index 1 - Zone 2 - Subzone 3 - Unit 4.. - String up to the maximum data size of 483 digits including a possible decimal point. The decimal point should always be a "." independent of locale. @param {number[]} data - Data array/buffer @return {number} Value as float */ var decodeMeasurementClass1040 = function(data) { var str = ""; var i = 0; // If argument is array convert to buffer if ( Array.isArray(data) ) { data = Buffer.from(data); } // We must have a buffer if ( !Buffer.isBuffer(data) ) { throw(new Error("Parameter error: 'data' should be a numeric array or buffer.")) } // We must have size that fit the expected data if ( data.length < 4 ) { throw(new Error("Parameter error: 'data' should have a length >= 4.")) } for (i = 4; i < data.length; i++) { str += String.fromCharCode(data[i]); } return parseFloat(str); }; /*! decodeMeasurementClass1060 Decode a class 1060 measurement CLASS2.MEASUREMENT_FLOAT Data is measurement in floating point double form. 0 - Sensor index 1 - Zone 2 - Subzone 3 - Unit 4-11 - 64-bit double precision floating point value stored MSB first. @param {number[]} data - Data array/buffer @return {number} Value as float */ var decodeMeasurementClass1060 = function(data) { // If argument is array convert to buffer if ( Array.isArray(data) ) { data = Buffer.from(data); } // We must have a buffer if ( !Buffer.isBuffer(data) ) { throw(new Error("Parameter error: 'data' should be a numeric array or buffer.")) } // We must have size that fit the expected data if ( data.length < 12 ) { throw(new Error("Parameter error: 'data' should have a length >= 12.")) } return data.readDoubleBE(4); } /*! getMeasurementData Return measurement information including value for a measurement event. @param {object} e Measurement event object @return a measurement object on the following form { unit: {number} sensorindex: {number} datacoding: {number} index: {number} zone: {number} subzone: {number} value: {number} } Items that are not defined for a particular event is not returned and will be left undefined. This is typical for zone/subzone that is only available for a few of the measurement events. If the event is not a measurement event an empty object is returned. */ var getMeasurementData = function(e) { var rvobj = {}; // unit: {number}, // sensorindex: {number}, // datacoding: {number}, // index: {number}, // zone: {number}, // subzone: {number}, // value: {number} // Check parameters if ( typeof e !== 'object' ) { throw(new Error("Parameter error: 'e' should be a VSCP event object.")); } if ( !isMeasurement(e.vscpClass) ) { throw(new Error("Parameter error: 'e' should be a VSCP measurement event.")); } if ( ( (e.vscpClass >= vscp_class.VSCP_CLASS1_MEASUREMENT ) && (e.vscpClass <= vscp_class.VSCP_CLASS1_MEASUREMENTX4 ) ) ) { rvobj.datacoding = getDataCoding(e.vscpData[0]); rvobj.unit = getUnit(e.vscpData[0]); rvobj.sensorindex = getSensorIndex(e.vscpData[0]); rvobj.value = decodeMeasurementClass10(e.vscpData); } else if ( e.vscpClass == vscp_class.VSCP_CLASS1_DATA ) { rvobj.datacoding = getDataCoding(e.vscpData[0]); rvobj.unit = getUnit(e.vscpData[0]); rvobj.sensorindex = getSensorIndex(e.vscpData[0]); rvobj.value = decodeMeasurementClass10(e.vscpData); } else if ( (e.vscpClass >= vscp_class.VSCP_CLASS1_MEASUREMENT64 ) && (e.vscpClass <= vscp_class.VSCP_CLASS1_MEASUREMENT64X4 ) ) { // Always double, unit=0,sensorindex=0 rvobj.datacoding = measurementDataCoding.DATACODING_DOUBLE; rvobj.unit = 0; rvobj.sensorindex = 0; rvobj.value = decodeMeasurementClass60(e.vscpData); } else if ( (e.vscpClass >= vscp_class.VSCP_CLASS1_MEASUREZONE ) && (e.vscpClass <= vscp_class.VSCP_CLASS1_MEASUREZONEX4 ) ) { rvobj.datacoding = getDataCoding(e.vscpData[3]); rvobj.unit = getUnit(e.vscpData[3]); rvobj.sensorindex = getSensorIndex(e.vscpData[3]); rvobj.index = e.vscpData[0]; rvobj.zone = e.vscpData[1]; rvobj.subzone = e.vscpData[2]; rvobj.value = decodeMeasurementClass65(e.vscpData); } else if ( (e.vscpClass >= vscp_class.VSCP_CLASS1_MEASUREMENT32 ) && (e.vscpClass <= vscp_class.VSCP_CLASS1_MEASUREMENT32X4 ) ) { // A