@iotile/iotile-device
Version:
A typescript library for interfacing with IOTile BLE devices
473 lines • 19.6 kB
JavaScript
;
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