@iotile/iotile-device
Version:
A typescript library for interfacing with IOTile BLE devices
240 lines • 11.5 kB
JavaScript
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
var iotile_reports_1 = require("./iotile-reports");
var iotile_common_1 = require("@iotile/iotile-common");
var ReportReassembler = /** @class */ (function () {
function ReportReassembler(report) {
this.currentReport = report;
this.header = iotile_reports_1.SignedListReport.extractHeader(report);
this.sigCalculator = new iotile_common_1.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 iotile_common_1.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 iotile_common_1.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 = iotile_common_1.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 = iotile_common_1.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 = iotile_common_1.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 = iotile_common_1.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] !== iotile_reports_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;
}());
exports.ReportReassembler = ReportReassembler;
//# sourceMappingURL=report-reassembler.js.map