@neurodevs/node-lsl 18.0.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/.vscode/launch.json +54 -58
- package/.vscode/settings.json +61 -66
- package/.vscode/tasks.json +98 -129
- package/README.md +14 -12
- package/build/__tests__/AbstractPackageTest.d.ts +16 -12
- package/build/__tests__/AbstractPackageTest.js +48 -20
- package/build/__tests__/AbstractPackageTest.js.map +1 -1
- package/build/__tests__/impl/BleDeviceController.test.d.ts +59 -0
- package/build/__tests__/impl/BleDeviceController.test.js +364 -0
- package/build/__tests__/impl/BleDeviceController.test.js.map +1 -0
- package/build/__tests__/impl/LslEventMarkerEmitter.test.js +1 -1
- package/build/__tests__/impl/LslEventMarkerEmitter.test.js.map +1 -1
- package/build/__tests__/impl/LslStreamInlet.test.js +2 -2
- package/build/__tests__/impl/LslStreamInlet.test.js.map +1 -1
- package/build/__tests__/impl/LslStreamOutlet.test.js +11 -13
- package/build/__tests__/impl/LslStreamOutlet.test.js.map +1 -1
- package/build/__tests__/impl/LslWebSocketBridge.test.js +3 -3
- package/build/__tests__/impl/LslWebSocketBridge.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 +3 -2
- package/build/impl/LslStreamOutlet.js +4 -3
- package/build/impl/LslStreamOutlet.js.map +1 -1
- package/build/impl/LslWebSocketBridge.js +2 -2
- package/build/impl/LslWebSocketBridge.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 +60 -0
- package/build/impl/controllers/BleDeviceController.js +153 -0
- package/build/impl/controllers/BleDeviceController.js.map +1 -0
- 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/impl/workers/inlet/StreamInletWorker.d.ts +4 -4
- package/build/impl/workers/inlet/StreamInletWorker.js +12 -12
- package/build/impl/workers/inlet/StreamInletWorker.js.map +1 -1
- package/build/impl/workers/outlet/LslStreamOutlet.worker.d.ts +3 -3
- package/build/impl/workers/outlet/LslStreamOutlet.worker.js +7 -7
- package/build/impl/workers/outlet/LslStreamOutlet.worker.js.map +1 -1
- package/build/index.d.ts +14 -4
- package/build/index.js +17 -5
- package/build/index.js.map +1 -1
- package/build/scripts/discoverMuse.d.ts +1 -0
- package/build/scripts/discoverMuse.js +40 -0
- package/build/scripts/discoverMuse.js.map +1 -0
- package/build/scripts/runMuse.d.ts +1 -0
- package/build/scripts/runMuse.js +40 -0
- package/build/scripts/runMuse.js.map +1 -0
- 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 +26 -0
- package/build/testDoubles/BleController/FakeBleController.js +47 -0
- package/build/testDoubles/BleController/FakeBleController.js.map +1 -0
- package/build/testDoubles/BleController/SpyBleController.d.ts +14 -0
- package/build/testDoubles/BleController/SpyBleController.js +48 -0
- package/build/testDoubles/BleController/SpyBleController.js.map +1 -0
- 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/StreamOutlet/FakeStreamOutlet.d.ts +2 -2
- package/build/testDoubles/StreamOutlet/FakeStreamOutlet.js +5 -2
- package/build/testDoubles/StreamOutlet/FakeStreamOutlet.js.map +1 -1
- 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/build/testDoubles/generateRandomOutletOptions.js +1 -1
- package/build/testDoubles/generateRandomOutletOptions.js.map +1 -1
- package/eslint.config.js +3 -0
- package/package.json +19 -57
- package/prettier.config.js +3 -0
- package/src/__tests__/AbstractPackageTest.ts +86 -29
- package/src/__tests__/impl/BleDeviceController.test.ts +535 -0
- package/src/__tests__/impl/LslEventMarkerEmitter.test.ts +2 -1
- package/src/__tests__/impl/LslStreamInlet.test.ts +2 -2
- package/src/__tests__/impl/LslStreamOutlet.test.ts +15 -14
- package/src/__tests__/impl/LslWebSocketBridge.test.ts +4 -3
- 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 +5 -4
- package/src/impl/LslWebSocketBridge.ts +2 -2
- package/src/impl/WindowedClockRegressor.ts +136 -0
- package/src/impl/controllers/BleDeviceController.ts +240 -0
- package/src/impl/controllers/UsbDeviceController.ts +65 -0
- package/src/impl/workers/inlet/StreamInletWorker.ts +12 -12
- package/src/impl/workers/outlet/LslStreamOutlet.worker.ts +8 -8
- package/src/index.ts +24 -5
- package/src/scripts/discoverMuse.ts +50 -0
- package/src/scripts/runMuse.ts +50 -0
- package/src/scripts/runUsb.ts +20 -0
- package/src/testDoubles/BleController/FakeBleController.ts +81 -0
- package/src/testDoubles/BleController/SpyBleController.ts +61 -0
- package/src/testDoubles/ClockRegressor/FakeClockRegressor.ts +46 -0
- package/src/testDoubles/StreamOutlet/FakeStreamOutlet.ts +6 -3
- package/src/testDoubles/UsbController/FakeUsbController.ts +34 -0
- package/src/testDoubles/WebSocketBridge/FakeWebSocketBridge.ts +1 -2
- package/src/testDoubles/generateRandomOutletOptions.ts +2 -1
- package/tsconfig.json +23 -27
- package/.regressionproof.json +0 -7
- package/build/.spruce/settings.json +0 -15
- package/build/__tests__/impl/LslBackpressureMonitor.test.d.ts +0 -7
- package/build/__tests__/impl/LslBackpressureMonitor.test.js +0 -26
- package/build/__tests__/impl/LslBackpressureMonitor.test.js.map +0 -1
- package/build/impl/LslBackpressureMonitor.d.ts +0 -8
- package/build/impl/LslBackpressureMonitor.js +0 -8
- package/build/impl/LslBackpressureMonitor.js.map +0 -1
- package/build/testDoubles/BackpressureMonitor/FakeBackpressureMonitor.d.ts +0 -6
- package/build/testDoubles/BackpressureMonitor/FakeBackpressureMonitor.js +0 -10
- package/build/testDoubles/BackpressureMonitor/FakeBackpressureMonitor.js.map +0 -1
- package/eslint.config.mjs +0 -3
- package/src/.spruce/settings.json +0 -15
- package/src/__tests__/impl/LslBackpressureMonitor.test.ts +0 -25
- package/src/impl/LslBackpressureMonitor.ts +0 -13
- package/src/testDoubles/BackpressureMonitor/FakeBackpressureMonitor.ts +0 -13
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { test, assert } from '@neurodevs/node-tdd'
|
|
2
|
+
|
|
3
|
+
import UsbDeviceController, {
|
|
4
|
+
UsbController,
|
|
5
|
+
} from '../../impl/controllers/UsbDeviceController.js'
|
|
6
|
+
import AbstractPackageTest from '../AbstractPackageTest.js'
|
|
7
|
+
import { FakeLibndx } from '@neurodevs/ndx-native'
|
|
8
|
+
|
|
9
|
+
export default class UsbDeviceControllerTest extends AbstractPackageTest {
|
|
10
|
+
private static instance: UsbController
|
|
11
|
+
|
|
12
|
+
private static readonly serialNumber = this.generateId()
|
|
13
|
+
private static readonly valueToWrite = this.generateId()
|
|
14
|
+
|
|
15
|
+
private static readonly callsToOnData: {
|
|
16
|
+
data: Buffer
|
|
17
|
+
length: number
|
|
18
|
+
timestampSec: number
|
|
19
|
+
}[] = []
|
|
20
|
+
|
|
21
|
+
private static readonly onData = (
|
|
22
|
+
data: Buffer,
|
|
23
|
+
length: number,
|
|
24
|
+
timestampSec: number
|
|
25
|
+
) => {
|
|
26
|
+
this.callsToOnData.push({ data, length, timestampSec })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected static async beforeEach() {
|
|
30
|
+
await super.beforeEach()
|
|
31
|
+
|
|
32
|
+
this.instance = this.UsbDeviceController()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@test()
|
|
36
|
+
protected static async createsInstance() {
|
|
37
|
+
assert.isTruthy(this.instance, 'Failed to create instance!')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@test()
|
|
41
|
+
protected static async connectCallsLibndxCreateUsbBackend() {
|
|
42
|
+
await this.connect()
|
|
43
|
+
|
|
44
|
+
assert.isEqualDeep(
|
|
45
|
+
FakeLibndx.callsToCreateUsbBackend[0],
|
|
46
|
+
{
|
|
47
|
+
serialNumber: this.serialNumber,
|
|
48
|
+
},
|
|
49
|
+
'Did not call create_usb_backend!'
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@test()
|
|
54
|
+
protected static async connectCallsLibndxStartUsbBackend() {
|
|
55
|
+
await this.connect()
|
|
56
|
+
|
|
57
|
+
assert.isEqualDeep(
|
|
58
|
+
FakeLibndx.callsToStartUsbBackend[0],
|
|
59
|
+
{
|
|
60
|
+
serialNumber: this.serialNumber,
|
|
61
|
+
onData: this.onData,
|
|
62
|
+
},
|
|
63
|
+
'Did not call start_usb_backend!'
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@test()
|
|
68
|
+
protected static async disconnectCallsLibndxStopUsbBackend() {
|
|
69
|
+
await this.connect()
|
|
70
|
+
await this.disconnect()
|
|
71
|
+
|
|
72
|
+
assert.isEqualDeep(
|
|
73
|
+
FakeLibndx.callsToStopUsbBackend[0],
|
|
74
|
+
{
|
|
75
|
+
serialNumber: this.serialNumber,
|
|
76
|
+
},
|
|
77
|
+
'Did not call stop_usb_backend!'
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@test()
|
|
82
|
+
protected static async writeUsbCallsLibndxWriteUsbBackend() {
|
|
83
|
+
await this.instance.writeUsb(this.valueToWrite)
|
|
84
|
+
|
|
85
|
+
assert.isEqualDeep(
|
|
86
|
+
FakeLibndx.callsToWriteUsbBackend[0],
|
|
87
|
+
{
|
|
88
|
+
serialNumber: this.serialNumber,
|
|
89
|
+
value: this.valueToWrite,
|
|
90
|
+
},
|
|
91
|
+
'Did not call write_usb_backend!'
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private static async connect() {
|
|
96
|
+
await this.instance.connect()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private static async disconnect() {
|
|
100
|
+
await this.instance.disconnect()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private static UsbDeviceController() {
|
|
104
|
+
return UsbDeviceController.Create({
|
|
105
|
+
onData: this.onData,
|
|
106
|
+
serialNumber: this.serialNumber,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -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
|
|
@@ -131,12 +132,12 @@ export default class LslStreamOutlet implements StreamOutlet {
|
|
|
131
132
|
string: 'pushSampleStringTimestamp',
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
public pushSample(sample: LslSample,
|
|
135
|
+
public pushSample(sample: LslSample, timestampSec?: number) {
|
|
135
136
|
this.worker.postMessage({
|
|
136
137
|
type: 'pushSample',
|
|
137
138
|
payload: {
|
|
138
139
|
sample,
|
|
139
|
-
|
|
140
|
+
timestampSec,
|
|
140
141
|
},
|
|
141
142
|
})
|
|
142
143
|
}
|
|
@@ -153,13 +154,13 @@ 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
|
}
|
|
160
161
|
|
|
161
162
|
export interface StreamOutlet {
|
|
162
|
-
pushSample(sample: LslSample,
|
|
163
|
+
pushSample(sample: LslSample, timestampSec?: number): void
|
|
163
164
|
destroy(): void
|
|
164
165
|
readonly name: string
|
|
165
166
|
readonly type: string
|
|
@@ -66,7 +66,7 @@ export default class LslWebSocketBridge implements WebSocketBridge {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
private readonly cannotActivateMessage = `\n\
|
|
69
|
+
private readonly cannotActivateMessage = `\n\nCannot activate bridge after destroying it! \n\nPlease create and activate a new instance. \n`
|
|
70
70
|
|
|
71
71
|
private async startPullingData() {
|
|
72
72
|
await this.inlet.startPulling()
|
|
@@ -77,7 +77,7 @@ export default class LslWebSocketBridge implements WebSocketBridge {
|
|
|
77
77
|
this.stopPullingData()
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
private readonly cannotDeactivateMessage = `\n\
|
|
80
|
+
private readonly cannotDeactivateMessage = `\n\nCannot deactivate bridge after destroying it! \n\nPlease create and activate a new instance. \n`
|
|
81
81
|
|
|
82
82
|
private stopPullingData() {
|
|
83
83
|
this.inlet.stopPulling()
|
|
@@ -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
|
+
}
|