@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>;
|
|
@@ -15,7 +15,7 @@ const rxjs_2 = require("rxjs");
|
|
|
15
15
|
const operators_1 = require("rxjs/operators");
|
|
16
16
|
const WebBluetoothTransport_1 = require("./web/WebBluetoothTransport");
|
|
17
17
|
const ReactNativeTransport_1 = require("./react-native/ReactNativeTransport");
|
|
18
|
-
const
|
|
18
|
+
const binaryBufferToEpoch_1 = require("./utils/binaryBufferToEpoch");
|
|
19
19
|
const types_1 = require("./types");
|
|
20
20
|
class BluetoothClient {
|
|
21
21
|
constructor(options) {
|
|
@@ -63,9 +63,10 @@ class BluetoothClient {
|
|
|
63
63
|
this._focus$ = this._subscribeWhileAuthenticated("focus");
|
|
64
64
|
this._calm$ = this._subscribeWhileAuthenticated("calm");
|
|
65
65
|
this._accelerometer$ = this._subscribeWhileAuthenticated("accelerometer");
|
|
66
|
-
this._brainwavesRaw$ = this._subscribeWhileAuthenticated("raw"
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
this._brainwavesRaw$ = this._subscribeWhileAuthenticated("raw", true // skipJSONDecoding
|
|
67
|
+
);
|
|
68
|
+
this._brainwavesRawUnfiltered$ = this._subscribeWhileAuthenticated("rawUnfiltered", true // skipJSONDecoding
|
|
69
|
+
);
|
|
69
70
|
this._brainwavesPSD$ = this._subscribeWhileAuthenticated("psd");
|
|
70
71
|
this._brainwavesPowerByBand$ =
|
|
71
72
|
this._subscribeWhileAuthenticated("powerByBand");
|
|
@@ -120,9 +121,17 @@ class BluetoothClient {
|
|
|
120
121
|
}
|
|
121
122
|
isAuthenticated() {
|
|
122
123
|
return __awaiter(this, void 0, void 0, function* () {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
try {
|
|
125
|
+
const [isAuthenticated, expiresIn] = yield this.transport.readCharacteristic("auth", true);
|
|
126
|
+
this.isAuthenticated$.next(isAuthenticated);
|
|
127
|
+
return [isAuthenticated, expiresIn];
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const failedResponse = [false, null];
|
|
131
|
+
this.transport.addLog(`Authentication error -> ${error}`);
|
|
132
|
+
this.isAuthenticated$.next(false);
|
|
133
|
+
return failedResponse;
|
|
134
|
+
}
|
|
126
135
|
});
|
|
127
136
|
}
|
|
128
137
|
// Method for React Native only
|
|
@@ -179,10 +188,11 @@ class BluetoothClient {
|
|
|
179
188
|
return yield getter();
|
|
180
189
|
});
|
|
181
190
|
}
|
|
182
|
-
_subscribeWhileAuthenticated(characteristicName) {
|
|
191
|
+
_subscribeWhileAuthenticated(characteristicName, skipJSONDecoding = false) {
|
|
183
192
|
return this.osHasBluetoothSupport$.pipe((0, operators_1.switchMap)((osHasBluetoothSupport) => osHasBluetoothSupport ? this.isAuthenticated$ : rxjs_2.EMPTY), (0, operators_1.distinctUntilChanged)(), (0, operators_1.switchMap)((isAuthenticated) => isAuthenticated
|
|
184
193
|
? this.transport.subscribeToCharacteristic({
|
|
185
|
-
characteristicName
|
|
194
|
+
characteristicName,
|
|
195
|
+
skipJSONDecoding
|
|
186
196
|
})
|
|
187
197
|
: rxjs_2.EMPTY), (0, operators_1.share)());
|
|
188
198
|
}
|
|
@@ -199,9 +209,9 @@ class BluetoothClient {
|
|
|
199
209
|
switch (label) {
|
|
200
210
|
default:
|
|
201
211
|
case "raw":
|
|
202
|
-
return (0, rxjs_1.defer)(() => this.getInfo()).pipe((0, operators_1.switchMap)((deviceInfo) => this._brainwavesRaw$.pipe((0,
|
|
212
|
+
return (0, rxjs_1.defer)(() => this.getInfo()).pipe((0, operators_1.switchMap)((deviceInfo) => this._brainwavesRaw$.pipe((0, binaryBufferToEpoch_1.binaryBufferToEpoch)(deviceInfo))));
|
|
203
213
|
case "rawUnfiltered":
|
|
204
|
-
return (0, rxjs_1.defer)(() => this.getInfo()).pipe((0, operators_1.switchMap)((deviceInfo) => this._brainwavesRawUnfiltered$.pipe((0,
|
|
214
|
+
return (0, rxjs_1.defer)(() => this.getInfo()).pipe((0, operators_1.switchMap)((deviceInfo) => this._brainwavesRawUnfiltered$.pipe((0, binaryBufferToEpoch_1.binaryBufferToEpoch)(deviceInfo))));
|
|
205
215
|
case "psd":
|
|
206
216
|
return this._brainwavesPSD$;
|
|
207
217
|
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;
|
|
@@ -19,19 +19,20 @@ const operators_1 = require("rxjs/operators");
|
|
|
19
19
|
const operators_2 = require("rxjs/operators");
|
|
20
20
|
const operators_3 = require("rxjs/operators");
|
|
21
21
|
const create6DigitPin_1 = require("../utils/create6DigitPin");
|
|
22
|
-
const
|
|
23
|
-
const encoding_1 = require("../utils/encoding");
|
|
22
|
+
const textCodec_1 = require("../utils/textCodec");
|
|
24
23
|
const types_1 = require("../types");
|
|
25
24
|
const constants_1 = require("../constants");
|
|
26
25
|
const constants_2 = require("../constants");
|
|
27
26
|
const constants_3 = require("../constants");
|
|
28
27
|
const constants_4 = require("../constants");
|
|
28
|
+
const decodeJSONChunks_1 = require("../utils/decodeJSONChunks");
|
|
29
29
|
const defaultOptions = {
|
|
30
30
|
autoConnect: true
|
|
31
31
|
};
|
|
32
32
|
class ReactNativeTransport {
|
|
33
33
|
constructor(options) {
|
|
34
34
|
this.type = types_1.TRANSPORT_TYPE.REACT_NATIVE;
|
|
35
|
+
this.textCodec = new textCodec_1.TextCodec(this.type);
|
|
35
36
|
this.characteristicsByName = {};
|
|
36
37
|
this.connection$ = new rxjs_1.BehaviorSubject(types_1.BLUETOOTH_CONNECTION.DISCONNECTED);
|
|
37
38
|
this.pendingActions$ = new rxjs_1.BehaviorSubject([]);
|
|
@@ -182,7 +183,8 @@ class ReactNativeTransport {
|
|
|
182
183
|
// it only has peripheral.advertising.localName as deviceNickname
|
|
183
184
|
// and OS v16 has both as deviceNickname
|
|
184
185
|
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 : "";
|
|
185
|
-
const manufactureDataString = (_h = (_g =
|
|
186
|
+
const manufactureDataString = (_h = (_g = this.textCodec
|
|
187
|
+
.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
|
|
186
188
|
return Object.assign(Object.assign({}, acc), { [peripheral.id]: Object.assign(Object.assign({}, peripheral), { name: peripheralName, manufactureDataString }) });
|
|
187
189
|
}, {}), (0, operators_2.distinctUntilChanged)((a, b) => JSON.stringify(a) === JSON.stringify(b)), (0, operators_1.map)((peripheralMap) => Object.values(peripheralMap)), (0, operators_3.share)());
|
|
188
190
|
return peripherals$;
|
|
@@ -257,7 +259,7 @@ class ReactNativeTransport {
|
|
|
257
259
|
}
|
|
258
260
|
return (_a = this.characteristicsByName) === null || _a === void 0 ? void 0 : _a[characteristicName];
|
|
259
261
|
}
|
|
260
|
-
subscribeToCharacteristic({ characteristicName, manageNotifications = true }) {
|
|
262
|
+
subscribeToCharacteristic({ characteristicName, manageNotifications = true, skipJSONDecoding = false }) {
|
|
261
263
|
const getData = ({ peripheralId, serviceUUID, characteristicUUID }) => (0, rxjs_2.defer)(() => __awaiter(this, void 0, void 0, function* () {
|
|
262
264
|
var _a;
|
|
263
265
|
if (manageNotifications) {
|
|
@@ -280,18 +282,16 @@ class ReactNativeTransport {
|
|
|
280
282
|
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}`);
|
|
281
283
|
}
|
|
282
284
|
}
|
|
283
|
-
})), (0, operators_1.filter)(({ characteristic }) => characteristic === characteristicUUID), (0, operators_1.map)(({ value }) =>
|
|
284
|
-
var _a;
|
|
285
|
-
try {
|
|
286
|
-
return JSON.parse(payload);
|
|
287
|
-
}
|
|
288
|
-
catch (error) {
|
|
289
|
-
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}`);
|
|
290
|
-
return payload;
|
|
291
|
-
}
|
|
292
|
-
}));
|
|
285
|
+
})), (0, operators_1.filter)(({ characteristic }) => characteristic === characteristicUUID), (0, operators_1.map)(({ value }) => new Uint8Array(value)));
|
|
293
286
|
return this.connection$.pipe((0, operators_1.switchMap)((connection) => connection === types_1.BLUETOOTH_CONNECTION.CONNECTED
|
|
294
|
-
? getData(this.getCharacteristicByName(characteristicName))
|
|
287
|
+
? getData(this.getCharacteristicByName(characteristicName)).pipe(skipJSONDecoding
|
|
288
|
+
? rxjs_2.identity // noop
|
|
289
|
+
: (0, decodeJSONChunks_1.decodeJSONChunks)({
|
|
290
|
+
textCodec: this.textCodec,
|
|
291
|
+
characteristicName,
|
|
292
|
+
delimiter: ipk_2.BLUETOOTH_CHUNK_DELIMITER,
|
|
293
|
+
addLog: (message) => this.addLog(message)
|
|
294
|
+
}))
|
|
295
295
|
: rxjs_1.NEVER));
|
|
296
296
|
}
|
|
297
297
|
readCharacteristic(characteristicName, parse = false) {
|
|
@@ -304,7 +304,7 @@ class ReactNativeTransport {
|
|
|
304
304
|
}
|
|
305
305
|
try {
|
|
306
306
|
const value = yield this.BleManager.read(peripheralId, serviceUUID, characteristicUUID);
|
|
307
|
-
const decodedValue =
|
|
307
|
+
const decodedValue = this.textCodec.decode(new Uint8Array(value));
|
|
308
308
|
const data = parse ? JSON.parse(decodedValue) : decodedValue;
|
|
309
309
|
this.addLog(`Received read data from ${characteristicName} characteristic: \n${data}`);
|
|
310
310
|
return data;
|
|
@@ -321,7 +321,7 @@ class ReactNativeTransport {
|
|
|
321
321
|
if (!characteristicUUID) {
|
|
322
322
|
return Promise.reject(new Error(`Did not find characteristic matching ${characteristicName}`));
|
|
323
323
|
}
|
|
324
|
-
const encoded =
|
|
324
|
+
const encoded = this.textCodec.encode(data);
|
|
325
325
|
yield this.BleManager.write(peripheralId, serviceUUID, characteristicUUID, encoded, constants_4.REACT_NATIVE_MAX_BYTE_SIZE);
|
|
326
326
|
});
|
|
327
327
|
}
|
|
@@ -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,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encodedSampleSize = exports.decode = exports.binaryBufferToSamples = exports.binaryBufferToEpoch = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
const index_js_1 = require("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
|
|
7
|
+
const pipes_1 = require("../../../utils/pipes");
|
|
8
|
+
const EPOCH_BUFFER_SIZE = 16;
|
|
9
|
+
const SAMPLING_RATE_FALLBACK = 256; // Crown's sampling rate
|
|
10
|
+
/** Size in bytes for each channel's payload. */
|
|
11
|
+
const TimestampSize = 8; // UInt64
|
|
12
|
+
const MarkerSize = 2; // UInt16
|
|
13
|
+
const ChannelDataSize = 8; // Double
|
|
14
|
+
/** Size in bytes for the static payload of every sample (Timestamp + Marker) */
|
|
15
|
+
const SampleFixedSize = TimestampSize + MarkerSize;
|
|
16
|
+
/**
|
|
17
|
+
* @hidden
|
|
18
|
+
*/
|
|
19
|
+
function binaryBufferToEpoch(deviceInfo) {
|
|
20
|
+
var _a;
|
|
21
|
+
if (!(deviceInfo === null || deviceInfo === void 0 ? void 0 : deviceInfo.samplingRate)) {
|
|
22
|
+
console.warn(`Didn't receive a sampling rate, defaulting to ${SAMPLING_RATE_FALLBACK}`);
|
|
23
|
+
}
|
|
24
|
+
return (0, rxjs_1.pipe)(binaryBufferToSamples(deviceInfo.channels), (0, pipes_1.epoch)({
|
|
25
|
+
duration: EPOCH_BUFFER_SIZE,
|
|
26
|
+
interval: EPOCH_BUFFER_SIZE,
|
|
27
|
+
samplingRate: (_a = deviceInfo === null || deviceInfo === void 0 ? void 0 : deviceInfo.samplingRate) !== null && _a !== void 0 ? _a : SAMPLING_RATE_FALLBACK
|
|
28
|
+
}), (0, pipes_1.addInfo)({
|
|
29
|
+
channelNames: deviceInfo.channelNames,
|
|
30
|
+
samplingRate: deviceInfo.samplingRate
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
exports.binaryBufferToEpoch = binaryBufferToEpoch;
|
|
34
|
+
/**
|
|
35
|
+
* @hidden
|
|
36
|
+
*/
|
|
37
|
+
function binaryBufferToSamples(channelCount) {
|
|
38
|
+
return (0, rxjs_1.pipe)((0, operators_1.mergeMap)((arrayBuffer) => {
|
|
39
|
+
const buffer = index_js_1.Buffer.from(arrayBuffer);
|
|
40
|
+
const decoded = decode(buffer, channelCount);
|
|
41
|
+
return (0, rxjs_1.from)(decoded); // `from` creates an Observable emission from each item (Sample) in the array
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
exports.binaryBufferToSamples = binaryBufferToSamples;
|
|
45
|
+
/**
|
|
46
|
+
* @hidden
|
|
47
|
+
*
|
|
48
|
+
* Decode the supplied Buffer as a list of Sample.
|
|
49
|
+
*
|
|
50
|
+
* Supplied buffer's length must be multiple of
|
|
51
|
+
* `encodedSampleSize(channelCount)`.
|
|
52
|
+
*
|
|
53
|
+
* NB: This method does not guarantee validity of decoded samples. When
|
|
54
|
+
* supplied with a buffer of appropriate length, it will always return a
|
|
55
|
+
* matching number of Sample8. Since the encoding protocol defines no
|
|
56
|
+
* metadata/checksum, correctness must be guaranteed via test coverage.
|
|
57
|
+
*
|
|
58
|
+
* @param buffer Buffer with binary payload to decode.
|
|
59
|
+
* @param channelCount Number of expected channels in each sample.
|
|
60
|
+
*
|
|
61
|
+
* @returns List of decoded Samples present in buffer.
|
|
62
|
+
*/
|
|
63
|
+
function decode(buffer, channelCount) {
|
|
64
|
+
let sampleLen = encodedSampleSize(channelCount);
|
|
65
|
+
// Alternative: relax this check, process sampleLen at a time, discard remainder?
|
|
66
|
+
if (buffer.length % sampleLen != 0) {
|
|
67
|
+
throw new Error(`buffer.length (${buffer.length}) for ${channelCount} channels must be multiple of ${sampleLen}B)`);
|
|
68
|
+
}
|
|
69
|
+
let sampleCount = buffer.length / sampleLen;
|
|
70
|
+
let samples = new Array(sampleCount);
|
|
71
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
72
|
+
let offset = i * sampleLen;
|
|
73
|
+
let channelData = new Array(channelCount);
|
|
74
|
+
// Read 8 bytes for timestamp & advance offset
|
|
75
|
+
let ts = buffer.readBigUInt64BE(offset);
|
|
76
|
+
offset += TimestampSize;
|
|
77
|
+
// Read 1 byte for marker & advance offset
|
|
78
|
+
let marker = buffer.readUInt16BE(offset);
|
|
79
|
+
offset += MarkerSize;
|
|
80
|
+
// Read 8 bytes for each channel & advance offset
|
|
81
|
+
for (let i = 0; i < channelCount; i++) {
|
|
82
|
+
channelData[i] = buffer.readDoubleBE(offset);
|
|
83
|
+
offset += ChannelDataSize;
|
|
84
|
+
}
|
|
85
|
+
samples[i] = {
|
|
86
|
+
timestamp: Number(ts),
|
|
87
|
+
// TODO: uncomment when ready
|
|
88
|
+
// marker: marker,
|
|
89
|
+
data: channelData
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return samples;
|
|
93
|
+
}
|
|
94
|
+
exports.decode = decode;
|
|
95
|
+
/**
|
|
96
|
+
* @hidden
|
|
97
|
+
*
|
|
98
|
+
* Calculate the size of each sample based on the number of channels.
|
|
99
|
+
*
|
|
100
|
+
* Each sample has the following 3 segments:
|
|
101
|
+
* - Timestamp: 8 bytes (UInt64); contains current time in millis since epoch)
|
|
102
|
+
* - Marker: 2 bytes (UInt16); for classifier data
|
|
103
|
+
* - Data: N * 8 bytes (Double), each entry representing data from a different
|
|
104
|
+
* electrode.
|
|
105
|
+
*
|
|
106
|
+
* +-----------+--------+------------------+
|
|
107
|
+
* | timestamp | marker | data (e1 ... eN) |
|
|
108
|
+
* +-----------+--------+------------------+
|
|
109
|
+
*
|
|
110
|
+
* The number of entries for Data varies per hardware model. It can be assumed
|
|
111
|
+
* to remain constant for the lifetime of the program.
|
|
112
|
+
*/
|
|
113
|
+
function encodedSampleSize(channelCount) {
|
|
114
|
+
return SampleFixedSize + channelCount * ChannelDataSize;
|
|
115
|
+
}
|
|
116
|
+
exports.encodedSampleSize = encodedSampleSize;
|
|
@@ -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,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decodeJSONChunks = void 0;
|
|
4
|
+
const rxjs_1 = require("rxjs");
|
|
5
|
+
const stitch_1 = require("./stitch");
|
|
6
|
+
/**
|
|
7
|
+
* @hidden
|
|
8
|
+
*/
|
|
9
|
+
function decodeJSONChunks({ textCodec, characteristicName, delimiter, addLog }) {
|
|
10
|
+
return (0, rxjs_1.pipe)((0, rxjs_1.map)((arrayBuffer) => {
|
|
11
|
+
const decoded = textCodec.decode(arrayBuffer);
|
|
12
|
+
addLog(`Received chunk with buffer size of ${arrayBuffer.byteLength} and decoded size ${decoded.length} for ${characteristicName} characteristic: \n${decoded}`);
|
|
13
|
+
return decoded;
|
|
14
|
+
}), (0, stitch_1.stitchChunks)({ delimiter }), (0, rxjs_1.map)((payload) => {
|
|
15
|
+
var _a;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(payload);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
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}`);
|
|
21
|
+
return payload;
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
exports.decodeJSONChunks = decodeJSONChunks;
|
|
@@ -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,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TextCodec = void 0;
|
|
4
|
+
const index_js_1 = require("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
|
|
5
|
+
const types_1 = require("../types");
|
|
6
|
+
/**
|
|
7
|
+
* @hidden
|
|
8
|
+
*/
|
|
9
|
+
class TextCodec {
|
|
10
|
+
constructor(transportType) {
|
|
11
|
+
this.transportType = transportType;
|
|
12
|
+
if (transportType === types_1.TRANSPORT_TYPE.WEB) {
|
|
13
|
+
this.webEncoder = new TextEncoder();
|
|
14
|
+
this.webDecoder = new TextDecoder("utf-8");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
encode(data) {
|
|
18
|
+
if (this.transportType === types_1.TRANSPORT_TYPE.WEB) {
|
|
19
|
+
const encoded = this.webEncoder.encode(data);
|
|
20
|
+
return encoded;
|
|
21
|
+
}
|
|
22
|
+
if (this.transportType === types_1.TRANSPORT_TYPE.REACT_NATIVE) {
|
|
23
|
+
// React Native BLE Manager expects a number[] instead of a Uint8Array
|
|
24
|
+
const encoded = [...index_js_1.Buffer.from(data)];
|
|
25
|
+
return encoded;
|
|
26
|
+
}
|
|
27
|
+
const encoded = index_js_1.Buffer.from(data);
|
|
28
|
+
return encoded;
|
|
29
|
+
}
|
|
30
|
+
decode(arrayBuffer) {
|
|
31
|
+
if (this.transportType === types_1.TRANSPORT_TYPE.WEB) {
|
|
32
|
+
const decoded = this.webDecoder.decode(arrayBuffer);
|
|
33
|
+
return decoded;
|
|
34
|
+
}
|
|
35
|
+
// For React Native, and as a default
|
|
36
|
+
const decoded = index_js_1.Buffer.from(arrayBuffer).toString("utf-8");
|
|
37
|
+
return decoded;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.TextCodec = TextCodec;
|
|
@@ -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;
|
|
@@ -21,17 +21,18 @@ const operators_2 = require("rxjs/operators");
|
|
|
21
21
|
const operators_3 = require("rxjs/operators");
|
|
22
22
|
const isWebBluetoothSupported_1 = require("./isWebBluetoothSupported");
|
|
23
23
|
const create6DigitPin_1 = require("../utils/create6DigitPin");
|
|
24
|
-
const
|
|
25
|
-
const encoding_1 = require("../utils/encoding");
|
|
24
|
+
const textCodec_1 = require("../utils/textCodec");
|
|
26
25
|
const types_1 = require("../types");
|
|
27
26
|
const constants_1 = require("../constants");
|
|
28
27
|
const constants_2 = require("../constants");
|
|
28
|
+
const decodeJSONChunks_1 = require("../utils/decodeJSONChunks");
|
|
29
29
|
const defaultOptions = {
|
|
30
30
|
autoConnect: true
|
|
31
31
|
};
|
|
32
32
|
class WebBluetoothTransport {
|
|
33
33
|
constructor(options = {}) {
|
|
34
34
|
this.type = types_1.TRANSPORT_TYPE.WEB;
|
|
35
|
+
this.textCodec = new textCodec_1.TextCodec(this.type);
|
|
35
36
|
this.characteristicsByName = {};
|
|
36
37
|
this.connection$ = new rxjs_1.BehaviorSubject(types_1.BLUETOOTH_CONNECTION.DISCONNECTED);
|
|
37
38
|
this.pendingActions$ = new rxjs_1.BehaviorSubject([]);
|
|
@@ -211,7 +212,7 @@ class WebBluetoothTransport {
|
|
|
211
212
|
return (_a = this.characteristicsByName) === null || _a === void 0 ? void 0 : _a[characteristicName];
|
|
212
213
|
});
|
|
213
214
|
}
|
|
214
|
-
subscribeToCharacteristic({ characteristicName, manageNotifications = true }) {
|
|
215
|
+
subscribeToCharacteristic({ characteristicName, manageNotifications = true, skipJSONDecoding = false }) {
|
|
215
216
|
const data$ = (0, rxjs_2.defer)(() => this.getCharacteristicByName(characteristicName)).pipe((0, operators_1.switchMap)((characteristic) => __awaiter(this, void 0, void 0, function* () {
|
|
216
217
|
var _a;
|
|
217
218
|
if (this.isConnected() && manageNotifications) {
|
|
@@ -237,33 +238,17 @@ class WebBluetoothTransport {
|
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
}));
|
|
240
|
-
}), (0, operators_1.map)((event) =>
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
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}`);
|
|
252
|
-
return payload;
|
|
253
|
-
}
|
|
254
|
-
})
|
|
255
|
-
// when streaming at ultra-low latency, the logs will slow down rendering
|
|
256
|
-
// tap((data) => {
|
|
257
|
-
// this.addLog(
|
|
258
|
-
// `Received data for ${characteristicName} characteristic: \n${JSON.stringify(
|
|
259
|
-
// data,
|
|
260
|
-
// null,
|
|
261
|
-
// 2
|
|
262
|
-
// )}`
|
|
263
|
-
// );
|
|
264
|
-
// })
|
|
265
|
-
);
|
|
266
|
-
return this.connection$.pipe((0, operators_1.switchMap)((connection) => connection === types_1.BLUETOOTH_CONNECTION.CONNECTED ? data$ : rxjs_2.NEVER));
|
|
241
|
+
}), (0, operators_1.map)((event) => event.target.value.buffer));
|
|
242
|
+
return this.connection$.pipe((0, operators_1.switchMap)((connection) => connection === types_1.BLUETOOTH_CONNECTION.CONNECTED
|
|
243
|
+
? data$.pipe(skipJSONDecoding
|
|
244
|
+
? rxjs_1.identity // noop
|
|
245
|
+
: (0, decodeJSONChunks_1.decodeJSONChunks)({
|
|
246
|
+
textCodec: this.textCodec,
|
|
247
|
+
characteristicName,
|
|
248
|
+
delimiter: ipk_2.BLUETOOTH_CHUNK_DELIMITER,
|
|
249
|
+
addLog: (message) => this.addLog(message)
|
|
250
|
+
}))
|
|
251
|
+
: rxjs_2.NEVER));
|
|
267
252
|
}
|
|
268
253
|
readCharacteristic(characteristicName, parse = false) {
|
|
269
254
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -274,9 +259,9 @@ class WebBluetoothTransport {
|
|
|
274
259
|
this.addLog(`Did not fund ${characteristicName} characteristic`);
|
|
275
260
|
return Promise.reject(`Did not find characteristic by the name: ${characteristicName}`);
|
|
276
261
|
}
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
const decodedValue =
|
|
262
|
+
const dataview = yield characteristic.readValue();
|
|
263
|
+
const arrayBuffer = dataview.buffer;
|
|
264
|
+
const decodedValue = this.textCodec.decode(arrayBuffer);
|
|
280
265
|
const data = parse ? JSON.parse(decodedValue) : decodedValue;
|
|
281
266
|
this.addLog(`Received read data from ${characteristicName} characteristic: \n${data}`);
|
|
282
267
|
return data;
|
|
@@ -294,7 +279,7 @@ class WebBluetoothTransport {
|
|
|
294
279
|
this.addLog(`Did not fund ${characteristicName} characteristic`);
|
|
295
280
|
return Promise.reject(`Did not find characteristic by the name: ${characteristicName}`);
|
|
296
281
|
}
|
|
297
|
-
const encoded =
|
|
282
|
+
const encoded = this.textCodec.encode(data);
|
|
298
283
|
yield characteristic.writeValueWithResponse(encoded);
|
|
299
284
|
});
|
|
300
285
|
}
|