UNPKG

@neurosity/sdk

Version:
322 lines (321 loc) 13.8 kB
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()); }); }; import { defer, timer } from "rxjs"; import { ReplaySubject, firstValueFrom, EMPTY } from "rxjs"; import { switchMap, share, tap, distinctUntilChanged } from "rxjs/operators"; import { WebBluetoothTransport } from "./web/WebBluetoothTransport"; import { ReactNativeTransport } from "./react-native/ReactNativeTransport"; import { binaryBufferToEpoch } from "./utils/binaryBufferToEpoch"; import { BLUETOOTH_CONNECTION } from "./types"; export class BluetoothClient { constructor(options) { this.selectedDevice$ = new ReplaySubject(1); this.osHasBluetoothSupport$ = new ReplaySubject(1); this.isAuthenticated$ = new ReplaySubject(1); const { transport, selectedDevice$, osHasBluetoothSupport$, createBluetoothToken } = options !== null && options !== void 0 ? options : {}; if (!transport) { throw new Error(`No bluetooth transport provided.`); } this.transport = transport; // Pass events to the internal selectedDevice$ if selectedDevice$ is passed via options if (selectedDevice$) { selectedDevice$.subscribe(this.selectedDevice$); } // Pass events to the internal osHasBluetoothSupport$ if osHasBluetoothSupport$ is passed via options if (osHasBluetoothSupport$) { osHasBluetoothSupport$.subscribe(this.osHasBluetoothSupport$); } this.osHasBluetoothSupport$ .pipe(switchMap((osHasBluetoothSupport) => osHasBluetoothSupport ? this.transport._autoConnect(this.selectedDevice$) : EMPTY)) .subscribe({ error: (error) => { var _a; this.transport.addLog(`Auto connect: error -> ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}`); } }); // Auto authentication if (typeof createBluetoothToken === "function") { this.transport.addLog("Auto authentication enabled"); this._autoAuthenticate(createBluetoothToken).subscribe(); } else { this.transport.addLog("Auto authentication not enabled"); } // Auto manage action notifications this.osHasBluetoothSupport$ .pipe(switchMap((osHasBluetoothSupport) => osHasBluetoothSupport ? this.transport._autoToggleActionNotifications() : EMPTY)) .subscribe(); // Multicast metrics (share) this._focus$ = this._subscribeWhileAuthenticated("focus"); this._calm$ = this._subscribeWhileAuthenticated("calm"); this._accelerometer$ = this._subscribeWhileAuthenticated("accelerometer"); this._brainwavesRaw$ = this._subscribeWhileAuthenticated("raw", true // skipJSONDecoding ); this._brainwavesRawUnfiltered$ = this._subscribeWhileAuthenticated("rawUnfiltered", true // skipJSONDecoding ); this._brainwavesPSD$ = this._subscribeWhileAuthenticated("psd"); this._brainwavesPowerByBand$ = this._subscribeWhileAuthenticated("powerByBand"); this._signalQuality$ = this._subscribeWhileAuthenticated("signalQuality"); this._status$ = this._subscribeWhileAuthenticated("status"); this._settings$ = this._subscribeWhileAuthenticated("settings"); this._wifiNearbyNetworks$ = this._subscribeWhileAuthenticated("wifiNearbyNetworks"); this._wifiConnections$ = this._subscribeWhileAuthenticated("wifiConnections"); } _autoAuthenticate(createBluetoothToken) { const REAUTHENTICATE_INTERVAL = 3600000; // 1 hour const reauthenticateInterval$ = timer(0, REAUTHENTICATE_INTERVAL).pipe(tap(() => { this.transport.addLog(`Auto authentication in progress...`); })); return this.osHasBluetoothSupport$.pipe(switchMap((osHasBluetoothSupport) => osHasBluetoothSupport ? this.connection() : EMPTY), switchMap((connection) => connection === BLUETOOTH_CONNECTION.CONNECTED ? reauthenticateInterval$ : EMPTY), switchMap(() => __awaiter(this, void 0, void 0, function* () { return yield this.isAuthenticated(); })), tap(([isAuthenticated]) => __awaiter(this, void 0, void 0, function* () { if (!isAuthenticated) { const token = yield createBluetoothToken(); yield this.authenticate(token); } else { this.transport.addLog(`Already authenticated`); } }))); } enableAutoConnect(autoConnect) { this.transport.enableAutoConnect(autoConnect); } _hasBluetoothSupport() { return __awaiter(this, void 0, void 0, function* () { return yield firstValueFrom(this.osHasBluetoothSupport$); }); } authenticate(token) { return __awaiter(this, void 0, void 0, function* () { const hasBluetoothSupport = yield this._hasBluetoothSupport(); if (!hasBluetoothSupport) { const errorMessage = `authenticate method: The OS version does not support Bluetooth.`; this.transport.addLog(errorMessage); return Promise.reject(errorMessage); } yield this.transport.writeCharacteristic("auth", token); const isAuthenticatedResponse = yield this.isAuthenticated(); const [isAuthenticated] = isAuthenticatedResponse; this.transport.addLog(`Authentication ${isAuthenticated ? "succeeded" : "failed"}`); this.isAuthenticated$.next(isAuthenticated); return isAuthenticatedResponse; }); } isAuthenticated() { return __awaiter(this, void 0, void 0, function* () { try { const [isAuthenticated, expiresIn] = yield this.transport.readCharacteristic("auth", true); this.isAuthenticated$.next(isAuthenticated); return [isAuthenticated, expiresIn]; } catch (error) { const failedResponse = [false, null]; this.transport.addLog(`Authentication error -> ${error}`); this.isAuthenticated$.next(false); return failedResponse; } }); } // Method for React Native only scan(options) { if (this.transport instanceof ReactNativeTransport) { return this.transport.scan(options); } if (this.transport instanceof WebBluetoothTransport) { throw new Error(`scan method is compatibly with the React Native transport only`); } throw new Error(`unknown transport`); } // Argument for React Native only connect(deviceNicknameORPeripheral) { if (this.transport instanceof ReactNativeTransport) { return this.transport.connect(deviceNicknameORPeripheral); } if (this.transport instanceof WebBluetoothTransport) { return deviceNicknameORPeripheral ? this.transport.connect(deviceNicknameORPeripheral) : this.transport.connect(); } } disconnect() { return this.transport.disconnect(); } connection() { return this.transport.connection(); } logs() { return this.transport.logs$.asObservable(); } getDeviceId() { return __awaiter(this, void 0, void 0, function* () { // This is a public characteristic and does not require authentication return this.transport.readCharacteristic("deviceId"); }); } _withAuthentication(getter) { return __awaiter(this, void 0, void 0, function* () { // First check if the OS supports Bluetooth before checking if the device is authenticated const hasBluetoothSupport = yield this._hasBluetoothSupport(); if (!hasBluetoothSupport) { const errorMessage = `The OS version does not support Bluetooth.`; this.transport.addLog(errorMessage); return Promise.reject(errorMessage); } const isAuthenticated = yield firstValueFrom(this.isAuthenticated$); if (!isAuthenticated) { const errorMessage = `Authentication required.`; this.transport.addLog(errorMessage); return Promise.reject(errorMessage); } return yield getter(); }); } _subscribeWhileAuthenticated(characteristicName, skipJSONDecoding = false) { return this.osHasBluetoothSupport$.pipe(switchMap((osHasBluetoothSupport) => osHasBluetoothSupport ? this.isAuthenticated$ : EMPTY), distinctUntilChanged(), switchMap((isAuthenticated) => isAuthenticated ? this.transport.subscribeToCharacteristic({ characteristicName, skipJSONDecoding }) : EMPTY), share()); } focus() { return this._focus$; } calm() { return this._calm$; } accelerometer() { return this._accelerometer$; } brainwaves(label) { switch (label) { default: case "raw": return defer(() => this.getInfo()).pipe(switchMap((deviceInfo) => this._brainwavesRaw$.pipe(binaryBufferToEpoch(deviceInfo)))); case "rawUnfiltered": return defer(() => this.getInfo()).pipe(switchMap((deviceInfo) => this._brainwavesRawUnfiltered$.pipe(binaryBufferToEpoch(deviceInfo)))); case "psd": return this._brainwavesPSD$; case "powerByBand": return this._brainwavesPowerByBand$; } } signalQuality() { return this._signalQuality$; } addMarker(label) { return __awaiter(this, void 0, void 0, function* () { yield this.dispatchAction({ action: "marker", command: "add", message: { timestamp: Date.now(), label } }); }); } getInfo() { return __awaiter(this, void 0, void 0, function* () { return yield this._withAuthentication(() => firstValueFrom(this.transport.subscribeToCharacteristic({ characteristicName: "deviceInfo" }))); }); } status() { return this._status$; } dispatchAction(action) { return __awaiter(this, void 0, void 0, function* () { return yield this._withAuthentication(() => this.transport.dispatchAction({ characteristicName: "actions", action })); }); } settings() { return this._settings$; } haptics(effects) { const metric = "haptics"; return this.dispatchAction({ action: metric, command: "queue", responseRequired: true, responseTimeout: 4000, // @TODO: implement validation logic as per SDK message: { effects } }); } get wifi() { return { nearbyNetworks: () => this._wifiNearbyNetworks$, connections: () => this._wifiConnections$, connect: (ssid, password) => { if (!ssid) { return Promise.reject(`Missing ssid`); } return this.dispatchAction({ action: "wifi", command: "connect", responseRequired: true, responseTimeout: 1000 * 60 * 2, message: { ssid, password: password !== null && password !== void 0 ? password : null } }); }, forgetConnection: (ssid) => { if (!ssid) { return Promise.reject(`Missing ssid`); } return this.dispatchAction({ action: "wifi", command: "forget-connection", responseRequired: true, responseTimeout: 1000 * 15, message: { ssid } }); }, reset: () => { return this.dispatchAction({ action: "wifi", command: "reset", responseRequired: true, responseTimeout: 1000 * 30, message: { // without this, the action will resolve as soon as the // action is received by the OS respondOnSuccess: true } }); }, speedTest: () => { return this.dispatchAction({ action: "wifi", command: "speed-test", responseRequired: true, responseTimeout: 1000 * 60 * 1 // 1 minute }); } }; } }