@neurosity/sdk
Version:
Neurosity SDK
322 lines (321 loc) • 13.8 kB
JavaScript
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
});
}
};
}
}