@iotile/iotile-device
Version:
A typescript library for interfacing with IOTile BLE devices
397 lines • 16.5 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var 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 function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
///<reference path="../../typings/cordova_plugins.d.ts"/>
var iotile_common_1 = require("@iotile/iotile-common");
var report_reassembler_1 = require("./report-reassembler");
var config_1 = require("../config");
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 iotile_common_1.numberToHexString(this.stream, 4);
},
enumerable: true,
configurable: true
});
return RawReading;
}());
exports.RawReading = RawReading;
var IOTileReport = /** @class */ (function () {
function IOTileReport() {
}
return IOTileReport;
}());
exports.IOTileReport = IOTileReport;
var IndividualReport = /** @class */ (function (_super) {
__extends(IndividualReport, _super);
function IndividualReport(uuid, sentTime, reading) {
var _this = _super.call(this) || this;
_this._uuid = uuid;
_this._reading = reading;
_this._sentTimestamp = sentTime;
return _this;
}
/**
* Update the IndividualReport using formatting data from the cloud to be able to show
* accurate realtime data about the device stream.
*
* @param fmt: a raw_value_format code from the cloud that indicates what format
* the binary device data should be interpreted as.
*/
IndividualReport.prototype.decodeUsingFormat = function (fmt) {
if (fmt == '<l') {
var signedValue = void 0;
var rawData = iotile_common_1.packArrayBuffer('L', this._reading.value);
signedValue = iotile_common_1.unpackArrayBuffer('l', rawData)[0];
this._reading = new RawReading(this._reading.stream, signedValue, this._reading.timestamp, this._reading.time, this._reading.id);
}
};
Object.defineProperty(IndividualReport.prototype, "deviceID", {
get: function () {
return this._uuid;
},
enumerable: true,
configurable: true
});
Object.defineProperty(IndividualReport.prototype, "reading", {
get: function () {
return this._reading;
},
enumerable: true,
configurable: true
});
Object.defineProperty(IndividualReport.prototype, "sentTimestamp", {
get: function () {
return this._sentTimestamp;
},
enumerable: true,
configurable: true
});
return IndividualReport;
}(IOTileReport));
exports.IndividualReport = IndividualReport;
var REPORT_HEADER_SIZE = 20;
exports.USER_REPORT_STREAMER = 0;
exports.SYSTEM_REPORT_STREAMER = 1;
exports.COMBINED_REPORT_STREAMER = 0xFF;
var SignedReportSelectors;
(function (SignedReportSelectors) {
SignedReportSelectors[SignedReportSelectors["UserOutputs"] = 22527] = "UserOutputs";
SignedReportSelectors[SignedReportSelectors["SystemOutputs"] = 24575] = "SystemOutputs";
SignedReportSelectors[SignedReportSelectors["CombinedOutputs"] = 55295] = "CombinedOutputs";
})(SignedReportSelectors = exports.SignedReportSelectors || (exports.SignedReportSelectors = {}));
var SignatureFlags;
(function (SignatureFlags) {
SignatureFlags[SignatureFlags["HashOnly"] = 0] = "HashOnly";
SignatureFlags[SignatureFlags["SignedUserKey"] = 1] = "SignedUserKey";
SignatureFlags[SignatureFlags["SignedDeviceKey"] = 2] = "SignedDeviceKey";
})(SignatureFlags = exports.SignatureFlags || (exports.SignatureFlags = {}));
var SignatureStatus;
(function (SignatureStatus) {
SignatureStatus[SignatureStatus["Valid"] = 0] = "Valid";
SignatureStatus[SignatureStatus["Invalid"] = 1] = "Invalid";
SignatureStatus[SignatureStatus["Unknown"] = 2] = "Unknown";
})(SignatureStatus = exports.SignatureStatus || (exports.SignatureStatus = {}));
var StreamMatchOperator;
(function (StreamMatchOperator) {
StreamMatchOperator[StreamMatchOperator["UserOnly"] = 0] = "UserOnly";
StreamMatchOperator[StreamMatchOperator["SystemOnly"] = 1] = "SystemOnly";
StreamMatchOperator[StreamMatchOperator["UserAndBreaks"] = 2] = "UserAndBreaks";
StreamMatchOperator[StreamMatchOperator["UserAndSystem"] = 3] = "UserAndSystem";
})(StreamMatchOperator = exports.StreamMatchOperator || (exports.StreamMatchOperator = {}));
var StreamType;
(function (StreamType) {
StreamType[StreamType["Storage"] = 0] = "Storage";
StreamType[StreamType["Unbuffered"] = 1] = "Unbuffered";
StreamType[StreamType["Constant"] = 2] = "Constant";
StreamType[StreamType["Input"] = 3] = "Input";
StreamType[StreamType["Count"] = 4] = "Count";
StreamType[StreamType["Output"] = 5] = "Output";
StreamType[StreamType["Realtime"] = 6] = "Realtime";
})(StreamType = exports.StreamType || (exports.StreamType = {}));
var StreamSelector = /** @class */ (function () {
function StreamSelector(encodedSelector) {
var _a;
_a = StreamSelector.decode(encodedSelector), this.type = _a[0], this.code = _a[1], this.match_op = _a[2];
this.isWildcard = (this.code === StreamSelector.WILDCARD);
}
StreamSelector.prototype.matches = function (streamID) {
var _a = StreamSelector.decode(streamID), type = _a[0], code = _a[1], op = _a[2];
//Stream IDs must be either system or user, not any of the combined selectors
if (op === StreamMatchOperator.UserAndSystem || op === StreamMatchOperator.UserAndBreaks)
return false;
if (!this.isWildcard && this.code !== code)
return false;
if (this.type !== type)
return false;
if (this.match_op === StreamMatchOperator.SystemOnly && op == StreamMatchOperator.UserOnly)
return false;
if (this.match_op === StreamMatchOperator.UserOnly && op == StreamMatchOperator.SystemOnly)
return false;
/*
* The UserAndBreaks selector matches all user streams + the global reboot stream but no other system streams.
*/
if (this.match_op === StreamMatchOperator.UserAndBreaks && op == StreamMatchOperator.SystemOnly && streamID !== StreamSelector.REBOOT_STREAM)
return false;
return true;
};
StreamSelector.decode = function (encodedSelector) {
if (encodedSelector < 0 || encodedSelector > 0xFFFF)
throw new iotile_common_1.ArgumentError("invalid number in StreamSelector that is not in [0, 65535]: " + encodedSelector);
if (encodedSelector !== Math.floor(encodedSelector))
throw new iotile_common_1.ArgumentError("You must create a StreamSelector with a whole number: " + encodedSelector);
var isSystem = !!(encodedSelector & (1 << 11));
var includeBreaks = !!(encodedSelector & (1 << 15));
var match_op = StreamSelector.getOperator(isSystem, includeBreaks);
var code = encodedSelector & ((1 << 11) - 1);
var type = (encodedSelector >> 12) & 7;
return [type, code, match_op];
};
StreamSelector.getOperator = function (isSystem, includeBreaks) {
if (isSystem && !includeBreaks)
return StreamMatchOperator.SystemOnly;
if (!isSystem && !includeBreaks)
return StreamMatchOperator.UserOnly;
if (isSystem && includeBreaks)
return StreamMatchOperator.UserAndSystem;
//!isSystem && includeBreaks
return StreamMatchOperator.UserAndBreaks;
};
StreamSelector.WILDCARD = (1 << 11) - 1;
StreamSelector.REBOOT_STREAM = 0x5C00;
return StreamSelector;
}());
exports.StreamSelector = StreamSelector;
var SignedListReport = /** @class */ (function (_super) {
__extends(SignedListReport, _super);
function SignedListReport(uuid, streamer, rawData, receivedTime) {
var _this = _super.call(this) || this;
_this._highestID = 0;
_this._lowestID = 0;
_this._uuid = uuid;
_this._readings = [];
_this._rawData = rawData;
_this._streamer = streamer;
_this._receivedTime = receivedTime;
_this._header = SignedListReport.extractHeader(rawData);
_this._valid = _this.validateSignature();
if (_this._valid === SignatureStatus.Valid) {
_this.updateReadings(_this._rawData);
_this.updateReadingRange(_this._readings);
}
return _this;
}
SignedListReport.extractHeader = function (data) {
if (data.byteLength < REPORT_HEADER_SIZE) {
throw new iotile_common_1.ArgumentError("Invalid report that was not long enough to contain a valid header");
}
var header = iotile_common_1.unpackArrayBuffer('BBHLLLBBH', data.slice(0, REPORT_HEADER_SIZE));
return {
format: header[0],
lengthLow: header[1],
lengthHigh: header[2],
uuid: header[3],
reportID: header[4],
sentTime: header[5],
signatureFlags: header[6],
streamer: header[7],
selector: header[8],
decodedSelector: new StreamSelector(header[8])
};
};
Object.defineProperty(SignedListReport.prototype, "deviceID", {
get: function () {
return this._uuid;
},
enumerable: true,
configurable: true
});
Object.defineProperty(SignedListReport.prototype, "readings", {
get: function () {
return this._readings;
},
enumerable: true,
configurable: true
});
Object.defineProperty(SignedListReport.prototype, "validity", {
get: function () {
return this._valid;
},
enumerable: true,
configurable: true
});
Object.defineProperty(SignedListReport.prototype, "rawData", {
get: function () {
return this._rawData;
},
enumerable: true,
configurable: true
});
Object.defineProperty(SignedListReport.prototype, "streamer", {
get: function () {
return this._streamer;
},
enumerable: true,
configurable: true
});
Object.defineProperty(SignedListReport.prototype, "receivedTime", {
get: function () {
return this._receivedTime;
},
enumerable: true,
configurable: true
});
Object.defineProperty(SignedListReport.prototype, "readingIDRange", {
get: function () {
return [this._lowestID, this._highestID];
},
enumerable: true,
configurable: true
});
Object.defineProperty(SignedListReport.prototype, "header", {
get: function () {
return this._header;
},
enumerable: true,
configurable: true
});
SignedListReport.prototype.updateReadingRange = function (readings) {
//Calculate lowest and highest ids based on decoded readings
if (readings.length == 0) {
this._lowestID = 0;
this._highestID = 0;
}
else {
//Initialize with the first reading to avoid needing to use sentinal values
this._lowestID = readings[0].id;
this._highestID = readings[0].id;
for (var i = 1; i < readings.length; ++i) {
var id = readings[i].id;
if (id < this._lowestID) {
this._lowestID = id;
}
if (id > this._highestID) {
this._highestID = id;
}
}
}
};
SignedListReport.prototype.updateReadings = function (rawData) {
var readings = [];
var onTime = new Date(this._receivedTime.valueOf() - (this._header.sentTime * 1000));
//Now decode and parse the actual readings in the report
// get length of readings
var allReadingsData = rawData.slice(20, rawData.byteLength - 24);
for (var i = 0; i < allReadingsData.byteLength; i += 16) {
var readingData = allReadingsData.slice(i, i + 16);
var reading = iotile_common_1.unpackArrayBuffer("HHLLL", readingData);
var stream = reading[0]; //reading[1] is reserved
var readingID = reading[2];
var readingTimestamp = reading[3];
var readingValue = reading[4];
var parsedReading = new RawReading(stream, readingValue, readingTimestamp, onTime, readingID);
readings.push(parsedReading);
}
this._readings = readings;
this.updateReadingRange(readings);
};
/**
* Note that this method is designed to be called from the constructor
* only. It needs to be followed by calls to updateReadings() and updateReadingRange()
* since it may modify the raw report data.
*/
SignedListReport.prototype.validateSignature = function () {
var calc = new iotile_common_1.SHA256Calculator();
if (this._header.signatureFlags != SignatureFlags.HashOnly) {
return SignatureStatus.Unknown;
}
var signedData = this._rawData.slice(0, this._rawData.byteLength - 16);
var embeddedSig = this._rawData.slice(this._rawData.byteLength - 16);
var signature = calc.calculateSignature(signedData);
if (calc.compareSignatures(embeddedSig, signature)) {
return SignatureStatus.Valid;
}
/**
* If the signature is invalid, attempt to automatically fix it assuming that the
* cause is due to the Android bluetooth stack sending us out-of-order report chunks.
*/
config_1.catReports.warn("Report signature invalid, attempting to reassemble");
var reassembler = new report_reassembler_1.ReportReassembler(this.rawData);
if (reassembler.fixOutOfOrderChunks()) {
config_1.catReports.info("Report successfully fixed");
var errors = reassembler.getTranspositions();
var errorStrings = [];
for (var _i = 0, errors_1 = errors; _i < errors_1.length; _i++) {
var error = errors_1[_i];
var errorString_1 = error.src + " -> " + error.dst;
errorStrings.push(errorString_1);
}
var errorString = errorStrings.join(", ");
config_1.catReports.info("Report transpositions were: " + errorString);
var newReport = reassembler.getFixedReport();
this._rawData = newReport;
return SignatureStatus.Valid;
}
config_1.catReports.error("Unable to correct report signature", null);
return SignatureStatus.Invalid;
};
return SignedListReport;
}(IOTileReport));
exports.SignedListReport = SignedListReport;
//# sourceMappingURL=iotile-reports.js.map