@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.
Files changed (122) hide show
  1. package/README.md +2 -5
  2. package/build/__tests__/AbstractPackageTest.d.ts +9 -3
  3. package/build/__tests__/AbstractPackageTest.js +37 -3
  4. package/build/__tests__/AbstractPackageTest.js.map +1 -1
  5. package/build/__tests__/impl/LiblslAdapter.test.d.ts +6 -0
  6. package/build/__tests__/impl/LiblslAdapter.test.js +133 -12
  7. package/build/__tests__/impl/LiblslAdapter.test.js.map +1 -1
  8. package/build/__tests__/impl/LslEventMarkerOutlet.test.js +5 -5
  9. package/build/__tests__/impl/LslEventMarkerOutlet.test.js.map +1 -1
  10. package/build/__tests__/impl/LslStreamInfo.test.js +3 -3
  11. package/build/__tests__/impl/LslStreamInfo.test.js.map +1 -1
  12. package/build/__tests__/impl/LslStreamInlet.test.d.ts +30 -2
  13. package/build/__tests__/impl/LslStreamInlet.test.js +181 -18
  14. package/build/__tests__/impl/LslStreamInlet.test.js.map +1 -1
  15. package/build/__tests__/impl/LslStreamOutlet.test.d.ts +16 -7
  16. package/build/__tests__/impl/LslStreamOutlet.test.js +84 -23
  17. package/build/__tests__/impl/LslStreamOutlet.test.js.map +1 -1
  18. package/build/__tests__/impl/LslWebSocketBridge.test.d.ts +15 -0
  19. package/build/__tests__/impl/LslWebSocketBridge.test.js +112 -2
  20. package/build/__tests__/impl/LslWebSocketBridge.test.js.map +1 -1
  21. package/build/assertions.d.ts +3 -3
  22. package/build/assertions.js +6 -6
  23. package/build/assertions.js.map +1 -1
  24. package/build/handleError.d.ts +8 -0
  25. package/build/handleError.js +25 -0
  26. package/build/handleError.js.map +1 -0
  27. package/build/impl/LiblslAdapter.d.ts +128 -13
  28. package/build/impl/LiblslAdapter.js +149 -71
  29. package/build/impl/LiblslAdapter.js.map +1 -1
  30. package/build/impl/LslEventMarkerOutlet.d.ts +4 -1
  31. package/build/impl/LslEventMarkerOutlet.js +3 -3
  32. package/build/impl/LslEventMarkerOutlet.js.map +1 -1
  33. package/build/impl/LslStreamInfo.d.ts +3 -3
  34. package/build/impl/LslStreamInfo.js +5 -5
  35. package/build/impl/LslStreamInfo.js.map +1 -1
  36. package/build/impl/LslStreamInlet.d.ts +52 -14
  37. package/build/impl/LslStreamInlet.js +157 -20
  38. package/build/impl/LslStreamInlet.js.map +1 -1
  39. package/build/impl/LslStreamOutlet.d.ts +13 -13
  40. package/build/impl/LslStreamOutlet.js +22 -21
  41. package/build/impl/LslStreamOutlet.js.map +1 -1
  42. package/build/impl/LslWebSocketBridge.d.ts +35 -3
  43. package/build/impl/LslWebSocketBridge.js +66 -3
  44. package/build/impl/LslWebSocketBridge.js.map +1 -1
  45. package/build/index.d.ts +0 -1
  46. package/build/index.js +0 -1
  47. package/build/index.js.map +1 -1
  48. package/build/scripts/runLslStreamOutlet.js +2 -3
  49. package/build/scripts/runLslStreamOutlet.js.map +1 -1
  50. package/build/scripts/runOutletToInlet.d.ts +1 -0
  51. package/build/scripts/runOutletToInlet.js +45 -0
  52. package/build/scripts/runOutletToInlet.js.map +1 -0
  53. package/build/testDoubles/EventMarkerOutlet/FakeEventMarkerOutlet.d.ts +1 -2
  54. package/build/testDoubles/EventMarkerOutlet/FakeEventMarkerOutlet.js.map +1 -1
  55. package/build/testDoubles/EventMarkerOutlet/MockEventMarkerOutlet.d.ts +2 -2
  56. package/build/testDoubles/EventMarkerOutlet/MockEventMarkerOutlet.js.map +1 -1
  57. package/build/testDoubles/EventMarkerOutlet/ThrowingEventMarkerOutlet.d.ts +1 -2
  58. package/build/testDoubles/EventMarkerOutlet/ThrowingEventMarkerOutlet.js.map +1 -1
  59. package/build/testDoubles/Liblsl/FakeLiblsl.d.ts +15 -3
  60. package/build/testDoubles/Liblsl/FakeLiblsl.js +42 -0
  61. package/build/testDoubles/Liblsl/FakeLiblsl.js.map +1 -1
  62. package/build/testDoubles/StreamInfo/FakeStreamInfo.d.ts +1 -1
  63. package/build/testDoubles/StreamInfo/SpyStreamInfo.d.ts +1 -1
  64. package/build/testDoubles/StreamInlet/FakeStreamInlet.d.ts +12 -3
  65. package/build/testDoubles/StreamInlet/FakeStreamInlet.js +25 -5
  66. package/build/testDoubles/StreamInlet/FakeStreamInlet.js.map +1 -1
  67. package/build/testDoubles/StreamInlet/SpyStreamInlet.d.ts +3 -3
  68. package/build/testDoubles/StreamInlet/SpyStreamInlet.js +2 -2
  69. package/build/testDoubles/StreamInlet/SpyStreamInlet.js.map +1 -1
  70. package/build/testDoubles/StreamOutlet/FakeStreamOutlet.d.ts +4 -4
  71. package/build/testDoubles/StreamOutlet/FakeStreamOutlet.js +6 -6
  72. package/build/testDoubles/StreamOutlet/FakeStreamOutlet.js.map +1 -1
  73. package/build/testDoubles/StreamTransportBridge/FakeStreamTransportBridge.d.ts +6 -0
  74. package/build/testDoubles/StreamTransportBridge/FakeStreamTransportBridge.js +15 -0
  75. package/build/testDoubles/StreamTransportBridge/FakeStreamTransportBridge.js.map +1 -1
  76. package/build/testDoubles/StreamTransportBridge/SpyLslWebSocketBridge.d.ts +4 -0
  77. package/build/testDoubles/StreamTransportBridge/SpyLslWebSocketBridge.js +7 -0
  78. package/build/testDoubles/StreamTransportBridge/SpyLslWebSocketBridge.js.map +1 -0
  79. package/build/testDoubles/WebSockets/FakeWebSocket.d.ts +10 -0
  80. package/build/testDoubles/WebSockets/FakeWebSocket.js +14 -0
  81. package/build/testDoubles/WebSockets/FakeWebSocket.js.map +1 -0
  82. package/build/testDoubles/WebSockets/FakeWebSocketServer.d.ts +13 -0
  83. package/build/testDoubles/WebSockets/FakeWebSocketServer.js +22 -0
  84. package/build/testDoubles/WebSockets/FakeWebSocketServer.js.map +1 -0
  85. package/build/testDoubles/generateRandomOutletOptions.js +3 -3
  86. package/build/testDoubles/generateRandomOutletOptions.js.map +1 -1
  87. package/package.json +5 -3
  88. package/src/__tests__/AbstractPackageTest.ts +52 -4
  89. package/src/__tests__/impl/LiblslAdapter.test.ts +181 -16
  90. package/src/__tests__/impl/LslEventMarkerOutlet.test.ts +5 -5
  91. package/src/__tests__/impl/LslStreamInfo.test.ts +3 -3
  92. package/src/__tests__/impl/LslStreamInlet.test.ts +253 -20
  93. package/src/__tests__/impl/LslStreamOutlet.test.ts +111 -24
  94. package/src/__tests__/impl/LslWebSocketBridge.test.ts +163 -5
  95. package/src/assertions.ts +7 -7
  96. package/src/handleError.ts +24 -0
  97. package/src/impl/LiblslAdapter.ts +322 -97
  98. package/src/impl/LslEventMarkerOutlet.ts +8 -4
  99. package/src/impl/LslStreamInfo.ts +9 -9
  100. package/src/impl/LslStreamInlet.ts +233 -25
  101. package/src/impl/LslStreamOutlet.ts +43 -33
  102. package/src/impl/LslWebSocketBridge.ts +115 -5
  103. package/src/index.ts +0 -2
  104. package/src/scripts/runLslStreamOutlet.ts +2 -3
  105. package/src/scripts/runOutletToInlet.ts +53 -0
  106. package/src/testDoubles/EventMarkerOutlet/FakeEventMarkerOutlet.ts +4 -2
  107. package/src/testDoubles/EventMarkerOutlet/MockEventMarkerOutlet.ts +5 -2
  108. package/src/testDoubles/EventMarkerOutlet/ThrowingEventMarkerOutlet.ts +4 -2
  109. package/src/testDoubles/Liblsl/FakeLiblsl.ts +61 -1
  110. package/src/testDoubles/StreamInfo/FakeStreamInfo.ts +1 -1
  111. package/src/testDoubles/StreamInlet/FakeStreamInlet.ts +36 -5
  112. package/src/testDoubles/StreamInlet/SpyStreamInlet.ts +7 -2
  113. package/src/testDoubles/StreamOutlet/FakeStreamOutlet.ts +7 -7
  114. package/src/testDoubles/StreamTransportBridge/FakeStreamTransportBridge.ts +18 -0
  115. package/src/testDoubles/StreamTransportBridge/SpyLslWebSocketBridge.ts +9 -0
  116. package/src/testDoubles/WebSockets/FakeWebSocket.ts +17 -0
  117. package/src/testDoubles/WebSockets/FakeWebSocketServer.ts +32 -0
  118. package/src/testDoubles/generateRandomOutletOptions.ts +3 -3
  119. package/build/types.d.ts +0 -95
  120. package/build/types.js +0 -2
  121. package/build/types.js.map +0 -1
  122. 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 { BoundInlet, ChannelFormat } from '../types.js'
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
- protected name: string
15
+ public isRunning = false
16
+
14
17
  protected info: StreamInfo
15
- protected inlet!: BoundInlet
18
+ protected name: string
19
+ private channelNames: string[]
20
+ private channelCount: number
16
21
  private chunkSize: number
17
- private maxBuffered: number
22
+ private maxBufferedMs: number
23
+ private timeoutMs: number
24
+
25
+ private onData: OnDataCallback
18
26
 
19
- protected constructor(info: StreamInfo, options: StreamInletOptions) {
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
- maxBuffered,
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.createStreamInlet()
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 { maxBuffered, chunkSize, ...infoOptions } = options
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 createStreamInlet() {
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
- maxBuffered: this.maxBuffered,
93
+ maxBufferedMs: this.maxBufferedMs / 1000,
46
94
  })
47
95
  }
48
96
 
49
- private get boundStreamInfo() {
50
- return this.info.boundStreamInfo
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 get lsl() {
54
- return LiblslAdapter.getInstance()
106
+ private createWriteableBuffers() {
107
+ this.createDataBuffer()
108
+ this.createTimestampBuffer()
109
+ this.createErrorCodeBuffer()
55
110
  }
56
111
 
57
- private readonly defaultName = `lsl-inlet-${generateId()}`
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
- public flushSamples() {
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
- flushSamples(): void
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
- maxBuffered: number
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
- assertValidMaxBuffered,
6
- assertValidSampleRate,
5
+ assertValidMaxBufferedMs,
6
+ assertValidSampleRateHz,
7
7
  } from '../assertions.js'
8
- import { ChannelFormat, BoundOutlet, Liblsl, LslSample } from '../types.js'
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 pushSampleByType!: (options: any) => void
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.setPushSampleType()
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 streamInfoOptions = {
41
+ const infoOptions = {
36
42
  channelNames: options.channelNames,
37
43
  channelFormat: options.channelFormat,
38
- sampleRate: options.sampleRate,
44
+ sampleRateHz: options.sampleRateHz,
39
45
  name: options.name,
40
46
  type: options.type,
41
47
  sourceId: options.sourceId,
42
- units: options.unit,
48
+ units: options.units,
43
49
  }
44
50
 
45
- const info = this.LslStreamInfo(streamInfoOptions)
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
- assertValidSampleRate(this.sampleRate)
61
+ assertValidSampleRateHz(this.sampleRateHz)
56
62
  assertValidChannelFormat(this.channelFormat)
57
63
  assertValidChunkSize(this.chunkSize)
58
- assertValidMaxBuffered(this.maxBuffered)
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
- maxBuffered: this.maxBuffered,
71
+ maxBufferedMs: this.maxBufferedMs,
66
72
  })
67
73
  }
68
74
 
69
- private setPushSampleType() {
75
+ private setPushSampleMethod() {
70
76
  this.validateChannelFormat()
71
77
 
72
- this.pushSampleByType = this.lsl[this.pushMethod].bind(this.lsl)
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
- this.pushSampleByType({
102
- outlet: this.outlet,
103
- sample,
104
- timestamp,
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 sampleRate() {
137
- return this.options.sampleRate
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 maxBuffered() {
145
- return this.options.maxBuffered
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 unit() {
153
- return this.options.unit
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 sampleRate: number
191
+ readonly sampleRateHz: number
182
192
  readonly chunkSize: number
183
- readonly maxBuffered: number
193
+ readonly maxBufferedMs: number
184
194
  readonly manufacturer: string
185
- readonly unit: string
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
- sampleRate: number
209
+ sampleRateHz: number
200
210
  chunkSize: number
201
- maxBuffered: number
202
- manufacturer: string
203
- unit: string
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
- protected constructor() {}
52
+ public destroy() {
53
+ this.destroyCppBoundInlet()
54
+ this.closeWebSocketServer()
55
+ this.isDestroyed = true
56
+ }
5
57
 
6
- public static Create() {
7
- return new (this.Class ?? this)()
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 interface StreamTransportBridge {}
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 type StreamTransportBridgeConstructor = new () => StreamTransportBridge
119
+ export interface StreamTransportBridgeConstructorOptions
120
+ extends StreamTransportBridgeOptions {
121
+ inlet: StreamInlet
122
+ wss: WebSocketServer
123
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,3 @@
1
- export * from './types.js'
2
-
3
1
  // EventMarkerOutlet
4
2
 
5
3
  export { default as LslEventMarkerOutlet } from './impl/LslEventMarkerOutlet.js'
@@ -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
- sampleRate: 256,
8
+ sampleRateHz: 256,
9
9
  channelFormat: 'float32',
10
10
  sourceId: 'muse-s-eeg',
11
11
  manufacturer: 'Interaxon Inc.',
12
- unit: 'microvolt',
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
+ }