@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.
Files changed (40) hide show
  1. package/dist/browser/neurosity.iife.js +2590 -105
  2. package/dist/browser/neurosity.js +129 -113
  3. package/dist/browser/neurosity.js.map +1 -1
  4. package/dist/cjs/api/bluetooth/BluetoothClient.d.ts +1 -1
  5. package/dist/cjs/api/bluetooth/BluetoothClient.js +21 -11
  6. package/dist/cjs/api/bluetooth/react-native/ReactNativeTransport.d.ts +3 -1
  7. package/dist/cjs/api/bluetooth/react-native/ReactNativeTransport.js +17 -17
  8. package/dist/cjs/api/bluetooth/types/index.d.ts +1 -0
  9. package/dist/cjs/api/bluetooth/utils/binaryBufferToEpoch.d.ts +51 -0
  10. package/dist/cjs/api/bluetooth/utils/binaryBufferToEpoch.js +116 -0
  11. package/dist/cjs/api/bluetooth/utils/decodeJSONChunks.d.ts +15 -0
  12. package/dist/cjs/api/bluetooth/utils/decodeJSONChunks.js +25 -0
  13. package/dist/cjs/api/bluetooth/utils/textCodec.d.ts +17 -0
  14. package/dist/cjs/api/bluetooth/utils/textCodec.js +40 -0
  15. package/dist/cjs/api/bluetooth/web/WebBluetoothTransport.d.ts +3 -1
  16. package/dist/cjs/api/bluetooth/web/WebBluetoothTransport.js +19 -34
  17. package/dist/electron/index.js +15 -13
  18. package/dist/electron/index.js.map +1 -1
  19. package/dist/esm/api/bluetooth/BluetoothClient.d.ts +1 -1
  20. package/dist/esm/api/bluetooth/BluetoothClient.js +21 -11
  21. package/dist/esm/api/bluetooth/react-native/ReactNativeTransport.d.ts +3 -1
  22. package/dist/esm/api/bluetooth/react-native/ReactNativeTransport.js +18 -18
  23. package/dist/esm/api/bluetooth/types/index.d.ts +1 -0
  24. package/dist/esm/api/bluetooth/utils/binaryBufferToEpoch.d.ts +51 -0
  25. package/dist/esm/api/bluetooth/utils/binaryBufferToEpoch.js +109 -0
  26. package/dist/esm/api/bluetooth/utils/decodeJSONChunks.d.ts +15 -0
  27. package/dist/esm/api/bluetooth/utils/decodeJSONChunks.js +21 -0
  28. package/dist/esm/api/bluetooth/utils/textCodec.d.ts +17 -0
  29. package/dist/esm/api/bluetooth/utils/textCodec.js +36 -0
  30. package/dist/esm/api/bluetooth/web/WebBluetoothTransport.d.ts +3 -1
  31. package/dist/esm/api/bluetooth/web/WebBluetoothTransport.js +20 -35
  32. package/dist/esm/neurosity.mjs +2590 -105
  33. package/dist/examples/neurosity.iife.js +2590 -105
  34. package/dist/examples/neurosity.js +129 -113
  35. package/dist/examples/neurosity.mjs +2590 -105
  36. package/package.json +2 -1
  37. package/dist/cjs/api/bluetooth/utils/encoding.d.ts +0 -3
  38. package/dist/cjs/api/bluetooth/utils/encoding.js +0 -22
  39. package/dist/esm/api/bluetooth/utils/encoding.d.ts +0 -3
  40. 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 { csvBufferToEpoch } from "./utils/csvBufferToEpoch";
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
- this._brainwavesRawUnfiltered$ =
65
- this._subscribeWhileAuthenticated("rawUnfiltered");
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
- const [isAuthenticated, expiresIn] = yield this.transport.readCharacteristic("auth", true);
121
- this.isAuthenticated$.next(isAuthenticated);
122
- return [isAuthenticated, expiresIn];
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(csvBufferToEpoch(deviceInfo))));
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(csvBufferToEpoch(deviceInfo))));
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 { stitchChunks } from "../utils/stitch";
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 = decode(this.type, (_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
+ 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 }) => decode(this.type, value)), stitchChunks({ delimiter: BLUETOOTH_CHUNK_DELIMITER }), map((payload) => {
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(this.type, value);
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(this.type, data);
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
  }
@@ -11,6 +11,7 @@ export declare type ActionOptions = {
11
11
  export declare type SubscribeOptions = {
12
12
  characteristicName: string;
13
13
  manageNotifications?: boolean;
14
+ skipJSONDecoding?: boolean;
14
15
  };
15
16
  /**
16
17
  * @hidden
@@ -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 { stitchChunks } from "../utils/stitch";
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
- const buffer = event.target.value;
239
- const decoded = decode(this.type, buffer);
240
- this.addLog(`Received chunk with buffer size of ${buffer.byteLength} and decoded size ${decoded.length} for ${characteristicName} characteristic: \n${decoded}`);
241
- return decoded;
242
- }), stitchChunks({ delimiter: BLUETOOTH_CHUNK_DELIMITER }), map((payload) => {
243
- var _a;
244
- try {
245
- return JSON.parse(payload);
246
- }
247
- catch (error) {
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 value = yield characteristic.readValue();
275
- const uint8Array = value;
276
- const decodedValue = decode(this.type, uint8Array);
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(this.type, data);
279
+ const encoded = this.textCodec.encode(data);
295
280
  yield characteristic.writeValueWithResponse(encoded);
296
281
  });
297
282
  }