iotile-common
Version:
Common utilities for IoTile Packages and Applications
549 lines • 20.9 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: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