@iotile/iotile-device
Version:
A typescript library for interfacing with IOTile BLE devices
573 lines • 26.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Errors = require("../common/error-space");
const iotile_types_1 = require("../common/iotile-types");
const iotile_advert_serv_1 = require("./iotile-advert-serv");
const iotile_iface_rpc_1 = require("./iotile-iface-rpc");
const iotile_iface_script_1 = require("./iotile-iface-script");
const iotile_iface_streaming_1 = require("./iotile-iface-streaming");
const iotile_iface_tracing_1 = require("./iotile-iface-tracing");
const iotile_common_1 = require("@iotile/iotile-common");
const iotile_ble_optimizer_1 = require("./iotile-ble-optimizer");
const iotile_base_types_1 = require("./iotile-base-types");
const iotile_device_1 = require("./iotile-device");
const config_1 = require("../config");
const mock_ble_serv_1 = require("../mocks/mock-ble-serv");
const sliding_window_1 = require("../common/sliding-window");
const ReceiveHeaderCharacteristic = '2001';
const ReceivePayloadCharacteristic = '2002';
const SendHeaderCharacteristic = '2003';
const SendPayloadCharacteristic = '2004';
const StreamingCharacteristic = '2005';
const HighspeedDataCharacteristic = '2006';
const 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 = {}));
let IOTileServiceName = '00002000-3FF7-53BA-E611-132C0FF60F63';
class IOTileAdapter extends iotile_base_types_1.AbstractIOTileAdapter {
constructor(Config, notificationService, platform) {
super();
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';
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.");
}
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();
if (Config.BLE && Config.BLE.MOCK_BLE) {
this.mockBLEService = new mock_ble_serv_1.MockBleService(Config);
window.ble = this.mockBLEService;
window.device = { 'platform': Config.BLE.MOCK_BLE_DEVICE };
}
let optimizer = new iotile_ble_optimizer_1.BLEConnectionOptimizer(platform);
this.registerConnectionHook((device, adapter) => { return optimizer.optimizeConnection(device, adapter); });
this.registerConnectionHook((device, adapter) => {
return this.setReportSize(device, adapter);
});
}
async setReportSize(device, adapter) {
try {
await adapter.errorHandlingRPC(8, 0x0A05, "LB", "L", [1024 * 1024, 0], 5.0);
let [maxPacket, _comp1, _comp2] = await adapter.typedRPC(8, 0x0A06, "", "LBB", [], 5.0);
if (maxPacket != 1024 * 1024) {
this.catAdapter.error("Device report size failed to update", Error);
}
else {
this.catAdapter.info("Large device report size successfully configured");
}
}
catch (err) {
this.catAdapter.info("Couldn't configure sending larger reports on this device: " + JSON.stringify(err));
}
return null;
}
getConnectedDevice() {
return this.connectedDevice;
}
registerConnectionHook(hook) {
this.connectionHooks.push(hook);
}
registerPreconnectionHook(hook) {
this.preconnectionHooks.push(hook);
}
pause() {
this.streamingInterface.stop();
}
resume() {
this.notify(iotile_types_1.AdapterEvent.StreamingInterrupted, null);
}
async enabled() {
return new Promise(function (resolve, reject) {
window.ble.isEnabled(() => resolve(true), () => resolve(false));
});
}
async scan(scanPeriod) {
let foundDevices = [];
let uniqueDevices = {};
this.ensureIdle('scanning');
this.state = iotile_types_1.AdapterState.Scanning;
this.notify(iotile_types_1.AdapterEvent.ScanStarted, {});
let that = this;
try {
window.ble.startScan([], function (peripheral) {
try {
let device = that.createIOTileAdvertisement(peripheral);
if (device == null)
return;
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)));
}
});
await iotile_common_1.delay(scanPeriod * 1000);
await this.stopScan();
}
catch (err) {
this.catAdapter.error("Problem calling BLE startScan", new Error(JSON.stringify(err)));
}
finally {
this.state = iotile_types_1.AdapterState.Idle;
this.notify(iotile_types_1.AdapterEvent.ScanFinished, { "count": foundDevices.length });
}
this.lastScanResults = foundDevices;
return foundDevices;
}
async connectTo(slug, options) {
for (let i = 0; i < this.lastScanResults.length; ++i) {
let advert = this.lastScanResults[i];
if (advert.slug == slug) {
let device = await this.connect(advert, options);
return device;
}
}
if (options && options.scanIfNotFound) {
await this.scan(2.0);
for (let i = 0; i < this.lastScanResults.length; ++i) {
let advert = this.lastScanResults[i];
if (advert.slug == slug) {
let device = await this.connect(advert, options);
return device;
}
}
}
throw new Errors.ConnectionError("Could not find device slug in scan results");
}
async connect(advert, options) {
this.ensureIdle('connecting');
let openRPC = true;
let openStreaming = true;
let 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;
}
}
this.catAdapter.info("Running preconnectionHooks");
for (let i = 0; i < this.preconnectionHooks.length; ++i) {
let hook = this.preconnectionHooks[i];
let redirect = await hook(advert, this);
if (redirect) {
this.catAdapter.error(`Error running preconnection hooks`, new Error(redirect.reason));
throw new Errors.ConnectionCancelledError(redirect);
}
}
this.state = iotile_types_1.AdapterState.Connecting;
this.notify(iotile_types_1.AdapterEvent.ConnectionStarted, {});
try {
this.connectedDevice = await this.connectInternal(advert);
if (this.config.ENV.CONNECTION_DELAY) {
this.catAdapter.info("Connection delay: " + this.config.ENV.CONNECTION_DELAY);
await iotile_common_1.delay(this.config.ENV.CONNECTION_DELAY);
}
try {
if (openRPC) {
await this.openInterface(Interface.RPC);
}
await this.openInterface(Interface.Script);
this.catAdapter.info(`Running ${this.connectionHooks.length} connectionHooks`);
for (let i = 0; i < this.connectionHooks.length; ++i) {
let hook = this.connectionHooks[i];
let redirect = await hook(this.connectedDevice, this);
if (redirect) {
this.catAdapter.error(`Error running connection hooks`, new Error(redirect.reason));
throw new Errors.ConnectionCancelledError(redirect);
}
}
this.catAdapter.info('Finished connectionHooks');
if (prestreamingHook != null) {
this.catAdapter.info("Running prestreamingHooks");
await prestreamingHook(this.connectedDevice, this);
}
if (openStreaming) {
this.catAdapter.info("Running openStreaming interface");
await this.openInterface(Interface.Streaming);
}
}
catch (err) {
await this.disconnect();
this.catAdapter.error(`Connection Cancelled`, new Error(JSON.stringify(err)));
throw err;
}
}
finally {
this.notify(iotile_types_1.AdapterEvent.ConnectionFinished, {});
}
this.notify(iotile_types_1.AdapterEvent.Connected, { device: this.connectedDevice });
return this.connectedDevice;
}
resetStreaming() {
this.ensureConnected('resetting stream interface');
this.streamingInterface.reset();
}
async rpc(address, rpcID, payload, timeout) {
this.ensureConnected('sending rpc');
let response = await this.rpcInterface.rpc(address, rpcID, payload, timeout);
return response;
}
async sendScript(script, notifier) {
this.ensureConnected('sending script');
await this.scriptInterface.send(script, notifier);
}
clearTrace() {
this.tracingInterface.clearData();
}
waitForTracingData(numBytes, timeout = 1000) {
return this.tracingInterface.waitForData(numBytes, timeout);
}
async typedRPC(address, rpcID, callFormat, respFormat, args, timeout) {
let callPayload = iotile_common_1.packArrayBuffer(callFormat, ...args);
let respBuffer = await this.rpc(address, rpcID, callPayload, timeout);
let resp = iotile_common_1.unpackArrayBuffer(respFormat, respBuffer);
return resp;
}
async errorHandlingRPC(address, rpcID, callFormat, respFormat, args, timeout) {
if (respFormat.length === 0 || respFormat[0] != 'L') {
throw new iotile_common_1.ArgumentError('Invalid response format for errorHandlingRPC that did not start with an L code for the error.');
}
let resp = await this.typedRPC(address, rpcID, callFormat, respFormat, args, timeout);
let 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 resp;
}
async disconnect() {
if (this.state !== iotile_types_1.AdapterState.Connected) {
return;
}
this.state = iotile_types_1.AdapterState.Disconnecting;
if (this.connectedDevice) {
await this.disconnectInternal(this.connectedDevice.connectionID);
}
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;
}
subscribe(event, callback) {
let eventName = this.adapterEventNames[event];
let handler = this.notification.subscribe(eventName, callback);
return handler;
}
async addNotificationListener(char, callback) {
this.ensureConnected('enable notifications');
if (!(char in this.charManagers)) {
throw new iotile_common_1.UnknownKeyError("Characteristic cannot be listened to: " + char);
}
let charManager = this.charManagers[char];
let handlerID = charManager.addListener(callback);
let that = this;
let removeHandler = () => { return that.removeNotificationListener(char, handlerID); };
if (charManager.numListeners() > 1) {
return removeHandler;
}
return new Promise(function (resolve, reject) {
if (that.connectedDevice) {
let slidingWindow = new sliding_window_1.SlidingWindowReorderer((value) => { charManager.handleData(value); });
window.ble.startNotification(that.connectedDevice.connectionID, IOTileServiceName, that.characteristicNames[char], function (data, counter) {
slidingWindow.call(data, counter);
}, function (failure) {
charManager.removeListener(handlerID);
reject(failure);
});
setTimeout(function () { resolve(removeHandler); }, 100);
}
});
}
async removeNotificationListener(char, handlerID) {
if (!(char in this.charManagers)) {
throw new iotile_common_1.UnknownKeyError("Characteristic cannot be listened to: " + char);
}
let charManager = this.charManagers[char];
let stopNotifications = charManager.removeListener(handlerID);
let that = this;
if (stopNotifications) {
return new Promise(function (resolve, reject) {
if (that.connectedDevice) {
window.ble.stopNotification(that.connectedDevice.connectionID, IOTileServiceName, that.characteristicNames[char], () => resolve(), (reason) => reject(reason));
}
});
}
}
createChannel() {
let that = this;
return {
write: function (char, value) {
return that.write(char, value);
},
subscribe: function (char, callback) {
return that.addNotificationListener(char, callback);
},
notify: function (event, value) {
return that.notify(event, value);
}
};
}
async openInterface(iface) {
this.ensureConnected('open device interface');
switch (iface) {
case Interface.RPC:
await this.rpcInterface.open(this.createChannel());
break;
case Interface.Streaming:
await this.streamingInterface.open(this.createChannel());
break;
case Interface.Script:
if (this.connectedDevice) {
await this.scriptInterface.open(this.connectedDevice, this.createChannel());
}
break;
case Interface.Tracing:
await this.tracingInterface.open(this.createChannel());
break;
}
}
async enableTracing() {
if (this.tracingOpen)
return;
await this.tracingInterface.open(this.createChannel());
this.tracingOpen = true;
}
async closeInterface(iface) {
this.ensureConnected('close device interface');
switch (iface) {
case Interface.RPC:
await this.rpcInterface.close();
break;
case Interface.Streaming:
await this.streamingInterface.close();
break;
case Interface.Script:
await this.scriptInterface.close();
break;
case Interface.Tracing:
await this.tracingInterface.close();
break;
}
}
ensureIdle(action) {
if (this.state != iotile_types_1.AdapterState.Idle) {
throw new Errors.OperationAtInvalidTimeError("action: '" + action + "'' started at invalid time, adapter was not idle.", this.state);
}
}
ensureConnected(action, userMessage) {
if (this.state != iotile_types_1.AdapterState.Connected) {
throw new Errors.OperationAtInvalidTimeError("action: '" + action + "'' started at invalid time, adapter was not connected.", this.state, userMessage);
}
}
notify(event, args) {
let eventName = this.adapterEventNames[event];
this.notification.notify(eventName, args);
}
createIOTileAdvertisement(peripheral) {
return this.adParser.processAdvertisement(peripheral.id, peripheral.rssi, peripheral.advertising);
}
async stopScan() {
return new Promise((resolve, reject) => {
window.ble.stopScan(resolve, reject);
});
}
async write(char, value) {
this.ensureConnected('writing to characteristic', "Error sending data to device");
let that = this;
return new Promise(function (resolve, reject) {
let removeHandler = setTimeout(function () {
reject(new Errors.WriteError("Timeout sending data"));
}, 2000);
let resolveFunction = () => {
clearTimeout(removeHandler);
resolve();
};
if (that.connectedDevice) {
if (that.supportsFastWrites || that.platform === iotile_types_1.Platform.Android) {
window.ble.writeWithoutResponse(that.connectedDevice.connectionID, IOTileServiceName, that.characteristicNames[char], value, resolveFunction, function (err) {
clearTimeout(removeHandler);
reject(new Errors.WriteError(err));
});
}
else {
window.ble.write(that.connectedDevice.connectionID, IOTileServiceName, that.characteristicNames[char], value, resolveFunction, function (err) {
clearTimeout(removeHandler);
reject(new Errors.WriteError(err));
});
}
}
});
}
async connectInternal(advert) {
let that = this;
return new Promise(function (resolve, reject) {
window.ble.connect(advert.connectionID, function (peripheral) {
that.supportsFastWrites = that.checkFastWriteSupport(peripheral);
if (that.supportsFastWrites) {
that.catAdapter.info("Device supports fast writes, increasing RPC and script speed.");
}
resolve(new iotile_device_1.IOTileDevice(that, advert));
that.state = iotile_types_1.AdapterState.Connected;
}, function (reason) {
if (that.state == iotile_types_1.AdapterState.Connecting) {
that.state = iotile_types_1.AdapterState.Idle;
reject(new Errors.ConnectionFailedError(reason));
}
else {
that.disconnectCallback(reason);
}
});
});
}
checkFastWriteSupport(peripheral) {
let highspeed = this.findCharacteristic(peripheral, IOTileServiceName, HighspeedDataCharacteristic);
let header = this.findCharacteristic(peripheral, IOTileServiceName, SendHeaderCharacteristic);
let payload = this.findCharacteristic(peripheral, IOTileServiceName, SendPayloadCharacteristic);
if (highspeed == null || header == null || payload == null)
return false;
return this.checkProperty(highspeed, "WriteWithoutResponse") && this.checkProperty(header, "WriteWithoutResponse") && this.checkProperty(payload, "WriteWithoutResponse");
}
findCharacteristic(peripheral, service, charName) {
if (peripheral.characteristics == null || peripheral.characteristics.length == null)
return null;
for (let char of peripheral.characteristics) {
if (char.service.toLowerCase() !== service.toLowerCase())
continue;
if (char.characteristic.toLowerCase() === charName.toLowerCase())
return char;
}
return null;
}
checkProperty(char, propToFind) {
for (let prop of char.properties) {
if (prop.toLowerCase() === propToFind.toLowerCase()) {
return true;
}
}
return false;
}
async disconnectInternal(deviceID) {
return new Promise((resolve, reject) => {
window.ble.disconnect(deviceID, resolve, reject);
});
}
disconnectCallback(reason) {
this.catAdapter.info("Disconnect callback: " + JSON.stringify(reason));
if (this.state == iotile_types_1.AdapterState.Connected) {
this.state = iotile_types_1.AdapterState.Idle;
let device = this.connectedDevice;
this.connectedDevice = null;
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.notify(iotile_types_1.AdapterEvent.Disconnected, { device: device });
}
}
}
exports.IOTileAdapter = IOTileAdapter;
class CharacteristicManager {
constructor() {
this.lastID = 0;
this.listenerCount = 0;
this.callbacks = {};
}
addListener(callback) {
let id = this.lastID;
this.lastID += 1;
this.listenerCount += 1;
this.callbacks[id] = callback;
return id;
}
removeAll() {
this.callbacks = {};
this.listenerCount = 0;
}
handleData(data) {
for (let key in this.callbacks) {
try {
this.callbacks[key](data);
}
catch (err) {
}
}
}
numListeners() {
return this.listenerCount;
}
removeListener(listenerID) {
if (!(listenerID in this.callbacks)) {
throw new iotile_common_1.UnknownKeyError('Unknown characteristic listener key: ' + listenerID);
}
delete this.callbacks[listenerID];
this.listenerCount -= 1;
return (this.listenerCount == 0);
}
}
//# sourceMappingURL=iotile-serv.js.map