KNeuron is a modular desktop platform for EEG/BCI experiments developed for a scientific student project.
The application combines multiple brain-computer interface workflows in one desktop interface and separates:
- hardware communication,
- EEG/brain-metric acquisition,
- signal processing and classification,
- interactive modules,
- visualization,
- device-specific integrations.
The current application contains three production modules:
- Cortex 3D — live EEG visualization on an interactive 3D brain,
- TaaLON Miner — SSVEP control using FBCCA,
- Neuorrun — attention-based interaction using BrainLink Lite / ThinkGear metrics.
- Tauri 2
- React
- TypeScript
- Vite
- Three.js / WebGL
- Unity WebGL for Neuorrun
- Python sidecars
- NumPy
- SciPy
- scikit-learn
- BrainAccess MAXI 009
- BrainLink Lite BL002 V2.0
KNeuron is designed so that application modules do not communicate directly with hardware.
KNeuron
│
DeviceManager
┌─────────┴─────────┐
│ │
BrainAccess MAXI 009 BrainLink Lite
│ │
raw EEG ThinkGear
│ │
EEGStreamService BrainMetricsService
│ │
┌───────┴────────┐ │
│ │ │
Cortex 3D TaaLON Miner │
│ │
FBCCA Neuorrun
The key architectural rule is:
Modules consume normalized application-level data and do not depend on hardware-native channel indexes, Bluetooth implementation details, COM ports, or vendor SDK internals.
BrainAccess is used as the raw EEG device for Cortex 3D and TaaLON Miner.
Current normalized 32-channel layout:
0 AF3
1 AFz
2 AF4
3 F7
4 F3
5 Fz
6 F4
7 F8
8 FC5
9 FC1
10 FC2
11 FC6
12 T7
13 C3
14 Cz
15 C4
16 T8
17 CP5
18 CP1
19 CP2
20 CP6
21 P7
22 P3
23 Pz
24 P4
25 P8
26 PO3
27 POz
28 PO4
29 O1
30 Oz
31 O2
The physical cap uses:
Fp1 = REF
Fp2 = BIAS
Therefore these two positions are not exposed as EEG measurement channels.
BrainAccess communication is handled by a Python sidecar:
BrainAccess MAXI 009
↓
BrainAccess Python SDK
↓
brainaccess-bridge
↓
BrainAccessEEGAdapter
↓
DeviceManager
↓
EEGStreamService
Neuorrun uses BrainLink Lite and native ThinkGear/eSense metrics.
The relevant values are:
attention
meditation
poorSignalLevel
signalQualityPercent
The application does not use FBCCA for Neuorrun.
BrainLink Lite
↓
Bluetooth serial / COM or RFCOMM
↓
brainlink-bridge
↓
BrainLinkAdapter
↓
BrainMetricsService
↓
Neuorrun
The UI presents signal quality as a percentage derived from poorSignalLevel.
Cortex 3D visualizes live EEG activity on an interactive 3D brain.
Main features:
- live BrainAccess EEG,
- interactive cortex,
- electrode visualization,
- band filtering,
- FAST activity,
- delta / theta / alpha / beta / gamma band power,
- calibration,
- filter warm-up,
- frozen baseline,
- robust median/MAD normalization,
- artifact telemetry,
- editable electrode positions,
- LOW / MEDIUM / HIGH mesh quality,
- persistent electrode layout.
raw EEG
↓
filtering
↓
feature extraction
↓
baseline normalization
↓
relative activity
↓
3D cortex visualization
The baseline is intentionally frozen after calibration so the visualization continues to represent deviation from the initial session state rather than adapting the reference continuously.
TaaLON Miner is an SSVEP-controlled game.
The module requests only posterior channels:
POz
PO3
PO4
Oz
O1
O2
The BrainAccess device itself still streams the full 32-channel EEG.
UP 10.25 Hz
LEFT 13.75 Hz
RIGHT 14.25 Hz
DOWN 14.75 Hz
EEG trial
↓
DC removal
↓
5 filter-bank subbands
↓
CCA against target references
↓
5 harmonics
↓
weighted squared canonical correlations
↓
argmax
↓
UP / LEFT / RIGHT / DOWN
The classifier is implemented in Python and uses NumPy, SciPy and scikit-learn CCA.
Neuorrun is the original Unity game integrated into KNeuron as a WebGL module.
The game is controlled by BrainLink attention.
BrainLink
↓
attention
↓
threshold
↓
interaction
The Unity project is built locally to WebGL and copied into:
public/neuorrun/
KNeuron injects BrainLink metrics into Unity through:
KNeuron React module
↓
Unity SendMessage
↓
KNeuronBridge.cs
↓
TGCConnectionController
↓
original game Controller
No FBCCA is used in Neuorrun.
KNeuronInterFace/
│
├── brainaccess-sidecar/
├── brainlink-sidecar/
├── ssvep-sidecar/
│
├── unity-patch/
│ └── prepare_webgl_build.py
│
├── public/
│ ├── modules/
│ │ └── cortex/
│ └── neuorrun/
│ └── Build/
│
├── src/
│ ├── core/
│ │ ├── devices/
│ │ ├── eeg/
│ │ ├── brainMetrics/
│ │ └── ssvep/
│ ├── features/
│ ├── modules/
│ │ ├── cortex/
│ │ ├── miner/
│ │ └── neuorrun/
│ └── styles/
│
├── src-tauri/
│ ├── binaries/
│ ├── capabilities/
│ ├── src/
│ └── tauri.conf.json
│
├── setup-and-run.ps1
├── setup-and-run.sh
├── package.json
├── package-lock.json
├── vite.config.ts
├── .gitignore
└── README.md
KNeuron includes bootstrap scripts for both Windows and Linux.
The goal is that after cloning the repository, the development environment and Python sidecars can be recreated locally instead of storing generated dependencies and build artifacts in Git.
git clone <REPOSITORY_URL>
cd KNeuronInterFacepowershell -ExecutionPolicy Bypass -File .\setup-and-run.ps1The script checks or installs the required development environment and then builds the local sidecars.
It handles:
Node.js / npm
Python
Rust / Cargo
Visual Studio C++ Build Tools
npm dependencies
BrainAccess sidecar
SSVEP classifier sidecar
BrainLink sidecar
After setup it verifies that Tauri sidecar binaries exist and starts:
npm run tauri:devpowershell -ExecutionPolicy Bypass -File .\setup-and-run.ps1 -NoLaunchThen start manually:
npm run tauri:devThe bootstrap expects:
- Windows 10/11,
- internet access during first setup,
winget/ Microsoft App Installer,- permission to install development tools.
The first build can take significantly longer because Node packages, Rust dependencies and Python environments are created from scratch.
The included Linux bootstrap currently targets Ubuntu/Debian-family distributions.
git clone <REPOSITORY_URL>
cd KNeuronInterFacechmod +x setup-and-run.sh./setup-and-run.shThe script installs/checks:
Tauri Linux system dependencies
Node.js / npm
Python 3
Rust / Cargo
Bluetooth utilities
Python virtual environments
all three Python sidecars
It also checks device permissions used by serial/Bluetooth devices.
./setup-and-run.sh --no-launchIf system dependencies are already installed:
./setup-and-run.sh --skip-systemIf the script adds the current user to:
dialout
log out and log back in before using serial/RFCOMM devices.
The BrainAccess Python SDK is the component most likely to require platform-specific verification. The bootstrap can rebuild the sidecar only if the BrainAccess SDK/dependency used by brainaccess-sidecar/requirements.txt is available for the target Linux environment.
The repository intentionally does not need to store every generated dependency.
After bootstrap, the local machine may contain:
node_modules/
src-tauri/target/
brainaccess-sidecar/.venv/
brainaccess-sidecar/build/
brainaccess-sidecar/dist/
ssvep-sidecar/.venv/
ssvep-sidecar/build/
ssvep-sidecar/dist/
brainlink-sidecar/.venv/
brainlink-sidecar/build/
brainlink-sidecar/dist/
src-tauri/binaries/*
These directories/files can be regenerated and should generally not be treated as source code.
Use:
# =========================================================
# KNeuron — .gitignore
# =========================================================
# ---------------------------------------------------------
# Node / React
# ---------------------------------------------------------
node_modules/
dist/
# ---------------------------------------------------------
# Rust / Tauri
# ---------------------------------------------------------
src-tauri/target/
# ---------------------------------------------------------
# Python
# ---------------------------------------------------------
**/.venv/
**/venv/
**/__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
.mypy_cache/
.ruff_cache/
# ---------------------------------------------------------
# PyInstaller build artifacts
# ---------------------------------------------------------
brainaccess-sidecar/build/
brainaccess-sidecar/dist/
brainlink-sidecar/build/
brainlink-sidecar/dist/
ssvep-sidecar/build/
ssvep-sidecar/dist/
# ---------------------------------------------------------
# Generated Tauri sidecar binaries
# ---------------------------------------------------------
src-tauri/binaries/*
!src-tauri/binaries/.gitkeep
# ---------------------------------------------------------
# IDE / editor files
# ---------------------------------------------------------
.vscode/
.idea/
# ---------------------------------------------------------
# Operating system files
# ---------------------------------------------------------
.DS_Store
Thumbs.db
desktop.ini
# ---------------------------------------------------------
# Logs / temporary files
# ---------------------------------------------------------
*.log
*.tmp
*.temp
# ---------------------------------------------------------
# Miscellaneous caches
# ---------------------------------------------------------
.cache/Contains installed npm packages.
It can always be recreated from:
package.json
package-lock.json
with:
npm ciIt should not be committed because it is large, platform-dependent and generated.
Contains Rust/Tauri compilation output:
debug builds
release builds
incremental compilation cache
compiled dependencies
temporary linker artifacts
This directory can grow to several gigabytes.
It is regenerated by Cargo/Tauri and is not source code.
Python virtual environments contain copies of the Python interpreter and installed packages.
They are platform-specific and can be rebuilt from each sidecar's:
requirements.txt
These are PyInstaller output directories.
They can contain:
.pkg
.pyz
.toc
temporary compiled Python files
generated executables
They are build artifacts, not application source.
!src-tauri/binaries/.gitkeep`
The platform-specific sidecar executables can be rebuilt locally by the bootstrap/build scripts:
brainaccess-bridge
ssvep-classifier
brainlink-bridge
Keeping generated sidecar binaries out of the repository substantially reduces repository size and avoids mixing Windows and Linux artifacts in Git.
src-tauri/binaries/.gitkeep may be committed only to preserve the otherwise-empty directory.
Files such as:
.vscode/
.idea/
.DS_Store
Thumbs.db
desktop.ini
describe a local editor or operating system and are not required to build KNeuron.
Logs, temporary files and caches contain no source-of-truth project state and should not be versioned.
Do not ignore the following:
package.json
package-lock.json
src/
src-tauri/src/
src-tauri/Cargo.toml
src-tauri/Cargo.lock
src-tauri/tauri.conf.json
src-tauri/capabilities/
brainaccess-sidecar/*.py
brainaccess-sidecar/*.spec
brainaccess-sidecar/requirements.txt
brainaccess-sidecar/build.ps1
ssvep-sidecar/*.py
ssvep-sidecar/*.spec
ssvep-sidecar/requirements.txt
ssvep-sidecar/build.ps1
brainlink-sidecar/*.py
brainlink-sidecar/*.spec
brainlink-sidecar/requirements.txt
brainlink-sidecar/build.ps1
public/modules/cortex/*.glb
public/neuorrun/Build/
public/neuorrun/manifest.json
unity-patch/
setup-and-run.ps1
setup-and-run.sh
README.md
.gitignore
Lock files make dependency resolution reproducible across machines.
PyInstaller spec files describe how sidecar executables are packaged.
They are part of the reproducible build definition.
The LOW / MEDIUM / HIGH brain meshes are runtime application assets, not generated caches.
The current KNeuron repository contains the built Unity WebGL runtime so a developer cloning KNeuron does not also need the complete Unity project and Unity Editor just to run Neuorrun.
The relevant runtime files include:
*.data
*.wasm
*.framework.js
*.loader.js
If Neuorrun source is later maintained in a separate repository and built automatically in CI, this policy can be changed.
To list all tracked or untracked files that are not ignored:
git ls-files -co --exclude-standard$files = git ls-files -co --exclude-standard
$total = 0
foreach ($file in $files) {
if (Test-Path $file -PathType Leaf) {
$total += (Get-Item $file).Length
}
}
[PSCustomObject]@{
MB = [math]::Round($total / 1MB, 2)
GB = [math]::Round($total / 1GB, 3)
}$files = git ls-files -co --exclude-standard
$files |
Where-Object { Test-Path $_ -PathType Leaf } |
ForEach-Object {
$item = Get-Item $_
[PSCustomObject]@{
File = $_
MB = [math]::Round($item.Length / 1MB, 2)
}
} |
Sort-Object MB -Descending |
Select-Object -First 30 |
Format-Table -AutoSizegit count-objects -vHThis is useful because deleting a large file from the current working tree does not automatically remove it from old Git history.
The bootstrap scripts are the preferred setup method.
If dependencies are already installed, frontend packages can be installed manually:
npm ciRun the application:
npm run tauri:devBefore committing or preparing a release:
npm run format
npm run typecheck
npm test
npm run lintIf defined:
npm run qualityKNeuron uses three production sidecars:
brainaccess-bridge
ssvep-classifier
brainlink-bridge
Tauri's externalBin configuration references names without a platform target suffix:
"externalBin": [
"binaries/brainaccess-bridge",
"binaries/ssvep-classifier",
"binaries/brainlink-bridge"
]On Windows, generated files use names similar to:
brainaccess-bridge-x86_64-pc-windows-msvc.exe
ssvep-classifier-x86_64-pc-windows-msvc.exe
brainlink-bridge-x86_64-pc-windows-msvc.exe
On a typical x86_64 Linux machine:
brainaccess-bridge-x86_64-unknown-linux-gnu
ssvep-classifier-x86_64-unknown-linux-gnu
brainlink-bridge-x86_64-unknown-linux-gnu
The bootstrap scripts create/copy these locally into:
src-tauri/binaries/
On Windows:
cd .\brainlink-sidecar
.\.venv\Scripts\python.exe .\diagnose.pyA valid connection should produce changing values such as:
attention
meditation
poorSignalLevel
signalQualityPercent
If BrainLink is connected to a phone/tablet, disconnect it there before attempting to connect from KNeuron.
The repository normally keeps the generated WebGL runtime in:
public/neuorrun/Build/
so Unity is not required on every development machine.
If rebuilding Neuorrun is required, use the original Unity project.
Unity 2022.3.7f1
with WebGL Build Support.
Compression Format: Disabled
Data Caching: Disabled
Threads: Disabled
Build/*.loader.js
Build/*.framework.js
Build/*.data
Build/*.wasm
Windows example:
python .\unity-patch\prepare_webgl_build.py `
"C:\Users\<user>\Desktop\NeuorrunWebGL" `
".\public\neuorrun"After copying, verify:
public/neuorrun/manifest.json
contains:
{
"ready": true
}Neuorrun is loaded into a canvas rather than an iframe.
Relevant directives:
script-src 'self' 'wasm-unsafe-eval'
worker-src 'self' blob:
frame-src 'none'
After setup on a new machine:
1. Start KNeuron.
2. Connect BrainAccess.
3. Open Cortex 3D.
4. Verify live EEG.
5. Exit Cortex.
6. Open TaaLON Miner.
7. Run an SSVEP trial.
8. Exit Miner.
9. Disconnect BrainAccess.
10. Connect BrainLink Lite.
11. Open Neuorrun.
12. Verify ATTENTION / MEDITATION / SIGNAL QUALITY.
13. Verify attention-driven interaction.
14. Exit Neuorrun.
15. Enter Neuorrun again.
16. Close KNeuron.
The application should not require a restart while switching between modules.
The production Dashboard should contain:
Cortex 3D
TaaLON Miner
Neuorrun
The production Device screen should contain:
BrainAccess MAXI 009
BrainLink Lite
Development/test modules and Simulation EEG should not be registered in the production application.
npm run tauri:buildor:
npx tauri buildWindows NSIS output is typically created under:
src-tauri/target/release/bundle/nsis/
Linux package output is created by Tauri under the corresponding bundle directories for the configured Linux targets.
Verify:
package.json
index.html
exist in the repository root.
Test Vite separately:
npm run devRerun the bootstrap:
powershell -ExecutionPolicy Bypass -File .\setup-and-run.ps1 -NoLaunch./setup-and-run.sh --no-launchThen inspect:
src-tauri/binaries/
Check COM ports:
[System.IO.Ports.SerialPort]::GetPortNames()and:
Get-CimInstance Win32_SerialPort |
Select-Object DeviceID, Name, DescriptionA port can be forced before starting KNeuron:
$env:KNEURON_BRAINLINK_PORT="COM7"
npm run tauri:devReplace COM7 with the actual outgoing Bluetooth COM port.
Verify membership:
groupsIf dialout is missing:
sudo usermod -aG dialout "$USER"then log out and log back in.
Also verify Bluetooth:
rfkill list bluetooth
systemctl status bluetoothVerify:
public/neuorrun/Build/
public/neuorrun/manifest.json
If missing, rebuild/copy the Unity WebGL output.
First verify that ATTENTION changes in the KNeuron telemetry.
Then verify the Unity integration contains:
KNeuronBridge.cs
patched TGCConnectionController.cs
The bridge GameObject must be named:
KNeuronBridge
and the Unity receiver method must be:
SetMetricsJson
This section is the canonical guide for extending KNeuron with new modules, devices and sidecars.
The central rule is:
hardware / vendor SDK
↓
device adapter / bridge
↓
DeviceManager
↓
application service
↓
module
↓
UI
A module consumes normalized KNeuron APIs. It must not know how a specific manufacturer transports or indexes the data.
Production modules must not:
open COM/RFCOMM ports directly
call BrainAccess or another vendor SDK directly
spawn hardware sidecars directly
use physical/vendor EEG channel indexes
start a second independent hardware EEG stream
own the global active-device state
leave subscriptions/timers/render loops alive after unmount
Device adapters must not contain:
game logic
module-specific rendering
Dashboard/UI state
manufacturer-specific behavior exposed above the adapter boundary
Use this dependency direction:
module → service → adapter → bridge / SDK → hardware
Never make a core service depend on a concrete module.
The generic device contract is represented by DeviceAdapter.
The core responsibilities are:
info
getStatus()
connect()
disconnect()
subscribeStatus()
The exact TypeScript contract is the source of truth in the repository.
To locate it:
git grep -n "interface DeviceAdapter" src/core/devicesDeviceManager owns the application-level connection lifecycle and the currently active device.
Current architectural limitation:
KNeuron uses one active physical device at a time through
DeviceManager.
This does not prevent multiple modules from consuming one active EEG stream.
Raw EEG devices implement:
EEGDeviceAdapter
The canonical file is:
src/core/devices/contracts/EEGDeviceAdapter.ts
The EEG-specific operations used by the shared stream service are:
getStreamInfo()
startStream()
stopStream()
isStreaming()
subscribeSamples()
The existing EEGStreamService also uses:
adapter.info
adapter.getStatus()
The service lives at:
src/core/eeg/EEGStreamService.ts
Device guards live at:
src/core/devices/contracts/deviceGuards.ts
EEG models live at:
src/core/devices/models/eeg.ts
Channel-selection helpers live under:
src/core/devices/eeg/
Devices such as BrainLink expose already-derived metrics instead of a raw multichannel EEG stream.
The current production reference path is:
BrainLinkAdapter
↓
DeviceManager
↓
BrainMetricsService
↓
Neuorrun
Typical normalized metrics are:
attention
meditation
poorSignalLevel
signalQualityPercent
When implementing a similar device, reuse the existing BrainMetrics contract/service rather than exposing vendor packet details to a module.
Locate the exact current files with:
git grep -n "BrainLinkAdapter" src
git grep -n "BrainMetricsService" srcUse:
raw time-series EEG
→ EEGDeviceAdapter + EEGStreamService
derived attention/meditation-style values
→ BrainMetrics contract + BrainMetricsService
vendor SDK / Python scientific code / serial parser
→ sidecar below the adapter
Choose based on the data exposed to KNeuron, not the marketing category of the device.
Production modules should follow the existing module architecture.
Recommended structure:
src/modules/<module-id>/
├── <ModuleName>Module.tsx
├── <moduleName>Manifest.ts
├── <moduleName>ModuleDefinition.ts
├── components/
├── hooks/
├── services/
├── styles/
└── tests/
Not every module needs every subdirectory.
For example:
src/modules/neurofeedback/
├── NeurofeedbackModule.tsx
├── neurofeedbackManifest.ts
├── neurofeedbackModuleDefinition.ts
├── hooks/
│ └── useNeurofeedbackEEG.ts
└── styles/
└── neurofeedback.css
KNeuron module metadata is represented by KNeuronModuleManifest.
The existing contract contains the project-level module identity and capabilities.
A typical manifest follows this pattern:
import type {
KNeuronModuleManifest,
} from "../../types/module";
export const neurofeedbackManifest: KNeuronModuleManifest = {
schemaVersion: 1,
id: "neurofeedback",
name: "Neurofeedback",
version: "1.0.0",
description:
"Example EEG neurofeedback module.",
category: "VISUALIZATION",
appearance: {},
entryPoint: "/modules/neurofeedback",
capabilities: {
eeg: {
required: true,
},
},
};Use slug-style IDs:
neurofeedback
cortex-3d
ssvep-control
Avoid IDs containing spaces, underscores or display-name casing.
The current module component contract uses KNeuronModuleProps.
Example:
import type {
KNeuronModuleProps,
} from "../../features/modules/moduleDefinition";
export function NeurofeedbackModule({
onRequestClose,
}: KNeuronModuleProps) {
return (
<section>
<h1>Neurofeedback</h1>
<button
type="button"
onClick={onRequestClose}
>
Close
</button>
</section>
);
}A module component should contain module UI/orchestration, not device transport code.
The production module registry operates on complete definitions.
Example:
import {
NeurofeedbackModule,
} from "./NeurofeedbackModule";
import {
neurofeedbackManifest,
} from "./neurofeedbackManifest";
import type {
KNeuronModuleDefinition,
} from "../../features/modules/moduleDefinition";
export const neurofeedbackModuleDefinition:
KNeuronModuleDefinition = {
manifest: neurofeedbackManifest,
component: NeurofeedbackModule,
};The definition is the unit that binds:
manifest + React component
The built-in production registration entry point is:
src/features/modules/registerBuiltInModules.ts
Import the definition there:
import {
neurofeedbackModuleDefinition,
} from "../../modules/neurofeedback/neurofeedbackModuleDefinition";and include it in the returned/registered definitions using the same pattern as the existing Cortex, Miner and Neuorrun definitions.
To verify the current production registry:
git grep -n "CortexModule" src/features/modules src/modules
git grep -n "NeuorrunModule" src/features/modules src/modules
git grep -n "registerBuiltInModules" srcDo not add a hard-coded module card to:
App.tsx
Dashboard.tsx
ModuleHost.tsx
ModuleManager.ts
just because a new module was added.
The Dashboard should discover production modules through the canonical registry.
A generic EEG module:
capabilities: {
eeg: {
required: true,
},
}An SSVEP-style module that requires specific channels:
capabilities: {
eeg: {
required: true,
requiredChannels: [
"O1",
"O2",
"Oz",
"PO3",
"PO4",
"POz",
],
},
}Declare normalized EEG labels, not physical device indexes.
Allowed:
import {
eegStreamService,
} from "../../core/eeg";Not allowed:
import { BrainAccessEEGAdapter } from "...";
import { BrainAccessBridge } from "...";Acquire all channels:
const handle = await eegStreamService.acquire({
channels: "all",
});Acquire selected normalized channels:
const handle = await eegStreamService.acquire({
channels: ["O1", "Oz", "O2"],
});Use live batches:
const handle = await eegStreamService.acquire({
channels: ["O1", "Oz", "O2"],
onBatch: (batch) => {
// Consume normalized EEG data.
},
});Read a buffered historical window:
const window = eegStreamService.getLatestWindow(
2.0,
["O1", "Oz", "O2"],
);Do not create another global hardware ring buffer inside the module.
Every successful EEG acquisition must eventually be released.
A safe pattern for an asynchronous acquire is:
useEffect(() => {
let disposed = false;
let handle: EEGStreamHandle | null = null;
void (async () => {
try {
const acquired = await eegStreamService.acquire({
channels: ["O1", "Oz", "O2"],
onBatch: (batch) => {
if (disposed) {
return;
}
// Update module-specific processing/state.
},
});
if (disposed) {
await acquired.release();
return;
}
handle = acquired;
} catch (error) {
if (!disposed) {
// Surface module-level error state.
}
}
})();
return () => {
disposed = true;
if (handle) {
void handle.release();
}
};
}, []);The important invariant is:
every successful acquire() → one effective release()
The module must also handle the race where it unmounts before acquire() finishes.
For attention/meditation-style modules, copy the subscription lifecycle used by Neuorrun.
Locate it with:
git grep -n "BrainMetricsService" src/modules src/core
git grep -n "NeuorrunModule" src/modulesDo not connect to BrainLink or parse ThinkGear packets in the module.
On unmount, release:
EEGStreamHandle
brain-metrics subscription
classifier/event subscriptions
setInterval
setTimeout
requestAnimationFrame
DOM listeners
WebSocket/event listeners
module-owned Three.js resources
module-owned Unity/event hooks
For module-owned Three.js resources, dispose GPU resources that are no longer needed:
geometry.dispose()
material.dispose()
texture.dispose()
renderer.dispose() when the renderer itself is module-owned
At minimum test:
manifest validation
definition registration
unique module ID behavior
component mounting
component unmounting
module error boundary behavior
missing/incompatible device state
required EEG channels
EEG acquisition
EEG release on unmount
async acquire/unmount race
module re-entry
Manual flow:
Dashboard
→ module card visible
→ Open
→ module mounted
→ use core feature
→ Close/Back
→ resources released
→ open module again
→ works without application restart
Assume a new raw EEG device.
Recommended adapter structure:
src/core/devices/adapters/<device-slug>/
├── <DeviceName>Adapter.ts
├── <DeviceName>Bridge.ts # if a sidecar/native bridge is required
├── models.ts # optional device-private protocol types
└── <DeviceName>Adapter.test.ts
Example:
src/core/devices/adapters/openbci/
├── OpenBCIAdapter.ts
├── OpenBCIBridge.ts
└── OpenBCIAdapter.test.ts
For raw EEG:
export class OpenBCIAdapter
implements EEGDeviceAdapter
{
// ...
}For non-EEG devices, implement the appropriate generic/specialized contract instead.
The TypeScript interface is always the compiler-enforced source of truth.
Find existing implementations:
git grep -n "implements EEGDeviceAdapter" src/core/devicesExample:
readonly info = {
id: "openbci-cyton",
name: "OpenBCI Cyton",
kind: "eeg",
transport: "serial",
manufacturer: "OpenBCI",
model: "Cyton",
} as const;The ID should be:
stable
lowercase
unique
independent of COM port
independent of temporary Bluetooth address
The generic device lifecycle is:
disconnected
↓ connect
connecting
↓
connected
↓ disconnect
disconnecting
↓
disconnected
A failure should leave a clear recoverable error/disconnected state.
Status changes must propagate through the common status subscription mechanism.
Repeated disconnect/cleanup calls should be safe wherever practical.
For an EEG device, getStreamInfo() returns device-specific metadata such as:
{
sampleRateHz: 250,
channels: [
// normalized EEG channel descriptors
],
}Never assume globally that every EEG device has:
250 Hz
32 channels
the same electrodes
the same order
Each adapter reports its own stream characteristics.
A normalized channel can conceptually contain:
{
index: 0,
label: "O1",
type: "eeg",
unit: "uV",
sourceIndex: 11,
}Meaning:
index
= normalized KNeuron stream index
sourceIndex
= native/vendor source index
Modules use:
label / normalized index
Modules must never use:
sourceIndex
vendor array position
BrainAccess physical index
The current normalized EEG layout is:
values[channelIndex][sampleIndex]
A batch contains fields such as:
sequenceStart
timestampStartMs
sampleRateHz
sampleCount
channelCount
values
optional source/native sample-number information
If the hardware exposes a native monotonically increasing sample number, preserve it. It is useful for diagnosing dropped samples.
Once streaming starts, do not silently change:
sample rate
channel count
channel ordering
matrix orientation
EEGStreamService treats these as stream invariants.
The adapter must correctly implement the EEG methods required by the current interface:
getStreamInfo()
subscribeSamples()
startStream()
stopStream()
isStreaming()
subscribeSamples() must return an unsubscribe function.
startStream() should start the physical acquisition only once.
stopStream() must release streaming resources.
A module must never call these directly. EEGStreamService owns the shared physical stream lifecycle.
The built-in device registration entry point is:
src/core/devices/registerBuiltInDevices.ts
Add the new adapter using the same pattern as the production BrainAccess and BrainLink registrations.
Confirm the current composition with:
git grep -n "BrainAccessEEGAdapter" src/core/devices
git grep -n "BrainLinkAdapter" src/core/devices
git grep -n "registerBuiltInDevices" srcDo not modify these just because another manufacturer was added:
DeviceManager.ts
DevicePage.tsx
DevicePanel.tsx
EEGStreamService.ts
App.tsx
If one of those requires manufacturer-specific branching, the adapter abstraction is probably leaking.
Minimum tests:
initial status
metadata
connect
disconnect
status notifications
failed connect cleanup
reconnect
stream info
sample rate
channel map
stream cannot start in invalid state
stream start
stream stop
batch dimensions
sequence numbering
source sample numbering when available
subscribe/unsubscribe
disconnect while streaming
malformed bridge/vendor message
bridge process failure
Verify:
connect
disconnect
reconnect
start stream
stop stream
open Cortex / another consumer
leave module
open another consumer
close application while connected
lose Bluetooth/unplug hardware
recover and reconnect
A mocked unit test does not replace hardware validation.
Use a sidecar when a dependency should remain outside the Tauri/React process, for example:
Python scientific stack
vendor Python SDK
serial/RFCOMM parser
CPU-heavy classifier
native SDK wrapper
Do not use a sidecar for ordinary React/UI logic.
<name>-sidecar/
├── bridge.py
├── requirements.txt
├── <binary-name>.spec
├── build.ps1
├── diagnose.py # optional but recommended for hardware
└── tests/ # optional
The current production examples are:
brainaccess-sidecar/
ssvep-sidecar/
brainlink-sidecar/
Sidecars should use the same JSONL principle as the current bridges:
one JSON message per line
Example request:
{"id":17,"command":"status"}Example response:
{"id":17,"ok":true,"result":{"state":"connected"}}Example asynchronous event:
{"event":"samples","payload":{"sampleRateHz":250,"sampleCount":8}}The exact commands belong to the sidecar/TypeScript bridge protocol.
When stdout carries JSONL IPC:
stdout = protocol JSON only
stderr = diagnostic logs
Bad:
print("Connected!")
print(json.dumps(message))Good:
print("Connected!", file=sys.stderr)
print(json.dumps(message), flush=True)A single non-JSON diagnostic line on stdout can corrupt the protocol.
The TypeScript bridge should own:
sidecar spawn
stdin writes
stdout parsing
request correlation
async event routing
process errors
pending-request rejection
kill/shutdown
A React module should never spawn a hardware/classifier sidecar directly.
Commit:
requirements.txt
*.spec
build.ps1
source .py files
optional diagnose.py
Do not commit:
.venv/
build/
dist/
generated binary
Edit:
src-tauri/tauri.conf.json
and add the base binary name to externalBin.
Example:
"externalBin": [
"binaries/brainaccess-bridge",
"binaries/ssvep-classifier",
"binaries/brainlink-bridge",
"binaries/example-bridge"
]Do not add:
.exe
target triple
absolute path
to the externalBin base name.
Typical generated files are:
Windows:
example-bridge-x86_64-pc-windows-msvc.exe
Linux:
example-bridge-x86_64-unknown-linux-gnu
Review:
src-tauri/capabilities/
The sidecar must be included in the allowed spawn scope using the same pattern as the existing production sidecars.
Keep permissions narrow.
The current bridges require the equivalents of:
spawn
stdin write
kill
Do not authorize arbitrary shell commands simply to avoid defining the sidecar scope correctly.
A production sidecar is not fully integrated until a clean machine can recreate it.
Update:
setup-and-run.ps1
setup-and-run.sh
They must:
create/install the Python environment
install requirements
run the sidecar build
place/copy the target-specific binary into src-tauri/binaries/
verify that the expected binary exists
fail clearly when packaging fails
At minimum test:
valid request
unknown request
malformed JSON
SDK/serial exception
process EOF
process crash
shutdown
reconnect
multiple sequential requests
async event output
stderr logging
stdout protocol purity
For deterministic classifiers, keep test fixtures with expected results where possible.
The registry/composition layer is where code becomes part of the production application.
Do not create a second parallel registry.
Canonical entry point:
src/features/modules/registerBuiltInModules.ts
Procedure:
1. create manifest
2. create React component
3. create ModuleDefinition
4. import ModuleDefinition into registerBuiltInModules.ts
5. add it using the same production-registration pattern
6. verify unique ID
7. verify Dashboard card appears automatically
8. verify open/close lifecycle
Do not add duplicate conditions to ModuleHost.
Useful search:
git grep -n "registerBuiltInModules" src
git grep -n "CortexModule" src
git grep -n "MinerModule" src
git grep -n "NeuorrunModule" srcCanonical entry point:
src/core/devices/registerBuiltInDevices.ts
Procedure:
1. implement the adapter
2. import it into registerBuiltInDevices.ts
3. instantiate/register exactly once
4. verify DeviceManager sees it
5. verify Device page lists it
6. connect/disconnect
7. verify active-device state
Useful search:
git grep -n "registerBuiltInDevices" src
git grep -n "BrainAccessEEGAdapter" src
git grep -n "BrainLinkAdapter" srcDo not instantiate another copy inside a module.
Procedure:
1. implement Python/native sidecar
2. implement TypeScript bridge
3. connect bridge to adapter/service
4. add externalBin entry
5. add Tauri capability scope
6. update Windows bootstrap
7. update Linux bootstrap
8. update .gitignore if necessary
9. test from a clean checkout
Useful search:
git grep -n "externalBin" src-tauri
git grep -n "brainaccess-bridge" src-tauriResource ownership must always be explicit.
| Resource | Owner | Required cleanup |
|---|---|---|
| Physical device connection | Adapter / DeviceManager | disconnect |
| Physical raw EEG stream | EEGStreamService + adapter | stop after final consumer |
| EEG consumer | Module/hook calling acquire() |
handle.release() |
| Brain-metrics subscription | Module/hook | unsubscribe |
| Python sidecar process | TypeScript bridge/adapter | terminate |
| Classifier request | classifier client/service | resolve/reject/cancel |
| DOM listener | component/hook | remove listener |
| Timer | creator | clear timer |
requestAnimationFrame |
renderer/module | cancel frame |
| Three.js resource | renderer/module/cache | dispose when not shared |
| Unity bridge listener | Neuorrun integration | remove hook |
The shared service is reference-counted conceptually:
Cortex acquire
consumer count = 1
↓
Miner acquire
consumer count = 2
↓
Cortex release
consumer count = 1
physical stream remains active
↓
Miner release
consumer count = 0
physical stream stops
Therefore a module must never call:
adapter.stopStream()
directly.
Do not switch to another raw EEG adapter while consumers of the current stream are still active.
Correct order:
close/release consuming modules
stop shared stream through consumer lifecycle
disconnect old device
connect new device
A component may unmount while an async connection/acquire request is still resolving.
Always account for late completion.
If a resource finishes acquisition after the component was disposed, release it immediately.
On unexpected process exit:
mark adapter/service unhealthy
reject pending requests
remove listeners
clear request maps
allow future reconnect
do not leave promises waiting forever
- Stable unique
info.id. - Implements common device lifecycle.
- Implements
EEGDeviceAdapter. - Reports its own sample rate.
- Reports normalized channel metadata.
- Vendor indexes remain inside adapter/bridge.
- Channel order is stable during a stream.
-
subscribeSamples()can unsubscribe. -
startStream()is not called by modules. -
stopStream()releases resources. - Reconnect works.
- Failed connect leaves recoverable state.
- Registered once in
registerBuiltInDevices.ts. - Device page discovers it through normal architecture.
-
EEGStreamServicecan consume it. - Multiple consumers share one physical stream.
- Unit tests cover malformed input and lifecycle.
- Real Bluetooth/unplug-loss behavior tested.
- Reuses or deliberately extends BrainMetrics contract.
- Vendor packets stay below service boundary.
- Metrics use normalized names/units.
- Subscription cleanup works.
- Signal-quality semantics documented.
- Registered once through production device composition.
- Modules require no manufacturer-specific conditions.
- Lives under
src/modules/<module-id>/. - Has a manifest.
- Has a
KNeuronModuleDefinition. - Stable unique module ID.
- Registered in
registerBuiltInModules.ts. - Dashboard entry is registry-driven.
- Uses service, not concrete hardware.
- EEG requested by normalized labels.
- Every EEG handle is released.
- Every subscription is removed.
- Timers/animation frames are cleaned up.
- Handles no-device/incompatible-device state.
- Handles service/sidecar errors.
- Re-entry works without restarting app.
- Unit tests cover mounting/unmounting and cleanup.
- One clear responsibility.
- JSONL protocol documented.
- stdout contains only protocol JSON.
- logs go to stderr.
-
.speccommitted. -
requirements.txtcommitted. - generated
.venv/build/distignored. - generated binary ignored.
-
externalBinupdated. - Tauri capability updated narrowly.
- TypeScript bridge owns process lifecycle.
- Windows bootstrap builds/verifies it.
- Linux bootstrap builds/verifies it.
- clean-machine setup tested.
- crash/EOF/reconnect tested.
Run:
npm run format
npm run typecheck
npm test
npm run lintIf available:
npm run qualityThen perform:
fresh application start
connect device
open new module
exercise core feature
close module
open it again
disconnect
reconnect
close application
Hardware/sidecar changes must also regression-test:
Cortex 3D
TaaLON Miner
Neuorrun
Suppose a module needs:
O1
Oz
O2
Correct architecture:
active EEG hardware
↓
EEGDeviceAdapter
↓
EEGStreamService
↓
ExampleModule
Incorrect architecture:
ExampleModule
↓
BrainAccessBridge
↓
BrainAccess SDK
The module should acquire only application-level channels:
const handle = await eegStreamService.acquire({
channels: ["O1", "Oz", "O2"],
onBatch: (batch) => {
// Module-specific processing.
},
});and release:
await handle.release();when the module no longer owns the consumer.
Recommended dependency graph:
Example EEG hardware
↓
example-sidecar/bridge.py
↓ JSONL
ExampleBridge.ts
↓
ExampleEEGAdapter.ts
↓
DeviceManager
↓
EEGStreamService
↓
modules
Suggested new files:
example-sidecar/
├── bridge.py
├── requirements.txt
├── example-bridge.spec
├── build.ps1
└── diagnose.py
src/core/devices/adapters/example/
├── ExampleBridge.ts
├── ExampleEEGAdapter.ts
└── ExampleEEGAdapter.test.ts
Then update:
src/core/devices/registerBuiltInDevices.ts
src-tauri/tauri.conf.json
src-tauri/capabilities/
setup-and-run.ps1
setup-and-run.sh
README.md
That is the complete production integration surface for a new sidecar-backed device.
Bad:
const oz = batch.values[6];because the module assumes a physical/native channel index.
Prefer:
request "Oz" through EEGStreamService
Bad:
await brainAccessAdapter.startStream();inside a module.
Prefer:
const handle = await eegStreamService.acquire({
channels: "all",
});Bad:
Command.sidecar("brainaccess-bridge").spawn();inside React module code.
Prefer:
module
↓
core service / adapter
↓
bridge
↓
sidecar
Bad:
Dashboard hard-coded device list
+ DeviceManager registry
+ module-local device list
There should be one canonical production registration path for each type of extension.
- modular Tauri/React shell
- production device registry
- BrainAccess integration
- BrainLink Lite integration
- shared EEG stream service
- Cortex 3D
- live EEG visualization
- calibration and baseline normalization
- TaaLON Miner
- FBCCA classifier sidecar
- Neuorrun Unity WebGL integration
- BrainLink attention/meditation bridge
- Windows sidecar packaging
- Windows bootstrap script
- Linux bootstrap script
- developer extension guide
- device/module/sidecar implementation guide
- lifecycle/cleanup rules and extension checklists
KNeuron is developed as a scientific student project for experimentation with EEG and brain-computer interfaces.
It is not a medical device and is not intended for diagnosis, treatment, or clinical decision-making.
Modular Brain-Computer Interface Platform