@neurosity/sdk
Version:
Neurosity SDK
410 lines (409 loc) • 23.5 kB
JavaScript
"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;