@iotile/iotile-common
Version:
Common utilities for IoTile Packages and Applications
720 lines • 27.4 kB
JavaScript
"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:convertVariableLengthFormatCode
* @description
* Convert the variable length byte array format code ('V') into a fixed length
* format code ('#s') based on the size of the provided buffer and whether or not this
* is for packing (buffer is the last argument to pack) or unpacking (buffer is the entire
* response).
*
* ## See Also
* {@link Utilities.function:packArrayBuffer Utilities.packArrayBuffer}
*
* {@link Utilities.function:unpackArrayBuffer Utilities.unpackArrayBuffer}
*
* @param {string} fmt - The format code to convert
* @param {ArrayBuffer} buffer - Either the variable length arg to pack or the response buffer
* @param {boolean} pack - Whether this is for packing (true) or unpacking (false)
*
* @returns {string} - The converted format code
*/
function convertVariableLengthFormatCode(fmt, buffer, pack) {
if (fmt[fmt.length - 1] !== 'V') {
return fmt;
}
fmt = fmt.slice(0, -1);
var fixedSize = expectedBufferSize(fmt);
var varSize = pack ? buffer.byteLength : buffer.byteLength - fixedSize;
if (varSize > 0) {
fmt = fmt + varSize + 's';
}
return fmt;
}
exports.convertVariableLengthFormatCode = convertVariableLengthFormatCode;
/**
* @ngdoc object
* @name Utilities.function:toUint32
* @description
* Takes a number and returns the value as a Uint32
* See: http://2ality.com/2012/02/js-integers.html
* See also: https://stackoverflow.com/questions/22335853/hack-to-convert-javascript-number-to-uint32
*
* @param {number} num - The number to convert
*
* @returns {Uint32}
*/
function toUint32(num) {
return num >>> 0;
}
exports.toUint32 = toUint32;
/**
* @ngdoc object
* @name Utilities.function:stringToBuffer
* @description
* Takes a string and returns an ArrayBuffer containing each character's
* char code value.
*
* @param {string} str - The string to convert
*
* @returns {ArrayBuffer}
*/
function stringToBuffer(str) {
var buffer = new Uint8Array(str.length);
for (var i = 0; i < str.length; i++) {
buffer[i] = str.charCodeAt(i);
}
return buffer.buffer;
}
exports.stringToBuffer = stringToBuffer;
/**
* @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, argumentsConsumed: 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, argumentsConsumed: 1 });
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, argumentsConsumed: 1 });
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, argumentsConsumed: 1 });
break;
case 'x':
var size = Math.max(count, 1);
parsed.push({ count: count, code: 'x', size: size, argumentsConsumed: 0 });
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, argumentsConsumed: 1 });
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:padArrayBuffer
* @description
* Pad a string by appending null bytes to meet a fixed length
*
* @param {ArrayBuffer} input The buffer we are trying to pad.
* @param {number} length The length of the final buffer you want.
* @returns {ArrayBuffer} The correctly padded ArrayBuffer.
*/
function padArrayBuffer(input, length) {
if (input.byteLength === length) {
return input;
}
if (input.byteLength > length) {
throw new app_errors_1.ArgumentError("ArrayBuffer passed to padArrayBuffer is longer than the desired length: ArrayBuffer = " + input);
}
return appendArrayBuffer(input, new ArrayBuffer(length - input.byteLength));
}
exports.padArrayBuffer = padArrayBuffer;
/**
* @ngdoc object
* @name Utilities.function:appendArrayBuffer
* @description
* Append one ArrayBuffer to the end of another.
*
* @param {ArrayBuffer} buffer1 The first ArrayBuffer.
* @param {ArrayBuffer} buffer2 The second ArrayBuffer.
* @returns {ArrayBuffer} The resulting combined ArrayBuffer.
*/
function appendArrayBuffer(buffer1, buffer2) {
var result = new ArrayBuffer(buffer1.byteLength + buffer2.byteLength);
copyArrayBuffer(result, buffer1, 0, 0, buffer1.byteLength);
copyArrayBuffer(result, buffer2, 0, buffer1.byteLength, buffer2.byteLength);
return result;
}
exports.appendArrayBuffer = appendArrayBuffer;
/**
* @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;
//Calculate expected size
for (i = 0; i < parsed.length; ++i) {
size += parsed[i].size;
}
return size;
}
exports.expectedBufferSize = expectedBufferSize;
function expectedArraySize(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].argumentsConsumed;
}
return size;
}
exports.expectedArraySize = expectedArraySize;
/**
* @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.
* UPDATE: As of v0.2 '#s' may also represent an ArrayBuffer with a bytelength given by the
* number preceding 's'. If the bytelength of the ArrayBuffer argument is shorter than what
* is specified, the ArrayBuffer will NOT be padded and an error will be thrown.
* - V: A variable length byte array. This must come as the last format code
*
* ## 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];
}
if (fmt[fmt.length - 1] === 'V') {
fmt = convertVariableLengthFormatCode(fmt, args[args.length - 1], true);
}
var parsed = parseBufferFormatCode(fmt);
var size = expectedBufferSize(fmt);
var argsConsumed = expectedArraySize(fmt);
if (arguments.length !== (argsConsumed + 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;
var arg_idx = 0;
for (var i = 0; i < parsed.length; ++i) {
var curr = parsed[i];
var arg = arguments[arg_idx + 1];
switch (curr.code) {
case 'B':
if ((arguments[arg_idx + 1] <= 0xFF) && (arguments[arg_idx + 1] >= 0)) {
view.setUint8(offset, arguments[arg_idx + 1]);
offset += 1;
arg_idx += 1;
}
else {
throw new app_errors_1.ArgumentError("Value must be a valid unsigned 8 bit integer");
}
break;
case 'H':
if ((arguments[arg_idx + 1] <= 0xFFFF) && (arguments[arg_idx + 1] >= 0)) {
view.setUint16(offset, arguments[arg_idx + 1], true);
offset += 2;
arg_idx += 1;
}
else {
throw new app_errors_1.ArgumentError("Value must be a valid unsigned 16 bit integer");
}
break;
case 'L':
if ((arguments[arg_idx + 1] <= 0xFFFFFFFF) && (arguments[arg_idx + 1] >= 0)) {
view.setUint32(offset, arguments[arg_idx + 1], true);
offset += 4;
arg_idx += 1;
}
else {
throw new app_errors_1.ArgumentError("Value must be a valid unsigned 32 bit integer");
}
break;
case 'l':
if ((arguments[arg_idx + 1] <= 0x7FFFFFFF) && (arguments[arg_idx + 1] >= -2147483648)) {
view.setInt32(offset, arguments[arg_idx + 1], true);
offset += 4;
arg_idx += 1;
}
else {
throw new app_errors_1.ArgumentError("Value must be a valid signed 32 bit integer");
}
break;
case 'x':
for (var j = 0; j < curr.size; ++j) {
view.setUint8(offset++, 0);
}
break;
case 's':
if (typeof arg === 'string') {
//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));
}
}
else if (arg instanceof ArrayBuffer) {
if (arg.byteLength !== curr.size) {
throw new app_errors_1.ArgumentError("ArrayBuffer size does not match format code: expected=" + curr.size + ", actual=" + arg.byteLength);
}
copyArrayBuffer(arrayBuffer, arg, 0, offset, curr.size);
offset += curr.size;
}
arg_idx += 1;
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
* - [#]x: one or more padding bytes
* - #s: A fixed length string. # should be a decimal number, e.g. 5s or 18s
* - V: A variable length byte array. This must come as the last format code.
* NOTE: Data upacked from a 'V' code will be returned as a string.
*
* ## 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) {
fmt = convertVariableLengthFormatCode(fmt, buffer, false);
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 'x':
val = undefined;
offset += Math.max(entry.count, 1);
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]);
}
if (val != undefined) {
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;
/**
* @ngdoc object
* @name Utilities.object:arrayBufferToBase64
* @description
* Encode an ArrayBuffer to a Base 64 encoded string
*
* @param {ArrayBuffer} buffer The ArrayBuffer
* @returns {ArrayBuffer} The Base 64 encoded string
*/
function arrayBufferToBase64(buffer) {
return window.btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
}
exports.arrayBufferToBase64 = arrayBufferToBase64;
//# sourceMappingURL=utilities.js.map