UNPKG

@iotile/iotile-device

Version:

A typescript library for interfacing with IOTile BLE devices

473 lines 19.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const iotile_common_1 = require("@iotile/iotile-common"); const Errors = require("../common/error-space"); const iotile_reports_1 = require("../common/iotile-reports"); const iotile_types_1 = require("../common/iotile-types"); const config_1 = require("../config"); ; var MatchBy; (function (MatchBy) { MatchBy[MatchBy["MatchBySlot"] = 1] = "MatchBySlot"; MatchBy[MatchBy["MatchController"] = 2] = "MatchController"; MatchBy[MatchBy["MatchByName"] = 3] = "MatchByName"; })(MatchBy = exports.MatchBy || (exports.MatchBy = {})); var RemoteBridgeState; (function (RemoteBridgeState) { RemoteBridgeState[RemoteBridgeState["Idle"] = 0] = "Idle"; RemoteBridgeState[RemoteBridgeState["WaitingForScript"] = 1] = "WaitingForScript"; RemoteBridgeState[RemoteBridgeState["ReceivingScript"] = 2] = "ReceivingScript"; RemoteBridgeState[RemoteBridgeState["ReceivedCompleteScript"] = 3] = "ReceivedCompleteScript"; RemoteBridgeState[RemoteBridgeState["ValidatedScript"] = 4] = "ValidatedScript"; RemoteBridgeState[RemoteBridgeState["ExecutingScript"] = 5] = "ExecutingScript"; })(RemoteBridgeState = exports.RemoteBridgeState || (exports.RemoteBridgeState = {})); function convertToSecondsSince2000(date) { let millisecondsAt2000 = Date.UTC(2000, 0, 1); let secondsSince2000 = Math.ceil((date.valueOf() - millisecondsAt2000) / 1000); return secondsSince2000; } exports.convertToSecondsSince2000 = convertToSecondsSince2000; class RemoteBridge { constructor(adapter) { this.adapter = adapter; } beginScript() { return this.adapter.errorHandlingRPC(8, 0x2100, "", "L", [], 10.0); } endScript() { return this.adapter.errorHandlingRPC(8, 0x2102, "", "L", [], 2.0); } triggerScript() { return this.adapter.errorHandlingRPC(8, 0x2103, "", "L", [], 2.0); } resetScript() { return this.adapter.errorHandlingRPC(8, 0x2105, "", "L", [], 20.0); } async queryStatus() { let [state, error] = await this.adapter.typedRPC(8, 0x2104, "", "LL", [], 2.0); return { state: state, lastError: error }; } } exports.RemoteBridge = RemoteBridge; class Config { constructor(adapter) { this.adapter = adapter; this.configLock = new iotile_common_1.Mutex; } async setConfigVariable(target, id, fmt, data) { let releaseConfig = await this.configLock.acquire(); try { let db_status = await this.getConfigDatabaseInfo(fmt); if (!db_status) { await this.compactConfigDatabase(); } let err = await this.startEntry(id, target); if (err) { throw new Error('Failed to start Config entry'); } await this.pushData(fmt, data); await this.finishEntry(); } finally { releaseConfig(); } } async startEntry(id, target) { let args; if (target == 'controller') { args = iotile_common_1.packArrayBuffer("H7xB", id, MatchBy.MatchController); } else if (target.includes('slot')) { let slot = target.split(" ")[1]; if (+slot >= 0 && +slot <= 255) { args = iotile_common_1.packArrayBuffer("HB6xB", id, slot, MatchBy.MatchBySlot); } else { throw new iotile_common_1.ArgumentError("Slot number must be between 0 and 255"); } } else { throw new iotile_common_1.ArgumentError("Only controller and numbered slot targets are supported"); } let resp = await this.adapter.rpc(8, 0x2a07, args, 5.0); [resp] = iotile_common_1.unpackArrayBuffer("L", resp); return resp; } async pushData(type, data) { await this.adapter.errorHandlingRPC(8, 0x2a08, type, 'L', [data], 5.0); } async finishEntry() { await this.adapter.errorHandlingRPC(8, 0x2a09, "", "L", [], 5.0); } async compactConfigDatabase() { await this.adapter.errorHandlingRPC(8, 0x2a0f, "", "L", [], 5.0); } async getConfigDatabaseInfo(type) { let resp = await this.adapter.typedRPC(8, 0x2a10, "", "LHHHHHBB", [], 5.0); let [max_data, data_size, invalid_data, entry_count, invalid_count, max_entries] = resp; let typeSize = { 'B': 1, 'b': 1, 'H': 2, 'h': 2, 'L': 4, 'l': 4 }; return (data_size + typeSize[type] < max_data); } } exports.Config = Config; class IOTileDevice extends iotile_common_1.LoggingBase { constructor(adapter, advData) { super(config_1.catIOTileDevice); this.advertisement = advData; this.deviceID = advData.deviceID; this.adapter = adapter; this.slug = iotile_common_1.deviceIDToSlug(this.deviceID); this.connectionID = advData.connectionID; this.downloadLock = new iotile_common_1.Mutex; } async acknowledgeStreamerRPC(streamer, highestID, force) { if (streamer > 255) { throw new iotile_common_1.ArgumentError('Acknowledgement RPC called with invalid streamer index'); } let args = iotile_common_1.packArrayBuffer("HHL", streamer, force ? 1 : 0, highestID); let resp = await this.adapter.rpc(8, 0x200f, args, 2.0); let decoded = iotile_common_1.unpackArrayBuffer("L", resp); let err = decoded[0]; if (!force && err === 0x8003801e) { return; } else if (err != 0) { throw new Errors.RPCError(8, 0x200f, err); } } async queryStreamerRPC(streamer) { let resp = await this.adapter.typedRPC(8, 0x200a, "H", "LLLLBBBB", [streamer], 2.0); let info = { lastAttemptTime: resp[0], lastSuccessTime: resp[1], lastError: resp[2], highestAck: resp[3], lastStatus: resp[4], backoffNumber: resp[5], commStatus: resp[6] }; return info; } async tileVersionRPC(address) { let resp = await this.adapter.typedRPC(address, 0x4, "", "H6sBBBB", [], 2.0); let major = resp[2]; let minor = resp[3]; let patch = resp[4]; return `${major}.${minor}.${patch}`; } controllerVersionRPC() { return this.tileVersionRPC(8); } async controllerHWVersionRPC() { try { let [version] = await this.adapter.typedRPC(8, 0x2, "", "10s", [], 2.0); return version.replace(/[\0]+$/g, ''); } catch (err) { if (err instanceof Errors.RPCError && err.errorCode == Errors.RPCProtocolError.CommandNotFound) return ""; throw err; } } async getDeviceInfo() { let [uuid, stateFlags, flags, res1, res2, res3, osInfo, appInfo] = await this.adapter.typedRPC(8, 0x1008, "", "LLBBBBLL", [], 2.0); let osTag = osInfo & ((1 << 20) - 1); let osEncVersion = osInfo >> 20; let appTag = appInfo & ((1 << 20) - 1); let appEnvVersion = appInfo >> 20; return { uuid: uuid, stateFlags: stateFlags, flags: flags, osTag: osTag, osVersion: this.convertEncodedVersion(osEncVersion), appTag: appTag, appVersion: this.convertEncodedVersion(appEnvVersion) }; } convertEncodedVersion(encVersion) { let major = (encVersion >> 6) & ((1 << 6) - 1); let minor = (encVersion >> 0) & ((1 << 6) - 1); return `${major}.${minor}`; } async highestUniqueIDRPC() { let [highestID] = await this.adapter.errorHandlingRPC(8, 0x2011, "", "LL", [], 2.0); return highestID; } async graphInput(stream, value) { if (typeof stream == 'string') { stream = iotile_common_1.mapStreamName(stream); } await this.adapter.errorHandlingRPC(8, 0x2004, "LH", "L", [value, stream], 1.0); } async clearAllReadings() { await this.adapter.errorHandlingRPC(8, 0x200c, "", "L", [], 2.0); } async triggerStreamer(streamer) { let [error] = await this.adapter.typedRPC(8, 0x2010, "H", "L", [streamer], 1.0); return error; } async waitReports(notifier, streamers) { let reportInProgress = false; let reportFailed = false; let reportCount = 0; let invalidReports = 0; let streamerNames = {}; let streamerNumbers = []; if (streamers instanceof Array) { for (let streamer of streamers) streamerNames[streamer] = `Report Type ${streamer}`; streamerNumbers = streamers; } else { streamerNames = streamers; for (let streamer of Object.keys(streamers)) streamerNumbers.push(+streamer); } let subNotifier = null; let progressCallback = (eventName, event) => { this.logTrace(`waitReports received event: ${eventName}`, event); switch (event.name) { case 'ReportInvalidEvent': reportInProgress = false; invalidReports += 1; notifier.finishOne(); break; case 'ReportFinishedEvent': reportInProgress = false; reportCount += 1; notifier.finishOne(); break; case 'ReportStartedEvent': subNotifier = notifier.startOne(`Receiving ${getReportName(event.reportIndex, streamerNames)}`, 20); reportInProgress = true; break; case 'ReportProgressEvent': if (subNotifier != null) subNotifier.finishOne(); reportInProgress = true; break; case 'ReportStalledEvent': case 'ReportParsingError': reportInProgress = false; reportFailed = true; break; } }; let disconnectCallback = function (eventName, event) { reportInProgress = false; }; let reportStartedHandler = this.adapter.subscribe(iotile_types_1.AdapterEvent.RobustReportStarted, progressCallback); let reportStalledHandler = this.adapter.subscribe(iotile_types_1.AdapterEvent.RobustReportStalled, progressCallback); let reportParseErrorHandler = this.adapter.subscribe(iotile_types_1.AdapterEvent.UnrecoverableStreamingError, progressCallback); let reportProgressHandler = this.adapter.subscribe(iotile_types_1.AdapterEvent.RobustReportProgress, progressCallback); let reportInvalidHandler = this.adapter.subscribe(iotile_types_1.AdapterEvent.RobustReportInvalid, progressCallback); let reportFinishedHandler = this.adapter.subscribe(iotile_types_1.AdapterEvent.RobustReportFinished, progressCallback); let reportDisconnectedHandler = this.adapter.subscribe(iotile_types_1.AdapterEvent.Disconnected, disconnectCallback); this.adapter.resetStreaming(); try { for (let key of streamerNumbers) { let info = await this.queryStreamerRPC(+key); this.logDebug(`Queried streamer ${key}`, { info: info }); if (info.commStatus === 0) { let err = await this.triggerStreamer(+key); this.logDebug(`Triggered streamer ${key}`, { returnValue: err }); if (err && (err !== 0x8003801f)) throw new Errors.FatalStreamingError(`Error triggering streamer ${key}, error: ${err}`, "Download Failed - Please Try Again"); } } while (true) { await iotile_common_1.delay(500); if (reportInProgress) continue; await iotile_common_1.delay(100); if (!reportInProgress) break; } if (reportFailed) throw new Errors.FatalStreamingError("Stall while receiving a report", "Streaming Failed - Please Try Again"); } finally { reportStartedHandler(); reportStalledHandler(); reportProgressHandler(); reportFinishedHandler(); reportDisconnectedHandler(); reportInvalidHandler(); reportParseErrorHandler(); } for (let i = reportCount; i < streamerNumbers.length; ++i) { notifier.startOne(`Skipping Report with No New Data`, 1); notifier.finishOne(); } return { numInvalid: invalidReports, numReceived: reportCount }; } async receiveReports(options, progress) { let result = { reports: [], receivedFromAll: true, receivedExtra: false }; if (!progress) progress = new iotile_common_1.ProgressNotifier(); let receivedNames = []; let clearSubscription = this.adapter.subscribe(iotile_types_1.AdapterEvent.RawRobustReport, async (event, report) => { try { if (report.streamer in options.expectedStreamers && !(report.streamer in receivedNames)) { result.reports.push(report); receivedNames.push(report.streamer); } else { result.receivedExtra = true; } } catch (err) { this.logException(`Could not process report: ${options.expectedStreamers[report.streamer]}`, new Error(err)); } }); try { let reportInfo = await this.waitReports(progress, options.expectedStreamers); this.logDebug("Result of waitReports", { streamers: options.expectedStreamers, result: reportInfo }); if (reportInfo.numInvalid > 0) throw new Errors.FatalStreamingError(`Received ${reportInfo.numInvalid} invalid reports`, 'Download Failed - Please Try Again'); } finally { clearSubscription(); } for (let key of Object.keys(options.expectedStreamers)) { if (!(+key in receivedNames)) { result.receivedFromAll = false; } } if (options.requireAll && !result.receivedFromAll) { this.logError(`Failed to receive all required streamers`, { result: result, options: options }); throw new Errors.FatalStreamingError(`Missing required report`, 'Download Failed - Please Try Again'); } return result; } remoteBridge() { return new RemoteBridge(this.adapter); } config() { return new Config(this.adapter); } async currentTime(synchronizationSlopSeconds = 60) { let deviceTime; let [timestamp] = await this.adapter.typedRPC(8, 0x1001, "", "L", []); config_1.catAdapter.info(`Timestamp is: ${timestamp}`); if (!!(timestamp & (1 << 31)) === true) { let secondsSince2000 = timestamp & ((1 << 31) - 1); config_1.catAdapter.info(`Seconds since 2000: ${secondsSince2000}`); let convertedSeconds = (Date.UTC(2000, 0, 1) / 1000) + secondsSince2000; let convertedTime = new Date(convertedSeconds * 1000); let currentSeconds = Date.now() / 1000; let synched = (Math.abs(convertedSeconds - currentSeconds) <= synchronizationSlopSeconds); deviceTime = { isUTC: true, isSynchronized: synched, currentTime: convertedTime }; } else { deviceTime = { isUTC: false, isSynchronized: false, currentTime: timestamp }; } return deviceTime; } async synchronizeTime(forcedTime) { if (!forcedTime) { forcedTime = new Date(); } let secondsSince2000 = convertToSecondsSince2000(forcedTime); config_1.catAdapter.info(`Sending time to RTC: ${secondsSince2000}`); await this.adapter.errorHandlingRPC(8, 0x1010, "L", "L", [secondsSince2000]); return secondsSince2000; } async downloadStream(streamName, progress) { let releaseStream = await this.downloadLock.acquire(); try { let streamId = iotile_common_1.mapStreamName(streamName); let [err, count, device_time] = await this.adapter.errorHandlingRPC(8, 0x2008, "H", "LLLL", [streamId], 3.0); let now = new Date(); let readings = []; let subNotifier = undefined; if (err) { throw new iotile_common_1.ArgumentError(`Error starting stream download: ${err}`); } if (progress) { subNotifier = progress.startOne(`Downloading ${count} readings`, count); } for (let i = 0; i < count; i++) { let [timestamp, raw_reading] = await this.adapter.errorHandlingRPC(8, 0x2009, "", "LLL", [], 1.0); let timebase = new Date(now.valueOf() - (device_time * 1000)); let reading = new iotile_reports_1.RawReading(streamId, raw_reading, timestamp, timebase); readings.push(reading); if (subNotifier) { subNotifier.finishOne(); } } return readings; } finally { releaseStream(); } } async inspectVirtualStream(stream) { if (typeof stream == 'string') { stream = iotile_common_1.mapStreamName(stream); } let [val] = await this.adapter.errorHandlingRPC(8, 0x200b, "H", "LL", [stream]); return val; } async queryBLEConnectionInfo() { let [interval, timeout, prefMin, prefMax, prefTimeout] = await this.adapter.errorHandlingRPC(8, 0x8000, "", "LHHHHHH", [], 1.0); return { intervalMS: interval * 1.25, preferredMinMS: prefMin * 1.25, preferredMaxMS: prefMax * 1.25, timeoutMS: timeout * 10 }; } async updateBLEParams(minIntervalMS, maxIntervalMS, timeoutMS) { let minInterval = Math.floor(minIntervalMS / 1.25); let maxInterval = Math.floor(maxIntervalMS / 1.25); let timeout = Math.floor(timeoutMS / 10); if (minIntervalMS < 7.5 || maxIntervalMS < minIntervalMS) { throw new iotile_common_1.ArgumentError(`Invalid interval given [${minIntervalMS}, ${maxIntervalMS}], must be min >= 7.5 ms, max >= min`); } if (timeoutMS < 100) { throw new iotile_common_1.ArgumentError(`Invalid connection timeout given (${timeoutMS} ms), must be >= 100 ms.`); } let [err] = await this.adapter.typedRPC(8, 0x8001, "HHHH", "L", [minInterval, maxInterval, timeout, 0], 1.0); return err; } } exports.IOTileDevice = IOTileDevice; function getReportName(streamer, streamerNames) { if (!(streamer in streamerNames)) return "Extra Report"; return streamerNames[streamer]; } //# sourceMappingURL=iotile-device.js.map