UNPKG

@neurosity/sdk

Version:
410 lines (409 loc) 23.5 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 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) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReactNativeTransport = void 0; const ipk_1 = require("@neurosity/ipk"); const ipk_2 = require("@neurosity/ipk"); const ipk_3 = require("@neurosity/ipk"); const rxjs_1 = require("rxjs"); const rxjs_2 = require("rxjs"); const operators_1 = require("rxjs/operators"); const operators_2 = require("rxjs/operators"); const operators_3 = require("rxjs/operators"); const create6DigitPin_1 = require("../utils/create6DigitPin"); const textCodec_1 = require("../utils/textCodec"); const types_1 = require("../types"); const constants_1 = require("../constants"); const constants_2 = require("../constants"); const constants_3 = require("../constants"); const constants_4 = require("../constants"); const decodeJSONChunks_1 = require("../utils/decodeJSONChunks"); const defaultOptions = { autoConnect: true }; class ReactNativeTransport { constructor(options) { this.type = types_1.TRANSPORT_TYPE.REACT_NATIVE; this.textCodec = new textCodec_1.TextCodec(this.type); this.characteristicsByName = {}; this.connection$ = new rxjs_1.BehaviorSubject(types_1.BLUETOOTH_CONNECTION.DISCONNECTED); this.pendingActions$ = new rxjs_1.BehaviorSubject([]); this.logs$ = new rxjs_1.ReplaySubject(10); this.connectionStream$ = this.connection$ .asObservable() .pipe((0, operators_1.filter)((connection) => !!connection), (0, operators_2.distinctUntilChanged)(), (0, operators_2.shareReplay)(1)); this._isAutoConnectEnabled$ = new rxjs_1.ReplaySubject(1); if (!options) { const errorMessage = "React Native transport: missing options."; this.addLog(errorMessage); throw new Error(errorMessage); } this.options = Object.assign(Object.assign({}, defaultOptions), options); const { BleManager, bleManagerEmitter, platform, autoConnect } = this.options; if (!BleManager) { const errorMessage = "React Native option: BleManager not provided."; this.addLog(errorMessage); throw new Error(errorMessage); } if (!bleManagerEmitter) { const errorMessage = "React Native option: bleManagerEmitter not provided."; this.addLog(errorMessage); throw new Error(errorMessage); } if (!platform) { const errorMessage = "React Native option: platform not provided."; this.addLog(errorMessage); throw new Error(errorMessage); } this.BleManager = BleManager; this.bleManagerEmitter = bleManagerEmitter; this.platform = platform; this._isAutoConnectEnabled$.next(autoConnect); this._isAutoConnectEnabled$.subscribe((autoConnect) => { this.addLog(`Auto connect: ${autoConnect ? "enabled" : "disabled"}`); }); // We create a single listener per event type to // avoid missing events when multiple listeners are attached. this.bleEvents = { stopScan$: this._fromEvent("BleManagerStopScan"), discoverPeripheral$: this._fromEvent("BleManagerDiscoverPeripheral"), connectPeripheral$: this._fromEvent("BleManagerConnectPeripheral"), disconnectPeripheral$: this._fromEvent("BleManagerDisconnectPeripheral"), didUpdateValueForCharacteristic$: this._fromEvent("BleManagerDidUpdateValueForCharacteristic"), didUpdateState$: this._fromEvent("BleManagerDidUpdateState") }; this.onDisconnected$ = this.bleEvents.disconnectPeripheral$.pipe((0, operators_3.share)()); // Initializes the module. This can only be called once. this.BleManager.start({ showAlert: false }) .then(() => { this.addLog(`BleManger started`); }) .catch((error) => { var _a; this.addLog(`BleManger failed to start. ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}`); }); this.connection$.asObservable().subscribe((connection) => { this.addLog(`connection status is ${connection}`); }); this.onDisconnected$.subscribe(() => { this.connection$.next(types_1.BLUETOOTH_CONNECTION.DISCONNECTED); }); } addLog(log) { this.logs$.next(log); } isConnected() { const connection = this.connection$.getValue(); return connection === types_1.BLUETOOTH_CONNECTION.CONNECTED; } _autoConnect(selectedDevice$) { const selectedDeviceAfterDisconnect$ = this.onDisconnected$.pipe((0, operators_1.switchMap)(() => selectedDevice$)); return this._isAutoConnectEnabled$.pipe((0, operators_1.switchMap)((isAutoConnectEnabled) => isAutoConnectEnabled ? (0, rxjs_2.merge)(selectedDevice$, selectedDeviceAfterDisconnect$) : rxjs_1.NEVER), (0, operators_1.switchMap)((selectedDevice) => this.scan().pipe((0, operators_1.switchMap)((peripherals) => { const peripheralMatch = peripherals.find((peripheral) => peripheral.name === (selectedDevice === null || selectedDevice === void 0 ? void 0 : selectedDevice.deviceNickname)); return peripheralMatch ? (0, rxjs_2.of)(peripheralMatch) : rxjs_1.NEVER; }), (0, operators_3.distinct)((peripheral) => peripheral.id), (0, operators_3.take)(1))), (0, operators_1.switchMap)((peripheral) => __awaiter(this, void 0, void 0, function* () { return yield this.connect(peripheral); }))); } enableAutoConnect(autoConnect) { this._isAutoConnectEnabled$.next(autoConnect); } connection() { return this.connectionStream$; } _fromEvent(eventName) { return (0, rxjs_2.fromEventPattern)((addHandler) => { this.bleManagerEmitter.addListener(eventName, addHandler); }, () => { this.bleManagerEmitter.removeAllListeners(eventName); }).pipe( // @important: we need to share the subscription // to avoid missing events (0, operators_3.share)()); } scan(options) { var _a, _b, _c; const RESCAN_INTERVAL = 10000; // 10 seconds const seconds = (_a = options === null || options === void 0 ? void 0 : options.seconds) !== null && _a !== void 0 ? _a : RESCAN_INTERVAL / 1000; const once = (_b = options === null || options === void 0 ? void 0 : options.once) !== null && _b !== void 0 ? _b : false; // If we are already connected to a peripheral and start scanning, // be default, it will set the connection status to SCANNING and not // update it back if no device is connected to const skipConnectionUpdate = (_c = options === null || options === void 0 ? void 0 : options.skipConnectionUpdate) !== null && _c !== void 0 ? _c : false; const serviceUUIDs = [ipk_1.BLUETOOTH_PRIMARY_SERVICE_UUID_STRING]; const allowDuplicates = true; const scanOptions = {}; const scanOnce$ = new rxjs_1.Observable((subscriber) => { var _a; try { this.BleManager.scan(serviceUUIDs, seconds, allowDuplicates, scanOptions).then(() => { this.addLog(`BleManger scanning ${once ? "once" : "indefintely"}`); subscriber.next(); }); } catch (error) { this.addLog(`BleManger scanning ${once ? "once" : "indefintely"} failed. ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}`); subscriber.error(error); } return () => { this.BleManager.stopScan(); }; }); const scan$ = once ? scanOnce$ : (0, rxjs_2.timer)(0, RESCAN_INTERVAL).pipe((0, operators_1.switchMap)(() => scanOnce$)); const peripherals$ = scan$.pipe((0, operators_1.tap)(() => { if (!skipConnectionUpdate) { this.connection$.next(types_1.BLUETOOTH_CONNECTION.SCANNING); } }), (0, operators_1.takeUntil)(this.onDisconnected$), (0, operators_1.switchMap)(() => this.bleEvents.discoverPeripheral$), // Filter out devices that are not Neurosity devices (0, operators_1.filter)((peripheral) => { var _a, _b, _c; const peripheralName = (_c = (_b = (_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertising) === null || _a === void 0 ? void 0 : _a.localName) !== null && _b !== void 0 ? _b : peripheral.name) !== null && _c !== void 0 ? _c : ""; if (!peripheralName) { return false; } const startsWithPrefix = ipk_3.BLUETOOTH_DEVICE_NAME_PREFIXES.findIndex((prefix) => peripheralName.startsWith(prefix)) !== -1; return startsWithPrefix; }), (0, operators_3.scan)((acc, peripheral) => { var _a, _b, _c, _d, _e, _f, _g, _h; // normalized peripheral name for backwards compatibility // Neurosity OS v15 doesn't have peripheral.name as deviceNickname // it only has peripheral.advertising.localName as deviceNickname // and OS v16 has both as deviceNickname const peripheralName = (_c = (_b = (_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertising) === null || _a === void 0 ? void 0 : _a.localName) !== null && _b !== void 0 ? _b : peripheral.name) !== null && _c !== void 0 ? _c : ""; const manufactureDataString = (_h = (_g = this.textCodec .decode((_f = (_e = (_d = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertising) === null || _d === void 0 ? void 0 : _d.manufacturerData) === null || _e === void 0 ? void 0 : _e.bytes) !== null && _f !== void 0 ? _f : [])) === null || _g === void 0 ? void 0 : _g.slice) === null || _h === void 0 ? void 0 : _h.call(_g, 2); // First 2 bytes are reserved for the Neurosity company code return Object.assign(Object.assign({}, acc), { [peripheral.id]: Object.assign(Object.assign({}, peripheral), { name: peripheralName, manufactureDataString }) }); }, {}), (0, operators_2.distinctUntilChanged)((a, b) => JSON.stringify(a) === JSON.stringify(b)), (0, operators_1.map)((peripheralMap) => Object.values(peripheralMap)), (0, operators_3.share)()); return peripherals$; } connect(peripheral) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { try { if (!peripheral) { this.addLog("Peripheral not found"); return; } this.connection$.next(types_1.BLUETOOTH_CONNECTION.CONNECTING); yield this.BleManager.connect(peripheral.id); this.addLog(`Getting service...`); const peripheralInfo = yield this.BleManager.retrieveServices(peripheral.id, [ ipk_1.BLUETOOTH_PRIMARY_SERVICE_UUID_STRING ]); if (!peripheralInfo) { this.addLog("Could not retreive services"); reject(new Error(`Could not retreive services`)); return; } this.addLog(`Got service ${ipk_1.BLUETOOTH_PRIMARY_SERVICE_UUID_STRING}, getting characteristics...`); this.device = peripheral; this.characteristicsByName = Object.fromEntries(peripheralInfo.characteristics.map((characteristic) => [ constants_2.CHARACTERISTIC_UUIDS_TO_NAMES[characteristic.characteristic.toLowerCase() // react native uses uppercase ], { characteristicUUID: characteristic.characteristic, serviceUUID: characteristic.service, peripheralId: peripheral.id } ])); this.addLog(`Got characteristics.`); if (this.platform === "android") { yield this.BleManager.requestMTU(peripheral.id, constants_3.ANDROID_MAX_MTU) .then((updatedMTU) => { this.addLog(`Successfully updated Android MTU to ${updatedMTU} bytes. Requested MTU: ${constants_3.ANDROID_MAX_MTU} bytes.`); }) .catch((error) => { this.addLog(`Failed to set Android MTU of ${constants_3.ANDROID_MAX_MTU} bytes. Error: ${error}`); }); } this.addLog(`Successfully connected to peripheral ${peripheral.id}`); this.connection$.next(types_1.BLUETOOTH_CONNECTION.CONNECTED); resolve(); } catch (error) { reject(error); } })); }); } disconnect() { var _a; return __awaiter(this, void 0, void 0, function* () { try { if (this.isConnected() && ((_a = this === null || this === void 0 ? void 0 : this.device) === null || _a === void 0 ? void 0 : _a.id)) { yield this.BleManager.disconnect(this.device.id); } } catch (error) { return Promise.reject(error); } }); } getCharacteristicByName(characteristicName) { var _a; if (!(characteristicName in this.characteristicsByName)) { throw new Error(`Characteristic by name ${characteristicName} is not found`); } return (_a = this.characteristicsByName) === null || _a === void 0 ? void 0 : _a[characteristicName]; } subscribeToCharacteristic({ characteristicName, manageNotifications = true, skipJSONDecoding = false }) { const getData = ({ peripheralId, serviceUUID, characteristicUUID }) => (0, rxjs_2.defer)(() => __awaiter(this, void 0, void 0, function* () { var _a; if (manageNotifications) { try { yield this.BleManager.startNotification(peripheralId, serviceUUID, characteristicUUID); this.addLog(`Started notifications for ${characteristicName} characteristic`); } catch (error) { this.addLog(`Attemped to stop notifications for ${characteristicName} characteristic: ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}`); } } })).pipe((0, operators_1.switchMap)(() => this.bleEvents.didUpdateValueForCharacteristic$), (0, operators_2.finalize)(() => __awaiter(this, void 0, void 0, function* () { var _b; if (manageNotifications) { try { yield this.BleManager.stopNotification(peripheralId, serviceUUID, characteristicUUID); this.addLog(`Stopped notifications for ${characteristicName} characteristic`); } catch (error) { this.addLog(`Attemped to stop notifications for ${characteristicName} characteristic: ${(_b = error === null || error === void 0 ? void 0 : error.message) !== null && _b !== void 0 ? _b : error}`); } } })), (0, operators_1.filter)(({ characteristic }) => characteristic === characteristicUUID), (0, operators_1.map)(({ value }) => new Uint8Array(value))); return this.connection$.pipe((0, operators_1.switchMap)((connection) => connection === types_1.BLUETOOTH_CONNECTION.CONNECTED ? getData(this.getCharacteristicByName(characteristicName)).pipe(skipJSONDecoding ? rxjs_2.identity // noop : (0, decodeJSONChunks_1.decodeJSONChunks)({ textCodec: this.textCodec, characteristicName, delimiter: ipk_2.BLUETOOTH_CHUNK_DELIMITER, addLog: (message) => this.addLog(message) })) : rxjs_1.NEVER)); } readCharacteristic(characteristicName, parse = false) { var _a; return __awaiter(this, void 0, void 0, function* () { this.addLog(`Reading characteristic: ${characteristicName}`); const { peripheralId, serviceUUID, characteristicUUID } = this.getCharacteristicByName(characteristicName); if (!characteristicUUID) { return Promise.reject(new Error(`Did not find characteristic matching ${characteristicName}`)); } try { const value = yield this.BleManager.read(peripheralId, serviceUUID, characteristicUUID); const decodedValue = this.textCodec.decode(new Uint8Array(value)); const data = parse ? JSON.parse(decodedValue) : decodedValue; this.addLog(`Received read data from ${characteristicName} characteristic: \n${data}`); return data; } catch (error) { return Promise.reject(new Error(`readCharacteristic ${characteristicName} error. ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}`)); } }); } writeCharacteristic(characteristicName, data) { return __awaiter(this, void 0, void 0, function* () { this.addLog(`Writing characteristic: ${characteristicName}`); const { peripheralId, serviceUUID, characteristicUUID } = this.getCharacteristicByName(characteristicName); if (!characteristicUUID) { return Promise.reject(new Error(`Did not find characteristic matching ${characteristicName}`)); } const encoded = this.textCodec.encode(data); yield this.BleManager.write(peripheralId, serviceUUID, characteristicUUID, encoded, constants_4.REACT_NATIVE_MAX_BYTE_SIZE); }); } _addPendingAction(actionId) { const actions = this.pendingActions$.getValue(); this.pendingActions$.next([...actions, actionId]); } _removePendingAction(actionId) { const actions = this.pendingActions$.getValue(); this.pendingActions$.next(actions.filter((id) => id !== actionId)); } _autoToggleActionNotifications() { let started = false; return this.connection$.asObservable().pipe((0, operators_1.switchMap)((connection) => connection === types_1.BLUETOOTH_CONNECTION.CONNECTED ? this.pendingActions$ : rxjs_1.NEVER), (0, operators_1.tap)((pendingActions) => __awaiter(this, void 0, void 0, function* () { var _a, _b; const { peripheralId, serviceUUID, characteristicUUID } = this.getCharacteristicByName("actions"); const hasPendingActions = !!pendingActions.length; if (hasPendingActions && !started) { started = true; try { yield this.BleManager.startNotification(peripheralId, serviceUUID, characteristicUUID); this.addLog(`Started notifications for [actions] characteristic`); } catch (error) { this.addLog(`Attemped to start notifications for [actions] characteristic: ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}`); } } if (!hasPendingActions && started) { started = false; try { yield this.BleManager.stopNotification(peripheralId, serviceUUID, characteristicUUID); this.addLog(`Stopped notifications for actions characteristic`); } catch (error) { this.addLog(`Attemped to stop notifications for [actions] characteristic: ${(_b = error === null || error === void 0 ? void 0 : error.message) !== null && _b !== void 0 ? _b : error}`); } } }))); } dispatchAction({ characteristicName, action }) { return __awaiter(this, void 0, void 0, function* () { const { responseRequired = false, responseTimeout = constants_1.DEFAULT_ACTION_RESPONSE_TIMEOUT } = action; return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { const actionId = (0, create6DigitPin_1.create6DigitPin)(); // use to later identify and filter response const payload = JSON.stringify(Object.assign({ actionId }, action)); // add the response id to the action this.addLog(`Dispatched action with id ${actionId}`); if (responseRequired && responseTimeout) { this._addPendingAction(actionId); const timeout = (0, rxjs_2.timer)(responseTimeout).subscribe(() => { this._removePendingAction(actionId); reject(new Error(`Action with id ${actionId} timed out after ${responseTimeout}ms`)); }); // listen for a response before writing this.subscribeToCharacteristic({ characteristicName, manageNotifications: false }) .pipe((0, operators_1.filter)((response) => (response === null || response === void 0 ? void 0 : response.actionId) === actionId), (0, operators_3.take)(1)) .subscribe((response) => { timeout.unsubscribe(); this._removePendingAction(actionId); resolve(response); }); // register action by writing this.writeCharacteristic(characteristicName, payload).catch((error) => { this._removePendingAction(actionId); reject(error); }); } else { this.writeCharacteristic(characteristicName, payload) .then(() => { resolve(null); }) .catch((error) => { reject(error); }); } })); }); } } exports.ReactNativeTransport = ReactNativeTransport;