neurosys
is a software development kit for system-level neurofeedback applications that support multiple devices (EEG, fNIRS, HEG, etc.).
Most neurofeedback systems ask you to interrupt your routine for a dedicated session—but the Neurosys SDK seeks to meet people where they’re at.
Applications built on neurosys
allow you to leverage your daily activities for neurofeedback, making it easier to integrate into your life.
- System-level Neurofeedback Outputs: Brightness, Volume, and Cursor Animation.
- Multi-Device Support: Compatible with most biofeedback devices, with initial support for the Muse 2 and the HEGduino.
- System Tray Integration: Easily connect and disconnect devices, change evaluation metrics, and select outputs without leaving your current task.
- Modular Architecture: Easily extend and customize the system with new devices, features, evaluations, and outputs.
- Neurosys Starter Kit is a template application for providing system-level neurofeedback.
- HEGBeta is an HEGduino-focused build for training your HEG ratio.
You will need to have Node.js installed on your machine.
This repository uses PNPM for package management. Install PNPM by running the following command:
npm install -g pnpm
Install all dependencies by running the following command:
pnpm install
Finally, build all SDK packages by running:
pnpm build
Each devices plugin has:
- A
devices
array, where each item has aname
- A dictionary of
protocols
- A
connect
function that starts the data stream, uses the providednotify
function to update the application with new data, and returns metadata about the structure of returned data - A
disconnect
function that stops the data stream.
import { Devices, Device } from 'neurosys/plugins'
export const devices = new Devices([
new Device({
name: 'Random Data',
protocols: { start: "Start" },
disconnect() {
clearInterval(this.__interval)
},
connect( { protocol }, notify ) {
const montage = [ 'Fp1', 'Fp2' ]
const sfreq = 512
// Genereate data every 1/sfreq seconds
const interval = setInterval(() => {
const data = montage.reduce((acc, ch) => ({ ...acc, [ch]: [ Math.random() * 100 ] }), {})
notify({ data, timestamps: [ performance.now() ] }, 'eeg') // Route to the correct data collection
}, 1000 / sfreq)
this.__interval = interval // Set the interval reference in the device context
return { eeg: { sfreq } } // Annotate with data collection
}
})
])
Each feature plugin has an id
field to allow references from other plugins, a duration
(optional) in seconds that controls the amount of data received, and a calculate
function that returns the relevant feature data.
The calculate
function receives an info
object that includes all data organized by channel name, which has been windowed by the duration
value. A settings
value is also provided, which is provided by the requesting evaluation plugin.
import { Feature } from 'neurosys/plugins'
export const windowData = new Feature({
id: 'window', // Unique identifier for the feature to be requested
duration: 1, // Automatically window the data by 1s
calculate({ data }, settings) { return data }
})
See the Evaluation section for an example of how to request this feature.
Each evaluation plugin has a label
field for the tray option names, features
for feature requirements with related settings, and a get
function that resolves a meaningful metric based on the resolved features.
import { Evaluate } from 'neurosys/plugins'
export const averageVoltage = Evaluate({
label: 'Average Voltage',
features: { window: true },
get({ window: windowedData }) {
const averagePerChannel = Object.entries(windowedData).reduce((acc, [ch, chData]) => ({ ...acc, [ch]: chData.reduce((acc, val) => acc + val, 0) / chData.length }), {})
return Object.values(averagePerChannel).reduce((acc, val) => acc + val, 0) / Object.values(averagePerChannel).length
}
})
Evaluated metrics are auto-normalized into a score using baseline data and min/max values detected during the session.
Each output plugin has a label
field for the tray option name and a set
function that consumes calculated features that are pre-populated with a score
value and a __score
metadata object.
Use the start
and stop
fields to specify reactions to being enabled / disabled, including the management of visualization.
import { Output } from 'neurosys/plugins'
export const printOutput = new Output({
label: 'Print',
start() {
const { cache = 0 } = this
const counter = cache + 1
console.log('Plugin activated', counter)
this.counter = counter
},
stop() {
console.log('Plugin deactivated')
this.cache = this.counter
},
set(features){
console.log(`Features (${this.counter})`, features)
}
})
To add Electron support for your plugin through Commoners, you can attach the desktop
Commoners field to your plugin.
Below is an example using an Output plugin.
import { Output } from 'neurosys/plugins'
const printInMainProcess = new Output({
label: 'Print — Main Process',
set (features) {
this.commoners.send("features", features)
}
})
// Hijack the desktop methods
printInMainProcess.desktop = {
load() {
this.on("features", (_, features) => console.log("Features:", features) )
}
}
You can declare server-side plugins (SSPs) and expose them using a standardized REST API.
neurosys/services
provides a set of utilities for creating server-side plugins, which can be used as follows:
import { Output } from 'neurosys/plugins';
import { createService } from 'neurosys/services';
import * as examples from './examples'; // A collection of the plugins defined above
const host = process.env.HOST || "localhost";
const port = process.env.PORT
const print = new Output({
label: "Print (SSP)",
set: ({ score }) => console.log("Score", score)
});
const server = createService({
device: examples.devices, // NOTE: Not yet supported
feature: examples.windowData,
evaluation: examples.averageVoltage,
output: examples.printOutput
});
server.listen(port, host, () => console.log(`Server running at http://${host}:${port}/`));
All GET
requests to the .neurosys
sub-route return a collection of available plugins.
{
"success": true,
"result": {
"print": {
"info": {
"label": "Print — Example SSP",
"settings": {},
"start": null,
"stop": null,
"set": "[Function: set]"
},
"type": "output"
}
}
}
{ "success": false, "error": "Error message" }
POST
requests the .neurosys
sub-route are handled to reference <type>/<name>/<method>
, receiving the necessary data for that plugin.
{ "success": true, "result": {} }
{ "success": false, "error": "Error message" }