@neurodevs/node-lsl 18.0.0 → 18.1.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 +4 -2
- package/build/__tests__/AbstractPackageTest.js +15 -6
- package/build/__tests__/AbstractPackageTest.js.map +1 -1
- package/build/__tests__/impl/BleDeviceController.test.d.ts +54 -0
- package/build/__tests__/impl/BleDeviceController.test.js +333 -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 +9 -9
- 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/impl/LslStreamOutlet.d.ts +2 -2
- package/build/impl/LslStreamOutlet.js +2 -2
- 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/controllers/BleDeviceController.d.ts +58 -0
- package/build/impl/controllers/BleDeviceController.js +145 -0
- package/build/impl/controllers/BleDeviceController.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 +6 -4
- package/build/index.js +7 -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/testDoubles/BleController/FakeBleController.d.ts +24 -0
- package/build/testDoubles/BleController/FakeBleController.js +42 -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/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/generateRandomOutletOptions.js +1 -1
- package/build/testDoubles/generateRandomOutletOptions.js.map +1 -1
- package/eslint.config.js +3 -0
- package/package.json +18 -56
- package/prettier.config.js +3 -0
- package/src/__tests__/AbstractPackageTest.ts +28 -9
- package/src/__tests__/impl/BleDeviceController.test.ts +492 -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 +10 -10
- package/src/__tests__/impl/LslWebSocketBridge.test.ts +4 -3
- package/src/impl/LslStreamOutlet.ts +3 -3
- package/src/impl/LslWebSocketBridge.ts +2 -2
- package/src/impl/controllers/BleDeviceController.ts +223 -0
- package/src/impl/workers/inlet/StreamInletWorker.ts +12 -12
- package/src/impl/workers/outlet/LslStreamOutlet.worker.ts +8 -8
- package/src/index.ts +8 -5
- package/src/scripts/discoverMuse.ts +50 -0
- package/src/scripts/runMuse.ts +50 -0
- package/src/testDoubles/BleController/FakeBleController.ts +70 -0
- package/src/testDoubles/BleController/SpyBleController.ts +61 -0
- package/src/testDoubles/StreamOutlet/FakeStreamOutlet.ts +6 -3
- 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,223 @@
|
|
|
1
|
+
import { LibndxAdapter, Libndx, NativePeripheral } from '@neurodevs/ndx-native'
|
|
2
|
+
|
|
3
|
+
export default class BleDeviceController implements BleController {
|
|
4
|
+
public static Class?: BleControllerConstructor
|
|
5
|
+
public static setTimeout = setTimeout
|
|
6
|
+
public static waitAfterMs = 1000
|
|
7
|
+
|
|
8
|
+
private static _ndx?: Libndx
|
|
9
|
+
|
|
10
|
+
public static get ndx() {
|
|
11
|
+
if (!this._ndx) {
|
|
12
|
+
this._ndx = LibndxAdapter.getInstance()
|
|
13
|
+
}
|
|
14
|
+
return this._ndx
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public static set ndx(value: Libndx) {
|
|
18
|
+
this._ndx = value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected charCallbacks: CharacteristicCallbacks
|
|
22
|
+
protected rssiIntervalMs?: number
|
|
23
|
+
protected connected = false
|
|
24
|
+
protected log = console
|
|
25
|
+
|
|
26
|
+
private deviceUuid?: string
|
|
27
|
+
private deviceNamePrefix?: string
|
|
28
|
+
private deviceName?: string
|
|
29
|
+
private onConnected?: (peripheral: NativePeripheral) => void
|
|
30
|
+
|
|
31
|
+
protected constructor(options: BleControllerOptions) {
|
|
32
|
+
const {
|
|
33
|
+
deviceUuid,
|
|
34
|
+
deviceNamePrefix,
|
|
35
|
+
charCallbacks,
|
|
36
|
+
onConnected,
|
|
37
|
+
rssiIntervalMs,
|
|
38
|
+
} = options
|
|
39
|
+
|
|
40
|
+
this.deviceUuid = deviceUuid
|
|
41
|
+
this.deviceNamePrefix = deviceNamePrefix
|
|
42
|
+
this.charCallbacks = charCallbacks
|
|
43
|
+
this.onConnected = onConnected
|
|
44
|
+
this.rssiIntervalMs = rssiIntervalMs
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static async Create(options: BleControllerOptions) {
|
|
48
|
+
return new (this.Class ?? this)(options)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async connect() {
|
|
52
|
+
if (!this.deviceUuid) {
|
|
53
|
+
await this.discoverUuid()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.createBleBackend()
|
|
57
|
+
this.startBleBackend()
|
|
58
|
+
|
|
59
|
+
await this.waitForOnConnected()
|
|
60
|
+
await this.waitToDiscoverServices()
|
|
61
|
+
|
|
62
|
+
this.setBleRssiInterval()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async discoverUuid() {
|
|
66
|
+
const { status, error } = this.ndx.discoverBleUuid({
|
|
67
|
+
namePrefix: this.deviceNamePrefix as string,
|
|
68
|
+
onDiscovered: (uuid: string) => {
|
|
69
|
+
this.deviceUuid = uuid
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
this.throwIfError(status, error)
|
|
74
|
+
|
|
75
|
+
await this.waitForDiscoveredUuid()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async waitForDiscoveredUuid() {
|
|
79
|
+
await new Promise<void>((resolve) => {
|
|
80
|
+
const checkDiscovered = () => {
|
|
81
|
+
if (this.deviceUuid) {
|
|
82
|
+
resolve()
|
|
83
|
+
} else {
|
|
84
|
+
BleDeviceController.setTimeout(checkDiscovered, 100)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
checkDiscovered()
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private createBleBackend() {
|
|
92
|
+
const { status, error } = this.ndx.createBleBackend({
|
|
93
|
+
deviceUuid: this.uuid,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
this.throwIfError(status, error)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private throwIfError(status: number, error: string | undefined) {
|
|
100
|
+
if (status !== 200) {
|
|
101
|
+
throw new Error(`${status} error: ${error ?? 'Unknown error'}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private setBleRssiInterval() {
|
|
106
|
+
if (this.rssiIntervalMs) {
|
|
107
|
+
this.ndx.setBleRssiInterval({
|
|
108
|
+
deviceUuid: this.uuid,
|
|
109
|
+
intervalMs: this.rssiIntervalMs,
|
|
110
|
+
onRssi: (rssi: number) => {
|
|
111
|
+
this.log.info(`[RSSI=${rssi}]`)
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private startBleBackend() {
|
|
118
|
+
const { status, error } = this.ndx.startBleBackend({
|
|
119
|
+
deviceUuid: this.uuid,
|
|
120
|
+
onConnected: (peripheral: NativePeripheral) => {
|
|
121
|
+
const { name } = peripheral
|
|
122
|
+
this.deviceName = name
|
|
123
|
+
this.connected = true
|
|
124
|
+
this.log.info(`Connected to device ${this.uuid}!`)
|
|
125
|
+
this.onConnected?.(peripheral)
|
|
126
|
+
},
|
|
127
|
+
charCallbacks: this.charCallbacks,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
this.throwIfError(status, error)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async waitForOnConnected() {
|
|
134
|
+
await new Promise<void>((resolve) => {
|
|
135
|
+
const checkConnected = () => {
|
|
136
|
+
if (this.connected) {
|
|
137
|
+
resolve()
|
|
138
|
+
} else {
|
|
139
|
+
BleDeviceController.setTimeout(checkConnected, 100)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
checkConnected()
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async waitToDiscoverServices() {
|
|
147
|
+
await new Promise<void>((resolve) =>
|
|
148
|
+
BleDeviceController.setTimeout(
|
|
149
|
+
resolve,
|
|
150
|
+
BleDeviceController.waitAfterMs
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public async writeCharacteristic(
|
|
156
|
+
charUuid: CharacteristicUuid,
|
|
157
|
+
value: string
|
|
158
|
+
) {
|
|
159
|
+
const { status, error } = this.ndx.writeBleCharacteristic({
|
|
160
|
+
deviceUuid: this.uuid,
|
|
161
|
+
charUuid,
|
|
162
|
+
value,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
this.throwIfError(status, error)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public async disconnect() {
|
|
169
|
+
const { status, error } = this.ndx.stopBleBackend({
|
|
170
|
+
deviceUuid: this.uuid,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
this.throwIfError(status, error)
|
|
174
|
+
this.connected = false
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public get uuid() {
|
|
178
|
+
return this.deviceUuid ?? ''
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public get name() {
|
|
182
|
+
return this.deviceName ?? 'N/A'
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private get ndx() {
|
|
186
|
+
return BleDeviceController.ndx
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface BleController {
|
|
191
|
+
uuid: string
|
|
192
|
+
name: string
|
|
193
|
+
|
|
194
|
+
connect(): Promise<void>
|
|
195
|
+
|
|
196
|
+
writeCharacteristic(
|
|
197
|
+
charUuid: CharacteristicUuid,
|
|
198
|
+
value: string
|
|
199
|
+
): Promise<void>
|
|
200
|
+
|
|
201
|
+
disconnect(): Promise<void>
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type BleControllerOptions = {
|
|
205
|
+
charCallbacks: CharacteristicCallbacks
|
|
206
|
+
onConnected?: (peripheral: NativePeripheral) => void
|
|
207
|
+
rssiIntervalMs?: number
|
|
208
|
+
} & (
|
|
209
|
+
| { deviceUuid: string; deviceNamePrefix?: string }
|
|
210
|
+
| { deviceUuid?: string; deviceNamePrefix: string }
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
export type BleControllerConstructor = new (
|
|
214
|
+
options: BleControllerOptions
|
|
215
|
+
) => BleController
|
|
216
|
+
|
|
217
|
+
export type CharacteristicCallbacks = {
|
|
218
|
+
charUuid: string
|
|
219
|
+
charName?: string
|
|
220
|
+
onData: (data: Buffer, length: number, timestampSec: number) => void
|
|
221
|
+
}[]
|
|
222
|
+
|
|
223
|
+
export type CharacteristicUuid = string
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
handleLslError,
|
|
3
3
|
InfoHandle,
|
|
4
4
|
InletHandle,
|
|
5
5
|
LiblslAdapter,
|
|
@@ -18,7 +18,7 @@ import { CreateInletPayload } from './LslStreamInlet.worker.js'
|
|
|
18
18
|
|
|
19
19
|
export default class StreamInletWorker {
|
|
20
20
|
public static lsl = LiblslAdapter.getInstance()
|
|
21
|
-
public static
|
|
21
|
+
public static handleLslError = handleLslError
|
|
22
22
|
public static freePointer = freePointer
|
|
23
23
|
|
|
24
24
|
private sourceId!: string
|
|
@@ -179,12 +179,12 @@ export default class StreamInletWorker {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
private pullSample = () => {
|
|
182
|
-
const
|
|
182
|
+
const timestampSec = this.doPullsample()
|
|
183
183
|
|
|
184
|
-
if (
|
|
184
|
+
if (timestampSec > 0) {
|
|
185
185
|
return {
|
|
186
186
|
samples: this.readSamplesFromBuffer(),
|
|
187
|
-
timestamps: [
|
|
187
|
+
timestamps: [timestampSec],
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
return { samples: undefined, timestamps: undefined }
|
|
@@ -210,9 +210,9 @@ export default class StreamInletWorker {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
private pullChunk = () => {
|
|
213
|
-
const
|
|
213
|
+
const firstTimestampSec = this.doPullChunk()
|
|
214
214
|
|
|
215
|
-
if (
|
|
215
|
+
if (firstTimestampSec > 0) {
|
|
216
216
|
return {
|
|
217
217
|
samples: this.readSamplesFromBuffer(),
|
|
218
218
|
timestamps: this.readTimestampsFromBuffer(),
|
|
@@ -274,16 +274,16 @@ export default class StreamInletWorker {
|
|
|
274
274
|
|
|
275
275
|
private async pullDataOnce() {
|
|
276
276
|
const { samples, timestamps } = this.pullMethod()
|
|
277
|
-
this.
|
|
277
|
+
this.handleLslErrorIfPresent()
|
|
278
278
|
|
|
279
279
|
if (samples && timestamps) {
|
|
280
280
|
this.onData(samples, timestamps)
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
private
|
|
284
|
+
private handleLslErrorIfPresent() {
|
|
285
285
|
const errorCode = this.pullErrorBuffer.readInt32LE()
|
|
286
|
-
this.
|
|
286
|
+
this.handleLslError(errorCode)
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
private async waitBetweenPulls() {
|
|
@@ -338,8 +338,8 @@ export default class StreamInletWorker {
|
|
|
338
338
|
return StreamInletWorker.lsl
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
-
private get
|
|
342
|
-
return StreamInletWorker.
|
|
341
|
+
private get handleLslError() {
|
|
342
|
+
return StreamInletWorker.handleLslError
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
private get freePointer() {
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
LiblslAdapter,
|
|
5
5
|
Liblsl,
|
|
6
6
|
OutletHandle,
|
|
7
|
-
|
|
7
|
+
handleLslError,
|
|
8
8
|
LslErrorCode,
|
|
9
9
|
} from '@neurodevs/ndx-native'
|
|
10
10
|
|
|
@@ -20,10 +20,10 @@ export function setOutletLiblslAdapter(adapter: Liblsl) {
|
|
|
20
20
|
lsl = LiblslAdapter.getInstance()
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
let
|
|
23
|
+
let handleLslErrorFn = handleLslError
|
|
24
24
|
|
|
25
|
-
export function
|
|
26
|
-
|
|
25
|
+
export function setOutletHandleLslError(fn: typeof handleLslError) {
|
|
26
|
+
handleLslErrorFn = fn
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
let info: StreamInfo
|
|
@@ -84,7 +84,7 @@ export function pushSample(msg: {
|
|
|
84
84
|
payload: PushSamplePayload
|
|
85
85
|
}) {
|
|
86
86
|
const { payload } = msg
|
|
87
|
-
const { sample,
|
|
87
|
+
const { sample, timestampSec = lsl.localClock() } = payload
|
|
88
88
|
|
|
89
89
|
if (!handle || !pushMethod) {
|
|
90
90
|
throw new Error('Outlet not initialized')
|
|
@@ -93,11 +93,11 @@ export function pushSample(msg: {
|
|
|
93
93
|
const errorCode = pushMethod({
|
|
94
94
|
outletHandle: handle,
|
|
95
95
|
sample,
|
|
96
|
-
|
|
96
|
+
timestampSec,
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
try {
|
|
100
|
-
|
|
100
|
+
handleLslErrorFn(errorCode)
|
|
101
101
|
} catch {
|
|
102
102
|
parentPort?.postMessage({
|
|
103
103
|
type: 'error',
|
|
@@ -140,5 +140,5 @@ export interface CreateOutletPayload {
|
|
|
140
140
|
|
|
141
141
|
export interface PushSamplePayload {
|
|
142
142
|
sample: unknown
|
|
143
|
-
|
|
143
|
+
timestampSec?: number
|
|
144
144
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
//
|
|
1
|
+
// DeviceController
|
|
2
2
|
|
|
3
|
-
export { default as
|
|
4
|
-
export * from './impl/
|
|
3
|
+
export { default as BleDeviceController } from './impl/controllers/BleDeviceController.js'
|
|
4
|
+
export * from './impl/controllers/BleDeviceController.js'
|
|
5
5
|
|
|
6
|
-
export { default as
|
|
7
|
-
export * from './testDoubles/
|
|
6
|
+
export { default as FakeBleController } from './testDoubles/BleController/FakeBleController.js'
|
|
7
|
+
export * from './testDoubles/BleController/FakeBleController.js'
|
|
8
|
+
|
|
9
|
+
export { default as SpyBleController } from './testDoubles/BleController/SpyBleController.js'
|
|
10
|
+
export * from './testDoubles/BleController/SpyBleController.js'
|
|
8
11
|
|
|
9
12
|
// EventMarkerEmitter
|
|
10
13
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import koffi from 'koffi'
|
|
2
|
+
|
|
3
|
+
import BleDeviceController from '../impl/controllers/BleDeviceController.js'
|
|
4
|
+
|
|
5
|
+
const MUSE_CHARACTERISTIC_UUIDS: Record<string, string> = {
|
|
6
|
+
CONTROL: '273E0001-4C4D-454D-96BE-F03BAC821358',
|
|
7
|
+
TELEMETRY: '273E000B-4C4D-454D-96BE-F03BAC821358',
|
|
8
|
+
GYROSCOPE: '273E0009-4C4D-454D-96BE-F03BAC821358',
|
|
9
|
+
ACCELEROMETER: '273E000A-4C4D-454D-96BE-F03BAC821358',
|
|
10
|
+
PPG_AMBIENT: '273E000F-4C4D-454D-96BE-F03BAC821358',
|
|
11
|
+
PPG_INFRARED: '273E0010-4C4D-454D-96BE-F03BAC821358',
|
|
12
|
+
PPG_RED: '273E0011-4C4D-454D-96BE-F03BAC821358',
|
|
13
|
+
EEG_TP9: '273E0003-4C4D-454D-96BE-F03BAC821358',
|
|
14
|
+
EEG_AF7: '273E0004-4C4D-454D-96BE-F03BAC821358',
|
|
15
|
+
EEG_AF8: '273E0005-4C4D-454D-96BE-F03BAC821358',
|
|
16
|
+
EEG_TP10: '273E0006-4C4D-454D-96BE-F03BAC821358',
|
|
17
|
+
EEG_AUX: '273E0007-4C4D-454D-96BE-F03BAC821358',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CONTROL_UUID = MUSE_CHARACTERISTIC_UUIDS['CONTROL']
|
|
21
|
+
|
|
22
|
+
const charCallbacks = Object.entries(MUSE_CHARACTERISTIC_UUIDS).map(
|
|
23
|
+
([name, uuid]) => {
|
|
24
|
+
return {
|
|
25
|
+
charUuid: uuid,
|
|
26
|
+
charName: name,
|
|
27
|
+
onData: (data: Buffer, length: number, timestampSec: number) => {
|
|
28
|
+
const bytes = koffi.decode(data, 'uint8', length) as number[]
|
|
29
|
+
console.info(`[${timestampSec}] length=${length}`, bytes)
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const muse = await BleDeviceController.Create({
|
|
36
|
+
deviceNamePrefix: 'Muse',
|
|
37
|
+
charCallbacks,
|
|
38
|
+
rssiIntervalMs: 1000,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
await muse.connect()
|
|
42
|
+
|
|
43
|
+
for (const cmd of ['h', 'p50', 's', 'd']) {
|
|
44
|
+
await muse.writeCharacteristic(CONTROL_UUID, cmd)
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 5 * 1000))
|
|
49
|
+
|
|
50
|
+
await muse.disconnect()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import koffi from 'koffi'
|
|
2
|
+
|
|
3
|
+
import BleDeviceController from '../impl/controllers/BleDeviceController.js'
|
|
4
|
+
|
|
5
|
+
const MUSE_CHARACTERISTIC_UUIDS: Record<string, string> = {
|
|
6
|
+
CONTROL: '273E0001-4C4D-454D-96BE-F03BAC821358',
|
|
7
|
+
TELEMETRY: '273E000B-4C4D-454D-96BE-F03BAC821358',
|
|
8
|
+
GYROSCOPE: '273E0009-4C4D-454D-96BE-F03BAC821358',
|
|
9
|
+
ACCELEROMETER: '273E000A-4C4D-454D-96BE-F03BAC821358',
|
|
10
|
+
PPG_AMBIENT: '273E000F-4C4D-454D-96BE-F03BAC821358',
|
|
11
|
+
PPG_INFRARED: '273E0010-4C4D-454D-96BE-F03BAC821358',
|
|
12
|
+
PPG_RED: '273E0011-4C4D-454D-96BE-F03BAC821358',
|
|
13
|
+
EEG_TP9: '273E0003-4C4D-454D-96BE-F03BAC821358',
|
|
14
|
+
EEG_AF7: '273E0004-4C4D-454D-96BE-F03BAC821358',
|
|
15
|
+
EEG_AF8: '273E0005-4C4D-454D-96BE-F03BAC821358',
|
|
16
|
+
EEG_TP10: '273E0006-4C4D-454D-96BE-F03BAC821358',
|
|
17
|
+
EEG_AUX: '273E0007-4C4D-454D-96BE-F03BAC821358',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CONTROL_UUID = MUSE_CHARACTERISTIC_UUIDS['CONTROL']
|
|
21
|
+
|
|
22
|
+
const charCallbacks = Object.entries(MUSE_CHARACTERISTIC_UUIDS).map(
|
|
23
|
+
([name, uuid]) => {
|
|
24
|
+
return {
|
|
25
|
+
charUuid: uuid,
|
|
26
|
+
charName: name,
|
|
27
|
+
onData: (data: Buffer, length: number, timestampSec: number) => {
|
|
28
|
+
const bytes = koffi.decode(data, 'uint8', length) as number[]
|
|
29
|
+
console.info(`[${timestampSec}] length=${length}`, bytes)
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const muse = await BleDeviceController.Create({
|
|
36
|
+
deviceUuid: 'CA6A61B7-B7A8-AF24-3C9E-04A6A5012554',
|
|
37
|
+
charCallbacks,
|
|
38
|
+
rssiIntervalMs: 1000,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
await muse.connect()
|
|
42
|
+
|
|
43
|
+
for (const cmd of ['h', 'p50', 's', 'd']) {
|
|
44
|
+
await muse.writeCharacteristic(CONTROL_UUID, cmd)
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 5 * 1000))
|
|
49
|
+
|
|
50
|
+
await muse.disconnect()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import generateId from '@neurodevs/generate-id'
|
|
2
|
+
import {
|
|
3
|
+
BleController,
|
|
4
|
+
BleControllerOptions,
|
|
5
|
+
} from '../../impl/controllers/BleDeviceController.js'
|
|
6
|
+
|
|
7
|
+
export default class FakeBleController implements BleController {
|
|
8
|
+
public static callsToConstructor: (BleControllerOptions | undefined)[] = []
|
|
9
|
+
|
|
10
|
+
public static numCallsToConnect = 0
|
|
11
|
+
|
|
12
|
+
public static callsToWriteCharacteristic: {
|
|
13
|
+
characteristicUuid: string
|
|
14
|
+
value: string
|
|
15
|
+
}[] = []
|
|
16
|
+
|
|
17
|
+
public static numCallsToDisconnect = 0
|
|
18
|
+
|
|
19
|
+
public static fakeCharacteristics: Record<string, unknown> = {}
|
|
20
|
+
public static fakeName = `fake-${generateId()}`
|
|
21
|
+
|
|
22
|
+
private _uuid: string
|
|
23
|
+
private _name: string
|
|
24
|
+
|
|
25
|
+
public constructor(options?: BleControllerOptions) {
|
|
26
|
+
const { deviceUuid } = options ?? {}
|
|
27
|
+
|
|
28
|
+
this._uuid = deviceUuid ?? generateId()
|
|
29
|
+
this._name = FakeBleController.fakeName
|
|
30
|
+
|
|
31
|
+
FakeBleController.callsToConstructor.push(options)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async connect() {
|
|
35
|
+
FakeBleController.numCallsToConnect++
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async writeCharacteristic(
|
|
39
|
+
characteristicUuid: string,
|
|
40
|
+
value: string
|
|
41
|
+
) {
|
|
42
|
+
FakeBleController.callsToWriteCharacteristic.push({
|
|
43
|
+
characteristicUuid,
|
|
44
|
+
value,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public async disconnect() {
|
|
49
|
+
FakeBleController.numCallsToDisconnect++
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public get uuid() {
|
|
53
|
+
return this._uuid
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public get name() {
|
|
57
|
+
return this._name
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public static resetTestDouble() {
|
|
61
|
+
this.callsToConstructor = []
|
|
62
|
+
this.numCallsToConnect = 0
|
|
63
|
+
this.callsToWriteCharacteristic = []
|
|
64
|
+
this.numCallsToDisconnect = 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CallToBleControllerConstructor {
|
|
69
|
+
options: BleControllerOptions
|
|
70
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import BleDeviceController, {
|
|
2
|
+
BleControllerOptions,
|
|
3
|
+
} from '../../impl/controllers/BleDeviceController.js'
|
|
4
|
+
|
|
5
|
+
export default class SpyBleController extends BleDeviceController {
|
|
6
|
+
public infoLogs: string[] = []
|
|
7
|
+
public warnLogs: string[] = []
|
|
8
|
+
public errorLogs: string[] = []
|
|
9
|
+
|
|
10
|
+
public constructor(options: BleControllerOptions) {
|
|
11
|
+
super(options)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public getCharacteristicCallbacks() {
|
|
15
|
+
return this.charCallbacks
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public getRssiIntervalMs() {
|
|
19
|
+
return this.rssiIntervalMs
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public getConnected() {
|
|
23
|
+
return this.connected
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public setLogInfoSpy() {
|
|
27
|
+
this.infoLogs = []
|
|
28
|
+
|
|
29
|
+
this.log.info = (...args: unknown[]) => {
|
|
30
|
+
const message = args.join(' ')
|
|
31
|
+
this.infoLogs.push(message)
|
|
32
|
+
return message
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public setLogWarnSpy() {
|
|
37
|
+
this.warnLogs = []
|
|
38
|
+
|
|
39
|
+
this.log.warn = (...args: unknown[]) => {
|
|
40
|
+
const message = args.join(' ')
|
|
41
|
+
this.warnLogs.push(message)
|
|
42
|
+
return message
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public setLogErrorSpy() {
|
|
47
|
+
this.errorLogs = []
|
|
48
|
+
|
|
49
|
+
this.log.error = (...args: unknown[]) => {
|
|
50
|
+
const message = args.join(' ')
|
|
51
|
+
this.errorLogs.push(message)
|
|
52
|
+
return message
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public resetTestDouble() {
|
|
57
|
+
this.infoLogs = []
|
|
58
|
+
this.warnLogs = []
|
|
59
|
+
this.errorLogs = []
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -13,7 +13,7 @@ export default class FakeStreamOutlet implements StreamOutlet {
|
|
|
13
13
|
|
|
14
14
|
public static callsToPushSample: {
|
|
15
15
|
sample: LslSample
|
|
16
|
-
|
|
16
|
+
timestampSec?: number
|
|
17
17
|
}[] = []
|
|
18
18
|
|
|
19
19
|
public static numCallsToDestroy = 0
|
|
@@ -29,8 +29,11 @@ export default class FakeStreamOutlet implements StreamOutlet {
|
|
|
29
29
|
FakeStreamOutlet.numCallsToDestroy++
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
public pushSample(sample: LslSample,
|
|
33
|
-
FakeStreamOutlet.callsToPushSample.push({
|
|
32
|
+
public pushSample(sample: LslSample, timestampSec?: number) {
|
|
33
|
+
FakeStreamOutlet.callsToPushSample.push({
|
|
34
|
+
sample,
|
|
35
|
+
timestampSec,
|
|
36
|
+
})
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
public get name() {
|
package/tsconfig.json
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"./src/*.ts",
|
|
26
|
-
"./src/**/*.ts"
|
|
27
|
-
]
|
|
28
|
-
}
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "nodenext",
|
|
4
|
+
"target": "esnext",
|
|
5
|
+
"lib": ["esnext"],
|
|
6
|
+
"types": ["node"],
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"outDir": "build",
|
|
9
|
+
"sourceMap": false,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noImplicitAny": true,
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noUnusedLocals": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"moduleDetection": "force",
|
|
19
|
+
"allowJs": true,
|
|
20
|
+
"resolveJsonModule": true,
|
|
21
|
+
"experimentalDecorators": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["./src/*.ts", "./src/**/*.ts"]
|
|
24
|
+
}
|