@neurosity/sdk 6.4.0 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/neurosity.iife.js +2590 -105
- package/dist/browser/neurosity.js +129 -113
- package/dist/browser/neurosity.js.map +1 -1
- package/dist/cjs/api/bluetooth/BluetoothClient.d.ts +1 -1
- package/dist/cjs/api/bluetooth/BluetoothClient.js +21 -11
- package/dist/cjs/api/bluetooth/react-native/ReactNativeTransport.d.ts +3 -1
- package/dist/cjs/api/bluetooth/react-native/ReactNativeTransport.js +17 -17
- package/dist/cjs/api/bluetooth/types/index.d.ts +1 -0
- package/dist/cjs/api/bluetooth/utils/binaryBufferToEpoch.d.ts +51 -0
- package/dist/cjs/api/bluetooth/utils/binaryBufferToEpoch.js +116 -0
- package/dist/cjs/api/bluetooth/utils/decodeJSONChunks.d.ts +15 -0
- package/dist/cjs/api/bluetooth/utils/decodeJSONChunks.js +25 -0
- package/dist/cjs/api/bluetooth/utils/textCodec.d.ts +17 -0
- package/dist/cjs/api/bluetooth/utils/textCodec.js +40 -0
- package/dist/cjs/api/bluetooth/web/WebBluetoothTransport.d.ts +3 -1
- package/dist/cjs/api/bluetooth/web/WebBluetoothTransport.js +19 -34
- package/dist/electron/index.js +15 -13
- package/dist/electron/index.js.map +1 -1
- package/dist/esm/api/bluetooth/BluetoothClient.d.ts +1 -1
- package/dist/esm/api/bluetooth/BluetoothClient.js +21 -11
- package/dist/esm/api/bluetooth/react-native/ReactNativeTransport.d.ts +3 -1
- package/dist/esm/api/bluetooth/react-native/ReactNativeTransport.js +18 -18
- package/dist/esm/api/bluetooth/types/index.d.ts +1 -0
- package/dist/esm/api/bluetooth/utils/binaryBufferToEpoch.d.ts +51 -0
- package/dist/esm/api/bluetooth/utils/binaryBufferToEpoch.js +109 -0
- package/dist/esm/api/bluetooth/utils/decodeJSONChunks.d.ts +15 -0
- package/dist/esm/api/bluetooth/utils/decodeJSONChunks.js +21 -0
- package/dist/esm/api/bluetooth/utils/textCodec.d.ts +17 -0
- package/dist/esm/api/bluetooth/utils/textCodec.js +36 -0
- package/dist/esm/api/bluetooth/web/WebBluetoothTransport.d.ts +3 -1
- package/dist/esm/api/bluetooth/web/WebBluetoothTransport.js +20 -35
- package/dist/esm/neurosity.mjs +2590 -105
- package/dist/examples/neurosity.iife.js +2590 -105
- package/dist/examples/neurosity.js +129 -113
- package/dist/examples/neurosity.mjs +2590 -105
- package/package.json +2 -1
- package/dist/cjs/api/bluetooth/utils/encoding.d.ts +0 -3
- package/dist/cjs/api/bluetooth/utils/encoding.js +0 -22
- package/dist/esm/api/bluetooth/utils/encoding.d.ts +0 -3
- package/dist/esm/api/bluetooth/utils/encoding.js +0 -17
|
@@ -50,7 +50,7 @@ export declare class BluetoothClient {
|
|
|
50
50
|
logs(): Observable<string>;
|
|
51
51
|
getDeviceId(): Promise<string>;
|
|
52
52
|
_withAuthentication<T>(getter: () => Promise<T>): Promise<T>;
|
|
53
|
-
_subscribeWhileAuthenticated(characteristicName: string): Observable<any>;
|
|
53
|
+
_subscribeWhileAuthenticated(characteristicName: string, skipJSONDecoding?: boolean): Observable<any>;
|
|
54
54
|
focus(): Observable<any>;
|
|
55
55
|
calm(): Observable<any>;
|
|
56
56
|
accelerometer(): Observable<any>;
|
|
@@ -12,7 +12,7 @@ import { ReplaySubject, firstValueFrom, EMPTY } from "rxjs";
|
|
|
12
12
|
import { switchMap, share, tap, distinctUntilChanged } from "rxjs/operators";
|
|
13
13
|
import { WebBluetoothTransport } from "./web/WebBluetoothTransport";
|
|
14
14
|
import { ReactNativeTransport } from "./react-native/ReactNativeTransport";
|
|
15
|
-
import {
|
|
15
|
+
import { binaryBufferToEpoch } from "./utils/binaryBufferToEpoch";
|
|
16
16
|
import { BLUETOOTH_CONNECTION } from "./types";
|
|
17
17
|
export class BluetoothClient {
|
|
18
18
|
constructor(options) {
|
|
@@ -60,9 +60,10 @@ export class BluetoothClient {
|
|
|
60
60
|
this._focus$ = this._subscribeWhileAuthenticated("focus");
|
|
61
61
|
this._calm$ = this._subscribeWhileAuthenticated("calm");
|
|
62
62
|
this._accelerometer$ = this._subscribeWhileAuthenticated("accelerometer");
|
|
63
|
-
this._brainwavesRaw$ = this._subscribeWhileAuthenticated("raw"
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
this._brainwavesRaw$ = this._subscribeWhileAuthenticated("raw", true // skipJSONDecoding
|
|
64
|
+
);
|
|
65
|
+
this._brainwavesRawUnfiltered$ = this._subscribeWhileAuthenticated("rawUnfiltered", true // skipJSONDecoding
|
|
66
|
+
);
|
|
66
67
|
this._brainwavesPSD$ = this._subscribeWhileAuthenticated("psd");
|
|
67
68
|
this._brainwavesPowerByBand$ =
|
|
68
69
|
this._subscribeWhileAuthenticated("powerByBand");
|
|
@@ -117,9 +118,17 @@ export class BluetoothClient {
|
|
|
117
118
|
}
|
|
118
119
|
isAuthenticated() {
|
|
119
120
|
return __awaiter(this, void 0, void 0, function* () {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
try {
|
|
122
|
+
const [isAuthenticated, expiresIn] = yield this.transport.readCharacteristic("auth", true);
|
|
123
|
+
this.isAuthenticated$.next(isAuthenticated);
|
|
124
|
+
return [isAuthenticated, expiresIn];
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const failedResponse = [false, null];
|
|
128
|
+
this.transport.addLog(`Authentication error -> ${error}`);
|
|
129
|
+
this.isAuthenticated$.next(false);
|
|
130
|
+
return failedResponse;
|
|
131
|
+
}
|
|
123
132
|
});
|
|
124
133
|
}
|
|
125
134
|
// Method for React Native only
|
|
@@ -176,10 +185,11 @@ export class BluetoothClient {
|
|
|
176
185
|
return yield getter();
|
|
177
186
|
});
|
|
178
187
|
}
|
|
179
|
-
_subscribeWhileAuthenticated(characteristicName) {
|
|
188
|
+
_subscribeWhileAuthenticated(characteristicName, skipJSONDecoding = false) {
|
|
180
189
|
return this.osHasBluetoothSupport$.pipe(switchMap((osHasBluetoothSupport) => osHasBluetoothSupport ? this.isAuthenticated$ : EMPTY), distinctUntilChanged(), switchMap((isAuthenticated) => isAuthenticated
|
|
181
190
|
? this.transport.subscribeToCharacteristic({
|
|
182
|
-
characteristicName
|
|
191
|
+
characteristicName,
|
|
192
|
+
skipJSONDecoding
|
|
183
193
|
})
|
|
184
194
|
: EMPTY), share());
|
|
185
195
|
}
|
|
@@ -196,9 +206,9 @@ export class BluetoothClient {
|
|
|
196
206
|
switch (label) {
|
|
197
207
|
default:
|
|
198
208
|
case "raw":
|
|
199
|
-
return defer(() => this.getInfo()).pipe(switchMap((deviceInfo) => this._brainwavesRaw$.pipe(
|
|
209
|
+
return defer(() => this.getInfo()).pipe(switchMap((deviceInfo) => this._brainwavesRaw$.pipe(binaryBufferToEpoch(deviceInfo))));
|
|
200
210
|
case "rawUnfiltered":
|
|
201
|
-
return defer(() => this.getInfo()).pipe(switchMap((deviceInfo) => this._brainwavesRawUnfiltered$.pipe(
|
|
211
|
+
return defer(() => this.getInfo()).pipe(switchMap((deviceInfo) => this._brainwavesRawUnfiltered$.pipe(binaryBufferToEpoch(deviceInfo))));
|
|
202
212
|
case "psd":
|
|
203
213
|
return this._brainwavesPSD$;
|
|
204
214
|
case "powerByBand":
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Observable, BehaviorSubject, ReplaySubject } from "rxjs";
|
|
2
2
|
import { BluetoothTransport } from "../BluetoothTransport";
|
|
3
|
+
import { TextCodec } from "../utils/textCodec";
|
|
3
4
|
import { ActionOptions, SubscribeOptions } from "../types";
|
|
4
5
|
import { TRANSPORT_TYPE, BLUETOOTH_CONNECTION } from "../types";
|
|
5
6
|
import { BleManager } from "./types/BleManagerTypes";
|
|
@@ -31,6 +32,7 @@ declare type BleManagerEvents = {
|
|
|
31
32
|
};
|
|
32
33
|
export declare class ReactNativeTransport implements BluetoothTransport {
|
|
33
34
|
type: TRANSPORT_TYPE;
|
|
35
|
+
textCodec: TextCodec;
|
|
34
36
|
options: Options;
|
|
35
37
|
BleManager: BleManager;
|
|
36
38
|
bleManagerEmitter: NativeEventEmitter;
|
|
@@ -59,7 +61,7 @@ export declare class ReactNativeTransport implements BluetoothTransport {
|
|
|
59
61
|
connect(peripheral: Peripheral): Promise<void>;
|
|
60
62
|
disconnect(): Promise<void>;
|
|
61
63
|
getCharacteristicByName(characteristicName: string): Characteristic;
|
|
62
|
-
subscribeToCharacteristic({ characteristicName, manageNotifications }: SubscribeOptions): Observable<any>;
|
|
64
|
+
subscribeToCharacteristic({ characteristicName, manageNotifications, skipJSONDecoding }: SubscribeOptions): Observable<any>;
|
|
63
65
|
readCharacteristic(characteristicName: string, parse?: boolean): Promise<any>;
|
|
64
66
|
writeCharacteristic(characteristicName: string, data: string): Promise<void>;
|
|
65
67
|
_addPendingAction(actionId: number): void;
|
|
@@ -11,24 +11,25 @@ import { BLUETOOTH_PRIMARY_SERVICE_UUID_STRING } from "@neurosity/ipk";
|
|
|
11
11
|
import { BLUETOOTH_CHUNK_DELIMITER } from "@neurosity/ipk";
|
|
12
12
|
import { BLUETOOTH_DEVICE_NAME_PREFIXES } from "@neurosity/ipk";
|
|
13
13
|
import { Observable, BehaviorSubject, ReplaySubject, NEVER } from "rxjs";
|
|
14
|
-
import { defer, merge, of, timer, fromEventPattern } from "rxjs";
|
|
14
|
+
import { defer, merge, of, timer, fromEventPattern, identity } from "rxjs";
|
|
15
15
|
import { switchMap, map, filter, takeUntil, tap } from "rxjs/operators";
|
|
16
16
|
import { shareReplay, distinctUntilChanged, finalize } from "rxjs/operators";
|
|
17
17
|
import { take, share, scan, distinct } from "rxjs/operators";
|
|
18
18
|
import { create6DigitPin } from "../utils/create6DigitPin";
|
|
19
|
-
import {
|
|
20
|
-
import { encode, decode } from "../utils/encoding";
|
|
19
|
+
import { TextCodec } from "../utils/textCodec";
|
|
21
20
|
import { TRANSPORT_TYPE, BLUETOOTH_CONNECTION } from "../types";
|
|
22
21
|
import { DEFAULT_ACTION_RESPONSE_TIMEOUT } from "../constants";
|
|
23
22
|
import { CHARACTERISTIC_UUIDS_TO_NAMES } from "../constants";
|
|
24
23
|
import { ANDROID_MAX_MTU } from "../constants";
|
|
25
24
|
import { REACT_NATIVE_MAX_BYTE_SIZE } from "../constants";
|
|
25
|
+
import { decodeJSONChunks } from "../utils/decodeJSONChunks";
|
|
26
26
|
const defaultOptions = {
|
|
27
27
|
autoConnect: true
|
|
28
28
|
};
|
|
29
29
|
export class ReactNativeTransport {
|
|
30
30
|
constructor(options) {
|
|
31
31
|
this.type = TRANSPORT_TYPE.REACT_NATIVE;
|
|
32
|
+
this.textCodec = new TextCodec(this.type);
|
|
32
33
|
this.characteristicsByName = {};
|
|
33
34
|
this.connection$ = new BehaviorSubject(BLUETOOTH_CONNECTION.DISCONNECTED);
|
|
34
35
|
this.pendingActions$ = new BehaviorSubject([]);
|
|
@@ -179,7 +180,8 @@ export class ReactNativeTransport {
|
|
|
179
180
|
// it only has peripheral.advertising.localName as deviceNickname
|
|
180
181
|
// and OS v16 has both as deviceNickname
|
|
181
182
|
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 : "";
|
|
182
|
-
const manufactureDataString = (_h = (_g =
|
|
183
|
+
const manufactureDataString = (_h = (_g = this.textCodec
|
|
184
|
+
.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
|
|
183
185
|
return Object.assign(Object.assign({}, acc), { [peripheral.id]: Object.assign(Object.assign({}, peripheral), { name: peripheralName, manufactureDataString }) });
|
|
184
186
|
}, {}), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), map((peripheralMap) => Object.values(peripheralMap)), share());
|
|
185
187
|
return peripherals$;
|
|
@@ -254,7 +256,7 @@ export class ReactNativeTransport {
|
|
|
254
256
|
}
|
|
255
257
|
return (_a = this.characteristicsByName) === null || _a === void 0 ? void 0 : _a[characteristicName];
|
|
256
258
|
}
|
|
257
|
-
subscribeToCharacteristic({ characteristicName, manageNotifications = true }) {
|
|
259
|
+
subscribeToCharacteristic({ characteristicName, manageNotifications = true, skipJSONDecoding = false }) {
|
|
258
260
|
const getData = ({ peripheralId, serviceUUID, characteristicUUID }) => defer(() => __awaiter(this, void 0, void 0, function* () {
|
|
259
261
|
var _a;
|
|
260
262
|
if (manageNotifications) {
|
|
@@ -277,18 +279,16 @@ export class ReactNativeTransport {
|
|
|
277
279
|
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}`);
|
|
278
280
|
}
|
|
279
281
|
}
|
|
280
|
-
})), filter(({ characteristic }) => characteristic === characteristicUUID), map(({ value }) =>
|
|
281
|
-
var _a;
|
|
282
|
-
try {
|
|
283
|
-
return JSON.parse(payload);
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
this.addLog(`Failed to parse JSON for ${characteristicName} characteristic. Falling back to unparsed string. ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}`);
|
|
287
|
-
return payload;
|
|
288
|
-
}
|
|
289
|
-
}));
|
|
282
|
+
})), filter(({ characteristic }) => characteristic === characteristicUUID), map(({ value }) => new Uint8Array(value)));
|
|
290
283
|
return this.connection$.pipe(switchMap((connection) => connection === BLUETOOTH_CONNECTION.CONNECTED
|
|
291
|
-
? getData(this.getCharacteristicByName(characteristicName))
|
|
284
|
+
? getData(this.getCharacteristicByName(characteristicName)).pipe(skipJSONDecoding
|
|
285
|
+
? identity // noop
|
|
286
|
+
: decodeJSONChunks({
|
|
287
|
+
textCodec: this.textCodec,
|
|
288
|
+
characteristicName,
|
|
289
|
+
delimiter: BLUETOOTH_CHUNK_DELIMITER,
|
|
290
|
+
addLog: (message) => this.addLog(message)
|
|
291
|
+
}))
|
|
292
292
|
: NEVER));
|
|
293
293
|
}
|
|
294
294
|
readCharacteristic(characteristicName, parse = false) {
|
|
@@ -301,7 +301,7 @@ export class ReactNativeTransport {
|
|
|
301
301
|
}
|
|
302
302
|
try {
|
|
303
303
|
const value = yield this.BleManager.read(peripheralId, serviceUUID, characteristicUUID);
|
|
304
|
-
const decodedValue = decode(
|
|
304
|
+
const decodedValue = this.textCodec.decode(new Uint8Array(value));
|
|
305
305
|
const data = parse ? JSON.parse(decodedValue) : decodedValue;
|
|
306
306
|
this.addLog(`Received read data from ${characteristicName} characteristic: \n${data}`);
|
|
307
307
|
return data;
|
|
@@ -318,7 +318,7 @@ export class ReactNativeTransport {
|
|
|
318
318
|
if (!characteristicUUID) {
|
|
319
319
|
return Promise.reject(new Error(`Did not find characteristic matching ${characteristicName}`));
|
|
320
320
|
}
|
|
321
|
-
const encoded = encode(
|
|
321
|
+
const encoded = this.textCodec.encode(data);
|
|
322
322
|
yield this.BleManager.write(peripheralId, serviceUUID, characteristicUUID, encoded, REACT_NATIVE_MAX_BYTE_SIZE);
|
|
323
323
|
});
|
|
324
324
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Observable, UnaryFunction } from "rxjs";
|
|
2
|
+
import { Buffer } from "buffer/index.js";
|
|
3
|
+
import { Sample } from "../../../types/sample";
|
|
4
|
+
import { Epoch } from "../../../types/epoch";
|
|
5
|
+
import { DeviceInfo } from "../../../types/deviceInfo";
|
|
6
|
+
/**
|
|
7
|
+
* @hidden
|
|
8
|
+
*/
|
|
9
|
+
export declare function binaryBufferToEpoch(deviceInfo: DeviceInfo): UnaryFunction<Observable<Epoch>, any>;
|
|
10
|
+
/**
|
|
11
|
+
* @hidden
|
|
12
|
+
*/
|
|
13
|
+
export declare function binaryBufferToSamples(channelCount: number): UnaryFunction<any, any>;
|
|
14
|
+
/**
|
|
15
|
+
* @hidden
|
|
16
|
+
*
|
|
17
|
+
* Decode the supplied Buffer as a list of Sample.
|
|
18
|
+
*
|
|
19
|
+
* Supplied buffer's length must be multiple of
|
|
20
|
+
* `encodedSampleSize(channelCount)`.
|
|
21
|
+
*
|
|
22
|
+
* NB: This method does not guarantee validity of decoded samples. When
|
|
23
|
+
* supplied with a buffer of appropriate length, it will always return a
|
|
24
|
+
* matching number of Sample8. Since the encoding protocol defines no
|
|
25
|
+
* metadata/checksum, correctness must be guaranteed via test coverage.
|
|
26
|
+
*
|
|
27
|
+
* @param buffer Buffer with binary payload to decode.
|
|
28
|
+
* @param channelCount Number of expected channels in each sample.
|
|
29
|
+
*
|
|
30
|
+
* @returns List of decoded Samples present in buffer.
|
|
31
|
+
*/
|
|
32
|
+
export declare function decode(buffer: Buffer, channelCount: number): Array<Sample>;
|
|
33
|
+
/**
|
|
34
|
+
* @hidden
|
|
35
|
+
*
|
|
36
|
+
* Calculate the size of each sample based on the number of channels.
|
|
37
|
+
*
|
|
38
|
+
* Each sample has the following 3 segments:
|
|
39
|
+
* - Timestamp: 8 bytes (UInt64); contains current time in millis since epoch)
|
|
40
|
+
* - Marker: 2 bytes (UInt16); for classifier data
|
|
41
|
+
* - Data: N * 8 bytes (Double), each entry representing data from a different
|
|
42
|
+
* electrode.
|
|
43
|
+
*
|
|
44
|
+
* +-----------+--------+------------------+
|
|
45
|
+
* | timestamp | marker | data (e1 ... eN) |
|
|
46
|
+
* +-----------+--------+------------------+
|
|
47
|
+
*
|
|
48
|
+
* The number of entries for Data varies per hardware model. It can be assumed
|
|
49
|
+
* to remain constant for the lifetime of the program.
|
|
50
|
+
*/
|
|
51
|
+
export declare function encodedSampleSize(channelCount: number): number;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { pipe, from } from "rxjs";
|
|
2
|
+
import { mergeMap } from "rxjs/operators";
|
|
3
|
+
import { Buffer } from "buffer/index.js"; // not including /index.js causes typescript to uses Node's native Buffer built-in and we want to use this npm package for both node and the browser
|
|
4
|
+
import { epoch, addInfo } from "../../../utils/pipes";
|
|
5
|
+
const EPOCH_BUFFER_SIZE = 16;
|
|
6
|
+
const SAMPLING_RATE_FALLBACK = 256; // Crown's sampling rate
|
|
7
|
+
/** Size in bytes for each channel's payload. */
|
|
8
|
+
const TimestampSize = 8; // UInt64
|
|
9
|
+
const MarkerSize = 2; // UInt16
|
|
10
|
+
const ChannelDataSize = 8; // Double
|
|
11
|
+
/** Size in bytes for the static payload of every sample (Timestamp + Marker) */
|
|
12
|
+
const SampleFixedSize = TimestampSize + MarkerSize;
|
|
13
|
+
/**
|
|
14
|
+
* @hidden
|
|
15
|
+
*/
|
|
16
|
+
export function binaryBufferToEpoch(deviceInfo) {
|
|
17
|
+
var _a;
|
|
18
|
+
if (!(deviceInfo === null || deviceInfo === void 0 ? void 0 : deviceInfo.samplingRate)) {
|
|
19
|
+
console.warn(`Didn't receive a sampling rate, defaulting to ${SAMPLING_RATE_FALLBACK}`);
|
|
20
|
+
}
|
|
21
|
+
return pipe(binaryBufferToSamples(deviceInfo.channels), epoch({
|
|
22
|
+
duration: EPOCH_BUFFER_SIZE,
|
|
23
|
+
interval: EPOCH_BUFFER_SIZE,
|
|
24
|
+
samplingRate: (_a = deviceInfo === null || deviceInfo === void 0 ? void 0 : deviceInfo.samplingRate) !== null && _a !== void 0 ? _a : SAMPLING_RATE_FALLBACK
|
|
25
|
+
}), addInfo({
|
|
26
|
+
channelNames: deviceInfo.channelNames,
|
|
27
|
+
samplingRate: deviceInfo.samplingRate
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* @hidden
|
|
32
|
+
*/
|
|
33
|
+
export function binaryBufferToSamples(channelCount) {
|
|
34
|
+
return pipe(mergeMap((arrayBuffer) => {
|
|
35
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
36
|
+
const decoded = decode(buffer, channelCount);
|
|
37
|
+
return from(decoded); // `from` creates an Observable emission from each item (Sample) in the array
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* @hidden
|
|
42
|
+
*
|
|
43
|
+
* Decode the supplied Buffer as a list of Sample.
|
|
44
|
+
*
|
|
45
|
+
* Supplied buffer's length must be multiple of
|
|
46
|
+
* `encodedSampleSize(channelCount)`.
|
|
47
|
+
*
|
|
48
|
+
* NB: This method does not guarantee validity of decoded samples. When
|
|
49
|
+
* supplied with a buffer of appropriate length, it will always return a
|
|
50
|
+
* matching number of Sample8. Since the encoding protocol defines no
|
|
51
|
+
* metadata/checksum, correctness must be guaranteed via test coverage.
|
|
52
|
+
*
|
|
53
|
+
* @param buffer Buffer with binary payload to decode.
|
|
54
|
+
* @param channelCount Number of expected channels in each sample.
|
|
55
|
+
*
|
|
56
|
+
* @returns List of decoded Samples present in buffer.
|
|
57
|
+
*/
|
|
58
|
+
export function decode(buffer, channelCount) {
|
|
59
|
+
let sampleLen = encodedSampleSize(channelCount);
|
|
60
|
+
// Alternative: relax this check, process sampleLen at a time, discard remainder?
|
|
61
|
+
if (buffer.length % sampleLen != 0) {
|
|
62
|
+
throw new Error(`buffer.length (${buffer.length}) for ${channelCount} channels must be multiple of ${sampleLen}B)`);
|
|
63
|
+
}
|
|
64
|
+
let sampleCount = buffer.length / sampleLen;
|
|
65
|
+
let samples = new Array(sampleCount);
|
|
66
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
67
|
+
let offset = i * sampleLen;
|
|
68
|
+
let channelData = new Array(channelCount);
|
|
69
|
+
// Read 8 bytes for timestamp & advance offset
|
|
70
|
+
let ts = buffer.readBigUInt64BE(offset);
|
|
71
|
+
offset += TimestampSize;
|
|
72
|
+
// Read 1 byte for marker & advance offset
|
|
73
|
+
let marker = buffer.readUInt16BE(offset);
|
|
74
|
+
offset += MarkerSize;
|
|
75
|
+
// Read 8 bytes for each channel & advance offset
|
|
76
|
+
for (let i = 0; i < channelCount; i++) {
|
|
77
|
+
channelData[i] = buffer.readDoubleBE(offset);
|
|
78
|
+
offset += ChannelDataSize;
|
|
79
|
+
}
|
|
80
|
+
samples[i] = {
|
|
81
|
+
timestamp: Number(ts),
|
|
82
|
+
// TODO: uncomment when ready
|
|
83
|
+
// marker: marker,
|
|
84
|
+
data: channelData
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return samples;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @hidden
|
|
91
|
+
*
|
|
92
|
+
* Calculate the size of each sample based on the number of channels.
|
|
93
|
+
*
|
|
94
|
+
* Each sample has the following 3 segments:
|
|
95
|
+
* - Timestamp: 8 bytes (UInt64); contains current time in millis since epoch)
|
|
96
|
+
* - Marker: 2 bytes (UInt16); for classifier data
|
|
97
|
+
* - Data: N * 8 bytes (Double), each entry representing data from a different
|
|
98
|
+
* electrode.
|
|
99
|
+
*
|
|
100
|
+
* +-----------+--------+------------------+
|
|
101
|
+
* | timestamp | marker | data (e1 ... eN) |
|
|
102
|
+
* +-----------+--------+------------------+
|
|
103
|
+
*
|
|
104
|
+
* The number of entries for Data varies per hardware model. It can be assumed
|
|
105
|
+
* to remain constant for the lifetime of the program.
|
|
106
|
+
*/
|
|
107
|
+
export function encodedSampleSize(channelCount) {
|
|
108
|
+
return SampleFixedSize + channelCount * ChannelDataSize;
|
|
109
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TextCodec } from "./textCodec";
|
|
2
|
+
/**
|
|
3
|
+
* @hidden
|
|
4
|
+
*/
|
|
5
|
+
declare type Options = {
|
|
6
|
+
textCodec: TextCodec;
|
|
7
|
+
characteristicName: string;
|
|
8
|
+
delimiter: string;
|
|
9
|
+
addLog: (message: string) => void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* @hidden
|
|
13
|
+
*/
|
|
14
|
+
export declare function decodeJSONChunks({ textCodec, characteristicName, delimiter, addLog }: Options): import("rxjs").UnaryFunction<import("rxjs").Observable<Uint8Array>, import("rxjs").Observable<any>>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { map, pipe } from "rxjs";
|
|
2
|
+
import { stitchChunks } from "./stitch";
|
|
3
|
+
/**
|
|
4
|
+
* @hidden
|
|
5
|
+
*/
|
|
6
|
+
export function decodeJSONChunks({ textCodec, characteristicName, delimiter, addLog }) {
|
|
7
|
+
return pipe(map((arrayBuffer) => {
|
|
8
|
+
const decoded = textCodec.decode(arrayBuffer);
|
|
9
|
+
addLog(`Received chunk with buffer size of ${arrayBuffer.byteLength} and decoded size ${decoded.length} for ${characteristicName} characteristic: \n${decoded}`);
|
|
10
|
+
return decoded;
|
|
11
|
+
}), stitchChunks({ delimiter }), map((payload) => {
|
|
12
|
+
var _a;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(payload);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
addLog(`Failed to parse JSON for ${characteristicName} characteristic. Falling back to unparsed string. ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}`);
|
|
18
|
+
return payload;
|
|
19
|
+
}
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TRANSPORT_TYPE } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* @hidden
|
|
4
|
+
* Uint8Array in Web | number[] in React Native
|
|
5
|
+
*/
|
|
6
|
+
export declare type BufferLike = Uint8Array | number[];
|
|
7
|
+
/**
|
|
8
|
+
* @hidden
|
|
9
|
+
*/
|
|
10
|
+
export declare class TextCodec {
|
|
11
|
+
transportType: TRANSPORT_TYPE;
|
|
12
|
+
webEncoder: TextEncoder;
|
|
13
|
+
webDecoder: TextDecoder;
|
|
14
|
+
constructor(transportType: TRANSPORT_TYPE);
|
|
15
|
+
encode(data: string): BufferLike;
|
|
16
|
+
decode(arrayBuffer: Uint8Array): string;
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Buffer } from "buffer/index.js"; // not including /index.js causes typescript to uses Node's native Buffer built-in and we want to use this npm package for both node and the browser
|
|
2
|
+
import { TRANSPORT_TYPE } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* @hidden
|
|
5
|
+
*/
|
|
6
|
+
export class TextCodec {
|
|
7
|
+
constructor(transportType) {
|
|
8
|
+
this.transportType = transportType;
|
|
9
|
+
if (transportType === TRANSPORT_TYPE.WEB) {
|
|
10
|
+
this.webEncoder = new TextEncoder();
|
|
11
|
+
this.webDecoder = new TextDecoder("utf-8");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
encode(data) {
|
|
15
|
+
if (this.transportType === TRANSPORT_TYPE.WEB) {
|
|
16
|
+
const encoded = this.webEncoder.encode(data);
|
|
17
|
+
return encoded;
|
|
18
|
+
}
|
|
19
|
+
if (this.transportType === TRANSPORT_TYPE.REACT_NATIVE) {
|
|
20
|
+
// React Native BLE Manager expects a number[] instead of a Uint8Array
|
|
21
|
+
const encoded = [...Buffer.from(data)];
|
|
22
|
+
return encoded;
|
|
23
|
+
}
|
|
24
|
+
const encoded = Buffer.from(data);
|
|
25
|
+
return encoded;
|
|
26
|
+
}
|
|
27
|
+
decode(arrayBuffer) {
|
|
28
|
+
if (this.transportType === TRANSPORT_TYPE.WEB) {
|
|
29
|
+
const decoded = this.webDecoder.decode(arrayBuffer);
|
|
30
|
+
return decoded;
|
|
31
|
+
}
|
|
32
|
+
// For React Native, and as a default
|
|
33
|
+
const decoded = Buffer.from(arrayBuffer).toString("utf-8");
|
|
34
|
+
return decoded;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="web-bluetooth" />
|
|
2
2
|
import { Observable, BehaviorSubject, ReplaySubject } from "rxjs";
|
|
3
3
|
import { BluetoothTransport } from "../BluetoothTransport";
|
|
4
|
+
import { TextCodec } from "../utils/textCodec";
|
|
4
5
|
import { ActionOptions, SubscribeOptions } from "../types";
|
|
5
6
|
import { TRANSPORT_TYPE, BLUETOOTH_CONNECTION } from "../types";
|
|
6
7
|
import { DeviceInfo } from "../../../types/deviceInfo";
|
|
@@ -9,6 +10,7 @@ declare type Options = {
|
|
|
9
10
|
};
|
|
10
11
|
export declare class WebBluetoothTransport implements BluetoothTransport {
|
|
11
12
|
type: TRANSPORT_TYPE;
|
|
13
|
+
textCodec: TextCodec;
|
|
12
14
|
options: Options;
|
|
13
15
|
device: BluetoothDevice;
|
|
14
16
|
server: BluetoothRemoteGATTServer;
|
|
@@ -42,7 +44,7 @@ export declare class WebBluetoothTransport implements BluetoothTransport {
|
|
|
42
44
|
* after reconnecting.
|
|
43
45
|
*/
|
|
44
46
|
getCharacteristicByName(characteristicName: string): Promise<BluetoothRemoteGATTCharacteristic>;
|
|
45
|
-
subscribeToCharacteristic({ characteristicName, manageNotifications }: SubscribeOptions): Observable<any>;
|
|
47
|
+
subscribeToCharacteristic({ characteristicName, manageNotifications, skipJSONDecoding }: SubscribeOptions): Observable<any>;
|
|
46
48
|
readCharacteristic(characteristicName: string, parse?: boolean): Promise<any>;
|
|
47
49
|
writeCharacteristic(characteristicName: string, data: string): Promise<void>;
|
|
48
50
|
_addPendingAction(actionId: number): void;
|
|
@@ -11,24 +11,25 @@ import { BLUETOOTH_PRIMARY_SERVICE_UUID_HEX } from "@neurosity/ipk";
|
|
|
11
11
|
import { BLUETOOTH_CHUNK_DELIMITER } from "@neurosity/ipk";
|
|
12
12
|
import { BLUETOOTH_DEVICE_NAME_PREFIXES } from "@neurosity/ipk";
|
|
13
13
|
import { BLUETOOTH_COMPANY_IDENTIFIER_HEX } from "@neurosity/ipk";
|
|
14
|
-
import { Observable, BehaviorSubject, ReplaySubject } from "rxjs";
|
|
14
|
+
import { Observable, BehaviorSubject, ReplaySubject, identity } from "rxjs";
|
|
15
15
|
import { defer, merge, timer, fromEventPattern, NEVER } from "rxjs";
|
|
16
16
|
import { switchMap, map, filter, tap } from "rxjs/operators";
|
|
17
17
|
import { shareReplay, distinctUntilChanged } from "rxjs/operators";
|
|
18
18
|
import { take, share } from "rxjs/operators";
|
|
19
19
|
import { isWebBluetoothSupported } from "./isWebBluetoothSupported";
|
|
20
20
|
import { create6DigitPin } from "../utils/create6DigitPin";
|
|
21
|
-
import {
|
|
22
|
-
import { encode, decode } from "../utils/encoding";
|
|
21
|
+
import { TextCodec } from "../utils/textCodec";
|
|
23
22
|
import { TRANSPORT_TYPE, BLUETOOTH_CONNECTION } from "../types";
|
|
24
23
|
import { DEFAULT_ACTION_RESPONSE_TIMEOUT } from "../constants";
|
|
25
24
|
import { CHARACTERISTIC_UUIDS_TO_NAMES } from "../constants";
|
|
25
|
+
import { decodeJSONChunks } from "../utils/decodeJSONChunks";
|
|
26
26
|
const defaultOptions = {
|
|
27
27
|
autoConnect: true
|
|
28
28
|
};
|
|
29
29
|
export class WebBluetoothTransport {
|
|
30
30
|
constructor(options = {}) {
|
|
31
31
|
this.type = TRANSPORT_TYPE.WEB;
|
|
32
|
+
this.textCodec = new TextCodec(this.type);
|
|
32
33
|
this.characteristicsByName = {};
|
|
33
34
|
this.connection$ = new BehaviorSubject(BLUETOOTH_CONNECTION.DISCONNECTED);
|
|
34
35
|
this.pendingActions$ = new BehaviorSubject([]);
|
|
@@ -208,7 +209,7 @@ export class WebBluetoothTransport {
|
|
|
208
209
|
return (_a = this.characteristicsByName) === null || _a === void 0 ? void 0 : _a[characteristicName];
|
|
209
210
|
});
|
|
210
211
|
}
|
|
211
|
-
subscribeToCharacteristic({ characteristicName, manageNotifications = true }) {
|
|
212
|
+
subscribeToCharacteristic({ characteristicName, manageNotifications = true, skipJSONDecoding = false }) {
|
|
212
213
|
const data$ = defer(() => this.getCharacteristicByName(characteristicName)).pipe(switchMap((characteristic) => __awaiter(this, void 0, void 0, function* () {
|
|
213
214
|
var _a;
|
|
214
215
|
if (this.isConnected() && manageNotifications) {
|
|
@@ -234,33 +235,17 @@ export class WebBluetoothTransport {
|
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
}));
|
|
237
|
-
}), map((event) =>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
this.addLog(`Failed to parse JSON for ${characteristicName} characteristic. Falling back to unparsed string. ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}`);
|
|
249
|
-
return payload;
|
|
250
|
-
}
|
|
251
|
-
})
|
|
252
|
-
// when streaming at ultra-low latency, the logs will slow down rendering
|
|
253
|
-
// tap((data) => {
|
|
254
|
-
// this.addLog(
|
|
255
|
-
// `Received data for ${characteristicName} characteristic: \n${JSON.stringify(
|
|
256
|
-
// data,
|
|
257
|
-
// null,
|
|
258
|
-
// 2
|
|
259
|
-
// )}`
|
|
260
|
-
// );
|
|
261
|
-
// })
|
|
262
|
-
);
|
|
263
|
-
return this.connection$.pipe(switchMap((connection) => connection === BLUETOOTH_CONNECTION.CONNECTED ? data$ : NEVER));
|
|
238
|
+
}), map((event) => event.target.value.buffer));
|
|
239
|
+
return this.connection$.pipe(switchMap((connection) => connection === BLUETOOTH_CONNECTION.CONNECTED
|
|
240
|
+
? data$.pipe(skipJSONDecoding
|
|
241
|
+
? identity // noop
|
|
242
|
+
: decodeJSONChunks({
|
|
243
|
+
textCodec: this.textCodec,
|
|
244
|
+
characteristicName,
|
|
245
|
+
delimiter: BLUETOOTH_CHUNK_DELIMITER,
|
|
246
|
+
addLog: (message) => this.addLog(message)
|
|
247
|
+
}))
|
|
248
|
+
: NEVER));
|
|
264
249
|
}
|
|
265
250
|
readCharacteristic(characteristicName, parse = false) {
|
|
266
251
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -271,9 +256,9 @@ export class WebBluetoothTransport {
|
|
|
271
256
|
this.addLog(`Did not fund ${characteristicName} characteristic`);
|
|
272
257
|
return Promise.reject(`Did not find characteristic by the name: ${characteristicName}`);
|
|
273
258
|
}
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
const decodedValue = decode(
|
|
259
|
+
const dataview = yield characteristic.readValue();
|
|
260
|
+
const arrayBuffer = dataview.buffer;
|
|
261
|
+
const decodedValue = this.textCodec.decode(arrayBuffer);
|
|
277
262
|
const data = parse ? JSON.parse(decodedValue) : decodedValue;
|
|
278
263
|
this.addLog(`Received read data from ${characteristicName} characteristic: \n${data}`);
|
|
279
264
|
return data;
|
|
@@ -291,7 +276,7 @@ export class WebBluetoothTransport {
|
|
|
291
276
|
this.addLog(`Did not fund ${characteristicName} characteristic`);
|
|
292
277
|
return Promise.reject(`Did not find characteristic by the name: ${characteristicName}`);
|
|
293
278
|
}
|
|
294
|
-
const encoded = encode(
|
|
279
|
+
const encoded = this.textCodec.encode(data);
|
|
295
280
|
yield characteristic.writeValueWithResponse(encoded);
|
|
296
281
|
});
|
|
297
282
|
}
|