@iotile/iotile-device
Version:
A typescript library for interfacing with IOTile BLE devices
1,018 lines (997 loc) • 1.29 MB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@iotile/iotile-common'), require('typescript-logging'), require('msgpack-lite')) :
typeof define === 'function' && define.amd ? define(['exports', '@iotile/iotile-common', 'typescript-logging', 'msgpack-lite'], factory) :
(global = global || self, factory(global.iotileDevice = {}, global.Utilities, global.typescriptLogging, global.msgpackLite));
}(this, function (exports, Utilities, typescriptLogging, msgpackLite) { 'use strict';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = function(d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
function __decorate(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
function __metadata(metadataKey, metadataValue) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
}
function __awaiter(thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
(function (RPCProtocolError) {
RPCProtocolError[RPCProtocolError["CommandNotFound"] = 2] = "CommandNotFound";
})(exports.RPCProtocolError || (exports.RPCProtocolError = {}));
/**
* @ngdoc object
* @name Errors.type:RPCError
* @description
* There was an error executing an RPC on an IOTile Device. Information
* about the error is contained in the attached properties.
*
* @property {number} address The address of the tile that the RPC was directed toward
* @property {number} rpcID the 16-bit ID of the RPC that we were trying to
* @property {number} errorCode the numeric error code that was returned during RPC processing
*/
var RPCError = /** @class */ (function (_super) {
__extends(RPCError, _super);
function RPCError(address, rpcID, errorCode) {
var _this = this;
var message = 'Error sending RPC to Tile ' + address + ' with ID ' + rpcID + ', code was ' + errorCode;
// @ts-ignore
_this = _super.call(this, 'RPCError', message) || this;
_this.address = address;
_this.rpcID = rpcID;
_this.errorCode = errorCode;
return _this;
}
return RPCError;
}(Utilities.BaseError));
var StreamingError = /** @class */ (function (_super) {
__extends(StreamingError, _super);
function StreamingError(name, message) {
// @ts-ignore
return _super.call(this, name, message) || this;
}
return StreamingError;
}(Utilities.BaseError));
/**
* @ngdoc object
* @name Errors.type:WifiConfigError
* @description
* Error configuring wifi, usually while setting static IP.
*/
var WifiConfigError = /** @class */ (function (_super) {
__extends(WifiConfigError, _super);
function WifiConfigError(message, staticIp, dns, netmask, gateway) {
if (staticIp === void 0) { staticIp = null; }
if (dns === void 0) { dns = null; }
if (netmask === void 0) { netmask = null; }
if (gateway === void 0) { gateway = null; }
var _this = _super.call(this, 'WifiConfigError', message) || this;
_this.staticIp = staticIp;
_this.dns = dns;
_this.netmask = netmask;
_this.gateway = gateway;
return _this;
}
return WifiConfigError;
}(Utilities.BaseError));
/**
* @ngdoc object
* @name Errors.type:EthernetConfigError
* @description
* Error configuring ethernet, usually while setting static IP.
*/
var EthernetConfigError = /** @class */ (function (_super) {
__extends(EthernetConfigError, _super);
function EthernetConfigError(message, staticIp, dns, netmask, gateway) {
if (staticIp === void 0) { staticIp = null; }
if (dns === void 0) { dns = null; }
if (netmask === void 0) { netmask = null; }
if (gateway === void 0) { gateway = null; }
var _this = _super.call(this, 'EthernetConfigError', message) || this;
_this.staticIp = staticIp;
_this.dns = dns;
_this.netmask = netmask;
_this.gateway = gateway;
return _this;
}
return EthernetConfigError;
}(Utilities.BaseError));
/**
* @ngdoc object
* @name Errors.type:ReportParsingStoppedError
* @description
* Report parsing has been stopped due to a previous unrecoverable error.
* No more reports will be processed from the IOTile device until you reconnect to
* it to in order to reset the ReportParser.
*/
var ReportParsingStoppedError = /** @class */ (function (_super) {
__extends(ReportParsingStoppedError, _super);
function ReportParsingStoppedError(message) {
return _super.call(this, 'ReportParsingStoppedError', message) || this;
}
return ReportParsingStoppedError;
}(StreamingError));
var StreamingTimeoutError = /** @class */ (function (_super) {
__extends(StreamingTimeoutError, _super);
function StreamingTimeoutError(message) {
return _super.call(this, 'StreamingTimeoutError', message) || this;
}
return StreamingTimeoutError;
}(StreamingError));
/**
* @ngdoc object
* @name Errors.type:ReportParsingError
* @description
* A fatal error has occurs processing report data received from the IOTile device.
* No further data will be accepted from the device since it cannot be parsed correctly
* after this error. You will need to disconnect and reconnect to the device before
* more data will be accepted.
*/
var ReportParsingError = /** @class */ (function (_super) {
__extends(ReportParsingError, _super);
function ReportParsingError(message) {
return _super.call(this, 'ReportParsingError', message) || this;
}
return ReportParsingError;
}(StreamingError));
/**
* @ngdoc object
* @name Errors.type:FatalStreamingError
* @description
* A generic fatal error during report reception with a simple user facing message
* and a technical explanation.
*/
var FatalStreamingError = /** @class */ (function (_super) {
__extends(FatalStreamingError, _super);
function FatalStreamingError(message, userMessage) {
var _this = _super.call(this, "FatalStreamingError", message) || this;
_this.userMessage = userMessage;
return _this;
}
return FatalStreamingError;
}(StreamingError));
/**
* @ngdoc object
* @name Errors.type:RingBufferEmptyError
* @description
* You requested to pop/peek at more data than was currrently present in the ring buffer.
* Depending on your use case, this may not be a fatal error and you should catch it and
* try again later.
*/
var RingBufferEmptyError = /** @class */ (function (_super) {
__extends(RingBufferEmptyError, _super);
function RingBufferEmptyError(message) {
return _super.call(this, 'RingBufferEmptyError', message) || this;
}
return RingBufferEmptyError;
}(StreamingError));
var BluetoothError = /** @class */ (function (_super) {
__extends(BluetoothError, _super);
function BluetoothError(name, message) {
// @ts-ignore
return _super.call(this, name, message) || this;
}
return BluetoothError;
}(Utilities.BaseError));
/**
* @ngdoc object
* @name Errors.type:ConnectionError
* @description
* There was an error connecting to the IOTile device
*/
var ConnectionError = /** @class */ (function (_super) {
__extends(ConnectionError, _super);
function ConnectionError(message) {
return _super.call(this, 'ConnectionError', message) || this;
}
return ConnectionError;
}(BluetoothError));
/**
* @ngdoc object
* @name Errors.type:ConnectionCancelledError
* @description
* The connection attempt to a device was canceled due to either a
* user action or a preconnection hook indicating that connection
* was not desired. The info property has more details about what
* should be done.
*/
var ConnectionCancelledError = /** @class */ (function (_super) {
__extends(ConnectionCancelledError, _super);
function ConnectionCancelledError(redirect) {
var _this = _super.call(this, 'ConnectionCancelledError', redirect.reason) || this;
_this.info = redirect;
return _this;
}
return ConnectionCancelledError;
}(BluetoothError));
/**
* @ngdoc object
* @name Errors.type:ConnectionFailedError
* @description
* The connection attempt to a device was canceled due to either a
* user action or a preconnection hook indicating that connection
* was not desired. The info property has more details about what
* should be done.
*/
var ConnectionFailedError = /** @class */ (function (_super) {
__extends(ConnectionFailedError, _super);
function ConnectionFailedError(err) {
var _this = _super.call(this, 'ConnectionFailedError', "Connection attempt failed. This occassionally happens, please try connecting again.") || this;
_this.rawError = err;
return _this;
}
return ConnectionFailedError;
}(BluetoothError));
/**
* @ngdoc object
* @name Errors.type:WriteError
* @description
* There was an error writing data to the IOTile device over
* Bluetooth, this is a low level fatal error that it unrecoverable.
*/
var WriteError = /** @class */ (function (_super) {
__extends(WriteError, _super);
function WriteError(message) {
return _super.call(this, 'WriteError', message) || this;
}
return WriteError;
}(BluetoothError));
/**
* @ngdoc object
* @name Errors.type:BluetoothDisabledError
* @description
* You attempted to complete a bluetooth related operation that failed
* because Bluetooth is not enabled on the user's device.
*/
var BluetoothDisabledError = /** @class */ (function (_super) {
__extends(BluetoothDisabledError, _super);
function BluetoothDisabledError(message) {
return _super.call(this, 'BluetoothDisabledError', message) || this;
}
return BluetoothDisabledError;
}(BluetoothError));
/**
* @ngdoc object
* @name Errors.type:OperationAtInvalidTimeError
* @description
* You attempted to perform an operation over bluetooth that could not
* be completed because the IOTile device is in the wrong state. For example,
* you cannot send an RPC until after you connect to a device. The message
* field will have more information on what went wrong.
*
* @property {number} state The state that we expected to be in for this operation
* to be possible.
*/
var OperationAtInvalidTimeError = /** @class */ (function (_super) {
__extends(OperationAtInvalidTimeError, _super);
function OperationAtInvalidTimeError(message, state, userMessage) {
var _this = _super.call(this, 'OperationAtInvalidTimeError', message) || this;
_this.state = state;
_this.userMessage = userMessage || "";
return _this;
}
return OperationAtInvalidTimeError;
}(BluetoothError));
/**
* @ngdoc object
* @name Errors.type:ScriptSentAtInvalidTime
* @description
* You attempted to send a script to an IOTile device while another script was
* in the process of running. You have to wait until a script finishes before
* sending another one.
*/
var ScriptSentAtInvalidTime = /** @class */ (function (_super) {
__extends(ScriptSentAtInvalidTime, _super);
function ScriptSentAtInvalidTime(message) {
return _super.call(this, 'ScriptSentAtInvalidTime', message) || this;
}
return ScriptSentAtInvalidTime;
}(BluetoothError));
/**
* @ngdoc object
* @name Errors.type:InvalidAdvertisingData
* @description
* An advertising packet was received from a non IOTile device. This error is Typically
* not unexpected or fatal. It just means that you should ignore the device that sent this
* advertising packet because it is not an IOTile device.
*/
var InvalidAdvertisingData = /** @class */ (function (_super) {
__extends(InvalidAdvertisingData, _super);
function InvalidAdvertisingData(message) {
return _super.call(this, 'InvalidAdvertisingData', message) || this;
}
return InvalidAdvertisingData;
}(BluetoothError));
(function (BridgeEnum) {
BridgeEnum[BridgeEnum["Connection"] = 1] = "Connection";
BridgeEnum[BridgeEnum["Address"] = 2] = "Address";
BridgeEnum[BridgeEnum["RemoteUsername"] = 4] = "RemoteUsername";
BridgeEnum[BridgeEnum["RemotePassword"] = 5] = "RemotePassword";
BridgeEnum[BridgeEnum["Topic"] = 6] = "Topic";
BridgeEnum[BridgeEnum["TryPrivate"] = 7] = "TryPrivate";
BridgeEnum[BridgeEnum["TLS"] = 8] = "TLS";
})(exports.BridgeEnum || (exports.BridgeEnum = {}));
(function (TLSEnum) {
TLSEnum[TLSEnum["CAFile"] = 0] = "CAFile";
TLSEnum[TLSEnum["CertFile"] = 1] = "CertFile";
TLSEnum[TLSEnum["KeyFile"] = 2] = "KeyFile";
})(exports.TLSEnum || (exports.TLSEnum = {}));
/**
* A proxy object to configure a local MQTT broker that is in bridge mode
*/
var MQTTBridgeConfig = /** @class */ (function () {
function MQTTBridgeConfig(adapter, address) {
this.adapter = adapter;
this.address = address;
}
/**
* Brokers currently supported must be running with anonymous connections allowed,
or require a username and/or password for auth. If you are using TLS, you should
have sent the required files over using the TLS methods in this proxy first.
Please note that we currently only support a single TLS connected broker. If you
overwrite the TLS files that another connection is using, you will break it.
You can add connections that have either plain or password authentication in any quantity.
* @param {string} name Unique connection name.
* @param {string} url IP or URL that the bridged broker is running on
* @param {number} port Port used by the bridged broker. Defaults to 1883. Must be numeric.
* @param {string} remoteUsername Optional username if the bridge requires one
* @param {string} remotePassword Optional password if the bridge requires one
* @param {boolean} tls Optionally set TLS mode. Send certificates first.
*/
MQTTBridgeConfig.prototype.addBridge = function (name, url, port, remoteUsername, remotePassword, tls) {
if (port === void 0) { port = 1883; }
if (remoteUsername === void 0) { remoteUsername = null; }
if (remotePassword === void 0) { remotePassword = null; }
if (tls === void 0) { tls = null; }
return __awaiter(this, void 0, void 0, function () {
var offset, done, end, err_1, err_2;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (typeof port !== 'number') {
throw new Utilities.ArgumentError('Error, your port must contain only numbers. Please try again.');
}
return [4 /*yield*/, this._clearPendingBridge()
// Add name
];
case 1:
_a.sent();
offset = 0;
done = 0;
_a.label = 2;
case 2:
if (!!done) return [3 /*break*/, 7];
end = Math.min(offset + 19, name.length);
if (end === name.length) {
done = 1;
}
_a.label = 3;
case 3:
_a.trys.push([3, 5, , 6]);
return [4 /*yield*/, this.adapter.errorHandlingRPC(this.address, 0xAA02, 'BV', 'L', [done, Utilities.stringToBuffer(name.slice(offset, end))])];
case 4:
_a.sent();
return [3 /*break*/, 6];
case 5:
err_1 = _a.sent();
if (err_1 instanceof RPCError && err_1.errorCode === 2) {
throw new Utilities.ArgumentError('Error, that connection name is already taken, please try again.');
}
else {
throw err_1;
}
return [3 /*break*/, 6];
case 6:
offset += 19;
return [3 /*break*/, 2];
case 7:
// Add url:port combo
return [4 /*yield*/, this._buildBridge(exports.BridgeEnum.Address, url + ':' + port)];
case 8:
// Add url:port combo
_a.sent();
if ((tls && remoteUsername) || (tls && remotePassword)) {
throw new Utilities.ArgumentError('Cannot set both TLS and login mode');
}
if (!remoteUsername) return [3 /*break*/, 10];
return [4 /*yield*/, this._buildBridge(exports.BridgeEnum.RemoteUsername, remoteUsername)];
case 9:
_a.sent();
_a.label = 10;
case 10:
if (!remotePassword) return [3 /*break*/, 12];
return [4 /*yield*/, this._buildBridge(exports.BridgeEnum.RemotePassword, remotePassword)];
case 11:
_a.sent();
_a.label = 12;
case 12:
if (!tls) return [3 /*break*/, 14];
return [4 /*yield*/, this._buildBridge(exports.BridgeEnum.TLS, 'on')];
case 13:
_a.sent();
_a.label = 14;
case 14:
_a.trys.push([14, 16, , 17]);
return [4 /*yield*/, this.adapter.errorHandlingRPC(this.address, 0xAA04, '', 'B', [])];
case 15:
_a.sent();
return [3 /*break*/, 17];
case 16:
err_2 = _a.sent();
if (err_2 instanceof RPCError && err_2.errorCode === 2) {
throw new Utilities.ArgumentError('Error committing bridge, you set TLS but the certificates were incomplete.');
}
else {
throw err_2;
}
return [3 /*break*/, 17];
case 17: return [2 /*return*/];
}
});
});
};
/**
* Remove an existing bridge by name
* @param {string} bridgeName Name of the bridge that you wish to remove
*/
MQTTBridgeConfig.prototype.removeBridge = function (bridgeName) {
return __awaiter(this, void 0, void 0, function () {
var offset, done, end, err_3;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
offset = 0;
done = 0;
_a.label = 1;
case 1:
if (!!done) return [3 /*break*/, 6];
end = Math.min(offset + 19, bridgeName.length);
if (end === bridgeName.length) {
done = 1;
}
_a.label = 2;
case 2:
_a.trys.push([2, 4, , 5]);
return [4 /*yield*/, this.adapter.errorHandlingRPC(this.address, 0xAA05, 'BV', 'B', [done, Utilities.stringToBuffer(bridgeName.slice(offset, end))])];
case 3:
_a.sent();
return [3 /*break*/, 5];
case 4:
err_3 = _a.sent();
if (err_3 instanceof RPCError && err_3.errorCode === 2) {
throw new Utilities.ArgumentError('Bridge not removed because the name doesn\'t exist, please double check spelling.');
}
else if (err_3.errorCode !== 1) { // errorCode 1 is actually success
console.log("ERR:", err_3);
throw err_3;
}
return [3 /*break*/, 5];
case 5:
offset += 19;
return [3 /*break*/, 1];
case 6: return [2 /*return*/];
}
});
});
};
/**
* Your CA certificate. Needs to be pem encoded. Will overwrite an existing CA certificate.
* @param {string} source The source data file sent as a string
*/
MQTTBridgeConfig.prototype.sendCertfile = function (source) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this._sendTLSFile(source, exports.TLSEnum.CertFile)];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
};
/**
* The CA file is typically obtained from your certificate authority. Will overwrite an existing CA file.
It might show up with a name like rootCA.pem
* @param {string} source The source data file sent as a string
*/
MQTTBridgeConfig.prototype.sendCAFile = function (source) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this._sendTLSFile(source, exports.TLSEnum.CAFile)];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
};
/**
* The actual text content of the entire mosquitto configuration file.
Right now, this just contains bridge info, but in the future might also contain security settings,
certificate file locations for various bridges, and other configurable items.
@returns {Promise<string>} The full mosquitto config
*/
MQTTBridgeConfig.prototype.listFullMosquittoConfig = function () {
return __awaiter(this, void 0, void 0, function () {
var offset, full, res;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
offset = 0;
full = '';
return [4 /*yield*/, this.adapter.typedRPC(this.address, 0xAA00, 'L', 'V', [offset])];
case 1:
res = (_a.sent())[0];
if (!res) return [3 /*break*/, 4];
full += res;
_a.label = 2;
case 2:
if (!(res && res.length === 20)) return [3 /*break*/, 4];
offset += 20;
return [4 /*yield*/, this.adapter.typedRPC(this.address, 0xAA00, 'L', 'V', [offset])];
case 3:
res = (_a.sent())[0];
full += res;
return [3 /*break*/, 2];
case 4: return [2 /*return*/, full];
}
});
});
};
/**
* Your private keyfile. Should be pem encoded. Will overwrite an existing keyfile.
* @param {string} source The source data file sent as a string
*/
MQTTBridgeConfig.prototype.sendKeyFile = function (source) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this._sendTLSFile(source, exports.TLSEnum.KeyFile)];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
};
MQTTBridgeConfig.prototype._clearPendingBridge = function () {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.adapter.typedRPC(this.address, 0xAA06, '', '', [])];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
};
/**
* Builds all enumerated bridge setting values
*/
MQTTBridgeConfig.prototype._buildBridge = function (itemId, itemData) {
return __awaiter(this, void 0, void 0, function () {
var offset, done, end;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
offset = 0;
done = 0;
_a.label = 1;
case 1:
if (!!done) return [3 /*break*/, 3];
end = Math.min(offset + 18, itemData.length);
if (end === itemData.length) {
done = 1;
}
return [4 /*yield*/, this.adapter.typedRPC(this.address, 0xAA03, 'BBV', '', [itemId, done, Utilities.stringToBuffer(itemData.slice(offset, end))])];
case 2:
_a.sent();
offset += 18;
return [3 /*break*/, 1];
case 3: return [2 /*return*/];
}
});
});
};
/**
*
* @param source The source data file sent as a string
*/
MQTTBridgeConfig.prototype._sendTLSFile = function (source, tlsFileType) {
return __awaiter(this, void 0, void 0, function () {
var offset, done, end;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.adapter.typedRPC(this.address, 0xAA10, '', '', [])];
case 1:
_a.sent(); // clear
return [4 /*yield*/, this.adapter.typedRPC(this.address, 0xAA11, 'B', '', [tlsFileType])];
case 2:
_a.sent(); // set which file
offset = 0;
done = 0;
_a.label = 3;
case 3:
if (!!done) return [3 /*break*/, 5];
end = Math.min(offset + 20, source.length);
if (end === source.length) {
done = 1;
}
return [4 /*yield*/, this.adapter.typedRPC(this.address, 0xAA13, 'V', '', [Utilities.stringToBuffer(source.slice(offset, end))])];
case 4:
_a.sent();
offset += 20;
return [3 /*break*/, 3];
case 5: return [2 /*return*/];
}
});
});
};
return MQTTBridgeConfig;
}());
/**
* Helper class that is able to rearrange the bluetooth notitications inside a SignedListReport.
*
* This class is necessary because some bluetooth stacks, notably Android, don't push notifications
* in order to applications when they come in very near to each other in time. This causes
* reports that span multiple notification packets (20 bytes each) to be corrupted since the
* chunks of the report are reassembled out of order.
*
* ReportReassembler uses heuristics and other knowledge of the internal structure and invariants
* of a SignedListReport to detect when out-of-order packets are received and place them back
* into the correct order.
*
* It works by recognizing that the individual readings in a report are 16 bytes long
* whereas the reports are chunked into 20-byte packets. So every packet contains at
* least part of 2 readings including the majority or all of 1 reading. By looking
* at if readings that cross packet boundaries make sense we can infer what order the
* packets should have been received in. There are 4 main criteria we use to determine
* if a reading makes sense:
*
* 1. The stream id must be selected by the report selector. Each report has specific
* criteria for what readings are included so every stream id must match the selector
* included in the report header.
* 2. The reading id must be monotonically increasing.
* 3. The reading timestamp can only decrease if there has been a reboot (included as
* a reboot stream event).
* 4. There is a 16-bit reserved field in each reading for alignment purposes that must be
* 0.
*
* If there are multiple potential chunks that match all of those 4 criteria, then the one
* with the lowest reading id is chosen. In practice we have found that this reliably fixes
* out of order packets with near 100% success.
*/
var ReportReassembler = /** @class */ (function () {
function ReportReassembler(report) {
this.currentReport = report;
this.header = SignedListReport.extractHeader(report);
this.sigCalculator = new Utilities.SHA256Calculator();
this.errors = [];
this.originalSignature = this.currentReport.slice(this.currentReport.byteLength - 16);
}
ReportReassembler.prototype.isValid = function () {
return this.checkSignature();
};
ReportReassembler.prototype.getTranspositions = function () {
return this.errors;
};
ReportReassembler.prototype.getFixedReport = function () {
if (this.checkSignature()) {
return this.currentReport;
}
else {
throw new Utilities.InvalidOperationError("Report has invalid signature");
}
};
ReportReassembler.prototype.fixOutOfOrderChunks = function () {
var _a;
var startI = 1;
var totalChunks = Math.floor(this.currentReport.byteLength / 20);
var endI = totalChunks - 1;
var offset = 0;
var lastStream = null;
var lastTS = null;
var lastID = null;
while (startI < endI) {
//console.log(`Searching for chunk ${startI}: ` + this.dumpChunk(startI));
var candidates = this.findCandidates(startI, totalChunks, offset, lastStream, lastTS, lastID);
var bestCandidate = null;
if (candidates.length === 1)
bestCandidate = candidates[0];
else if (candidates.length > 1) {
this.sortCandidates(candidates);
bestCandidate = candidates[0];
}
// If we could not find a candidate for this chunk of the report, we cannot fix it.
if (bestCandidate == null)
return false;
if (bestCandidate.index !== startI) {
//console.log(`Moving chunk ${bestCandidate.index} to ${startI}`);
this.errors.push({ src: bestCandidate.index, dst: startI });
this.moveChunk(startI, bestCandidate.index);
}
_a = this.extractLatest(bestCandidate), lastStream = _a[0], lastID = _a[1], lastTS = _a[2];
offset = (offset + 20) % 16;
startI += 1;
}
//Now that we have finished fixing everything, we should have a matching
return this.isValid();
};
ReportReassembler.prototype.sortCandidates = function (candidates) {
/*
* Return the first ID that is contained in this chunk and not filled
* in by the lastID for sequential comparison. We know what the correct
* ID is by looking at the offset and comparing with what is returned
* by decodeChunk
*/
function extractID(candidate) {
if (candidate.offset === 4 || candidate.offset === 0)
return candidate.ids[0];
return candidate.ids[1];
}
function compareIDs(a, b) {
return extractID(a) - extractID(b);
}
candidates.sort(compareIDs);
};
ReportReassembler.prototype.moveChunk = function (destIndex, srcIndex) {
var tmp = new Uint8Array(20);
if (destIndex >= srcIndex)
throw new Utilities.ArgumentError("Attempting to move chunk later rather than earlier in report.");
for (var curr = srcIndex; curr > destIndex; --curr) {
var swapDst = new Uint8Array(this.currentReport, (curr - 1) * 20, 20);
var swapSrc = new Uint8Array(this.currentReport, (curr) * 20, 20);
tmp.set(swapDst);
swapDst.set(swapSrc);
swapSrc.set(tmp);
}
};
ReportReassembler.prototype.extractLatest = function (chunk) {
var stream = chunk.streams[0];
var id = chunk.ids[0];
var ts = chunk.timestamps[0];
if (chunk.streams[1] !== null)
stream = chunk.streams[1];
if (chunk.ids[1] !== null)
id = chunk.ids[1];
if (chunk.timestamps[1] !== null)
ts = chunk.timestamps[1];
return [stream, id, ts];
};
ReportReassembler.prototype.fillChunk = function (chunk, lastStream, lastTS, lastID) {
if (chunk.streams[0] == null)
chunk.streams[0] = lastStream;
if (chunk.reserved[0] == null)
chunk.reserved[0] = 0;
if (chunk.timestamps[0] == null)
chunk.timestamps[0] = lastTS;
if (chunk.ids[0] == null)
chunk.ids[0] = lastID;
};
ReportReassembler.prototype.decodeChunk = function (startI, offset) {
var _a, _b, _c, _d;
var chunkData = this.currentReport.slice(startI * 20, startI * 20 + 20);
var _e = [null, null], stream1 = _e[0], stream2 = _e[1];
var _f = [null, null], id1 = _f[0], id2 = _f[1];
var _g = [null, null], res1 = _g[0], res2 = _g[1];
var _h = [null, null], ts1 = _h[0], ts2 = _h[1];
var _j = [null, null], val1 = _j[0], val2 = _j[1];
if (offset === 0) {
_a = Utilities.unpackArrayBuffer("HHLLLHH", chunkData), stream1 = _a[0], res1 = _a[1], id1 = _a[2], ts1 = _a[3], val1 = _a[4], stream2 = _a[5], res2 = _a[6];
}
else if (offset === 4) {
_b = Utilities.unpackArrayBuffer("LLLHHL", chunkData), id1 = _b[0], ts1 = _b[1], val1 = _b[2], stream2 = _b[3], res2 = _b[4], id2 = _b[5];
}
else if (offset === 8) {
_c = Utilities.unpackArrayBuffer("LLHHLL", chunkData), ts1 = _c[0], val1 = _c[1], stream2 = _c[2], res2 = _c[3], id2 = _c[4], ts2 = _c[5];
}
else { // (offset == 12)
_d = Utilities.unpackArrayBuffer("LHHLLL", chunkData), val1 = _d[0], stream2 = _d[1], res2 = _d[2], id2 = _d[3], ts2 = _d[4], val2 = _d[5];
}
return { streams: [stream1, stream2], reserved: [res1, res2], ids: [id1, id2], timestamps: [ts1, ts2], values: [val1, val2], index: startI, offset: offset };
};
ReportReassembler.prototype.dumpChunk = function (index) {
var data = new Uint8Array(this.currentReport, index * 20, 20);
return Array.prototype.map.call(data, function (x) { return ('00' + x.toString(16)).slice(-2); }).join(' ');
};
ReportReassembler.prototype.maskChunk = function (chunk) {
if (chunk.offset === 12)
return chunk;
chunk.streams[1] = null;
chunk.ids[1] = null;
chunk.reserved[1] = null;
chunk.timestamps[1] = null;
chunk.values[1] = null;
};
ReportReassembler.prototype.validateChunk = function (chunk, lastStream, lastTS, lastID) {
//console.log("potential chunk: " + JSON.stringify(chunk));
for (var _i = 0, _a = chunk.streams; _i < _a.length; _i++) {
var stream = _a[_i];
if (stream !== null && !this.header.decodedSelector.matches(stream)) {
//console.log(" - stream not selected");
return false;
}
}
for (var _b = 0, _c = chunk.reserved; _b < _c.length; _b++) {
var res = _c[_b];
if (res !== null && res !== 0) {
//console.log(" - reserved not 0");
return false;
}
}
if (chunk.ids[1] !== null && chunk.ids[1] <= chunk.ids[0]) {
//console.log(" - ids not monotonic");
return false;
}
// Timestamp can only decrease if there has been a reset
if (chunk.timestamps[1] !== null && chunk.timestamps[1] < chunk.timestamps[0] && chunk.streams[1] !== StreamSelector.REBOOT_STREAM) {
//console.log(" - timestamps not monotonic (except reboot)");
return false;
}
//For chunks that contain an ID in the first slot (so it's not filled in from lastID)
//make sure it is monotonic
if ((chunk.offset === 0 || chunk.offset === 4) && chunk.ids[0] <= lastID) {
//console.log(" - reading ID not greater than lastID");
return false;
}
//console.log(" - VALID!");
return true;
};
ReportReassembler.prototype.findCandidates = function (startI, totalChunks, offset, lastStream, lastTS, lastID) {
var candidates = [];
for (var i = 0; i < 4; ++i) {
if (startI + i >= totalChunks)
continue;
var chunk = this.decodeChunk(startI + i, offset);
if (startI === totalChunks - 2)
this.maskChunk(chunk);
this.fillChunk(chunk, lastStream, lastTS, lastID);
if (this.validateChunk(chunk, lastStream, lastTS, lastID))
candidates.push(chunk);
}
return candidates;
};
ReportReassembler.prototype.calculateSignature = function () {
var signedData = this.currentReport.slice(0, this.currentReport.byteLength - 16);
return this.sigCalculator.calculateSignature(signedData);
};
ReportReassembler.prototype.checkSignature = function (prefix) {
if (prefix == null)
prefix = 16;
var actual = this.calculateSignature();
return this.sigCalculator.compareSignatures(this.originalSignature.slice(0, prefix), actual);
};
return ReportReassembler;
}());
// Create categories, they will autoregister themselves, one category without parent (root) and a child category.
var catService = new typescriptLogging.Category("iotile.device");
var catAdapter = new typescriptLogging.Category("IOTileAdapter", catService);
var catReports = new typescriptLogging.Category("Reports", catService);
var catUTCAssigner = new typescriptLogging.Category('UTCAssigner', catService);
var catBLEOptimizer = new typescriptLogging.Category("BLEOptimizer", catService);
var catMockBLE = new typescriptLogging.Category("MockBLE", catService);
var catPOD1M = new typescriptLogging.Category("POD1M", catService);
var catStreaming = new typescriptLogging.Category("Streaming", catService);
var catNotify = new typescriptLogging.Category("Notifications", catService);
var catIOTileDevice = new typescriptLogging.Category("IOTileDevice", catService);
var RawReading = /** @class */ (function () {
function RawReading(stream, value, timestamp, timebase, id) {
this._stream = stream;
this._value = value;
this._raw_timestamp = timestamp;
this._time = new Date(timebase.valueOf() + timestamp * 1000);
if (id !== undefined) {
this._id = id;
}
else {
this._id = 0;
}
}
Object.defineProperty(RawReading.prototype, "timestamp", {
get: function () {
return this._raw_timestamp;
},
enumerable: true,
configurable: true
});
Object.defineProperty(RawReading.prototype, "value", {
get: function () {
return this._value;
},
enumerable: true,
configurable: true
});
Object.defineProperty(RawReading.prototype, "stream", {
get: function () {
return this._stream;
},
enumerable: true,
configurable: true
});
Object.defineProperty(RawReading.prototype, "id", {
get: function () {
return this._id;
},
enumerable: true,
configurable: true
});
Object.defineProperty(RawReading.prototype, "time", {
get: function () {
return this._time;
},
enumerable: true,
configurable: true
});
Object.defineProperty(RawReading.prototype, "variable", {
get: function () {
return Utilities.numberToHexString(this.stream, 4);
},
enumerable: true,
configurable: true
});
return RawReading;
}());
var IOTileReport = /** @class */ (function () {
function IOTileReport() {
}
return IOTileReport;
}());
var IndividualReport = /** @class */ (funct