UNPKG

@iotile/iotile-device

Version:

A typescript library for interfacing with IOTile BLE devices

573 lines 26.6 kB
"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