@neurodevs/node-lsl 12.0.5 → 13.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/README.md +2 -5
- package/build/__tests__/AbstractPackageTest.d.ts +9 -3
- package/build/__tests__/AbstractPackageTest.js +37 -3
- package/build/__tests__/AbstractPackageTest.js.map +1 -1
- package/build/__tests__/impl/LiblslAdapter.test.d.ts +6 -0
- package/build/__tests__/impl/LiblslAdapter.test.js +133 -12
- package/build/__tests__/impl/LiblslAdapter.test.js.map +1 -1
- package/build/__tests__/impl/LslEventMarkerOutlet.test.js +5 -5
- package/build/__tests__/impl/LslEventMarkerOutlet.test.js.map +1 -1
- package/build/__tests__/impl/LslStreamInfo.test.js +3 -3
- package/build/__tests__/impl/LslStreamInfo.test.js.map +1 -1
- package/build/__tests__/impl/LslStreamInlet.test.d.ts +30 -2
- package/build/__tests__/impl/LslStreamInlet.test.js +181 -18
- package/build/__tests__/impl/LslStreamInlet.test.js.map +1 -1
- package/build/__tests__/impl/LslStreamOutlet.test.d.ts +16 -7
- package/build/__tests__/impl/LslStreamOutlet.test.js +84 -23
- package/build/__tests__/impl/LslStreamOutlet.test.js.map +1 -1
- package/build/__tests__/impl/LslWebSocketBridge.test.d.ts +15 -0
- package/build/__tests__/impl/LslWebSocketBridge.test.js +112 -2
- package/build/__tests__/impl/LslWebSocketBridge.test.js.map +1 -1
- package/build/assertions.d.ts +3 -3
- package/build/assertions.js +6 -6
- package/build/assertions.js.map +1 -1
- package/build/handleError.d.ts +8 -0
- package/build/handleError.js +25 -0
- package/build/handleError.js.map +1 -0
- package/build/impl/LiblslAdapter.d.ts +128 -13
- package/build/impl/LiblslAdapter.js +149 -71
- package/build/impl/LiblslAdapter.js.map +1 -1
- package/build/impl/LslEventMarkerOutlet.d.ts +4 -1
- package/build/impl/LslEventMarkerOutlet.js +3 -3
- package/build/impl/LslEventMarkerOutlet.js.map +1 -1
- package/build/impl/LslStreamInfo.d.ts +3 -3
- package/build/impl/LslStreamInfo.js +5 -5
- package/build/impl/LslStreamInfo.js.map +1 -1
- package/build/impl/LslStreamInlet.d.ts +52 -14
- package/build/impl/LslStreamInlet.js +157 -20
- package/build/impl/LslStreamInlet.js.map +1 -1
- package/build/impl/LslStreamOutlet.d.ts +13 -13
- package/build/impl/LslStreamOutlet.js +22 -21
- package/build/impl/LslStreamOutlet.js.map +1 -1
- package/build/impl/LslWebSocketBridge.d.ts +35 -3
- package/build/impl/LslWebSocketBridge.js +66 -3
- package/build/impl/LslWebSocketBridge.js.map +1 -1
- package/build/index.d.ts +0 -1
- package/build/index.js +0 -1
- package/build/index.js.map +1 -1
- package/build/scripts/runLslStreamOutlet.js +2 -3
- package/build/scripts/runLslStreamOutlet.js.map +1 -1
- package/build/scripts/runOutletToInlet.d.ts +1 -0
- package/build/scripts/runOutletToInlet.js +45 -0
- package/build/scripts/runOutletToInlet.js.map +1 -0
- package/build/testDoubles/EventMarkerOutlet/FakeEventMarkerOutlet.d.ts +1 -2
- package/build/testDoubles/EventMarkerOutlet/FakeEventMarkerOutlet.js.map +1 -1
- package/build/testDoubles/EventMarkerOutlet/MockEventMarkerOutlet.d.ts +2 -2
- package/build/testDoubles/EventMarkerOutlet/MockEventMarkerOutlet.js.map +1 -1
- package/build/testDoubles/EventMarkerOutlet/ThrowingEventMarkerOutlet.d.ts +1 -2
- package/build/testDoubles/EventMarkerOutlet/ThrowingEventMarkerOutlet.js.map +1 -1
- package/build/testDoubles/Liblsl/FakeLiblsl.d.ts +15 -3
- package/build/testDoubles/Liblsl/FakeLiblsl.js +42 -0
- package/build/testDoubles/Liblsl/FakeLiblsl.js.map +1 -1
- package/build/testDoubles/StreamInfo/FakeStreamInfo.d.ts +1 -1
- package/build/testDoubles/StreamInfo/SpyStreamInfo.d.ts +1 -1
- package/build/testDoubles/StreamInlet/FakeStreamInlet.d.ts +12 -3
- package/build/testDoubles/StreamInlet/FakeStreamInlet.js +25 -5
- package/build/testDoubles/StreamInlet/FakeStreamInlet.js.map +1 -1
- package/build/testDoubles/StreamInlet/SpyStreamInlet.d.ts +3 -3
- package/build/testDoubles/StreamInlet/SpyStreamInlet.js +2 -2
- package/build/testDoubles/StreamInlet/SpyStreamInlet.js.map +1 -1
- package/build/testDoubles/StreamOutlet/FakeStreamOutlet.d.ts +4 -4
- package/build/testDoubles/StreamOutlet/FakeStreamOutlet.js +6 -6
- package/build/testDoubles/StreamOutlet/FakeStreamOutlet.js.map +1 -1
- package/build/testDoubles/StreamTransportBridge/FakeStreamTransportBridge.d.ts +6 -0
- package/build/testDoubles/StreamTransportBridge/FakeStreamTransportBridge.js +15 -0
- package/build/testDoubles/StreamTransportBridge/FakeStreamTransportBridge.js.map +1 -1
- package/build/testDoubles/StreamTransportBridge/SpyLslWebSocketBridge.d.ts +4 -0
- package/build/testDoubles/StreamTransportBridge/SpyLslWebSocketBridge.js +7 -0
- package/build/testDoubles/StreamTransportBridge/SpyLslWebSocketBridge.js.map +1 -0
- package/build/testDoubles/WebSockets/FakeWebSocket.d.ts +10 -0
- package/build/testDoubles/WebSockets/FakeWebSocket.js +14 -0
- package/build/testDoubles/WebSockets/FakeWebSocket.js.map +1 -0
- package/build/testDoubles/WebSockets/FakeWebSocketServer.d.ts +13 -0
- package/build/testDoubles/WebSockets/FakeWebSocketServer.js +22 -0
- package/build/testDoubles/WebSockets/FakeWebSocketServer.js.map +1 -0
- package/build/testDoubles/generateRandomOutletOptions.js +3 -3
- package/build/testDoubles/generateRandomOutletOptions.js.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/AbstractPackageTest.ts +52 -4
- package/src/__tests__/impl/LiblslAdapter.test.ts +181 -16
- package/src/__tests__/impl/LslEventMarkerOutlet.test.ts +5 -5
- package/src/__tests__/impl/LslStreamInfo.test.ts +3 -3
- package/src/__tests__/impl/LslStreamInlet.test.ts +253 -20
- package/src/__tests__/impl/LslStreamOutlet.test.ts +111 -24
- package/src/__tests__/impl/LslWebSocketBridge.test.ts +163 -5
- package/src/assertions.ts +7 -7
- package/src/handleError.ts +24 -0
- package/src/impl/LiblslAdapter.ts +322 -97
- package/src/impl/LslEventMarkerOutlet.ts +8 -4
- package/src/impl/LslStreamInfo.ts +9 -9
- package/src/impl/LslStreamInlet.ts +233 -25
- package/src/impl/LslStreamOutlet.ts +43 -33
- package/src/impl/LslWebSocketBridge.ts +115 -5
- package/src/index.ts +0 -2
- package/src/scripts/runLslStreamOutlet.ts +2 -3
- package/src/scripts/runOutletToInlet.ts +53 -0
- package/src/testDoubles/EventMarkerOutlet/FakeEventMarkerOutlet.ts +4 -2
- package/src/testDoubles/EventMarkerOutlet/MockEventMarkerOutlet.ts +5 -2
- package/src/testDoubles/EventMarkerOutlet/ThrowingEventMarkerOutlet.ts +4 -2
- package/src/testDoubles/Liblsl/FakeLiblsl.ts +61 -1
- package/src/testDoubles/StreamInfo/FakeStreamInfo.ts +1 -1
- package/src/testDoubles/StreamInlet/FakeStreamInlet.ts +36 -5
- package/src/testDoubles/StreamInlet/SpyStreamInlet.ts +7 -2
- package/src/testDoubles/StreamOutlet/FakeStreamOutlet.ts +7 -7
- package/src/testDoubles/StreamTransportBridge/FakeStreamTransportBridge.ts +18 -0
- package/src/testDoubles/StreamTransportBridge/SpyLslWebSocketBridge.ts +9 -0
- package/src/testDoubles/WebSockets/FakeWebSocket.ts +17 -0
- package/src/testDoubles/WebSockets/FakeWebSocketServer.ts +32 -0
- package/src/testDoubles/generateRandomOutletOptions.ts +3 -3
- package/build/types.d.ts +0 -95
- package/build/types.js +0 -2
- package/build/types.js.map +0 -1
- package/src/types.ts +0 -111
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import generateId from '@neurodevs/generate-id'
|
|
2
|
+
import { createPointer, DataType, JsExternal, unwrapPointer } from 'ffi-rs'
|
|
2
3
|
|
|
3
|
-
import
|
|
4
|
+
import handleError from '../handleError.js'
|
|
5
|
+
import { BoundInlet, ChannelFormat, Liblsl } from './LiblslAdapter.js'
|
|
4
6
|
import LiblslAdapter from './LiblslAdapter.js'
|
|
5
7
|
import LslStreamInfo, {
|
|
6
8
|
StreamInfo,
|
|
@@ -10,79 +12,285 @@ import LslStreamInfo, {
|
|
|
10
12
|
export default class LslStreamInlet implements StreamInlet {
|
|
11
13
|
public static Class?: StreamInletConstructor
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
public isRunning = false
|
|
16
|
+
|
|
14
17
|
protected info: StreamInfo
|
|
15
|
-
protected
|
|
18
|
+
protected name: string
|
|
19
|
+
private channelNames: string[]
|
|
20
|
+
private channelCount: number
|
|
16
21
|
private chunkSize: number
|
|
17
|
-
private
|
|
22
|
+
private maxBufferedMs: number
|
|
23
|
+
private timeoutMs: number
|
|
24
|
+
|
|
25
|
+
private onData: OnDataCallback
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
private pullDataMethod: () => {
|
|
28
|
+
samples: Float32Array | undefined
|
|
29
|
+
timestamps: Float64Array | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private lsl: Liblsl
|
|
33
|
+
|
|
34
|
+
protected inlet!: BoundInlet
|
|
35
|
+
|
|
36
|
+
private dataBuffer!: Buffer<ArrayBuffer>
|
|
37
|
+
private dataBufferPtr!: JsExternal
|
|
38
|
+
|
|
39
|
+
private timestampBuffer!: Buffer<ArrayBuffer>
|
|
40
|
+
private timestampBufferPtr!: JsExternal
|
|
41
|
+
|
|
42
|
+
private errorCodeBuffer!: Buffer<ArrayBuffer>
|
|
43
|
+
private errorCodeBufferPtr!: JsExternal
|
|
44
|
+
|
|
45
|
+
private readonly defaultName = `lsl-inlet-${generateId()}`
|
|
46
|
+
private readonly sixMinutesInMs = 360 * 1000
|
|
47
|
+
|
|
48
|
+
protected constructor(
|
|
49
|
+
info: StreamInfo,
|
|
50
|
+
options: StreamInletOptions,
|
|
51
|
+
onData: OnDataCallback
|
|
52
|
+
) {
|
|
20
53
|
const {
|
|
21
54
|
name = this.defaultName,
|
|
55
|
+
channelNames,
|
|
22
56
|
chunkSize,
|
|
23
|
-
|
|
57
|
+
maxBufferedMs,
|
|
58
|
+
timeoutMs,
|
|
24
59
|
} = options ?? {}
|
|
25
60
|
|
|
26
61
|
this.info = info
|
|
27
|
-
this.chunkSize = chunkSize
|
|
28
|
-
this.maxBuffered = maxBuffered
|
|
29
62
|
this.name = name
|
|
63
|
+
this.channelNames = channelNames
|
|
64
|
+
this.channelCount = this.channelNames.length
|
|
65
|
+
this.chunkSize = chunkSize
|
|
66
|
+
this.maxBufferedMs = maxBufferedMs ?? this.sixMinutesInMs
|
|
67
|
+
this.timeoutMs = timeoutMs ?? 0
|
|
30
68
|
|
|
31
|
-
this.
|
|
69
|
+
this.onData = onData
|
|
70
|
+
|
|
71
|
+
if (this.chunkSize === 1) {
|
|
72
|
+
this.pullDataMethod = this.pullSample
|
|
73
|
+
} else {
|
|
74
|
+
this.pullDataMethod = this.pullChunk
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.lsl = this.getLiblslSingleton()
|
|
78
|
+
|
|
79
|
+
this.createBoundInlet()
|
|
32
80
|
}
|
|
33
81
|
|
|
34
|
-
public static Create(options: StreamInletOptions) {
|
|
82
|
+
public static Create(options: StreamInletOptions, onData: OnDataCallback) {
|
|
35
83
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
36
|
-
const {
|
|
84
|
+
const { maxBufferedMs, chunkSize, ...infoOptions } = options
|
|
37
85
|
const info = this.LslStreamInfo(infoOptions)
|
|
38
|
-
return new (this.Class ?? this)(info, options)
|
|
86
|
+
return new (this.Class ?? this)(info, options, onData)
|
|
39
87
|
}
|
|
40
88
|
|
|
41
|
-
private
|
|
89
|
+
private createBoundInlet() {
|
|
42
90
|
this.inlet = this.lsl.createInlet({
|
|
43
|
-
info: this.boundStreamInfo,
|
|
91
|
+
info: this.info.boundStreamInfo,
|
|
44
92
|
chunkSize: this.chunkSize,
|
|
45
|
-
|
|
93
|
+
maxBufferedMs: this.maxBufferedMs / 1000,
|
|
46
94
|
})
|
|
47
95
|
}
|
|
48
96
|
|
|
49
|
-
|
|
50
|
-
|
|
97
|
+
public startPulling() {
|
|
98
|
+
this.isRunning = true
|
|
99
|
+
|
|
100
|
+
this.createWriteableBuffers()
|
|
101
|
+
this.createPointersToBuffers()
|
|
102
|
+
|
|
103
|
+
void this.pullOnLoop()
|
|
51
104
|
}
|
|
52
105
|
|
|
53
|
-
private
|
|
54
|
-
|
|
106
|
+
private createWriteableBuffers() {
|
|
107
|
+
this.createDataBuffer()
|
|
108
|
+
this.createTimestampBuffer()
|
|
109
|
+
this.createErrorCodeBuffer()
|
|
55
110
|
}
|
|
56
111
|
|
|
57
|
-
private
|
|
112
|
+
private createDataBuffer() {
|
|
113
|
+
const bytesPerFloat = 4
|
|
114
|
+
|
|
115
|
+
this.dataBuffer = Buffer.alloc(
|
|
116
|
+
bytesPerFloat * this.chunkSize * this.channelCount
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private createTimestampBuffer() {
|
|
121
|
+
const bytesPerDouble = 8
|
|
122
|
+
this.timestampBuffer = Buffer.alloc(bytesPerDouble * this.chunkSize)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private createErrorCodeBuffer() {
|
|
126
|
+
const bytesPerInt = 4
|
|
127
|
+
this.errorCodeBuffer = Buffer.alloc(bytesPerInt)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private createPointersToBuffers() {
|
|
131
|
+
this.createDataBufferPtr()
|
|
132
|
+
this.createTimestampBufferPtr()
|
|
133
|
+
this.createErrorCodeBufferPtr()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private createDataBufferPtr() {
|
|
137
|
+
this.dataBufferPtr = unwrapPointer(
|
|
138
|
+
createPointer({
|
|
139
|
+
paramsType: [DataType.U8Array],
|
|
140
|
+
paramsValue: [this.dataBuffer],
|
|
141
|
+
})
|
|
142
|
+
)[0]
|
|
143
|
+
}
|
|
58
144
|
|
|
59
|
-
|
|
145
|
+
private createTimestampBufferPtr() {
|
|
146
|
+
this.timestampBufferPtr = unwrapPointer(
|
|
147
|
+
createPointer({
|
|
148
|
+
paramsType: [DataType.U8Array],
|
|
149
|
+
paramsValue: [this.timestampBuffer],
|
|
150
|
+
})
|
|
151
|
+
)[0]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private createErrorCodeBufferPtr() {
|
|
155
|
+
this.errorCodeBufferPtr = unwrapPointer(
|
|
156
|
+
createPointer({
|
|
157
|
+
paramsType: [DataType.U8Array],
|
|
158
|
+
paramsValue: [this.errorCodeBuffer],
|
|
159
|
+
})
|
|
160
|
+
)[0]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async pullOnLoop() {
|
|
164
|
+
if (this.isRunning) {
|
|
165
|
+
const { samples, timestamps } = this.pullDataMethod()
|
|
166
|
+
this.handleErrorCodeIfPresent()
|
|
167
|
+
|
|
168
|
+
if (samples && timestamps) {
|
|
169
|
+
this.onData(samples, timestamps)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
setImmediate(() => {
|
|
173
|
+
void this.pullOnLoop()
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private pullSample() {
|
|
179
|
+
const timestamp = this.callPullSampleBinding()
|
|
180
|
+
|
|
181
|
+
if (timestamp) {
|
|
182
|
+
return {
|
|
183
|
+
samples: this.createFloatArrayFromDataBuffer(),
|
|
184
|
+
timestamps: new Float64Array([timestamp]),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { samples: undefined, timestamps: undefined }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private callPullSampleBinding() {
|
|
191
|
+
return this.lsl.pullSample({
|
|
192
|
+
inlet: this.inlet,
|
|
193
|
+
dataBufferPtr: this.dataBufferPtr,
|
|
194
|
+
dataBufferElements: this.channelCount,
|
|
195
|
+
timeout: this.timeoutMs / 1000,
|
|
196
|
+
errcodePtr: this.errorCodeBufferPtr,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private createFloatArrayFromDataBuffer() {
|
|
201
|
+
return new Float32Array(
|
|
202
|
+
this.dataBuffer.buffer,
|
|
203
|
+
this.dataBuffer.byteOffset,
|
|
204
|
+
this.chunkSize * this.channelCount
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private pullChunk() {
|
|
209
|
+
const firstTimestamp = this.callPullChunkBinding()
|
|
210
|
+
|
|
211
|
+
if (firstTimestamp) {
|
|
212
|
+
return {
|
|
213
|
+
samples: this.createFloatArrayFromDataBuffer(),
|
|
214
|
+
timestamps: this.createDoubleArrayFromTimestampBuffer(),
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { samples: undefined, timestamps: undefined }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private callPullChunkBinding() {
|
|
221
|
+
return this.lsl.pullChunk({
|
|
222
|
+
inlet: this.inlet,
|
|
223
|
+
dataBufferPtr: this.dataBufferPtr,
|
|
224
|
+
timestampBufferPtr: this.timestampBufferPtr,
|
|
225
|
+
dataBufferElements: this.chunkSize * this.channelCount,
|
|
226
|
+
timestampBufferElements: this.chunkSize,
|
|
227
|
+
timeout: this.timeoutMs / 1000,
|
|
228
|
+
errcodePtr: this.errorCodeBufferPtr,
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private createDoubleArrayFromTimestampBuffer() {
|
|
233
|
+
return new Float64Array(
|
|
234
|
+
this.timestampBuffer.buffer,
|
|
235
|
+
this.timestampBuffer.byteOffset,
|
|
236
|
+
this.chunkSize
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private handleErrorCodeIfPresent() {
|
|
241
|
+
handleError(this.errorCodeBuffer.readInt32LE())
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public stopPulling() {
|
|
245
|
+
this.isRunning = false
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public flushQueue() {
|
|
60
249
|
this.lsl.flushInlet({ inlet: this.inlet })
|
|
61
250
|
}
|
|
62
251
|
|
|
252
|
+
public destroy() {
|
|
253
|
+
this.lsl.destroyInlet({ inlet: this.inlet })
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private getLiblslSingleton() {
|
|
257
|
+
return LiblslAdapter.getInstance()
|
|
258
|
+
}
|
|
259
|
+
|
|
63
260
|
private static LslStreamInfo(options: StreamInfoOptions) {
|
|
64
261
|
return LslStreamInfo.Create(options)
|
|
65
262
|
}
|
|
66
263
|
}
|
|
67
264
|
|
|
68
265
|
export interface StreamInlet {
|
|
69
|
-
|
|
266
|
+
startPulling(): void
|
|
267
|
+
stopPulling(): void
|
|
268
|
+
flushQueue(): void
|
|
269
|
+
destroy(): void
|
|
270
|
+
isRunning: boolean
|
|
70
271
|
}
|
|
71
272
|
|
|
72
273
|
export type StreamInletConstructor = new (
|
|
73
274
|
info: StreamInfo,
|
|
74
|
-
options: StreamInletOptions
|
|
275
|
+
options: StreamInletOptions,
|
|
276
|
+
onData: OnDataCallback
|
|
75
277
|
) => StreamInlet
|
|
76
278
|
|
|
77
279
|
export interface StreamInletOptions {
|
|
78
|
-
sampleRate: number
|
|
79
280
|
channelNames: string[]
|
|
80
281
|
channelFormat: ChannelFormat
|
|
282
|
+
sampleRateHz: number
|
|
81
283
|
chunkSize: number
|
|
82
|
-
|
|
284
|
+
maxBufferedMs?: number
|
|
285
|
+
timeoutMs?: number
|
|
83
286
|
name?: string
|
|
84
287
|
type?: string
|
|
85
288
|
sourceId?: string
|
|
86
289
|
manufacturer?: string
|
|
87
290
|
units?: string
|
|
88
291
|
}
|
|
292
|
+
|
|
293
|
+
export type OnDataCallback = (
|
|
294
|
+
samples: Float32Array,
|
|
295
|
+
timestamps: Float64Array
|
|
296
|
+
) => void
|
|
@@ -2,10 +2,16 @@ import {
|
|
|
2
2
|
assertValidChannelCount,
|
|
3
3
|
assertValidChannelFormat,
|
|
4
4
|
assertValidChunkSize,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
assertValidMaxBufferedMs,
|
|
6
|
+
assertValidSampleRateHz,
|
|
7
7
|
} from '../assertions.js'
|
|
8
|
-
import
|
|
8
|
+
import handleError, { LslErrorCode } from '../handleError.js'
|
|
9
|
+
import {
|
|
10
|
+
ChannelFormat,
|
|
11
|
+
BoundOutlet,
|
|
12
|
+
Liblsl,
|
|
13
|
+
LslSample,
|
|
14
|
+
} from './LiblslAdapter.js'
|
|
9
15
|
import LiblslAdapter from './LiblslAdapter.js'
|
|
10
16
|
import LslStreamInfo, {
|
|
11
17
|
StreamInfo,
|
|
@@ -18,7 +24,7 @@ export default class LslStreamOutlet implements StreamOutlet {
|
|
|
18
24
|
private info: StreamInfo
|
|
19
25
|
private options: StreamOutletOptions
|
|
20
26
|
private outlet!: BoundOutlet
|
|
21
|
-
private
|
|
27
|
+
private pushSampleMethod!: (options: unknown) => LslErrorCode
|
|
22
28
|
|
|
23
29
|
protected constructor(info: StreamInfo, options: StreamOutletOptions) {
|
|
24
30
|
this.info = info
|
|
@@ -26,23 +32,23 @@ export default class LslStreamOutlet implements StreamOutlet {
|
|
|
26
32
|
|
|
27
33
|
this.validateOptions()
|
|
28
34
|
this.createStreamOutlet()
|
|
29
|
-
this.
|
|
35
|
+
this.setPushSampleMethod()
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
public static async Create(options: StreamOutletOptions) {
|
|
33
39
|
const { waitAfterConstructionMs = 10 } = options ?? {}
|
|
34
40
|
|
|
35
|
-
const
|
|
41
|
+
const infoOptions = {
|
|
36
42
|
channelNames: options.channelNames,
|
|
37
43
|
channelFormat: options.channelFormat,
|
|
38
|
-
|
|
44
|
+
sampleRateHz: options.sampleRateHz,
|
|
39
45
|
name: options.name,
|
|
40
46
|
type: options.type,
|
|
41
47
|
sourceId: options.sourceId,
|
|
42
|
-
units: options.
|
|
48
|
+
units: options.units,
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
const info = this.LslStreamInfo(
|
|
51
|
+
const info = this.LslStreamInfo(infoOptions)
|
|
46
52
|
const instance = new (this.Class ?? this)(info, options)
|
|
47
53
|
|
|
48
54
|
await this.wait(waitAfterConstructionMs)
|
|
@@ -52,24 +58,26 @@ export default class LslStreamOutlet implements StreamOutlet {
|
|
|
52
58
|
|
|
53
59
|
private validateOptions() {
|
|
54
60
|
assertValidChannelCount(this.channelCount)
|
|
55
|
-
|
|
61
|
+
assertValidSampleRateHz(this.sampleRateHz)
|
|
56
62
|
assertValidChannelFormat(this.channelFormat)
|
|
57
63
|
assertValidChunkSize(this.chunkSize)
|
|
58
|
-
|
|
64
|
+
assertValidMaxBufferedMs(this.maxBufferedMs)
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
private createStreamOutlet() {
|
|
62
68
|
this.outlet = this.lsl.createOutlet({
|
|
63
69
|
info: this.boundStreamInfo,
|
|
64
70
|
chunkSize: this.chunkSize,
|
|
65
|
-
|
|
71
|
+
maxBufferedMs: this.maxBufferedMs,
|
|
66
72
|
})
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
private
|
|
75
|
+
private setPushSampleMethod() {
|
|
70
76
|
this.validateChannelFormat()
|
|
71
77
|
|
|
72
|
-
this.
|
|
78
|
+
this.pushSampleMethod = (
|
|
79
|
+
this.lsl[this.pushMethod] as (options: unknown) => LslErrorCode
|
|
80
|
+
).bind(this.lsl)
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
private validateChannelFormat() {
|
|
@@ -98,11 +106,13 @@ export default class LslStreamOutlet implements StreamOutlet {
|
|
|
98
106
|
public pushSample(sample: LslSample) {
|
|
99
107
|
const timestamp = this.lsl.localClock()
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
handleError(
|
|
110
|
+
this.pushSampleMethod({
|
|
111
|
+
outlet: this.outlet,
|
|
112
|
+
sample,
|
|
113
|
+
timestamp,
|
|
114
|
+
})
|
|
115
|
+
)
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
public destroy() {
|
|
@@ -133,24 +143,24 @@ export default class LslStreamOutlet implements StreamOutlet {
|
|
|
133
143
|
return this.options.channelFormat
|
|
134
144
|
}
|
|
135
145
|
|
|
136
|
-
public get
|
|
137
|
-
return this.options.
|
|
146
|
+
public get sampleRateHz() {
|
|
147
|
+
return this.options.sampleRateHz
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
public get chunkSize() {
|
|
141
151
|
return this.options.chunkSize
|
|
142
152
|
}
|
|
143
153
|
|
|
144
|
-
public get
|
|
145
|
-
return this.options.
|
|
154
|
+
public get maxBufferedMs() {
|
|
155
|
+
return this.options.maxBufferedMs ?? 360 * 1000
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
public get manufacturer() {
|
|
149
|
-
return this.options.manufacturer
|
|
159
|
+
return this.options.manufacturer ?? 'N/A'
|
|
150
160
|
}
|
|
151
161
|
|
|
152
|
-
public get
|
|
153
|
-
return this.options.
|
|
162
|
+
public get units() {
|
|
163
|
+
return this.options.units ?? 'N/A'
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
private get boundStreamInfo() {
|
|
@@ -178,11 +188,11 @@ export interface StreamOutlet {
|
|
|
178
188
|
readonly sourceId: string
|
|
179
189
|
readonly channelNames: string[]
|
|
180
190
|
readonly channelFormat: ChannelFormat
|
|
181
|
-
readonly
|
|
191
|
+
readonly sampleRateHz: number
|
|
182
192
|
readonly chunkSize: number
|
|
183
|
-
readonly
|
|
193
|
+
readonly maxBufferedMs: number
|
|
184
194
|
readonly manufacturer: string
|
|
185
|
-
readonly
|
|
195
|
+
readonly units: string
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
export type StreamOutletConstructor = new (
|
|
@@ -196,10 +206,10 @@ export interface StreamOutletOptions {
|
|
|
196
206
|
sourceId: string
|
|
197
207
|
channelNames: string[]
|
|
198
208
|
channelFormat: ChannelFormat
|
|
199
|
-
|
|
209
|
+
sampleRateHz: number
|
|
200
210
|
chunkSize: number
|
|
201
|
-
|
|
202
|
-
manufacturer
|
|
203
|
-
|
|
211
|
+
maxBufferedMs?: number
|
|
212
|
+
manufacturer?: string
|
|
213
|
+
units?: string
|
|
204
214
|
waitAfterConstructionMs?: number
|
|
205
215
|
}
|
|
@@ -1,13 +1,123 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws'
|
|
2
|
+
import { ChannelFormat } from './LiblslAdapter.js'
|
|
3
|
+
import LslStreamInlet, { StreamInlet } from './LslStreamInlet.js'
|
|
4
|
+
|
|
1
5
|
export default class LslWebSocketBridge implements StreamTransportBridge {
|
|
2
6
|
public static Class?: StreamTransportBridgeConstructor
|
|
7
|
+
public static WSS = WebSocketServer
|
|
8
|
+
|
|
9
|
+
private inlet: StreamInlet
|
|
10
|
+
private wss: WebSocketServer
|
|
11
|
+
private isDestroyed = false
|
|
12
|
+
|
|
13
|
+
protected constructor(options: StreamTransportBridgeConstructorOptions) {
|
|
14
|
+
const { inlet, wss } = options
|
|
15
|
+
|
|
16
|
+
this.inlet = inlet
|
|
17
|
+
this.wss = wss
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public static Create(options: StreamTransportBridgeOptions) {
|
|
21
|
+
const wss = this.WebSocketServer()
|
|
22
|
+
const inlet = this.LslStreamInlet(options, wss)
|
|
23
|
+
|
|
24
|
+
return new (this.Class ?? this)({ ...options, inlet, wss })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public activate() {
|
|
28
|
+
this.throwIfBridgeIsDestroyed()
|
|
29
|
+
this.startPullingData()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private throwIfBridgeIsDestroyed() {
|
|
33
|
+
if (this.isDestroyed) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`\n\n Cannot re-activate bridge after destroying it! \n\n Please create and activate a new instance. \n`
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private startPullingData() {
|
|
41
|
+
this.inlet.startPulling()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public deactivate() {
|
|
45
|
+
this.stopPullingData()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private stopPullingData() {
|
|
49
|
+
this.inlet.stopPulling()
|
|
50
|
+
}
|
|
3
51
|
|
|
4
|
-
|
|
52
|
+
public destroy() {
|
|
53
|
+
this.destroyCppBoundInlet()
|
|
54
|
+
this.closeWebSocketServer()
|
|
55
|
+
this.isDestroyed = true
|
|
56
|
+
}
|
|
5
57
|
|
|
6
|
-
|
|
7
|
-
|
|
58
|
+
private destroyCppBoundInlet() {
|
|
59
|
+
this.inlet.destroy()
|
|
8
60
|
}
|
|
61
|
+
|
|
62
|
+
private closeWebSocketServer() {
|
|
63
|
+
this.wss.close()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static createOnDataCallback(wss: WebSocketServer) {
|
|
67
|
+
return (samples: Float32Array, timestamps: Float64Array) => {
|
|
68
|
+
this.broadcastToClients(wss, samples, timestamps)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private static broadcastToClients(
|
|
73
|
+
wss: WebSocketServer,
|
|
74
|
+
samples: Float32Array<ArrayBufferLike>,
|
|
75
|
+
timestamps: Float64Array<ArrayBufferLike>
|
|
76
|
+
) {
|
|
77
|
+
for (const client of wss.clients) {
|
|
78
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
79
|
+
client.send(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
samples,
|
|
82
|
+
timestamps,
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private static LslStreamInlet(
|
|
90
|
+
options: StreamTransportBridgeOptions,
|
|
91
|
+
wss: WebSocketServer
|
|
92
|
+
) {
|
|
93
|
+
const onData = this.createOnDataCallback(wss)
|
|
94
|
+
return LslStreamInlet.Create(options, onData)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private static WebSocketServer() {
|
|
98
|
+
return new this.WSS({ port: 8080 })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface StreamTransportBridge {
|
|
103
|
+
activate(): void
|
|
104
|
+
deactivate(): void
|
|
105
|
+
destroy(): void
|
|
9
106
|
}
|
|
10
107
|
|
|
11
|
-
export
|
|
108
|
+
export type StreamTransportBridgeConstructor = new (
|
|
109
|
+
options: StreamTransportBridgeConstructorOptions
|
|
110
|
+
) => StreamTransportBridge
|
|
111
|
+
|
|
112
|
+
export interface StreamTransportBridgeOptions {
|
|
113
|
+
channelNames: string[]
|
|
114
|
+
channelFormat: ChannelFormat
|
|
115
|
+
sampleRateHz: number
|
|
116
|
+
chunkSize: number
|
|
117
|
+
}
|
|
12
118
|
|
|
13
|
-
export
|
|
119
|
+
export interface StreamTransportBridgeConstructorOptions
|
|
120
|
+
extends StreamTransportBridgeOptions {
|
|
121
|
+
inlet: StreamInlet
|
|
122
|
+
wss: WebSocketServer
|
|
123
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,13 +5,12 @@ async function main() {
|
|
|
5
5
|
name: 'Muse S (2nd gen)',
|
|
6
6
|
type: 'EEG',
|
|
7
7
|
channelNames: ['TP9', 'AF7', 'AF8', 'TP10', 'AUX'],
|
|
8
|
-
|
|
8
|
+
sampleRateHz: 256,
|
|
9
9
|
channelFormat: 'float32',
|
|
10
10
|
sourceId: 'muse-s-eeg',
|
|
11
11
|
manufacturer: 'Interaxon Inc.',
|
|
12
|
-
|
|
12
|
+
units: 'microvolt',
|
|
13
13
|
chunkSize: 12,
|
|
14
|
-
maxBuffered: 360,
|
|
15
14
|
})
|
|
16
15
|
|
|
17
16
|
instance.pushSample([1, 2, 3, 4, 5])
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import LslStreamInlet from '../impl/LslStreamInlet.js'
|
|
2
|
+
import LslStreamOutlet from '../impl/LslStreamOutlet.js'
|
|
3
|
+
|
|
4
|
+
console.log('Creating outlet...')
|
|
5
|
+
const outlet = await LslStreamOutlet.Create({
|
|
6
|
+
name: 'TestOutlet',
|
|
7
|
+
type: 'TEST',
|
|
8
|
+
sourceId: 'TestOutletID',
|
|
9
|
+
sampleRateHz: 100,
|
|
10
|
+
channelNames: ['Ch1', 'Ch2', 'Ch3'],
|
|
11
|
+
channelFormat: 'float32',
|
|
12
|
+
chunkSize: 1,
|
|
13
|
+
units: 'test',
|
|
14
|
+
manufacturer: 'test',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
console.log('Creating inlet...')
|
|
18
|
+
|
|
19
|
+
const inletOptions = {
|
|
20
|
+
name: 'TestOutlet',
|
|
21
|
+
type: 'TEST',
|
|
22
|
+
sourceId: 'TestOutletID',
|
|
23
|
+
sampleRateHz: 100,
|
|
24
|
+
channelNames: ['Ch1', 'Ch2', 'Ch3'],
|
|
25
|
+
channelFormat: 'float32' as const,
|
|
26
|
+
chunkSize: 1,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const inlet = LslStreamInlet.Create(inletOptions, (samples, timestamps) => {
|
|
30
|
+
console.log('Received samples:', samples, 'at timestamps:', timestamps)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
console.log('Start pulling...')
|
|
34
|
+
inlet.startPulling()
|
|
35
|
+
|
|
36
|
+
console.log('Waiting for 100 ms...')
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
38
|
+
|
|
39
|
+
console.log('Pushing samples...')
|
|
40
|
+
for (let i = 0; i < 300; i++) {
|
|
41
|
+
const sample = [i, i, i]
|
|
42
|
+
outlet.pushSample(sample)
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
47
|
+
|
|
48
|
+
console.log('Pushing more samples...')
|
|
49
|
+
for (let i = 0; i < 30; i++) {
|
|
50
|
+
const sample = [i, i, i]
|
|
51
|
+
outlet.pushSample(sample)
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
53
|
+
}
|