@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>;
@@ -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 csvBufferToEpoch_1 = require("./utils/csvBufferToEpoch");
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
- this._brainwavesRawUnfiltered$ =
68
- this._subscribeWhileAuthenticated("rawUnfiltered");
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
- const [isAuthenticated, expiresIn] = yield this.transport.readCharacteristic("auth", true);
124
- this.isAuthenticated$.next(isAuthenticated);
125
- return [isAuthenticated, expiresIn];
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, csvBufferToEpoch_1.csvBufferToEpoch)(deviceInfo))));
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, csvBufferToEpoch_1.csvBufferToEpoch)(deviceInfo))));
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 stitch_1 = require("../utils/stitch");
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 = (0, encoding_1.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
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 }) => (0, encoding_1.decode)(this.type, value)), (0, stitch_1.stitchChunks)({ delimiter: ipk_2.BLUETOOTH_CHUNK_DELIMITER }), (0, operators_1.map)((payload) => {
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 = (0, encoding_1.decode)(this.type, value);
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 = (0, encoding_1.encode)(this.type, data);
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
  }
@@ -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,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 stitch_1 = require("../utils/stitch");
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
- const buffer = event.target.value;
242
- const decoded = (0, encoding_1.decode)(this.type, buffer);
243
- this.addLog(`Received chunk with buffer size of ${buffer.byteLength} and decoded size ${decoded.length} for ${characteristicName} characteristic: \n${decoded}`);
244
- return decoded;
245
- }), (0, stitch_1.stitchChunks)({ delimiter: ipk_2.BLUETOOTH_CHUNK_DELIMITER }), (0, operators_1.map)((payload) => {
246
- var _a;
247
- try {
248
- return JSON.parse(payload);
249
- }
250
- catch (error) {
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 value = yield characteristic.readValue();
278
- const uint8Array = value;
279
- const decodedValue = (0, encoding_1.decode)(this.type, uint8Array);
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 = (0, encoding_1.encode)(this.type, data);
282
+ const encoded = this.textCodec.encode(data);
298
283
  yield characteristic.writeValueWithResponse(encoded);
299
284
  });
300
285
  }