UNPKG

@iotile/iotile-device

Version:

A typescript library for interfacing with IOTile BLE devices

390 lines 17.4 kB
"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 }); var ring_buffer_1 = require("../common/ring-buffer"); var Errors = require("../common/error-space"); var iotile_reports_1 = require("../common/iotile-reports"); var ReceiveStatus; (function (ReceiveStatus) { ReceiveStatus[ReceiveStatus["Idle"] = 0] = "Idle"; ReceiveStatus[ReceiveStatus["InProgress"] = 1] = "InProgress"; })(ReceiveStatus = exports.ReceiveStatus || (exports.ReceiveStatus = {})); /** * When receiving long reports these events can be queried from the report parser * in order to update the user on progress. */ var ReportParserEvent = /** @class */ (function () { function ReportParserEvent(name, finishedPercentage, reportIndex) { this.name = name; this.finishedPercentage = finishedPercentage; this.reportIndex = reportIndex; } return ReportParserEvent; }()); exports.ReportParserEvent = ReportParserEvent; var ReportStartedEvent = /** @class */ (function (_super) { __extends(ReportStartedEvent, _super); function ReportStartedEvent(totalSize, reportIndex) { var _this = _super.call(this, 'ReportStartedEvent', 0, reportIndex) || this; _this.totalSize = totalSize; return _this; } return ReportStartedEvent; }(ReportParserEvent)); exports.ReportStartedEvent = ReportStartedEvent; var ReportStalledEvent = /** @class */ (function (_super) { __extends(ReportStalledEvent, _super); function ReportStalledEvent(finishedPercentage, reportIndex) { return _super.call(this, 'ReportStalledEvent', finishedPercentage, reportIndex) || this; } return ReportStalledEvent; }(ReportParserEvent)); exports.ReportStalledEvent = ReportStalledEvent; var ReportProgressEvent = /** @class */ (function (_super) { __extends(ReportProgressEvent, _super); function ReportProgressEvent(finishedPercentage, reportIndex) { return _super.call(this, 'ReportProgressEvent', finishedPercentage, reportIndex) || this; } return ReportProgressEvent; }(ReportParserEvent)); exports.ReportProgressEvent = ReportProgressEvent; var ReportFinishedEvent = /** @class */ (function (_super) { __extends(ReportFinishedEvent, _super); function ReportFinishedEvent(reportIndex) { return _super.call(this, 'ReportFinishedEvent', 100, reportIndex) || this; } return ReportFinishedEvent; }(ReportParserEvent)); exports.ReportFinishedEvent = ReportFinishedEvent; var ReportInvalidEvent = /** @class */ (function (_super) { __extends(ReportInvalidEvent, _super); function ReportInvalidEvent(reportIndex, rawData) { var _this = _super.call(this, 'ReportInvalidEvent', 100, reportIndex) || this; _this.rawData = rawData; return _this; } return ReportInvalidEvent; }(ReportParserEvent)); exports.ReportInvalidEvent = ReportInvalidEvent; /** * @ngdoc object * @name iotile.device.object:ReportParser * * @description * * A stream based report parser that knows how to construct IOTileReport * objects from data streamed from an IOTile Device. The ReportParser * has only 2 public methods (and several getters). You can call pushData * to send additional data received from an IOTileDevice to the ReportParser. * * The ReportParser will automatically try to see how many reports it can build * using all of the data it currently has and return a list of those reports to * you. * * If any error occurs during parsing, the ReportParser enters a broken state * and will not process any further data until you call the reset() function. * * ReportParser is built on top of a fixed size RingBuffer that it uses for * efficiently handling report data. * * See {@link Utilities.type:RingBuffer Utilities.RingBuffer} */ var ReportParser = /** @class */ (function () { function ReportParser(ringBufferSize, expand) { if (expand === void 0) { expand = false; } this.ringBuffer = new ring_buffer_1.RingBuffer(ringBufferSize, expand); this.broken = false; this._receiveState = ReceiveStatus.Idle; this._inProgressReceived = 0; this._inProgressTotal = 0; this._lastProgressReport = 0; this._lastEvent = null; this._lastUpdateTime = null; this._reportsReceived = 0; this._receivedTime = null; this._lastUpdateTime = null; this._progressReportInterval = 5; } Object.defineProperty(ReportParser.prototype, "state", { get: function () { return this._receiveState; }, enumerable: true, configurable: true }); Object.defineProperty(ReportParser.prototype, "inProgressReceived", { get: function () { return this._inProgressReceived; }, enumerable: true, configurable: true }); Object.defineProperty(ReportParser.prototype, "inProgressTotal", { get: function () { return this._inProgressTotal; }, enumerable: true, configurable: true }); ReportParser.prototype.stop = function () { this.broken = true; //Maintain the contract that when we send a start report event, we always send //a stop report event after it. if (this._receiveState == ReceiveStatus.InProgress) { this.updateStatus(false, 0, 0); } }; /** * @ngdoc method * @name iotile.device.object:ReportParser#pushData * @methodOf iotile.device.object:ReportParser * * @description * Add new data received from a device to the RingBuffer and try to parse reports out of it * * @returns {Array<IOTileReport>} A list of all of the reports that could be parsed out * of the currently received data. If no reports could * be parsed, returns an empty list. * @throws {Errors.ReportParsingError} If there is an unrecoverable error processing reports. * @throws {Errors.ReportParsingStoppedError} If there was a previous report parsing error and * processing is therefore stopped. * @throws {Errors.InsufficientSpaceError} If there is not enough space in the internal ring buffer * to hold the data for processing. */ ReportParser.prototype.pushData = function (chunk) { if (this.broken) { throw new Errors.ReportParsingStoppedError('attempting to push data to a stopped report parser'); } this.ringBuffer.push(chunk); return this.tryParseReports(); }; /** * @ngdoc method * @name iotile.device.object:ReportParser#reset * @methodOf iotile.device.object:ReportParser * * @description * Reset the internal state of the report parser back to a clean slate. * Should be called every time we connect to a new device. */ ReportParser.prototype.reset = function () { this.ringBuffer.reset(); this.broken = false; this._receiveState = ReceiveStatus.Idle; this._inProgressReceived = 0; this._inProgressTotal = 0; this._lastProgressReport = 0; this._lastEvent = null; this._lastUpdateTime = null; this._reportsReceived = 0; }; /** * @ngdoc method * @name iotile.device.object:ReportParser#popLastEvent * @methodOf iotile.device.object:ReportParser * * @description * If there has been a change in reportParser's progress getting a report * since the last time we called popLastEvent(), return that event, otherwise * return null; * * This function can be used to get progress information while receiving * long reports. There are three kinds of events you can get: * - ReportStartedEvent: when a new report header has been received; * - ReportProgressEvent: sent every time X percent (currently 5%) of the report * has been received. * - ReportFinishedEvent: sent whenever a long report is fully receiving. * @returns {ReportParserEvent} The last event that happened or null if nothing has * happened since the last call to this function. */ ReportParser.prototype.popLastEvent = function () { var event = this._lastEvent; // if we're mid-report and it's been more than a second since the last event was logged, we've stalled if (this._receivedTime && this._lastUpdateTime && this._lastEvent && (this._lastUpdateTime.getSeconds() + 1 < (new Date()).getSeconds())) { this._lastEvent = new ReportStalledEvent(this._lastEvent.finishedPercentage, this._lastEvent.reportIndex); } else { this._lastEvent = null; } return event; }; /** * @ngdoc method * @name iotile.device.object:ReportParser#tryParseReports * @methodOf iotile.device.object:ReportParser * * @description * Attempt to process as many reports as possible from the current ring buffer contents * * @returns {Array<IOTileReport>} A list of all of the reports that could be parsed out * of the currently received data. If no reports could * be parsed, returns an empty list. * @throws {Errors.ReportParsingError} If there is an unrecoverable error processing reports. * @throws {Errors.ReportParsingStoppedError} If there was a previous report parsing error and * processing is therefore stopped. */ ReportParser.prototype.tryParseReports = function () { if (this.broken) { throw new Errors.ReportParsingStoppedError('attempting to parse reports with a stopped parser'); } var reports = []; try { while (true) { var report = this.tryParseReport(); if (report != null) { reports.push(report); } } } catch (err) { if (err.name == 'RingBufferEmptyError') { //This is okay, it just means we have no more reports to parse } else { this.broken = true; throw new Errors.ReportParsingError(err.message); } } return reports; }; /** * Try to parse a single report from the report ring buffer. * We get one single byte to figure out the type and then, optionally * read the header and finally read the entire report. */ ReportParser.prototype.tryParseReport = function () { var val = this.ringBuffer.peekAs('B'); var reportType = val[0]; switch (reportType) { case 0: return this.tryParseIndividualReport(); case 1: return this.tryParseListReport(); default: throw new Errors.ReportParsingError('Unknown report format received: ' + reportType); } }; /** * Individual reports have a fixed size format of 20 bytes * with a fixed, known structure. */ ReportParser.prototype.tryParseIndividualReport = function () { var report = this.ringBuffer.popAs("BBHLLLL"); var format = report[0]; var stream = report[2]; var uuid = report[3]; var sentTimestamp = report[4]; var readingTimestamp = report[5]; var readingValue = report[6]; var now = new Date(); var onTime = new Date(now.valueOf() - (sentTimestamp * 1000)); var reading = new iotile_reports_1.RawReading(stream, readingValue, readingTimestamp, onTime); return new iotile_reports_1.IndividualReport(uuid, sentTimestamp, reading); }; /** * List reports have a variable size with a fixed size * header that contains the size of the total report. * * This function will also update the reportParser object's current * report progress status so that people who care can be informed * about progress receiving this report. * * NB * We need to keep track of the time when we first start receiving information * from a report because that the time the report was sent. So we keep an * internal state variable _receivedTime that we set to the current time when * we start receiving a report and to null when we're done processing a report. */ ReportParser.prototype.tryParseListReport = function () { var header = this.ringBuffer.peekAs("BBHLLLBBH"); var totalLength = (header[1] | (header[2] << 8)); var uuid = header[3]; var originStreamer = header[7]; //If we got this far, then we're in the process of receiving a signed report //Only update our status if we did not receive the entire report in one shot if (totalLength > this.ringBuffer.count) { this.updateStatus(true, totalLength, this.ringBuffer.count, originStreamer); } //If this is the first bit of the report, we need to save off the timestamp for //later so we know when it started being sent if (this._receivedTime == null) { this._receivedTime = new Date(); } var totalReport = this.ringBuffer.pop(totalLength); this.updateStatus(false, 0, 0, originStreamer); var report = new iotile_reports_1.SignedListReport(uuid, originStreamer, totalReport, this._receivedTime); /** * Clear the received time so that when the next report comes in we trigger ourselves to stamp it again * see the lines at the beginning of this function. */ this._receivedTime = null; /** * If the report has a corrupt signature, do not return it since we know that it is * not recoverable. We update the lastEvent so that people calling popLastEvent * know that they should send an Invalid report error. */ if (report.validity == iotile_reports_1.SignatureStatus.Invalid) { this._lastEvent = new ReportInvalidEvent(this._reportsReceived, totalReport); report = null; } this._reportsReceived += 1; return report; }; /** * Update our internal status information so that we can inform the user about progress * receiving long reports. We only send events when a report is not received in a single * shot. So if we are just dumped a complete report in a single pushData() call, then * no progress will be reported. */ ReportParser.prototype.updateStatus = function (inProgress, totalSize, receivedSize, streamer) { if (streamer == undefined) streamer = -1; if (inProgress && this._receiveState != ReceiveStatus.InProgress && receivedSize < totalSize) { //If we are just starting a report (and have not received the entire thing) this._lastEvent = new ReportStartedEvent(totalSize, streamer); } else if (!inProgress && this._receiveState == ReceiveStatus.InProgress) { //If we finished a report, send a finished event this._lastEvent = new ReportFinishedEvent(streamer); } else if (inProgress && this._inProgressReceived != receivedSize) { //See if we have received enough data to qualify for producing another progress event var lastPercentage = this._lastProgressReport / this._inProgressTotal * 100; var currentPercentage = receivedSize / this._inProgressTotal * 100; var lastProgress = Math.floor(lastPercentage / this._progressReportInterval); var currentProgress = Math.floor(currentPercentage / this._progressReportInterval); if (currentProgress != lastProgress) { this._lastEvent = new ReportProgressEvent(currentProgress * this._progressReportInterval, streamer); this._lastProgressReport = receivedSize; } } else if (inProgress && this._receiveState != ReceiveStatus.InProgress && receivedSize == totalSize) { //If we received the entire report in one shot, don't broadcast progress updates } this._lastUpdateTime = new Date(); if (inProgress) { this._receiveState = ReceiveStatus.InProgress; this._inProgressTotal = totalSize; this._inProgressReceived = receivedSize; } else { this._receiveState = ReceiveStatus.Idle; this._inProgressTotal = 0; this._inProgressReceived = 0; this._lastProgressReport = 0; } }; return ReportParser; }()); exports.ReportParser = ReportParser; //# sourceMappingURL=iotile-report-parser.js.map