UNPKG

@iotile/iotile-device

Version:

A typescript library for interfacing with IOTile BLE devices

240 lines 11.5 kB
"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