@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.
- package/build/__tests__/AbstractPackageTest.d.ts +12 -10
- package/build/__tests__/AbstractPackageTest.js +33 -14
- package/build/__tests__/AbstractPackageTest.js.map +1 -1
- package/build/__tests__/impl/BleDeviceController.test.d.ts +5 -0
- package/build/__tests__/impl/BleDeviceController.test.js +36 -5
- package/build/__tests__/impl/BleDeviceController.test.js.map +1 -1
- package/build/__tests__/impl/LslStreamOutlet.test.js +2 -4
- package/build/__tests__/impl/LslStreamOutlet.test.js.map +1 -1
- package/build/__tests__/impl/UsbDeviceController.test.d.ts +17 -0
- package/build/__tests__/impl/UsbDeviceController.test.js +81 -0
- package/build/__tests__/impl/UsbDeviceController.test.js.map +1 -0
- package/build/__tests__/impl/WindowedClockRegressor.test.d.ts +19 -0
- package/build/__tests__/impl/WindowedClockRegressor.test.js +172 -0
- package/build/__tests__/impl/WindowedClockRegressor.test.js.map +1 -0
- package/build/impl/LslStreamInlet.d.ts +1 -0
- package/build/impl/LslStreamInlet.js +2 -1
- package/build/impl/LslStreamInlet.js.map +1 -1
- package/build/impl/LslStreamOutlet.d.ts +1 -0
- package/build/impl/LslStreamOutlet.js +2 -1
- package/build/impl/LslStreamOutlet.js.map +1 -1
- package/build/impl/WindowedClockRegressor.d.ts +24 -0
- package/build/impl/WindowedClockRegressor.js +73 -0
- package/build/impl/WindowedClockRegressor.js.map +1 -0
- package/build/impl/controllers/BleDeviceController.d.ts +2 -0
- package/build/impl/controllers/BleDeviceController.js +8 -0
- package/build/impl/controllers/BleDeviceController.js.map +1 -1
- package/build/impl/controllers/UsbDeviceController.d.ts +23 -0
- package/build/impl/controllers/UsbDeviceController.js +40 -0
- package/build/impl/controllers/UsbDeviceController.js.map +1 -0
- package/build/index.d.ts +8 -0
- package/build/index.js +10 -0
- package/build/index.js.map +1 -1
- package/build/scripts/runUsb.d.ts +1 -0
- package/build/scripts/runUsb.js +14 -0
- package/build/scripts/runUsb.js.map +1 -0
- package/build/testDoubles/BleController/FakeBleController.d.ts +3 -1
- package/build/testDoubles/BleController/FakeBleController.js +5 -0
- package/build/testDoubles/BleController/FakeBleController.js.map +1 -1
- package/build/testDoubles/ClockRegressor/FakeClockRegressor.d.ts +17 -0
- package/build/testDoubles/ClockRegressor/FakeClockRegressor.js +23 -0
- package/build/testDoubles/ClockRegressor/FakeClockRegressor.js.map +1 -0
- package/build/testDoubles/UsbController/FakeUsbController.d.ts +12 -0
- package/build/testDoubles/UsbController/FakeUsbController.js +25 -0
- package/build/testDoubles/UsbController/FakeUsbController.js.map +1 -0
- package/build/testDoubles/WebSocketBridge/FakeWebSocketBridge.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/AbstractPackageTest.ts +59 -21
- package/src/__tests__/impl/BleDeviceController.test.ts +51 -8
- package/src/__tests__/impl/LslStreamOutlet.test.ts +5 -4
- package/src/__tests__/impl/UsbDeviceController.test.ts +109 -0
- package/src/__tests__/impl/WindowedClockRegressor.test.ts +262 -0
- package/src/impl/LslStreamInlet.ts +4 -1
- package/src/impl/LslStreamOutlet.ts +2 -1
- package/src/impl/WindowedClockRegressor.ts +136 -0
- package/src/impl/controllers/BleDeviceController.ts +17 -0
- package/src/impl/controllers/UsbDeviceController.ts +65 -0
- package/src/index.ts +16 -0
- package/src/scripts/runUsb.ts +20 -0
- package/src/testDoubles/BleController/FakeBleController.ts +11 -0
- package/src/testDoubles/ClockRegressor/FakeClockRegressor.ts +46 -0
- package/src/testDoubles/UsbController/FakeUsbController.ts +34 -0
- 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) =>
|
|
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
|
+
}
|