LSL Update#204
Open
jdpigeon wants to merge 23 commits into
Open
Conversation
Contributor
Author
|
Still need to test with an actual Muse and Crown, but things are looking pretty solid |
teonbrooks
reviewed
Apr 19, 2026
teonbrooks
left a comment
Collaborator
There was a problem hiding this comment.
I will give this a full pass at the beginning of next week. added some quick comments and a few questions.
I am liking how this is shaping up!
Deletes cortex.js and emotiv.ts entirely. Removes all Emotiv branches from device epics, experiment epics, pyodide epics, components, and constants. DEVICES.EMOTIV, EMOTIV_CHANNELS, parseEmotivSignalQuality, and Cortex credential env vars are all gone. Muse is now the only supported device, laying the groundwork for LSL-based connectivity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Electron 22+ no longer shows a native Bluetooth picker automatically. Instead it fires select-bluetooth-device on webContents, requiring the main process to call the callback with a deviceId. Without this handler requestDevice() hung silently, leaving the search in a perpetual SEARCHING state. Changes: - main/index.ts: register select-bluetooth-device handler that auto-selects the first Muse headset as BLE discovery progresses; add bluetooth:cancelSearch IPC handler so the renderer can reject a pending requestDevice() on timeout - preload/index.ts: expose cancelBluetoothSearch() to renderer - muse.ts: cache BluetoothDevice from getMuse() so connectToMuse() reuses it instead of firing a redundant requestDevice() call; add cancelMuseScan() - deviceEpics.ts: call cancelMuseScan() in searchTimerEpic so the pending requestDevice() promise is cleaned up when the 3s search window expires - docs/device-connectivity.md: full connectivity flow diagram and bug analysis Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In React 18, setState is always batched — calling setState in an async
componentDidMount continuation schedules a re-render but does not
immediately commit the DOM change. The subsequent querySelector('webview')
therefore returned null, the dom-ready listener was never attached, and
subscribeToObservable was never called.
Fix: defer webview setup to componentDidUpdate, triggered when viewerUrl
transitions from empty to set. At that point React has already committed
the DOM update, so the webview element exists. Because componentDidUpdate
runs synchronously before the browser event loop can process the webview
load, the dom-ready listener is in place before it fires.
This fixes signal not flowing on the Explore EEG screen when navigating
to it while already connected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Lab Streaming Layer support so BrainWaves can publish EEG and stimulus markers as LSL streams and ingest data from external LSL devices. Enables LabRecorder integration and multi-device experiments. Phase 1: Main-process LSLOutletManager + IPC bridge forwards batched Muse EEG samples as an LSL outlet. Adds @shared alias, asarUnpack for native bindings, and fixes MUSE_CHANNELS hardcoding in experimentEpics. Phase 2: Neurosity Crown SDK support — getNeurosity/connectToNeurosity mirror the Muse driver; deviceEpics route by deviceType. Phase 3: LSLInletManager + UI to discover and connect to external LSL streams. lslForwardEpic skips LSL inlet sources to avoid feedback loops. Phase 4: RunComponent emits stimulus markers via sendLSLMarker alongside the existing injectMuseMarker call, preserving the CSV-embedded marker path used by the Pyodide analysis pipeline. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Subscribes to lsl:status IPC at the App level and surfaces errors via react-toastify. Completes Phase 5 production hardening (decimation, BLE disconnect detection, error surfacing). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
node-labstreaminglayer 0.3.0 only ships an x86_64 liblsl.dylib in its prebuild dir, which fails to load on arm64 Macs. patchDeps.mjs now detects darwin-arm64 and symlinks the Homebrew-installed framework binary over the bundled stub. No-op on x64 macs, Linux, and Windows. Requires: brew install labstreaminglayer/tap/lsl Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two related fixes that together unbreak Pyodide in production: 1. Protocol handler in main was looking at resources/webworker/src/ but electron-builder copies pyodide assets to resources/pyodide/. Update pyodideRoot to match the actual extraResources destination. 2. Worker was relying on import.meta.url to find pyodide.asm.wasm and python_stdlib.zip relative to pyodide.mjs. That works in dev (Vite middleware serves siblings from node_modules) but fails in prod where the bundled .mjs has no siblings. Set indexURL so pyodide fetches runtime files through the pyodide:// protocol handler — works in both. Verified by installing the packaged dmg and running test plot. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Replace stale port-17173 http-server section with current pyodide:// protocol handler reality - Document the prod resourcesPath/pyodide/ extraResources destination - Add indexURL requirement for prod (siblings of pyodide.mjs aren't bundled, so import.meta.url resolution fails) — gotcha hit during packaging verification Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The .worktrees/modernization entry was an accidental submodule-style gitlink (mode 160000) pointing into a local git worktree. There is no .gitmodules and the worktree is not part of the repo tree, so it only produced perpetual 'modified' noise in git status. Untrack it and ignore .worktrees/ going forward. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
node-labstreaminglayer dlopen's liblsl at require time, so a static import crashed the whole app on startup when liblsl was missing/ incompatible (e.g. Apple Silicon without the Homebrew build) — even for Muse-only users who never need LSL. Load the native bindings lazily and fail soft so LSL becomes a true advanced, opt-in feature: - src/main/lsl/native.ts: guarded require() in try/catch (memoized), exposing loadLSL() and isLSLAvailable() - outlets.ts/inlets.ts: type-only imports + loadLSL() at call time; all ops no-op gracefully when liblsl is unavailable - lsl:isAvailable IPC + preload bridge for renderer feature detection - ConnectModal hides 'External LSL stream' when unavailable - lslBridge no-ops sendEpoch/sendMarker when unavailable (no IPC spam) Result: Muse/Neurosity work with zero LSL/LabRecorder dependency; LSL features appear only where liblsl loads. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Lab Streaming Layer (LSL) support to BrainWaves, enabling:
Architecture
Test plan