UNPKG

iotile-common

Version:

Common utilities for IoTile Packages and Applications

549 lines 20.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var app_errors_1 = require("./app-errors"); /** * @ngdoc object * @name Utilities * * @description * The utilities namespace contains common routines that are used in other modules * including delays and generic parsing functions. */ /** * @ngdoc object * @name Utilities.function:endsWith * @description * Check if a string ends with another string * * * @param {string} str The input string * @param {string} suffix The suffix to check * @returns {bool} Whether the string ends with the suffix */ function endsWith(str, suffix) { return str.indexOf(suffix, str.length - suffix.length) !== -1; } exports.endsWith = endsWith; /** * @ngdoc object * @name Utilities.function:startsWith * @description * Check if a string ends with another string * * * @param {string} str The input string * @param {string} prefix The prefix to check * @returns {bool} Whether the string ends with the suffix */ function startsWith(str, prefix) { return str.indexOf(prefix, 0) == 0; } exports.startsWith = startsWith; function joinPath(path1, path2) { if (path1[path1.length - 1] !== '/') { path1 += '/'; } if (path2[0] == '/') { path2 = path2.substring(1); } return path1 + path2; } exports.joinPath = joinPath; //From https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript function guid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } exports.guid = guid; /** * @ngdoc object * @name Utilities.function:delay * @description * Delay for a fixed number of milliseconds * * This function wraps setTimeout in a promise API that can be * used with async/await. * * @param {number} delayMS - The number of milliseconds to wait * @returns {Promise} A promise that is fullfilled after delayMS milliseconds */ function delay(delayMS) { return new Promise(function (resolve) { function doResolve() { resolve(); } setTimeout(doResolve, delayMS); }); } exports.delay = delay; /** * @ngdoc object * @name Utilities.function:deviceIDToSlug * @description * Convert a numeric deviceID to an IOTile cloud device slug * * This function converts a device id like 0x20 to a string slug of * the form: * d--XXXX-XXXX-XXXX-XXXX * * The slug always has the hex string in lowercase. * * @param {number} deviceID - The device ID to convert into a slug * @returns {string} The corresponding device slug */ function deviceIDToSlug(deviceID) { var hexString = Number(deviceID).toString(16); while (hexString.length < 16) { hexString = '0' + hexString; } hexString = hexString.toLowerCase(); return 'd--' + hexString.substr(0, 4) + '-' + hexString.substr(4, 4) + '-' + hexString.substr(8, 4) + '-' + hexString.substr(12, 4); } exports.deviceIDToSlug = deviceIDToSlug; /** * @ngdoc object * @name Utilities.function:createStreamerSlug * @description * Convert a numeric deviceID and streamer index to an IOTile cloud streamer slug * * This function converts a device id like 0x20 and streamer 0 to a string slug of * the form: * t--XXXX-XXXX-XXXX-XXXX--YYYY * * The slug always has the hex string in lowercase. The device id is converted * into the XXXX portion and the streamer is converted to the YYYY portion. * * @param {number} deviceID - The device ID to convert into a slug * @param {number} streamer - The streamer index to convert into a slug * @returns {string} The corresponding streamer slug */ function createStreamerSlug(deviceID, streamer) { var deviceString = numberToHexString(deviceID, 16); var streamerString = numberToHexString(streamer, 4); return 't--' + deviceString.substr(0, 4) + '-' + deviceString.substr(4, 4) + '-' + deviceString.substr(8, 4) + '-' + deviceString.substr(12, 4) + '--' + streamerString; } exports.createStreamerSlug = createStreamerSlug; /** * @ngdoc object * @name Utilities.function:numberToHexString * @description * Convert a number to lowercase hex string * * This function takes a number like 0x16 and returns * the string '16'. It would also take 0xAF and return * 'af'. It will pad the number out to a fixed length * using the second length parameter. * * The slug always has the hex string in lowercase. * * @param {number} inputNumber - The number to convert to a hex string * @param {number} length - The number of hex digits to pad out to. * @returns {string} The correspond lowercase hex string */ function numberToHexString(inputNumber, length) { var hexString = Number(inputNumber).toString(16); while (hexString.length < length) { hexString = '0' + hexString; } hexString = hexString.toLowerCase(); return hexString; } exports.numberToHexString = numberToHexString; /** * @ngdoc object * @name Utilities.function:mapStreamName * @description * Convert a string description of an IOTile variable into a number * * This function converts string like 'output 1' into 16 bit integers * like 0x5001. * * @param {string} streamName - The string name of the variable that you want to convert * @returns {number} The numerical stream identifier */ function mapStreamName(streamName) { var knownStreams = { 'buffered node': 0, 'unbuffered node': 1, 'constant': 2, 'input': 3, 'counter': 4, 'output': 5 }; var system = 0; var parts = streamName.split(' '); if (parts[0] === 'system') { system = 1; parts = parts.slice(1); } var name = parts.slice(0, parts.length - 1).join(' '); var id = parseInt(parts[parts.length - 1]); if (!(name in knownStreams)) { throw new app_errors_1.ArgumentError('Unknown stream name: ' + name); } var streamType = knownStreams[name]; return (streamType << 12) | (system << 11) | id; } exports.mapStreamName = mapStreamName; /** * @ngdoc object * @name Utilities.function:parseBufferFormatCode * @description * Parse a string format code describing the packing of a binary buffer * into an array of entries where each entry has a type code and a count * prefix. For example, * * "18sH" would turn into [{count: 18, code: 's'}, {count: 8, code: 'H'}] * * ## See Also * {@link Utilities.function:packArrayBuffer Utilities.packArrayBuffer} * * {@link Utilities.function:unpackArrayBuffer Utilities.unpackArrayBuffer} * * @param {string} fmt - The format we are trying to determine the size of * @returns {[FormatCode]} A list of the parsed format codes that were extracted * from the input format string. */ function parseBufferFormatCode(fmt) { var parsed = []; var i; var count = 0; //For accumulating counts like 18s //Calculate expected size for (i = 0; i < fmt.length; ++i) { if (fmt[i] >= '0' && fmt[i] <= '9') { count *= 10; count += parseInt(fmt[i]); } else { switch (fmt[i]) { case 'B': if (count !== 0) { throw new app_errors_1.ArgumentError('Invalid count in format code that does not take a count: count = ' + count); } parsed.push({ count: 0, code: 'B', size: 1 }); break; case 'H': if (count !== 0) { throw new app_errors_1.ArgumentError('Invalid count in format code that does not take a count: count = ' + count); } parsed.push({ count: 0, code: 'H', size: 2 }); break; case 'L': if (count !== 0) { throw new app_errors_1.ArgumentError('Invalid count in format code that does not take a count: count = ' + count); } parsed.push({ count: 0, code: 'L', size: 4 }); break; case 'l': if (count !== 0) { throw new app_errors_1.ArgumentError('Invalid count in format code that does not take a count: count = ' + count); } parsed.push({ count: 0, code: 'l', size: 4 }); break; case 's': if (count === 0) { throw new app_errors_1.ArgumentError('Invalid count in string that should be prefixed with a count: count = ' + count); } parsed.push({ count: count, code: 's', size: count }); break; default: throw new app_errors_1.ArgumentError('Unknown format code in expectedBufferSize: ' + fmt[i]); } count = 0; } } if (count != 0) { throw new app_errors_1.ArgumentError("Format code ended in a number: " + fmt); } return parsed; } exports.parseBufferFormatCode = parseBufferFormatCode; /** * @ngdoc object * @name Utilities.function:padString * @description * Pad a string by appended a given character until it reaches a fixed length * * @param {string} input The string we are trying to pad. * @param {string} pad The padding character to add. * @param {number} length The length of the final string you want. * @returns {string} The correctgly padded string. */ function padString(input, pad, length) { if (input.length === length) { return input; } if (input.length > length) { throw new app_errors_1.ArgumentError("String passed to padString is longer than the desired length: string = " + input); } while (input.length < length) { input += pad; } return input; } exports.padString = padString; /** * @ngdoc object * @name Utilities.function:expectedBufferSize * @description * Determine how large a buffer is given its binary format string * * This function takes a string describing how fixed width integers are packed * into a binary ArrayBuffer and calculates how large the buffer would need to * be to contain that many integers of those sizes. It also support packing * fixed length strings that must be prefixed with a number like 18s for an * exactly 18 character string. * * Alignment is not taken into account, so if you are trying to match the alignment * of a structure on, e.g. a 32 bit platform, you will need to insert alignment gaps as needed. * * ## See Also * {@link Utilities.function:packArrayBuffer Utilities.packArrayBuffer} * * {@link Utilities.function:unpackArrayBuffer Utilities.unpackArrayBuffer} * * @param {string} fmt - The format we are trying to determine the size of * @returns {number} The number of bytes required to store fmt */ function expectedBufferSize(fmt) { var size = 0; var parsed = parseBufferFormatCode(fmt); var i; var count = 0; //For accumulating counts like 18s //Calculate expected size for (i = 0; i < parsed.length; ++i) { size += parsed[i].size; } return size; } exports.expectedBufferSize = expectedBufferSize; /** * @ngdoc object * @name Utilities.function:packArrayBuffer * @description * Pack a series of arguments into an ArrayBuffer using a format string * * This function is a javascript equivalent of the python struct.pack function. * It takes a format string consisting of the letters l, L, B and H and a variable * list of numeric arguments. There must be exactly as many arguments as letters * in the format string. The format string is used to convert each argument into * a little endian binary representation of the number which is serialized into * an ArrayBuffer. The resulting ArrayBuffer is returned. * * The meaning of each format code is: * - B: An 8 bit wide unsigned integer * - H: A 16 bit wide unsigned integer * - L: A 32 bit wide unsigned integer * - l: A 32 bit wide signed integer * - #s: A fixed length string with length given by the number preceding s, e.g. 5s for a 5 * character string. If the string argument is shorter than what is specified, it is padded * with null characters. * * ## Exceptions * - **{@link type:ArgumentError} If there is an unknown format string code or the string * does not match the number or type of arguments received. * * @param {string} fmt The format string specifying the size of each argument * @param {number[]} arguments A variable list of numberic arguments that are packed to * create the resulting ArrayBuffer according to fmt. * @returns {ArrayBuffer} The packed resulting binary array buffer */ function packArrayBuffer(fmt) { var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } var parsed = parseBufferFormatCode(fmt); var size = expectedBufferSize(fmt); if (arguments.length !== (parsed.length + 1)) { throw new app_errors_1.ArgumentError('packArrayBuffer called with the wrong number of arguments for the format string'); } var arrayBuffer = new ArrayBuffer(size); var view = new DataView(arrayBuffer); //Fill in all the data (always little endian format) var offset = 0; for (var i = 0; i < parsed.length; ++i) { var curr = parsed[i]; var arg = arguments[i + 1]; switch (curr.code) { case 'B': if ((arguments[i + 1] <= 0xFF) && (arguments[i + 1] >= 0)) { view.setUint8(offset, arguments[i + 1]); offset += 1; } else { throw new app_errors_1.ArgumentError("Value must be a valid unsigned 8 bit integer"); } break; case 'H': if ((arguments[i + 1] <= 0xFFFF) && (arguments[i + 1] >= 0)) { view.setUint16(offset, arguments[i + 1], true); offset += 2; } else { throw new app_errors_1.ArgumentError("Value must be a valid unsigned 16 bit integer"); } break; case 'L': if ((arguments[i + 1] <= 0xFFFFFFFF) && (arguments[i + 1] >= 0)) { view.setUint32(offset, arguments[i + 1], true); offset += 4; } else { throw new app_errors_1.ArgumentError("Value must be a valid unsigned 32 bit integer"); } break; case 'l': if ((arguments[i + 1] <= 0x7FFFFFFF) && (arguments[i + 1] >= -2147483648)) { view.setInt32(offset, arguments[i + 1], true); offset += 4; } else { throw new app_errors_1.ArgumentError("Value must be a valid signed 32 bit integer"); } break; case 's': //If required add padding with nulls out to the fixed length specified arg = padString(arg, '\0', curr.size); for (var j = 0; j < curr.size; ++j) { view.setUint8(offset++, arg.charCodeAt(j)); } break; default: throw new app_errors_1.ArgumentError('Unknown format code in packArrayBuffer: ' + fmt[i]); } } return arrayBuffer; } exports.packArrayBuffer = packArrayBuffer; /** * @ngdoc object * @name Utilities.function:unpackArrayBuffer * @description * Unpack an ArrayBuffer into a list of numeric values using a format string * * This function is a javascript equivalent of the python struct.unpack function. * It takes a format string consisting of the letters l, L, B and H and a single ArrayBuffer. * The format string is used to decode the ArrayBuffer into a list of numbers assuming * that those numbers are encoded into fixed width integers in little endian format in * the ArrayBuffer. * * The meaning of each format code is: * - B: An 8 bit wide unsigned integer * - H: A 16 bit wide unsigned integer * - L: A 32 bit wide unsigned integer * - l: A 32 bit wide signed integer * - #s: A fixed length string. # should be a decimal number, e.g. 5s or 18s * * ## Exceptions * - **{@link type:ArgumentError} If there is an unknown format string code or the string * does not match the data contained inside the ArrayBuffer. * * @param {string} fmt The format string specifying the size of each argument * @param {ArrayBuffer} buffer The packed ArrayBuffer that should be decoded using fmt * @returns {number[]} A list of numbers decoded from the buffer using fmt */ function unpackArrayBuffer(fmt, buffer) { var size = expectedBufferSize(fmt); var parsed = parseBufferFormatCode(fmt); var i; if (size !== buffer.byteLength) { throw new app_errors_1.ArgumentError('unpackArrayBuffer called on buffer with invalid size'); } var view = new DataView(buffer); var args = []; //Fill in all the data (always little endian format) var offset = 0; var val; for (i = 0; i < parsed.length; ++i) { var entry = parsed[i]; var stringData = void 0; switch (entry.code) { case 'B': val = view.getUint8(offset); offset += 1; break; case 'H': val = view.getUint16(offset, true); offset += 2; break; case 'L': val = view.getUint32(offset, true); offset += 4; break; case 'l': val = view.getInt32(offset, true); offset += 4; break; case 's': stringData = new Uint8Array(buffer.slice(offset, offset + entry.size)); val = String.fromCharCode.apply(null, stringData); offset += entry.size; break; default: throw new app_errors_1.ArgumentError('Unknown format code in packArrayBuffer: ' + fmt[i]); } args.push(val); } return args; } exports.unpackArrayBuffer = unpackArrayBuffer; /** * @ngdoc object * @name Utilities.function:copyArrayBuffer * @description * Copy an ArrayBuffer into another one like memcpy * * This function is a javascript translation of memcpy. It takes a source and destination * ArrayBuffer, an offset into both and a length of bytes to copy. In slicing syntax, * this function does the following: * * dest[destOffset:destOffset+length] = src[srcOffset:srcOffset+length] * * * ## Exceptions * - **{@link type:InsufficientSpaceError InsufficentSpaceError}:** If there is not space in the destination buffer * to hold the copied data. This function will not expand the size of the destination buffer, so it must already be allocated * with enough space for the copied data. * * @param {ArrayBuffer} dest The destination buffer that we should copy into. There must be enough * space in dest to hold what you are copying. This function will not allocate * more space for you. * @param {ArrayBuffer} src The source buffer to copy from * @param {number} srcOffset The offset in src to start copying from, 0 would mean copy from the beginning * @param {number} destOffset The offset in dest to start copying into, 0 would mean to copy to the beginning * of dest. * @param {number} length The number of bytes to copy from src into dest. * @throws {InsufficientSpaceError} If there is not space in the destination buffer to hold the copied data. */ function copyArrayBuffer(dest, src, srcOffset, destOffset, length) { var srcArray = new Uint8Array(src, srcOffset, length); var dstArray = new Uint8Array(dest, 0); if ((destOffset + length) > dest.byteLength) { throw new app_errors_1.InsufficientSpaceError('Attempting to copy an ArrayBuffer without enough space in destination'); } dstArray.set(srcArray, destOffset); } exports.copyArrayBuffer = copyArrayBuffer; /** * @ngdoc object * @name Utilities.object:base64ToArrayBuffer * @description * Decode a Base 64 encoded string into an ArrayBuffer * * @param {string} encodedString The base 64 encoded string * @returns {ArrayBuffer} The decoded ArrayBuffer */ function base64ToArrayBuffer(encodedString) { var raw = window.atob(encodedString); var rawLength = raw.length; var rawArray = new ArrayBuffer(rawLength); var array = new Uint8Array(rawArray); for (var i = 0; i < rawLength; i++) { array[i] = raw.charCodeAt(i); } return rawArray; } exports.base64ToArrayBuffer = base64ToArrayBuffer; //# sourceMappingURL=utilities.js.map