@neurodevs/node-lsl 18.1.0 → 19.0.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 (62) hide show
  1. package/build/__tests__/AbstractPackageTest.d.ts +12 -10
  2. package/build/__tests__/AbstractPackageTest.js +33 -14
  3. package/build/__tests__/AbstractPackageTest.js.map +1 -1
  4. package/build/__tests__/impl/BleDeviceController.test.d.ts +5 -0
  5. package/build/__tests__/impl/BleDeviceController.test.js +36 -5
  6. package/build/__tests__/impl/BleDeviceController.test.js.map +1 -1
  7. package/build/__tests__/impl/LslStreamOutlet.test.js +2 -4
  8. package/build/__tests__/impl/LslStreamOutlet.test.js.map +1 -1
  9. package/build/__tests__/impl/UsbDeviceController.test.d.ts +17 -0
  10. package/build/__tests__/impl/UsbDeviceController.test.js +81 -0
  11. package/build/__tests__/impl/UsbDeviceController.test.js.map +1 -0
  12. package/build/__tests__/impl/WindowedClockRegressor.test.d.ts +19 -0
  13. package/build/__tests__/impl/WindowedClockRegressor.test.js +172 -0
  14. package/build/__tests__/impl/WindowedClockRegressor.test.js.map +1 -0
  15. package/build/impl/LslStreamInlet.d.ts +1 -0
  16. package/build/impl/LslStreamInlet.js +2 -1
  17. package/build/impl/LslStreamInlet.js.map +1 -1
  18. package/build/impl/LslStreamOutlet.d.ts +1 -0
  19. package/build/impl/LslStreamOutlet.js +2 -1
  20. package/build/impl/LslStreamOutlet.js.map +1 -1
  21. package/build/impl/WindowedClockRegressor.d.ts +24 -0
  22. package/build/impl/WindowedClockRegressor.js +73 -0
  23. package/build/impl/WindowedClockRegressor.js.map +1 -0
  24. package/build/impl/controllers/BleDeviceController.d.ts +2 -0
  25. package/build/impl/controllers/BleDeviceController.js +8 -0
  26. package/build/impl/controllers/BleDeviceController.js.map +1 -1
  27. package/build/impl/controllers/UsbDeviceController.d.ts +23 -0
  28. package/build/impl/controllers/UsbDeviceController.js +40 -0
  29. package/build/impl/controllers/UsbDeviceController.js.map +1 -0
  30. package/build/index.d.ts +8 -0
  31. package/build/index.js +10 -0
  32. package/build/index.js.map +1 -1
  33. package/build/scripts/runUsb.d.ts +1 -0
  34. package/build/scripts/runUsb.js +14 -0
  35. package/build/scripts/runUsb.js.map +1 -0
  36. package/build/testDoubles/BleController/FakeBleController.d.ts +3 -1
  37. package/build/testDoubles/BleController/FakeBleController.js +5 -0
  38. package/build/testDoubles/BleController/FakeBleController.js.map +1 -1
  39. package/build/testDoubles/ClockRegressor/FakeClockRegressor.d.ts +17 -0
  40. package/build/testDoubles/ClockRegressor/FakeClockRegressor.js +23 -0
  41. package/build/testDoubles/ClockRegressor/FakeClockRegressor.js.map +1 -0
  42. package/build/testDoubles/UsbController/FakeUsbController.d.ts +12 -0
  43. package/build/testDoubles/UsbController/FakeUsbController.js +25 -0
  44. package/build/testDoubles/UsbController/FakeUsbController.js.map +1 -0
  45. package/build/testDoubles/WebSocketBridge/FakeWebSocketBridge.js.map +1 -1
  46. package/package.json +4 -4
  47. package/src/__tests__/AbstractPackageTest.ts +59 -21
  48. package/src/__tests__/impl/BleDeviceController.test.ts +51 -8
  49. package/src/__tests__/impl/LslStreamOutlet.test.ts +5 -4
  50. package/src/__tests__/impl/UsbDeviceController.test.ts +109 -0
  51. package/src/__tests__/impl/WindowedClockRegressor.test.ts +262 -0
  52. package/src/impl/LslStreamInlet.ts +4 -1
  53. package/src/impl/LslStreamOutlet.ts +2 -1
  54. package/src/impl/WindowedClockRegressor.ts +136 -0
  55. package/src/impl/controllers/BleDeviceController.ts +17 -0
  56. package/src/impl/controllers/UsbDeviceController.ts +65 -0
  57. package/src/index.ts +16 -0
  58. package/src/scripts/runUsb.ts +20 -0
  59. package/src/testDoubles/BleController/FakeBleController.ts +11 -0
  60. package/src/testDoubles/ClockRegressor/FakeClockRegressor.ts +46 -0
  61. package/src/testDoubles/UsbController/FakeUsbController.ts +34 -0
  62. package/src/testDoubles/WebSocketBridge/FakeWebSocketBridge.ts +1 -2
@@ -0,0 +1,262 @@
1
+ import { randomInt } from 'node:crypto'
2
+
3
+ import { test, assert } from '@neurodevs/node-tdd'
4
+
5
+ import WindowedClockRegressor, {
6
+ ClockRegressor,
7
+ } from '../../impl/WindowedClockRegressor.js'
8
+ import AbstractPackageTest from '../AbstractPackageTest.js'
9
+
10
+ export default class WindowedClockRegressorTest extends AbstractPackageTest {
11
+ private static instance: ClockRegressor
12
+
13
+ protected static readonly chunkSize = randomInt(10, 20)
14
+
15
+ private static readonly nominalHz = 50 + Math.random() * 50
16
+ private static readonly deviceTime = Math.random()
17
+ private static readonly earliestLslTime = Math.random()
18
+
19
+ protected static async beforeEach() {
20
+ await super.beforeEach()
21
+
22
+ this.instance = this.WindowedClockRegressor()
23
+ }
24
+
25
+ @test()
26
+ protected static async createsInstance() {
27
+ assert.isTruthy(this.instance, 'Failed to create instance!')
28
+ }
29
+
30
+ @test()
31
+ protected static async deriveReturnsChunkSizeEntries() {
32
+ const timestamps = this.deriveTimestamps()
33
+
34
+ assert.isEqual(
35
+ timestamps.length,
36
+ this.chunkSize,
37
+ 'Should return an array with chunkSize entries!'
38
+ )
39
+ }
40
+
41
+ @test()
42
+ protected static async deriveAnchorsLastTimestampOnRegressionLine() {
43
+ const nominalChunkStep = this.chunkSize / this.nominalHz
44
+
45
+ let deviceTime = this.deviceTime
46
+ let lslTime = this.earliestLslTime
47
+
48
+ for (let i = 0; i < 100; i++) {
49
+ deviceTime += nominalChunkStep
50
+ lslTime += nominalChunkStep
51
+ this.deriveTimestamps(deviceTime, lslTime)
52
+ }
53
+
54
+ deviceTime += nominalChunkStep
55
+ lslTime += nominalChunkStep
56
+
57
+ const jitter = nominalChunkStep * Math.random()
58
+
59
+ const timestamps = this.deriveTimestamps(deviceTime, lslTime + jitter)
60
+
61
+ for (let i = 0; i < timestamps.length; i++) {
62
+ const stepsFromLast = timestamps.length - 1 - i
63
+ const expected = lslTime - stepsFromLast / this.nominalHz
64
+
65
+ assert.isBetweenInclusive(
66
+ timestamps[i],
67
+ expected - jitter * 0.1,
68
+ expected + jitter * 0.1,
69
+ `Timestamp at index ${i} should be anchored on the fitted regression line, not the jittery arrival time!`
70
+ )
71
+ }
72
+ }
73
+
74
+ @test()
75
+ protected static async deriveSpacesEntriesByNominalHz() {
76
+ const timestamps = this.deriveTimestamps()
77
+
78
+ const expected = Array(this.chunkSize)
79
+ .fill(0)
80
+ .map((_, i) => {
81
+ return (
82
+ this.earliestLslTime -
83
+ (this.chunkSize - 1 - i) / this.nominalHz
84
+ )
85
+ })
86
+
87
+ assert.isEqualDeep(
88
+ timestamps,
89
+ expected,
90
+ 'Entries should be spaced 1/nominalHz apart, working backward from earliestLslTime!'
91
+ )
92
+ }
93
+
94
+ @test()
95
+ protected static async deriveUsesFittedSlopeOnceHistoryAccumulates() {
96
+ const trueSlope = 2
97
+ const nominalStep = this.chunkSize / this.nominalHz
98
+ const lslStep = nominalStep * trueSlope
99
+
100
+ let deviceTime = this.deviceTime
101
+ let earliestLslTime = this.earliestLslTime
102
+
103
+ for (let i = 0; i < 50; i++) {
104
+ deviceTime += nominalStep
105
+ earliestLslTime += lslStep
106
+
107
+ this.deriveTimestamps(deviceTime, earliestLslTime)
108
+ }
109
+
110
+ deviceTime += nominalStep
111
+ earliestLslTime += lslStep
112
+
113
+ const timestamps = this.deriveTimestamps(deviceTime, earliestLslTime)
114
+
115
+ const expectedSpacing = trueSlope / this.nominalHz
116
+
117
+ for (let i = 1; i < timestamps.length; i++) {
118
+ const actualSpacing = timestamps[i] - timestamps[i - 1]
119
+
120
+ assert.isBetweenInclusive(
121
+ actualSpacing,
122
+ expectedSpacing * 0.99,
123
+ expectedSpacing * 1.01,
124
+ `Spacing at index ${i} should reflect the fitted slope once history has accumulated!`
125
+ )
126
+ }
127
+ }
128
+
129
+ @test()
130
+ protected static async deriveThrowsWhenDeviceTimeDoesNotIncreaseMonotonically() {
131
+ this.deriveTimestamps()
132
+
133
+ assert.doesThrow(
134
+ () =>
135
+ this.deriveTimestamps(
136
+ this.deviceTime,
137
+ this.earliestLslTime + Math.random()
138
+ ),
139
+ `\nDevice time (${this.deviceTime}) did not increase from the previous device time (${this.deviceTime})! Device time is expected to always increase between calls, so this likely indicates a bug in the calling code.\n`
140
+ )
141
+ }
142
+
143
+ @test()
144
+ protected static async deriveForgetsHistoryOutsideWindowLenSec() {
145
+ const nominalChunkStep = this.chunkSize / this.nominalHz
146
+
147
+ // With chunkSize in [10, 20) and nominalHz in [50, 100), nominalChunkStep
148
+ // stays within [0.1, 0.4). A fixed 700 iterations always spans at least
149
+ // 70 device-seconds — comfortably more than 2x the 30s windowLenSec.
150
+ const oldPhaseCount = 700
151
+ const newPhaseCount = 700
152
+
153
+ let deviceTime = this.deviceTime
154
+ let earliestLslTime = this.earliestLslTime
155
+
156
+ const oldSlope = 2
157
+
158
+ for (let i = 0; i < oldPhaseCount; i++) {
159
+ deviceTime += nominalChunkStep
160
+ earliestLslTime += nominalChunkStep * oldSlope
161
+
162
+ this.deriveTimestamps(deviceTime, earliestLslTime)
163
+ }
164
+
165
+ const newSlope = 5
166
+ let timestamps: number[] = []
167
+
168
+ for (let i = 0; i < newPhaseCount; i++) {
169
+ deviceTime += nominalChunkStep
170
+ earliestLslTime += nominalChunkStep * newSlope
171
+
172
+ timestamps = this.deriveTimestamps(deviceTime, earliestLslTime)
173
+ }
174
+
175
+ const expectedSpacing = newSlope / this.nominalHz
176
+
177
+ for (let i = 1; i < timestamps.length; i++) {
178
+ const actualSpacing = timestamps[i] - timestamps[i - 1]
179
+
180
+ assert.isBetweenInclusive(
181
+ actualSpacing,
182
+ expectedSpacing * 0.99,
183
+ expectedSpacing * 1.01,
184
+ `Spacing at index ${i} should reflect the new slope once old history falls outside windowLenSec!`
185
+ )
186
+ }
187
+ }
188
+
189
+ @test()
190
+ protected static async deriveAllowsOverridingWindowLenSec() {
191
+ const nominalChunkStep = this.chunkSize / this.nominalHz
192
+ const customWindowLenSec = 5
193
+
194
+ // With nominalChunkStep in [0.1, 0.4), 150 iterations always spans at
195
+ // least 15 device-seconds — more than 2x the 5s customWindowLenSec,
196
+ // but well short of 2x the 30s default, so this only passes if the
197
+ // override actually takes effect.
198
+ const phaseCount = 150
199
+
200
+ const instance = WindowedClockRegressor.Create(this.nominalHz, {
201
+ windowLenSec: customWindowLenSec,
202
+ })
203
+
204
+ let deviceTime = this.deviceTime
205
+ let earliestLslTime = this.earliestLslTime
206
+
207
+ const oldSlope = 2
208
+
209
+ for (let i = 0; i < phaseCount; i++) {
210
+ deviceTime += nominalChunkStep
211
+ earliestLslTime += nominalChunkStep * oldSlope
212
+
213
+ instance.deriveTimestamps(
214
+ deviceTime,
215
+ earliestLslTime,
216
+ this.chunkSize
217
+ )
218
+ }
219
+
220
+ const newSlope = 5
221
+ let timestamps: number[] = []
222
+
223
+ for (let i = 0; i < phaseCount; i++) {
224
+ deviceTime += nominalChunkStep
225
+ earliestLslTime += nominalChunkStep * newSlope
226
+
227
+ timestamps = instance.deriveTimestamps(
228
+ deviceTime,
229
+ earliestLslTime,
230
+ this.chunkSize
231
+ )
232
+ }
233
+
234
+ const expectedSpacing = newSlope / this.nominalHz
235
+
236
+ for (let i = 1; i < timestamps.length; i++) {
237
+ const actualSpacing = timestamps[i] - timestamps[i - 1]
238
+
239
+ assert.isBetweenInclusive(
240
+ actualSpacing,
241
+ expectedSpacing * 0.99,
242
+ expectedSpacing * 1.01,
243
+ `Spacing at index ${i} should reflect the new slope once old history falls outside the custom windowLenSec!`
244
+ )
245
+ }
246
+ }
247
+
248
+ private static deriveTimestamps(
249
+ deviceTime = this.deviceTime,
250
+ earliestLslTime = this.earliestLslTime
251
+ ) {
252
+ return this.instance.deriveTimestamps(
253
+ deviceTime,
254
+ earliestLslTime,
255
+ this.chunkSize
256
+ )
257
+ }
258
+
259
+ private static WindowedClockRegressor() {
260
+ return WindowedClockRegressor.Create(this.nominalHz)
261
+ }
262
+ }
@@ -4,6 +4,7 @@ export default class LslStreamInlet implements StreamInlet {
4
4
  public static Class?: StreamInletConstructor
5
5
  public static Worker = Worker
6
6
  public static waitAfterOpenStreamMs = 100
7
+ public static setTimeout = setTimeout
7
8
 
8
9
  private sourceId: string
9
10
  private chunkSize: number
@@ -91,7 +92,9 @@ export default class LslStreamInlet implements StreamInlet {
91
92
  this.postCreate()
92
93
 
93
94
  while (!this.workerReady) {
94
- await new Promise((resolve) => setTimeout(resolve, 10))
95
+ await new Promise((resolve) =>
96
+ LslStreamInlet.setTimeout(resolve, 10)
97
+ )
95
98
  }
96
99
 
97
100
  this.postStartPulling()
@@ -13,6 +13,7 @@ import {
13
13
  export default class LslStreamOutlet implements StreamOutlet {
14
14
  public static Class?: StreamOutletConstructor
15
15
  public static Worker = Worker
16
+ public static setTimeout = setTimeout
16
17
 
17
18
  public readonly name: string
18
19
  public readonly type: string
@@ -153,7 +154,7 @@ export default class LslStreamOutlet implements StreamOutlet {
153
154
 
154
155
  private static async waitForSetup(waitAfterConstructionMs: number) {
155
156
  await new Promise((resolve) =>
156
- setTimeout(resolve, waitAfterConstructionMs)
157
+ LslStreamOutlet.setTimeout(resolve, waitAfterConstructionMs)
157
158
  )
158
159
  }
159
160
  }
@@ -0,0 +1,136 @@
1
+ export default class WindowedClockRegressor implements ClockRegressor {
2
+ public static Class?: ClockRegressorConstructor
3
+
4
+ private readonly nominalHz: number
5
+ private readonly windowLenSec: number
6
+ private readonly defaultWindowLenSec = 30
7
+ private readonly history: {
8
+ deviceTime: number
9
+ earliestLslTime: number
10
+ }[] = []
11
+
12
+ private slope = 1 // lsl-host-seconds per device-second
13
+
14
+ protected constructor(
15
+ nominalHz: number,
16
+ options?: WindowedClockRegressorOptions
17
+ ) {
18
+ const { windowLenSec = this.defaultWindowLenSec } = options ?? {}
19
+
20
+ this.nominalHz = nominalHz
21
+ this.windowLenSec = windowLenSec
22
+ }
23
+
24
+ public static Create(
25
+ nominalHz: number,
26
+ options?: WindowedClockRegressorOptions
27
+ ) {
28
+ return new (this.Class ?? this)(nominalHz, options)
29
+ }
30
+
31
+ public deriveTimestamps(
32
+ deviceTime: number,
33
+ earliestLslTime: number,
34
+ chunkSize: number
35
+ ) {
36
+ this.throwIfDeviceTimeDoesNotIncrease(deviceTime)
37
+
38
+ this.history.push({ deviceTime, earliestLslTime })
39
+ this.pruneHistoryOutsideWindow()
40
+ this.refitSlope()
41
+
42
+ const anchor = this.anchorTimeFor(deviceTime)
43
+
44
+ const timestamps = Array(chunkSize).fill(0)
45
+
46
+ for (let i = 0; i <= chunkSize - 1; i++) {
47
+ const stepsFromLast = chunkSize - 1 - i
48
+ const deviceTimeOffset = -stepsFromLast / this.nominalHz
49
+ const lslTimeOffset = deviceTimeOffset * this.slope
50
+
51
+ timestamps[i] = anchor + lslTimeOffset
52
+ }
53
+
54
+ return timestamps
55
+ }
56
+
57
+ private anchorTimeFor(deviceTime: number) {
58
+ const xMean = this.currentMeanDeviceTime
59
+ const yMean = this.currentMeanLslTime
60
+
61
+ return yMean + this.slope * (deviceTime - xMean)
62
+ }
63
+
64
+ private throwIfDeviceTimeDoesNotIncrease(deviceTime: number) {
65
+ const last = this.history[this.history.length - 1]
66
+
67
+ if (last && deviceTime <= last.deviceTime) {
68
+ throw new Error(
69
+ `\nDevice time (${deviceTime}) did not increase from the previous device time (${last.deviceTime})! Device time is expected to always increase between calls, so this likely indicates a bug in the calling code.\n`
70
+ )
71
+ }
72
+ }
73
+
74
+ private pruneHistoryOutsideWindow() {
75
+ const { deviceTime: latest } = this.history[this.history.length - 1]
76
+ const cutoff = latest - this.windowLenSec
77
+
78
+ while (this.history.length > 0 && this.history[0].deviceTime < cutoff) {
79
+ this.history.shift()
80
+ }
81
+ }
82
+
83
+ private refitSlope() {
84
+ if (this.history.length < 2) {
85
+ return
86
+ }
87
+
88
+ const xMean = this.currentMeanDeviceTime
89
+ const yMean = this.currentMeanLslTime
90
+
91
+ let numerator = 0
92
+ let denominator = 0
93
+
94
+ for (const { deviceTime, earliestLslTime } of this.history) {
95
+ const xCentered = deviceTime - xMean
96
+
97
+ numerator += xCentered * (earliestLslTime - yMean)
98
+ denominator += xCentered * xCentered
99
+ }
100
+
101
+ this.slope = numerator / denominator
102
+ }
103
+
104
+ private get currentMeanDeviceTime() {
105
+ const totalDeviceTime = this.history.reduce(
106
+ (sum, { deviceTime }) => sum + deviceTime,
107
+ 0
108
+ )
109
+ return totalDeviceTime / this.history.length
110
+ }
111
+
112
+ private get currentMeanLslTime() {
113
+ const totalLslTime = this.history.reduce(
114
+ (sum, { earliestLslTime }) => sum + earliestLslTime,
115
+ 0
116
+ )
117
+ return totalLslTime / this.history.length
118
+ }
119
+ }
120
+
121
+ export interface ClockRegressor {
122
+ deriveTimestamps(
123
+ deviceTime: number,
124
+ earliestLslTime: number,
125
+ chunkSize: number
126
+ ): number[]
127
+ }
128
+
129
+ export type ClockRegressorConstructor = new (
130
+ nominalHz: number,
131
+ options?: WindowedClockRegressorOptions
132
+ ) => ClockRegressor
133
+
134
+ export interface WindowedClockRegressorOptions {
135
+ windowLenSec?: number
136
+ }
@@ -165,6 +165,19 @@ export default class BleDeviceController implements BleController {
165
165
  this.throwIfError(status, error)
166
166
  }
167
167
 
168
+ public async subscribeCharacteristics(
169
+ charCallbacks: CharacteristicCallbacks
170
+ ) {
171
+ const { status, error } = this.ndx.addBleCharCallbacks({
172
+ deviceUuid: this.uuid,
173
+ charCallbacks,
174
+ })
175
+
176
+ this.throwIfError(status, error)
177
+
178
+ this.charCallbacks = [...this.charCallbacks, ...charCallbacks]
179
+ }
180
+
168
181
  public async disconnect() {
169
182
  const { status, error } = this.ndx.stopBleBackend({
170
183
  deviceUuid: this.uuid,
@@ -193,6 +206,10 @@ export interface BleController {
193
206
 
194
207
  connect(): Promise<void>
195
208
 
209
+ subscribeCharacteristics(
210
+ charCallbacks: CharacteristicCallbacks
211
+ ): Promise<void>
212
+
196
213
  writeCharacteristic(
197
214
  charUuid: CharacteristicUuid,
198
215
  value: string
@@ -0,0 +1,65 @@
1
+ import { LibndxAdapter } from '@neurodevs/ndx-native'
2
+
3
+ export default class UsbDeviceController implements UsbController {
4
+ public static Class?: UsbControllerConstructor
5
+
6
+ private onData: (data: Buffer, length: number, timestampSec: number) => void
7
+ private serialNumber: string
8
+
9
+ private ndx = LibndxAdapter.getInstance()
10
+
11
+ protected constructor(options: UsbControllerOptions) {
12
+ const { onData, serialNumber } = options ?? {}
13
+
14
+ this.onData = onData
15
+ this.serialNumber = serialNumber ?? ''
16
+ }
17
+
18
+ public static Create(options: UsbControllerOptions) {
19
+ return new (this.Class ?? this)(options)
20
+ }
21
+
22
+ public async connect() {
23
+ this.ndx.createUsbBackend(this.usbControllerOptions)
24
+ this.ndx.startUsbBackend(this.startUsbControllerOptions)
25
+ }
26
+
27
+ private get usbControllerOptions() {
28
+ return {
29
+ serialNumber: this.serialNumber,
30
+ }
31
+ }
32
+
33
+ private get startUsbControllerOptions() {
34
+ return {
35
+ ...this.usbControllerOptions,
36
+ onData: this.onData,
37
+ }
38
+ }
39
+
40
+ public async writeUsb(value: string) {
41
+ this.ndx.writeUsbBackend({
42
+ ...this.usbControllerOptions,
43
+ value,
44
+ })
45
+ }
46
+
47
+ public async disconnect() {
48
+ this.ndx.stopUsbBackend(this.usbControllerOptions)
49
+ }
50
+ }
51
+
52
+ export interface UsbController {
53
+ connect(): Promise<void>
54
+ writeUsb(value: string): Promise<void>
55
+ disconnect(): Promise<void>
56
+ }
57
+
58
+ export interface UsbControllerOptions {
59
+ onData: (data: Buffer, length: number, timestampSec: number) => void
60
+ serialNumber?: string
61
+ }
62
+
63
+ export type UsbControllerConstructor = new (
64
+ options?: UsbControllerOptions
65
+ ) => UsbController
package/src/index.ts CHANGED
@@ -1,3 +1,11 @@
1
+ // ClockRegressor
2
+
3
+ export { default as WindowedClockRegressor } from './impl/WindowedClockRegressor.js'
4
+ export * from './impl/WindowedClockRegressor.js'
5
+
6
+ export { default as FakeClockRegressor } from './testDoubles/ClockRegressor/FakeClockRegressor.js'
7
+ export * from './testDoubles/ClockRegressor/FakeClockRegressor.js'
8
+
1
9
  // DeviceController
2
10
 
3
11
  export { default as BleDeviceController } from './impl/controllers/BleDeviceController.js'
@@ -50,6 +58,14 @@ export * from './impl/LslStreamOutlet.js'
50
58
  export { default as FakeStreamOutlet } from './testDoubles/StreamOutlet/FakeStreamOutlet.js'
51
59
  export * from './testDoubles/StreamOutlet/FakeStreamOutlet.js'
52
60
 
61
+ // UsbController
62
+
63
+ export { default as UsbDeviceController } from './impl/controllers/UsbDeviceController.js'
64
+ export * from './impl/controllers/UsbDeviceController.js'
65
+
66
+ export { default as FakeUsbController } from './testDoubles/UsbController/FakeUsbController.js'
67
+ export * from './testDoubles/UsbController/FakeUsbController.js'
68
+
53
69
  // WebSocketBridge
54
70
 
55
71
  export { default as LslWebSocketBridge } from './impl/LslWebSocketBridge.js'
@@ -0,0 +1,20 @@
1
+ import UsbDeviceController from '../impl/controllers/UsbDeviceController.js'
2
+
3
+ console.info('\nCreating UsbDeviceController...')
4
+
5
+ const controller = UsbDeviceController.Create({
6
+ serialNumber: 'DP04WG8J',
7
+ onData: (_data: Buffer, length: number, timestampSec: number) => {
8
+ console.info(`[${timestampSec}] Buffer length: ${length}`)
9
+ },
10
+ })
11
+
12
+ console.info('Connecting to UsbDeviceController...')
13
+
14
+ await controller.connect()
15
+
16
+ console.info('Waiting for 10 seconds...')
17
+
18
+ await new Promise((resolve) => setTimeout(resolve, 10000))
19
+
20
+ console.info('Done!')
@@ -2,6 +2,7 @@ import generateId from '@neurodevs/generate-id'
2
2
  import {
3
3
  BleController,
4
4
  BleControllerOptions,
5
+ CharacteristicCallbacks,
5
6
  } from '../../impl/controllers/BleDeviceController.js'
6
7
 
7
8
  export default class FakeBleController implements BleController {
@@ -14,6 +15,9 @@ export default class FakeBleController implements BleController {
14
15
  value: string
15
16
  }[] = []
16
17
 
18
+ public static callsToSubscribeCharacteristics: CharacteristicCallbacks[] =
19
+ []
20
+
17
21
  public static numCallsToDisconnect = 0
18
22
 
19
23
  public static fakeCharacteristics: Record<string, unknown> = {}
@@ -45,6 +49,12 @@ export default class FakeBleController implements BleController {
45
49
  })
46
50
  }
47
51
 
52
+ public async subscribeCharacteristics(
53
+ charCallbacks: CharacteristicCallbacks
54
+ ) {
55
+ FakeBleController.callsToSubscribeCharacteristics.push(charCallbacks)
56
+ }
57
+
48
58
  public async disconnect() {
49
59
  FakeBleController.numCallsToDisconnect++
50
60
  }
@@ -61,6 +71,7 @@ export default class FakeBleController implements BleController {
61
71
  this.callsToConstructor = []
62
72
  this.numCallsToConnect = 0
63
73
  this.callsToWriteCharacteristic = []
74
+ this.callsToSubscribeCharacteristics = []
64
75
  this.numCallsToDisconnect = 0
65
76
  }
66
77
  }
@@ -0,0 +1,46 @@
1
+ import {
2
+ ClockRegressor,
3
+ WindowedClockRegressorOptions,
4
+ } from '../../impl/WindowedClockRegressor.js'
5
+
6
+ export default class FakeClockRegressor implements ClockRegressor {
7
+ public static callsToConstructor: {
8
+ nominalHz: number
9
+ options?: WindowedClockRegressorOptions
10
+ }[] = []
11
+ public static callsToDeriveTimestamps: {
12
+ deviceTime: number
13
+ earliestLslTime: number
14
+ chunkSize: number
15
+ }[] = []
16
+
17
+ public static fakeResultValue = 0
18
+
19
+ public readonly nominalHz: number
20
+
21
+ public constructor(
22
+ nominalHz: number,
23
+ options?: WindowedClockRegressorOptions
24
+ ) {
25
+ FakeClockRegressor.callsToConstructor.push({ nominalHz, options })
26
+ this.nominalHz = nominalHz
27
+ }
28
+
29
+ public deriveTimestamps(
30
+ deviceTime: number,
31
+ earliestLslTime: number,
32
+ chunkSize: number
33
+ ) {
34
+ FakeClockRegressor.callsToDeriveTimestamps.push({
35
+ deviceTime,
36
+ earliestLslTime,
37
+ chunkSize,
38
+ })
39
+ return Array(chunkSize).fill(FakeClockRegressor.fakeResultValue)
40
+ }
41
+
42
+ public static resetTestDouble() {
43
+ FakeClockRegressor.callsToConstructor = []
44
+ FakeClockRegressor.callsToDeriveTimestamps = []
45
+ }
46
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ UsbController,
3
+ UsbControllerOptions,
4
+ } from '../../impl/controllers/UsbDeviceController.js'
5
+
6
+ export default class FakeUsbController implements UsbController {
7
+ public static callsToConstructor: (UsbControllerOptions | undefined)[] = []
8
+ public static numCallsToConnect = 0
9
+ public static callsToWriteUsb: string[] = []
10
+ public static numCallsToDisconnect = 0
11
+
12
+ public constructor(options?: UsbControllerOptions) {
13
+ FakeUsbController.callsToConstructor.push(options)
14
+ }
15
+
16
+ public async connect() {
17
+ FakeUsbController.numCallsToConnect++
18
+ }
19
+
20
+ public async writeUsb(value: string) {
21
+ FakeUsbController.callsToWriteUsb.push(value)
22
+ }
23
+
24
+ public async disconnect() {
25
+ FakeUsbController.numCallsToDisconnect++
26
+ }
27
+
28
+ public static resetTestDouble() {
29
+ FakeUsbController.callsToConstructor.length = 0
30
+ FakeUsbController.numCallsToConnect = 0
31
+ FakeUsbController.callsToWriteUsb.length = 0
32
+ FakeUsbController.numCallsToDisconnect = 0
33
+ }
34
+ }