@neurosity/sdk 6.4.1 → 6.5.1

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 (44) hide show
  1. package/dist/browser/neurosity.iife.js +2598 -107
  2. package/dist/browser/neurosity.js +129 -113
  3. package/dist/browser/neurosity.js.map +1 -1
  4. package/dist/cjs/Neurosity.js +5 -1
  5. package/dist/cjs/api/bluetooth/BluetoothClient.d.ts +1 -1
  6. package/dist/cjs/api/bluetooth/BluetoothClient.js +10 -8
  7. package/dist/cjs/api/bluetooth/react-native/ReactNativeTransport.d.ts +3 -1
  8. package/dist/cjs/api/bluetooth/react-native/ReactNativeTransport.js +17 -17
  9. package/dist/cjs/api/bluetooth/types/index.d.ts +1 -0
  10. package/dist/cjs/api/bluetooth/utils/binaryBufferToEpoch.d.ts +51 -0
  11. package/dist/cjs/api/bluetooth/utils/binaryBufferToEpoch.js +116 -0
  12. package/dist/cjs/api/bluetooth/utils/decodeJSONChunks.d.ts +15 -0
  13. package/dist/cjs/api/bluetooth/utils/decodeJSONChunks.js +25 -0
  14. package/dist/cjs/api/bluetooth/utils/textCodec.d.ts +17 -0
  15. package/dist/cjs/api/bluetooth/utils/textCodec.js +40 -0
  16. package/dist/cjs/api/bluetooth/web/WebBluetoothTransport.d.ts +3 -1
  17. package/dist/cjs/api/bluetooth/web/WebBluetoothTransport.js +19 -34
  18. package/dist/cjs/utils/heartbeat.js +14 -4
  19. package/dist/electron/index.js +17 -15
  20. package/dist/electron/index.js.map +1 -1
  21. package/dist/esm/Neurosity.js +6 -2
  22. package/dist/esm/api/bluetooth/BluetoothClient.d.ts +1 -1
  23. package/dist/esm/api/bluetooth/BluetoothClient.js +10 -8
  24. package/dist/esm/api/bluetooth/react-native/ReactNativeTransport.d.ts +3 -1
  25. package/dist/esm/api/bluetooth/react-native/ReactNativeTransport.js +18 -18
  26. package/dist/esm/api/bluetooth/types/index.d.ts +1 -0
  27. package/dist/esm/api/bluetooth/utils/binaryBufferToEpoch.d.ts +51 -0
  28. package/dist/esm/api/bluetooth/utils/binaryBufferToEpoch.js +109 -0
  29. package/dist/esm/api/bluetooth/utils/decodeJSONChunks.d.ts +15 -0
  30. package/dist/esm/api/bluetooth/utils/decodeJSONChunks.js +21 -0
  31. package/dist/esm/api/bluetooth/utils/textCodec.d.ts +17 -0
  32. package/dist/esm/api/bluetooth/utils/textCodec.js +36 -0
  33. package/dist/esm/api/bluetooth/web/WebBluetoothTransport.d.ts +3 -1
  34. package/dist/esm/api/bluetooth/web/WebBluetoothTransport.js +20 -35
  35. package/dist/esm/neurosity.mjs +2598 -107
  36. package/dist/esm/utils/heartbeat.js +14 -4
  37. package/dist/examples/neurosity.iife.js +2598 -107
  38. package/dist/examples/neurosity.js +129 -113
  39. package/dist/examples/neurosity.mjs +2598 -107
  40. package/package.json +2 -1
  41. package/dist/cjs/api/bluetooth/utils/encoding.d.ts +0 -3
  42. package/dist/cjs/api/bluetooth/utils/encoding.js +0 -22
  43. package/dist/esm/api/bluetooth/utils/encoding.d.ts +0 -3
  44. package/dist/esm/api/bluetooth/utils/encoding.js +0 -17
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { combineLatest, of, throwError } from "rxjs";
11
- import { ReplaySubject, firstValueFrom, EMPTY } from "rxjs";
11
+ import { ReplaySubject, firstValueFrom } from "rxjs";
12
12
  import { map, startWith, switchMap } from "rxjs/operators";
13
13
  import { distinctUntilChanged } from "rxjs/operators";
14
14
  import isEqual from "fast-deep-equal";
@@ -132,7 +132,11 @@ export class Neurosity {
132
132
  osHasBluetoothSupport: this._osHasBluetoothSupport()
133
133
  }).pipe(switchMap(({ selectedDevice, osHasBluetoothSupport }) => {
134
134
  if (!selectedDevice) {
135
- return EMPTY;
135
+ return of({
136
+ connected: false,
137
+ streamingMode,
138
+ activeMode: STREAMING_TYPE.WIFI
139
+ });
136
140
  }
137
141
  const isUnableToUseBluetooth = this.isMissingBluetoothTransport || !osHasBluetoothSupport;
138
142
  if (isUnableToUseBluetooth) {
@@ -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");
@@ -184,10 +185,11 @@ export class BluetoothClient {
184
185
  return yield getter();
185
186
  });
186
187
  }
187
- _subscribeWhileAuthenticated(characteristicName) {
188
+ _subscribeWhileAuthenticated(characteristicName, skipJSONDecoding = false) {
188
189
  return this.osHasBluetoothSupport$.pipe(switchMap((osHasBluetoothSupport) => osHasBluetoothSupport ? this.isAuthenticated$ : EMPTY), distinctUntilChanged(), switchMap((isAuthenticated) => isAuthenticated
189
190
  ? this.transport.subscribeToCharacteristic({
190
- characteristicName
191
+ characteristicName,
192
+ skipJSONDecoding
191
193
  })
192
194
  : EMPTY), share());
193
195
  }
@@ -204,9 +206,9 @@ export class BluetoothClient {
204
206
  switch (label) {
205
207
  default:
206
208
  case "raw":
207
- 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))));
208
210
  case "rawUnfiltered":
209
- 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))));
210
212
  case "psd":
211
213
  return this._brainwavesPSD$;
212
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
  }