@iotile/iotile-device
Version:
A typescript library for interfacing with IOTile BLE devices
957 lines • 66.9 kB
JavaScript
"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 __());
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
///<reference path="../../typings/cordova_plugins.d.ts"/>
var Errors = require("../common/error-space");
var iotile_types_1 = require("../common/iotile-types");
var iotile_advert_serv_1 = require("./iotile-advert-serv");
var iotile_iface_rpc_1 = require("./iotile-iface-rpc");
var iotile_iface_script_1 = require("./iotile-iface-script");
var iotile_iface_streaming_1 = require("./iotile-iface-streaming");
var iotile_iface_tracing_1 = require("./iotile-iface-tracing");
var iotile_common_1 = require("@iotile/iotile-common");
var iotile_ble_optimizer_1 = require("./iotile-ble-optimizer");
var iotile_base_types_1 = require("./iotile-base-types");
var iotile_device_1 = require("./iotile-device");
var config_1 = require("../config");
var mock_ble_serv_1 = require("../mocks/mock-ble-serv");
var sliding_window_1 = require("../common/sliding-window");
var ReceiveHeaderCharacteristic = '2001';
var ReceivePayloadCharacteristic = '2002';
var SendHeaderCharacteristic = '2003';
var SendPayloadCharacteristic = '2004';
var StreamingCharacteristic = '2005';
var HighspeedDataCharacteristic = '2006';
var TracingCharacteristic = '2007';
var Interface;
(function (Interface) {
Interface[Interface["RPC"] = 0] = "RPC";
Interface[Interface["Streaming"] = 1] = "Streaming";
Interface[Interface["Script"] = 2] = "Script";
Interface[Interface["Tracing"] = 3] = "Tracing";
})(Interface = exports.Interface || (exports.Interface = {}));
var IOTileServiceName = '00002000-3FF7-53BA-E611-132C0FF60F63';
var IOTileAdapter = /** @class */ (function (_super) {
__extends(IOTileAdapter, _super);
function IOTileAdapter(Config, notificationService, platform) {
var _this = _super.call(this) || this;
_this.charManagers = {};
_this.characteristicNames = {};
_this.adapterEventNames = {};
_this.platform = platform;
_this.adParser = new iotile_advert_serv_1.IOTileAdvertisementService();
_this.config = Config;
_this.catAdapter = config_1.catAdapter;
_this.notification = notificationService;
_this.state = iotile_types_1.AdapterState.Idle;
_this.connectionHooks = [];
_this.preconnectionHooks = [];
_this.lastScanResults = [];
_this.connectedDevice = null;
_this.tracingOpen = false;
_this.interactive = false;
_this.supportsFastWrites = false;
_this.connectionMessages = [];
_this.characteristicNames[iotile_types_1.IOTileCharacteristic.ReceiveHeader] = '2001';
_this.characteristicNames[iotile_types_1.IOTileCharacteristic.ReceivePayload] = '2002';
_this.characteristicNames[iotile_types_1.IOTileCharacteristic.SendHeader] = '2003';
_this.characteristicNames[iotile_types_1.IOTileCharacteristic.SendPayload] = '2004';
_this.characteristicNames[iotile_types_1.IOTileCharacteristic.Streaming] = '2005';
_this.characteristicNames[iotile_types_1.IOTileCharacteristic.HighspeedData] = '2006';
_this.characteristicNames[iotile_types_1.IOTileCharacteristic.Tracing] = '2007';
//Names of all of our adapter events for broadcasting using angular's emit
_this.adapterEventNames[iotile_types_1.AdapterEvent.ScanStarted] = "adapter_scanstarted";
_this.adapterEventNames[iotile_types_1.AdapterEvent.ScanFinished] = "adapter_scanfinished";
_this.adapterEventNames[iotile_types_1.AdapterEvent.Connected] = "adapter_connected";
_this.adapterEventNames[iotile_types_1.AdapterEvent.ConnectionStarted] = "adapter_connectionstarted";
_this.adapterEventNames[iotile_types_1.AdapterEvent.ConnectionFinished] = "adapter_connectionfinished";
_this.adapterEventNames[iotile_types_1.AdapterEvent.Disconnected] = "adapter_disconnected";
_this.adapterEventNames[iotile_types_1.AdapterEvent.UnrecoverableRPCError] = "adapter_unrecoverablerpcerror";
_this.adapterEventNames[iotile_types_1.AdapterEvent.UnrecoverableStreamingError] = "adapter_streamingerror";
_this.adapterEventNames[iotile_types_1.AdapterEvent.RawRealtimeReading] = "adapter_rawrealtimereading";
_this.adapterEventNames[iotile_types_1.AdapterEvent.RawRobustReport] = "adapter_rawrobustreport";
_this.adapterEventNames[iotile_types_1.AdapterEvent.RobustReportStarted] = "adapter_robustreportstarted";
_this.adapterEventNames[iotile_types_1.AdapterEvent.RobustReportStalled] = "adapter_robustreportstalled";
_this.adapterEventNames[iotile_types_1.AdapterEvent.RobustReportProgress] = "adapter_robustreportprogress";
_this.adapterEventNames[iotile_types_1.AdapterEvent.RobustReportFinished] = "adapter_robustreportfinished";
_this.adapterEventNames[iotile_types_1.AdapterEvent.RobustReportInvalid] = "adapter_robustreportinvalid";
_this.adapterEventNames[iotile_types_1.AdapterEvent.StreamingInterrupted] = "adapter_streaminginterrupted";
if (Object.keys(_this.adapterEventNames).length !== iotile_types_1.AdapterEvent.Length) {
throw new iotile_common_1.BaseError("UnrecoverableError", "IOTileAdapter has not assigned all adapter events. This is an internal coding error.");
}
/*
* We internally manage all notifications on characteristics using a pub/sub
* listener scheme to allow multiple people to act on the data coming in
*/
_this.charManagers[iotile_types_1.IOTileCharacteristic.Streaming] = new CharacteristicManager();
_this.charManagers[iotile_types_1.IOTileCharacteristic.Tracing] = new CharacteristicManager();
_this.charManagers[iotile_types_1.IOTileCharacteristic.ReceiveHeader] = new CharacteristicManager();
_this.charManagers[iotile_types_1.IOTileCharacteristic.ReceivePayload] = new CharacteristicManager();
_this.rpcInterface = new iotile_iface_rpc_1.IOTileRPCInterface();
_this.streamingInterface = new iotile_iface_streaming_1.IOTileStreamingInterface(Config.BLE.STREAMING_BUFFER_SIZE, true);
_this.scriptInterface = new iotile_iface_script_1.IOTileScriptInterface();
_this.tracingInterface = new iotile_iface_tracing_1.IOTileTracingInterface();
/*
* Check if we should install a mock ble plugin for development
* purposes. This replaces the cordova ble plugin and provides
* access to a fixed set of (virtual) testing devices defined in
* mock-ble-serv.js.
*/
if (Config.BLE && Config.BLE.MOCK_BLE) {
// this.catAdapter.info('Using Mock BLE implementation.');
_this.mockBLEService = new mock_ble_serv_1.MockBleService(Config);
window.ble = _this.mockBLEService;
window.device = { 'platform': Config.BLE.MOCK_BLE_DEVICE };
}
//Register our own connection hook that optimizes BLE connection speed on connect
var optimizer = new iotile_ble_optimizer_1.BLEConnectionOptimizer(platform);
_this.registerConnectionHook(function (device, adapter) { return optimizer.optimizeConnection(device, adapter); });
//Register our connection hook to support larger stream reports on newer devices
_this.registerConnectionHook(function (device, adapter) {
return _this.setReportSize(device, adapter);
});
return _this;
}
IOTileAdapter.prototype.setReportSize = function (device, adapter) {
return __awaiter(this, void 0, void 0, function () {
var _a, maxPacket, _comp1, _comp2, err_1;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_b.trys.push([0, 3, , 4]);
return [4 /*yield*/, adapter.errorHandlingRPC(8, 0x0A05, "LB", "L", [1024 * 1024, 0], 5.0)];
case 1:
_b.sent();
return [4 /*yield*/, adapter.typedRPC(8, 0x0A06, "", "LBB", [], 5.0)];
case 2:
_a = _b.sent(), maxPacket = _a[0], _comp1 = _a[1], _comp2 = _a[2];
if (maxPacket != 1024 * 1024) {
this.catAdapter.error("Device report size failed to update", Error);
}
else {
this.catAdapter.info("Large device report size successfully configured");
}
return [3 /*break*/, 4];
case 3:
err_1 = _b.sent();
this.catAdapter.info("Couldn't configure sending larger reports on this device: " + JSON.stringify(err_1));
return [3 /*break*/, 4];
case 4: return [2 /*return*/, null];
}
});
});
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#getConnectedDevice
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Get the currently connected device. If there is no device
* currently connected, returns null.
*
* @returns
* {IOTileDevice} The currently connected device or null
* if no device is connected currently.
*/
IOTileAdapter.prototype.getConnectedDevice = function () {
return this.connectedDevice;
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#registerConnectionHook
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Register a function to be called everytime someone connects to a device
*
* The function should be asynchronous and throw an exception if there is an error
* inside the hook that means the device should not be connected to. If any hook throws
* an exception, the device is disconnected from and that error is thrown to whomever
* tried to connect to the device.
*
* The signature of the method should be (IOTileDevice, IOTileAdapter) => Promise<void>
*
* @param {ConnectionHookCallback} hook The function that should be called every time we
* connect to a device.
*/
IOTileAdapter.prototype.registerConnectionHook = function (hook) {
this.connectionHooks.push(hook);
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#registerPreconnectionHook
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Register a function to be called before someone connects to a device
*
* The function should be asynchronous and return a boolean. If the function returns
* true, we can proceed with the connection. If the function returns false, the connection
* attempt will be stopped.
*
* The signature of the method should be (IOTileDevice, IOTileAdapter) => Promise<boolean>
*
* @param {ConnectionHookCallback} hook The function that should be called every time we
* connect to a device.
*/
IOTileAdapter.prototype.registerPreconnectionHook = function (hook) {
this.preconnectionHooks.push(hook);
};
/*
* If the app gets suspended, we don't receive bluetooth notifications so we need to
* stop the streaming service so that we don't get corrupted reports that have missing
* chunks when the user had the app closed.
*
* Device pages can hook into the StreamingInterrupted event to boot the user out of a
* connection to the device page when the app resumes operation after a suspend.
*/
IOTileAdapter.prototype.pause = function () {
this.streamingInterface.stop();
};
IOTileAdapter.prototype.resume = function () {
this.notify(iotile_types_1.AdapterEvent.StreamingInterrupted, null);
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#enabled
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Return is BLE is enabled on the device.
*
* **This is an async method!**
*
* @returns {boolean} Whether or not BLE is enabled on the device
*/
IOTileAdapter.prototype.enabled = function () {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
return [2 /*return*/, new Promise(function (resolve, reject) {
window.ble.isEnabled(function () { return resolve(true); }, function () { return resolve(false); });
})];
});
});
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#scan
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Scan for devices for a fixed period of time.
*
* **This is an async method!**
*
* Returns a list of IOTileAdvertisement objects for the IOTile devices that
* were found.
*
* ## Side Effects:
* - Notifies on AdapterEvent.ScanStarted when scanning has been started
* - Notifies on AdapterEvent.ScanFinished when scanning has finished.
*
* AdapterEvent.ScanFinished has one argument with a count property that
* contains the number of IOTile devices that were found. See the
* ScanFinishedArgs interface.
*
* @example
* <pre>
* //Scan for 1 second and return the devices seen
* var foundDevices = await IOTileAdapter.scan(1.0);
* console.log("Found " + foundDevices.length + " IOTile devices!");
* </pre>
*
* @param {number} scanPeriod The number of seconds to scan
* @returns {IOTileAdvertisement[]} A list of the IOTile devices seen during the scan
*/
IOTileAdapter.prototype.scan = function (scanPeriod) {
return __awaiter(this, void 0, void 0, function () {
var foundDevices, uniqueDevices, that, err_2;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
foundDevices = [];
uniqueDevices = {};
this.ensureIdle('scanning');
this.state = iotile_types_1.AdapterState.Scanning;
this.notify(iotile_types_1.AdapterEvent.ScanStarted, {});
that = this;
_a.label = 1;
case 1:
_a.trys.push([1, 4, 5, 6]);
window.ble.startScan([], function (peripheral) {
try {
var device = that.createIOTileAdvertisement(peripheral);
if (device == null)
return;
//Make sure we only report each device once even on OSes like iOS that
//can return multiple scan events per device in a given scan period.
if (device.slug in uniqueDevices) {
return;
}
uniqueDevices[device.slug] = true;
foundDevices.push(device);
}
catch (err) {
that.catAdapter.error("Error Scanning for Devices", new Error(JSON.stringify(err)));
}
});
return [4 /*yield*/, iotile_common_1.delay(scanPeriod * 1000)];
case 2:
_a.sent();
return [4 /*yield*/, this.stopScan()];
case 3:
_a.sent();
return [3 /*break*/, 6];
case 4:
err_2 = _a.sent();
this.catAdapter.error("Problem calling BLE startScan", new Error(JSON.stringify(err_2)));
return [3 /*break*/, 6];
case 5:
this.state = iotile_types_1.AdapterState.Idle;
this.notify(iotile_types_1.AdapterEvent.ScanFinished, { "count": foundDevices.length });
return [7 /*endfinally*/];
case 6:
this.lastScanResults = foundDevices;
return [2 /*return*/, foundDevices];
}
});
});
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#connectTo
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Connect to an IOTileDevice that has previously been scanned given its slug.
*
* **This is an async method**
*
* This method connects to an IOTile device over bluetooth. If there
* is already an IOTile device connected, it throws an error.
*
* Raises a ConnectionError if the device slug cannot be found in previously scanned
* devices.
*
* @param {string} slug The slug of the device that we want to connect to
* @param {ConnectionOptions} options (optional) Configure what checks and
* and actions are performed automatically on connection without any required interaction.
* By default, the RPC interface is opened and streaming is started.
*/
IOTileAdapter.prototype.connectTo = function (slug, options) {
return __awaiter(this, void 0, void 0, function () {
var i, advert, device, i, advert, device;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
i = 0;
_a.label = 1;
case 1:
if (!(i < this.lastScanResults.length)) return [3 /*break*/, 4];
advert = this.lastScanResults[i];
if (!(advert.slug == slug)) return [3 /*break*/, 3];
return [4 /*yield*/, this.connect(advert, options)];
case 2:
device = _a.sent();
return [2 /*return*/, device];
case 3:
++i;
return [3 /*break*/, 1];
case 4:
if (!(options && options.scanIfNotFound)) return [3 /*break*/, 9];
return [4 /*yield*/, this.scan(2.0)];
case 5:
_a.sent();
i = 0;
_a.label = 6;
case 6:
if (!(i < this.lastScanResults.length)) return [3 /*break*/, 9];
advert = this.lastScanResults[i];
if (!(advert.slug == slug)) return [3 /*break*/, 8];
return [4 /*yield*/, this.connect(advert, options)];
case 7:
device = _a.sent();
return [2 /*return*/, device];
case 8:
++i;
return [3 /*break*/, 6];
case 9: throw new Errors.ConnectionError("Could not find device slug in scan results");
}
});
});
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#connect
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Connect to an IOTileDevice.
*
* **This is an async method**
*
* This method connects to an IOTile device over bluetooth. If there
* is already an IOTile device connected, it throws an error.
*
* @param {IOTileAdvertisement} advert The IOTileDevice object that we should
* connect to. This device should been returned
* from a previous call to IOTileAdapter.scan.
* @param {ConnectionOptions} options (optional) Configure what checks and
* and actions are performed automatically on connection without any required interaction.
* By default, the RPC interface is opened and streaming is started.
*
* @example
* <pre>
* var foundAdverts = await IOTileAdapter.scan(1.0);
* if (foundAdverts.length > 0) {
* var device = await IOTileAdapter.connect(foundAdverts[0]);
* }
* </pre>
*/
IOTileAdapter.prototype.connect = function (advert, options) {
return __awaiter(this, void 0, void 0, function () {
var openRPC, openStreaming, prestreamingHook, i, hook, redirect, _a, i, hook, redirect, err_3;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
this.ensureIdle('connecting');
openRPC = true;
openStreaming = true;
prestreamingHook = null;
this.connectionMessages = [];
this.interactive = true;
this.supportsFastWrites = false;
this.tracingOpen = false;
if (options != null) {
if (options.noStreamInterface != null) {
openStreaming = !options.noStreamInterface;
}
if (options.noRPCInterface != null) {
openRPC = !options.noRPCInterface;
}
if (options.prestreamingHook != null) {
prestreamingHook = options.prestreamingHook;
}
if (options.noninteractive != null) {
this.interactive = !options.noninteractive;
}
}
//FIXME: Check to make sure BLE is enabled here
//Run all preconnection hooks here to make sure that we should
//connect to this device. Do this before notifying ConnectionStarted
//so that users can capture that event to show a loading modal.
this.catAdapter.info("Running preconnectionHooks");
i = 0;
_b.label = 1;
case 1:
if (!(i < this.preconnectionHooks.length)) return [3 /*break*/, 4];
hook = this.preconnectionHooks[i];
return [4 /*yield*/, hook(advert, this)];
case 2:
redirect = _b.sent();
if (redirect) {
this.catAdapter.error("Error running preconnection hooks", new Error(redirect.reason));
throw new Errors.ConnectionCancelledError(redirect);
}
_b.label = 3;
case 3:
++i;
return [3 /*break*/, 1];
case 4:
this.state = iotile_types_1.AdapterState.Connecting;
this.notify(iotile_types_1.AdapterEvent.ConnectionStarted, {});
_b.label = 5;
case 5:
_b.trys.push([5, , 23, 24]);
_a = this;
return [4 /*yield*/, this.connectInternal(advert)];
case 6:
_a.connectedDevice = _b.sent();
if (!this.config.ENV.CONNECTION_DELAY) return [3 /*break*/, 8];
this.catAdapter.info("Connection delay: " + this.config.ENV.CONNECTION_DELAY);
return [4 /*yield*/, iotile_common_1.delay(this.config.ENV.CONNECTION_DELAY)];
case 7:
_b.sent();
_b.label = 8;
case 8:
_b.trys.push([8, 20, , 22]);
if (!openRPC) return [3 /*break*/, 10];
return [4 /*yield*/, this.openInterface(Interface.RPC)];
case 9:
_b.sent();
_b.label = 10;
case 10:
//Always open the script interface in case we need to do a firmware update
return [4 /*yield*/, this.openInterface(Interface.Script)];
case 11:
//Always open the script interface in case we need to do a firmware update
_b.sent();
//Now run all of the connection hooks that are registered for this device
this.catAdapter.info("Running " + this.connectionHooks.length + " connectionHooks");
i = 0;
_b.label = 12;
case 12:
if (!(i < this.connectionHooks.length)) return [3 /*break*/, 15];
hook = this.connectionHooks[i];
return [4 /*yield*/, hook(this.connectedDevice, this)];
case 13:
redirect = _b.sent();
if (redirect) {
this.catAdapter.error("Error running connection hooks", new Error(redirect.reason));
throw new Errors.ConnectionCancelledError(redirect);
}
_b.label = 14;
case 14:
++i;
return [3 /*break*/, 12];
case 15:
this.catAdapter.info('Finished connectionHooks');
if (!(prestreamingHook != null)) return [3 /*break*/, 17];
this.catAdapter.info("Running prestreamingHooks");
return [4 /*yield*/, prestreamingHook(this.connectedDevice, this)];
case 16:
_b.sent();
_b.label = 17;
case 17:
if (!openStreaming) return [3 /*break*/, 19];
this.catAdapter.info("Running openStreaming interface");
return [4 /*yield*/, this.openInterface(Interface.Streaming)];
case 18:
_b.sent();
_b.label = 19;
case 19: return [3 /*break*/, 22];
case 20:
err_3 = _b.sent();
return [4 /*yield*/, this.disconnect()];
case 21:
_b.sent();
this.catAdapter.error("Connection Cancelled", new Error(JSON.stringify(err_3)));
throw err_3;
case 22: return [3 /*break*/, 24];
case 23:
this.notify(iotile_types_1.AdapterEvent.ConnectionFinished, {});
return [7 /*endfinally*/];
case 24:
this.notify(iotile_types_1.AdapterEvent.Connected, { device: this.connectedDevice });
return [2 /*return*/, this.connectedDevice];
}
});
});
};
/**
* Utility method to forcibly reset the streaming interface in case it has received corrupted data.
*/
IOTileAdapter.prototype.resetStreaming = function () {
this.ensureConnected('resetting stream interface');
this.streamingInterface.reset();
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#rpc
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Send an RPC to a connected IOTile device
*
* **This is an async method!**
*
* This function sends a single RPC to an IOTile device and waits for it to finish
* before returning. Internally the RPC is queued so that multiple users calling this
* function from different places simultaneously will properly be synchronized.
*
* This is provisional API that will change.
*
* @param {number} address The address of the tile that we want to send the RPC to
* @param {ArrayBuffer} payload The payload that we would like to send
* @param {number} rpcID The 16 bit id of the RPC that we would like to call
* @param {number} timeout The maximum amount of time that we would like to wait for this RPC
* to finish.
*
*
*/
IOTileAdapter.prototype.rpc = function (address, rpcID, payload, timeout) {
return __awaiter(this, void 0, void 0, function () {
var response;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this.ensureConnected('sending rpc');
return [4 /*yield*/, this.rpcInterface.rpc(address, rpcID, payload, timeout)];
case 1:
response = _a.sent();
return [2 /*return*/, response];
}
});
});
};
IOTileAdapter.prototype.sendScript = function (script, notifier) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
this.ensureConnected('sending script');
return [4 /*yield*/, this.scriptInterface.send(script, notifier)];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
};
IOTileAdapter.prototype.clearTrace = function () {
this.tracingInterface.clearData();
};
/**
* Wait for a given number of bytes to be received on the tracing interface.
*
* If no data has been received in more than 1 second after calling this function,
* the promise returned will be rejected by the tracing interface watchdog timer,
* making sure that the interface cannot stall. The recommended way to use this
* function is to trigger some event that sends data via the tracing interface
* (after making sure the interface is open...) and then calling:
*
* ```
* let data: ArrayBuffer = await adapter.waitForTracingData(numBytes);
* ```
*
* That is all that is required to synchronously receive that number of bytes
* from the tracing interface.
*
* @param numBytes The number of bytes to wait for. This exact
* number of bytes will be returned to you when the Promise
* is resolved.
* @param timeout The number of milliseconds to wait before receiving another chunk
* of tracing data before we give up and abort the wait (rejecting the promise).
* This defaults to 1000 ms if not passed.
*/
IOTileAdapter.prototype.waitForTracingData = function (numBytes, timeout) {
if (timeout === void 0) { timeout = 1000; }
return this.tracingInterface.waitForData(numBytes, timeout);
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#typedRPC
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Type converting wrapper around rpc()
*
* **This is an async method!**
*
* This function sends a single RPC to an IOTile device and waits for it to finish
* before returning. Internally it calls IOTileAdapter.rpc() to make the actual RPC
* call but this function builds the payload ArrayBuffer from a list of integers and
* a format code. Similarly, it decodes the response into a list of numbers as well.
*
* If an error occurs calling the RPC, an exception is thrown indicating what went wrong.
* If the error is unrecoverable, further RPCs to the device will not be allowed without
* disconnecting and reconnecting.
*
* @param {number} address The address of the tile that we want to send the RPC to
* @param {number} rpcID The 16 bit id of the RPC that we would like to call
* @param {number} timeout The maximum amount of time that we would like to wait for this RPC
* to finish.
* @param {string} callFormat A format code passed to packArrayBuffer to convert a list of numbers
* into an ArrayBuffer. Examples would be "LH" to pack an unsigned 32 bit integer followed by
* a 16 bit integer.
* @param {string} respFormat A format code passed to unpackArrayBuffer to convert the response Callback
* into a list of numbers that are returned.
* @param {number[]} args An array of arguments that should match the format of callFormat and is used to
* construct the payload for this RPC.
* @returns {number[]} The decoded list of numbers that were returned from the RPC.
*/
IOTileAdapter.prototype.typedRPC = function (address, rpcID, callFormat, respFormat, args, timeout) {
return __awaiter(this, void 0, void 0, function () {
var callPayload, respBuffer, resp;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
callPayload = iotile_common_1.packArrayBuffer.apply(void 0, [callFormat].concat(args));
return [4 /*yield*/, this.rpc(address, rpcID, callPayload, timeout)];
case 1:
respBuffer = _a.sent();
resp = iotile_common_1.unpackArrayBuffer(respFormat, respBuffer);
return [2 /*return*/, resp];
}
});
});
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#errorHandlingRPC
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Type converting wrapper around rpc() that throws if the first argument of response is nonzero
*
* **This is an async method!**
*
* This function sends a single RPC to an IOTile device and waits for it to finish
* before returning. Internally it calls IOTileAdapter.rpc() to make the actual RPC
* call but this function builds the payload ArrayBuffer from a list of integers and
* a format code. Similarly, it decodes the response into a list of numbers as well.
*
* If an error occurs calling the RPC, an exception is thrown indicating what went wrong.
* If the error is unrecoverable, further RPCs to the device will not be allowed without
* disconnecting and reconnecting.
*
* The RPC called must return a uint32_t as the first 4 bytes of its response and this value
* is checked to ensure it is zero. If it is nonzero, an exception is thrown rather saying
* there was an error executing the RPC.
*
* **The list of values returned does not include the error code since it will always be zero.**
*
* @param {number} address The address of the tile that we want to send the RPC to
* @param {number} rpcID The 16 bit id of the RPC that we would like to call
* @param {number} timeout The maximum amount of time that we would like to wait for this RPC
* to finish.
* @param {string} callFormat A format code passed to packArrayBuffer to convert a list of numbers
* into an ArrayBuffer. Examples would be "LH" to pack an unsigned 32 bit integer followed by
* a 16 bit integer.
* @param {string} respFormat A format code passed to unpackArrayBuffer to convert the response Callback
* into a list of numbers that are returned.
* @param {number[]} args An array of arguments that should match the format of callFormat and is used to
* construct the payload for this RPC. The first item (the error code) is shifted off and not returned.
* @returns {number[]} The decoded list of numbers that were returned from the RPC (excluding the error code).
*/
IOTileAdapter.prototype.errorHandlingRPC = function (address, rpcID, callFormat, respFormat, args, timeout) {
return __awaiter(this, void 0, void 0, function () {
var resp, errorCode;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
//The response must start with a 32 bit integer
if (respFormat.length === 0 || (respFormat[0] !== 'B' && respFormat[0] !== 'L' && respFormat[0] !== 'H')) {
throw new iotile_common_1.ArgumentError('Invalid response format for errorHandlingRPC that did not start with an B, L, or H code for the error.');
}
return [4 /*yield*/, this.typedRPC(address, rpcID, callFormat, respFormat, args, timeout)];
case 1:
resp = _a.sent();
errorCode = resp.shift();
if (errorCode != 0) {
this.catAdapter.error("Failed to execute rpc " + rpcID + " on tile " + address, Error);
throw new Errors.RPCError(address, rpcID, errorCode);
}
return [2 /*return*/, resp];
}
});
});
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#disconnect
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Disconnect from any currently connected IOTile Device.
*
* **This is an async method!**
*
* This method can never fail. After the method returns, the IOTile
* adapter will be in an Idle state and able to scan for or connect to
* other IOTile devices.
*/
IOTileAdapter.prototype.disconnect = function () {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (this.state !== iotile_types_1.AdapterState.Connected) {
return [2 /*return*/];
}
this.state = iotile_types_1.AdapterState.Disconnecting;
if (!this.connectedDevice) return [3 /*break*/, 2];
return [4 /*yield*/, this.disconnectInternal(this.connectedDevice.connectionID)];
case 1:
_a.sent();
_a.label = 2;
case 2:
this.charManagers[iotile_types_1.IOTileCharacteristic.Streaming].removeAll();
this.charManagers[iotile_types_1.IOTileCharacteristic.ReceiveHeader].removeAll();
this.charManagers[iotile_types_1.IOTileCharacteristic.ReceivePayload].removeAll();
this.charManagers[iotile_types_1.IOTileCharacteristic.Tracing].removeAll();
this.state = iotile_types_1.AdapterState.Idle;
this.connectedDevice = null;
return [2 /*return*/];
}
});
});
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#subscribe
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Subscribe to notifications on IOTileDevice related events.
*
* Currently supports scan started and scan finished events. The
* notifications proceed using the angular event system. You may
* pass a scope object and all notifications you register will be
* automatically removed when your scope is destroyed. If you are calling
* this method from a service, you can pass null for the scope to not automatically
* deregister the handler on scope destruction.
*
* @param {AdapterEvent} event The event that you want to subscribe to, must be an event
* in IOTileAdapterModule.AdapterEvent.
* @param {Callback} callback The function that will be called when the event happens.
* callback must have the signature callback(object) => void.
*
* @returns {Handler} A function that will deregister this handler before the scope
* is destroyed if necessary.
*/
IOTileAdapter.prototype.subscribe = function (event, callback) {
var eventName = this.adapterEventNames[event];
var handler = this.notification.subscribe(eventName, callback);
return handler;
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#addNotificationListener
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Listen for notifications on a specific BLE characteristic known to IOTileAdapter.
*
* **This is an async method!**
*
* If this is the first call to addNotificationListener on this characteristic,
* the BLE stack is called to enable notifications. However, if this is a subsequent call,
* it will complete immediately since nofications have already been enabled. The listener
* will just be added to an internal table of notification listeners for that characteristic
* @param {IOTileCharacteristic} char A numerical identifier for the characteristic that we want
* notifications about.
* @param {NotificationCallback} callback The callback with signature (ArrayBuffer) => void
* @returns {Promise} Asynchronously returns a function that takes no arguments and
* removes this notification listener when called. The signature
* of the function returned is () => Promise<void>
*/
IOTileAdapter.prototype.addNotificationListener = function (char, callback) {
return __awaiter(this, void 0, void 0, function () {
var charManager, handlerID, that, removeHandler;
return __generator(this, function (_a) {
this.ensureConnected('enable notifications');
if (!(char in this.charManagers)) {
throw new iotile_common_1.UnknownKeyError("Characteristic cannot be listened to: " + char);
}
charManager = this.charManagers[char];
handlerID = charManager.addListener(callback);
that = this;
removeHandler = function () { return that.removeNotificationListener(char, handlerID); };
if (charManager.numListeners() > 1) {
return [2 /*return*/, removeHandler];
}
//If this is the first listener registered, start notification on the characteristic.
return [2 /*return*/, new Promise(function (resolve, reject) {
if (that.connectedDevice) {
var slidingWindow_1 = new sliding_window_1.SlidingWindowReorderer(function (value) { charManager.handleData(value); });
window.ble.startNotification(that.connectedDevice.connectionID, IOTileServiceName, that.characteristicNames[char], function (data, counter) {
slidingWindow_1.call(data, counter);
}, function (failure) {
charManager.removeListener(handlerID);
reject(failure);
});
/**
* Unfortunately, there is no callback from the BLE stack when notifications are successfully enabled,
* but there is a failure callback, so given that multiple BLE events should have occurred
* within 100 ms, if we haven't heard a failure by then, assume that it worked and resolve
* the promise.
*/
setTimeout(function () { resolve(removeHandler); }, 100);
}
})];
});
});
};
/**
* @ngdoc method
* @name iotile.device.service:IOTileAdapter#removeNotificationListener
* @methodOf iotile.device.service:IOTileAdapter
*
* @description
* Remove a previously installed notification listener.
*
* **This is an async method!**
*
* This will deregister a callback that was listening for notified data on a characteristic.
* If this is the last callback registered on that characteristic, notifications will also be
* stopped.
* @param {IOTileCharacteristic} char A numerical identifier for the characteristic that we want
* notifications about.
* @param {number} handlerID the ID of the callback that should be removed, which is returned from the call
to addNotificationListener.
* @returns {Promise} A promise that is fullfilled when the listener has been removed
*/
IOTileAdapter.prototype.removeNotificationListener = function (char, handlerID) {
return __awaiter(this, void 0, void 0, function () {
var charManager, stopNotifications, that;
return __generator(this, function (_a) {
if (!(char in this.charManagers)) {
throw new iotile_common_1.UnknownKeyError("Characteristic cannot be listened to: " + char);
}
charManager = this.charManagers[char];
stopNotifications = charManager.removeListener(handlerID);
that = this;
if (stopNotifications) {
return [2 /*return*/, new Promise(function (resolve, reject) {
if (that.connectedDevice) {
window.ble.stopNotification(that.connectedDevice.connectionID, IOTileServiceName, that.characteristicNames[char], function () { return resolve(); }, function (reason) { return reject(reason); });
}
})];
}
return [2 /*return*/];
});
});
};
/**
* Create a channel object that can write and subscribe to characteristics
* over BLE. Channels are passed to the subinterfaces inside this IOTileAdapter
* in order to give them the ability to actually talk to the IOTile device without
* creating a public API for low level writes and notifications.
*
* The BLEChannel interface is intended to be minimalist