The Muse Headset Connector is a web application that connects to a Muse EEG headset via the browser's Web Bluetooth API, processes the raw EEG signal into frequency band powers (delta, theta, alpha, beta, gamma) for each electrode, and streams both raw samples and processed data to an MQTT broker. Other applications can subscribe to the configured MQTT topic to receive real-time EEG data for analysis, visualization, or control.
The user must first connect to an MQTT broker (e.g. HiveMQ Cloud) using the on-page form, then click "Connect to Muse" to pair with the headset. Once connected, the page displays a focus/engagement score (powerValue) derived from all four electrodes using relative beta, theta/beta ratio, and alpha blocking, plus per-electrode band powers (TP9, TP10, AF7, AF8), and publishes batched data to MQTT at a configurable rate. The page also provides links to external modules (Quick Diagnostic, Average Bandpower, PSD, PSD Spectrogram) that open in separate windows.
powerValue is the main focus/engagement metric sent to MQTT and shown in the UI. It is a number from 0 to 100 (formatted to 3 decimal places) derived from all four Muse channels (TP9, TP10, AF7, AF8) using relative band powers and three sub-scores.
Final formula
- Focus score (0 to 1):
focus = clamp( (avgRelativeBeta + thetaBetaComponent + alphaBlockingComponent) / 3 , 0 , 1 )
- powerValue (string, 0 to 100):
powerValue = (focus * 100).toFixed(3)
So powerValue is the focus score scaled to a percentage and rounded to 3 decimals. Higher values indicate higher estimated focus/engagement.
Relative band power (used in all components)
For each electrode and each band (delta, theta, alpha, beta, gamma), the code first computes absolute band power from the filtered EEG, then relative band power:
relativeBand = bandPower / (delta + theta + alpha + beta + gamma)for that channel.
So for each channel, the five relative band powers sum to 1. These values are stored in window.bands (e.g. bands.tp9.beta, bands.af7.alpha).
Component 1: avgRelativeBeta
- Formula:
avgRelativeBeta = (beta_tp9 + beta_tp10 + beta_af7 + beta_af8) / 4 - Each
beta_*is the relative beta power for that electrode (beta / total power for that channel). - Interpretation: Higher beta is often associated with alertness and active thinking; averaging over all four channels gives a global beta contribution to the focus score.
Component 2: thetaBetaComponent
- Per channel:
beta / (theta + beta)(iftheta + beta > 0; otherwise 0). Then average over the four channels:thetaBetaComponent = (1/4) * sum over tp9,tp10,af7,af8 of [ beta / (theta + beta) ]
- Interpretation: When beta is large relative to theta, this ratio is high; when theta dominates, it is low. The component is used as a simple attention/focus index (higher = more beta relative to theta, often associated with focused states).
Component 3: alphaBlockingComponent
- Formula:
alphaBlockingComponent = 1 - (alpha_tp9 + alpha_tp10 + alpha_af7 + alpha_af8) / 4 - So it is
1minus the average relative alpha across the four channels. - Interpretation: When the user is more mentally engaged, alpha often decreases (“alpha blocking”). So higher values of this component (lower alpha) correspond to higher estimated engagement.
Summary
| Symbol | Formula | Range |
|---|---|---|
| avgRelativeBeta | Mean of relative beta over TP9, TP10, AF7, AF8 | 0 to 1 |
| thetaBetaComponent | Mean over channels of beta/(theta + beta) | 0 to 1 |
| alphaBlockingComponent | 1 minus mean of relative alpha over the four channels | 0 to 1 |
| focus | (avgRelativeBeta + thetaBetaComponent + alphaBlockingComponent) / 3, clamped to [0, 1] | 0 to 1 |
| powerValue | (focus * 100).toFixed(3) | "0.000" to "100.000" |
These three components are exposed in the UI and in MQTT as processedData.focusComponents (avgRelativeBeta, thetaBetaComponent, alphaBlockingComponent).
| File | Purpose |
|---|---|
| index.html | Single-page UI and application logic. Contains the HTML structure (connection status, MQTT config form, Connect to Muse button, modules section, latest data display), all CSS for layout and theming, and a single inline script that: (1) connects to MQTT using the form settings, (2) creates a Physio instance and connects to the Muse when the user clicks Connect to Muse, (3) runs intervals that publish buffered raw samples and processed data to MQTT and update the "Latest Data" section. Depends on the MQTT script from CDN and the local scripts listed below. |
| js/physio.js | Defines the Physio constructor. Responsibilities: (1) Buffers incoming EEG samples per channel (TP9, TP10, AF7, AF8) in a sliding window used for PSD and band power; (2) Maintains a separate raw sample buffer (up to 1 second) for MQTT; (3) Applies a 7–30 Hz bandpass filter (Fili) and uses BCI.js to compute PSD and band power; (4) Computes relative band powers and stores them on window.bands and window.relativeBeta; (5) Wraps Blue.BCIDevice and passes each incoming packet to addData. The UI and MQTT logic in index.html read from window.bands, window.relativeBeta, and physio.getRawDataBuffer() / physio.clearRawDataBuffer(). |
| js/BCIDevice.build.js | Webpack bundle that exposes window.Blue.BCIDevice. This is the Web Bluetooth layer: it discovers and connects to the Muse device and invokes a callback with { electrode, data } for each EEG packet. physio.js creates new Blue.BCIDevice(callback) and calls device.connect() to start streaming. This file is third-party/bundled; it should not be edited unless rebuilding from source. |
| js/fili.min.js | Minified Fili.js library (referenced by index.html). Used in physio.js to design and apply a bandpass FIR filter (7–30 Hz) to the EEG signal before band power computation. |
| js/bci.min.js | Minified BCI.js library (referenced by index.html). Used in physio.js for window.bci.signal.getPSD and window.bci.signal.getBandPower to compute power spectral density and band powers for delta, theta, alpha, beta, gamma. |
| assets/net.png | Decorative background image referenced in index.html. |
Data flow in short: Muse (Bluetooth) -> BCIDevice.build.js -> physio.js (buffering, filtering, band powers) -> index.html (reads window.bands / window.relativeBeta and physio.getRawDataBuffer(), updates DOM, publishes to MQTT).
Each message published to the configured MQTT topic is a single JSON string. After parsing, the payload has the following structure.
Top-level object
| Field | Type | Description |
|---|---|---|
batchTimestamp |
string | ISO 8601 timestamp when the batch was created (e.g. "2025-03-04T12:00:00.000Z"). |
sampleCount |
number | Number of raw samples in the samples array. |
samples |
array | List of raw EEG sample objects (see below). |
processedData |
object | Summary of processed band powers and derived values (see below). |
Each element of samples (raw sample object)
| Field | Type | Description |
|---|---|---|
timestamp |
string | ISO 8601 timestamp when this sample was received. |
channel |
number | Muse channel ID: 2 = TP9, 16 = AF7, 3 = TP10, 17 = AF8. |
data |
array of numbers | One-element array containing the raw EEG value for this sample. |
sampleNumber |
number | Running count used for ordering (resets every second). |
packetSize |
number | Always 1 (one sample per object in the buffer). |
The processedData object
| Field | Type | Description |
|---|---|---|
relativeBeta |
number | Focus/engagement score 0-1: average of (1) avg relative beta across all 4 channels, (2) theta/beta component beta/(theta+beta), (3) alpha blocking 1-avg(relative alpha). |
powerValue |
string | Focus score multiplied by 100, 3 decimal places (e.g. "45.123"). Higher = more engaged/focused. |
focusComponents |
object or null | Breakdown: avgRelativeBeta, thetaBetaComponent, alphaBlockingComponent (each 0-1). |
bands |
object | Per-electrode relative band powers; keys are "tp9", "tp10", "af7", "af8". |
Each electrode in processedData.bands (e.g. processedData.bands.tp9)
| Field | Type | Description |
|---|---|---|
delta |
number | Relative delta band power. |
theta |
number | Relative theta band power. |
alpha |
number | Relative alpha band power. |
beta |
number | Relative beta band power. |
gamma |
number | Relative gamma band power. |
totalPower |
number | Sum of delta, theta, alpha, beta, and gamma for that electrode. |
Example (abbreviated)
{
"batchTimestamp": "2025-03-04T12:00:00.000Z",
"sampleCount": 48,
"samples": [
{
"timestamp": "2025-03-04T12:00:00.001Z",
"channel": 2,
"data": [ -12.34 ],
"sampleNumber": 1,
"packetSize": 1
}
],
"processedData": {
"relativeBeta": 0.451,
"powerValue": "45.100",
"focusComponents": {
"avgRelativeBeta": 0.20,
"thetaBetaComponent": 0.55,
"alphaBlockingComponent": 0.60
},
"bands": {
"tp9": { "delta": 0.2, "theta": 0.15, "alpha": 0.25, "beta": 0.2, "gamma": 0.2, "totalPower": 1.0 },
"tp10": { "delta": 0.22, "theta": 0.14, "alpha": 0.24, "beta": 0.21, "gamma": 0.19, "totalPower": 1.0 },
"af7": { "delta": 0.21, "theta": 0.16, "alpha": 0.23, "beta": 0.2, "gamma": 0.2, "totalPower": 1.0 },
"af8": { "delta": 0.19, "theta": 0.15, "alpha": 0.26, "beta": 0.2, "gamma": 0.2, "totalPower": 1.0 }
}
}
}Messages are published at the rate controlled by MQTT_UPDATE_INTERVAL in index.html (default 100 ms). The buffer is cleared after each publish, so samples typically contains only the raw samples collected since the previous publish.
-
Head
- Loads MQTT client (CDN), then Fili, BCI, BCIDevice, and Physio in order so that Physio can use Blue.BCIDevice, Fili, and BCI.
- CSS defines variables for colors, layout for body and status boxes, styles for connected/disconnected state, data grid and band sections, connection status spinner, wave decoration, MQTT config form, and module buttons.
-
Body
- Connection status area: spinner and text (e.g. "Searching for MQTT...", "Streaming Data").
- Two status divs:
#mqttStatusand#museStatus, toggled between "connected" and "disconnected" classes. - MQTT config: protocol (WSS/WS), broker URL, port, username, password, topic; buttons "Connect to MQTT" and "Connect to Muse".
- Modules section: buttons that open external pages (Quick Diagnostic, Average Bandpower, PSD, PSD Spectrogram) in new windows.
- Data display:
#latestData(a<pre>) is replaced with generated HTML showing timestamp, focus score (0-1), focus % (powerValue), optional focus components (avg relative beta, theta/beta component, alpha blocking), and per-electrode band powers (TP9, TP10, AF7, AF8).
-
Script (DOMContentLoaded)
-
Setup
- Verifies
mqttis defined (from CDN). - Defines
defaultConfigfor broker URL, port, username, password, topic. - Declares variables:
client(MQTT),physio,museDevice,museServer,connectionCheckInterval,dataProcessingInterval.
- Verifies
-
Cleanup
cleanupIntervals()clears the two intervals.cleanupResources()callscleanupIntervals(), disconnects Physio (if any), ends the MQTT client, and nulls references.
-
MQTT config
getMqttConfig()reads form fields (with defaults), normalizes the broker URL (protocol prefix and/mqttpath), and returns{ protocol, brokerUrl, port, username, password, topic }.
-
MQTT connection
setupMqttConnection()builds options (clientId, username, password, port, protocol), callsmqtt.connect(config.brokerUrl, options), and assigns the client. It then registers:connect: set MQTT status to connected, update status text, set button to "Refresh Connection" and disable false.error/close: callcleanupResources(), set status to disconnected/error, reset button to "Connect to MQTT".
-
MQTT button
- On click: if not connected, call
setupMqttConnection(); if connected, callcleanupResources(), wait 1 second, then callsetupMqttConnection()again (refresh).
- On click: if not connected, call
-
Muse button
- On click: if MQTT not connected, alert and return. Otherwise:
cleanupIntervals().- Set status text to "Searching for Muse...".
- Create
physio = new Physio()and callphysio.start()(this triggers Web Bluetooth and starts streaming). - Start
connectionCheckInterval(every 100 ms): whenwindow.relativeBetaandwindow.bandshave data, set Muse status to connected, status text to "Streaming Data...", and clear the interval. - Start
dataProcessingInterval(every 100 ms):- MQTT publish: If at least
MQTT_UPDATE_INTERVAL(100 ms) has passed, getphysio.getRawDataBuffer(). If non-empty, buildbatchData(batchTimestamp, sampleCount, samples, processedData with relativeBeta, powerValue, bands),JSON.stringifyit,client.publish(topic, message), thenphysio.clearRawDataBuffer()and updatelastMqttUpdateTime. - DOM update: If at least
DOM_UPDATE_INTERVAL(500 ms) has passed, readwindow.relativeBetaandwindow.bands, build adisplayDataobject (timestamp, relativeBeta, powerValue, bands for tp9/tp10/af7/af8), and callupdateLatestData('Processed EEG', displayData); then updatelastDomUpdateTime.
- MQTT publish: If at least
- On click: if MQTT not connected, alert and return. Otherwise:
-
updateLatestData(type, data)
- Finds
#latestData, then sets itsinnerHTMLto a large template string: a grid with "Basic Information" (timestamp, relative beta, power value) and four sections (TP9, TP10, AF7, AF8), each with delta/theta/alpha/beta/gamma values fromdata.bands.
- Finds
-
Other
- If
museDeviceexists, listen forgattserverdisconnectedto set Muse status to disconnected and callcleanupResources(). - On
beforeunload, callcleanupResources().
- If
-
- Physio constructor
-
State
buffer,rawDataBuffer,maxBufferSize(256), timing and sample-count fields,samplesPerPacket(12).- Bandpass filter: Fili bandpass 7–30 Hz, 250 Hz, order 128; stored in
filter. channels(per-channel arrays),window.psd,window.bands,tempSeriesData,isChannelDataReadyfor channels 2, 16, 3, 17.this.SECONDS = 4,this.BUFFER_SIZE = 4 * 256,window.channelSampleCount.
-
addData(sample, channel)
- Updates timing and sample count; initializes
channels[channel]andchannelSampleCount[channel]if needed. - For each value in
sample: append to channel buffer (sliding window of sizeBUFFER_SIZE), increment channel sample count, push a raw sample object (timestamp, channel, data, sampleNumber, packetSize) ontorawDataBuffer, and trimrawDataBuffertomaxBufferSize. - Set
tempSeriesData[channel]andisChannelDataReady[channel] = true.
- Updates timing and sample count; initializes
-
Getters
getLenght/getBuffer: legacy buffer.getRawDataBuffer(): returnsrawDataBuffer.clearRawDataBuffer(): clearsrawDataBuffer.
-
psdToPlotPSD(psd, max)
- Converts PSD array to
[{ x, y }, ...]for plotting, up to frequency indexmax.
- Converts PSD array to
-
getBandPower(channel, band)
- If channel buffer has fewer than
BUFFER_SIZEsamples, return 0. Otherwise: filter channel withfilter.simulate(channels[channel]), compute PSD withwindow.bci.signal.getPSD, store inwindow.psd[channel], then returnwindow.bci.signal.getBandPower(..., band).
- If channel buffer has fewer than
-
getRelativeBandPower(channel, band)
- Target band power divided by the sum of delta, theta, alpha, beta, and gamma band powers for that channel.
-
computeFocusScore()
- After bands are filled, computes a 0-1 focus/engagement score from all four channels: (1) average relative beta, (2) theta/beta component (beta/(theta+beta) per channel, averaged), (3) alpha blocking (1 - average relative alpha). Sets
window.relativeBetato the combined score (clamped 0-1) andwindow.focusComponentsto the three components.
- After bands are filled, computes a 0-1 focus/engagement score from all four channels: (1) average relative beta, (2) theta/beta component (beta/(theta+beta) per channel, averaged), (3) alpha blocking (1 - average relative alpha). Sets
-
checkForVisualizationRefresh()
- When all four channels (2, 16, 3, 17) are ready, reset their ready flags, then for each electrode (2->tp9, 3->tp10, 17->af8, 16->af7) compute relative band powers and set
window.bands["tp9"]etc. with delta, theta, alpha, beta, gamma, totalPower. CallscomputeFocusScore(). Ifwindow.bpGraphexists, update its series with band power data and callupdate(). Ifwindow.psdGraphand PSD data exist, fill series fromwindow.psd[2/3/16/17]viapsdToPlotPSDand callupdate().
- When all four channels (2, 16, 3, 17) are ready, reset their ready flags, then for each electrode (2->tp9, 3->tp10, 17->af8, 16->af7) compute relative band powers and set
-
Device and start
this.device = new Blue.BCIDevice(callback). The callback receivessamplewithelectrodeanddata, callsthis.addData(data, electrode), and callscheckForVisualizationRefresh()(which updates band powers and thencomputeFocusScore(), settingwindow.relativeBetato the focus score).this.start()callsthis.device.connect()to begin Web Bluetooth connection and streaming.
-
- This is a prebuilt webpack bundle. It attaches
window.Bluewith the BCIDevice API used by physio.js. The only integration point is: createnew Blue.BCIDevice(callback)and call.connect(); the callback receives EEG packets. No edits are required for normal use or modification of the rest of the project.
-
Change MQTT publish rate or payload: In index.html, adjust
MQTT_UPDATE_INTERVALand the structure ofbatchDatainsidedataProcessingInterval. To add more processed fields, ensure they are set onwindowin physio.js (or returned by a Physio method) and then include them inbatchData.processedData. -
Change band power or filtering: In physio.js, edit the Fili filter parameters (e.g.
lowFreq,highFreq,filterOrder) and the band power logic ingetBandPower/getRelativeBandPower. Band names and ranges come from BCI.js. -
Change UI or layout: Edit the HTML and CSS in index.html. The "Latest Data" content is generated in
updateLatestData(); change the template string there to add/remove or rename fields. -
Add new modules: Add another button in the "Modules" section and set its
onclicktowindow.open(...)with the module URL and window options. -
Default MQTT settings: Update the
defaultConfigobject and, if desired, the form placeholders in index.html. -
Electrode/channel mapping: Channel IDs 2, 16, 3, 17 map to TP9, AF7, TP10, AF8 in physio.js. Changing this mapping requires updating
checkForVisualizationRefreshand any code that assumeswindow.bands.tp9etc.
Running the project: serve the project directory over HTTPS (required for Web Bluetooth and secure MQTT). Open index.html, connect to MQTT, then connect to the Muse headset. Data will appear in the Latest Data section and be published to the configured MQTT topic.
The web app and the standalone MQTT client both use HiveMQ Cloud (WSS). Broker hostname, port, username, and password are set in initializeMQTT() in app.js and in mqtt_client.js. To use a different HiveMQ cluster, update the hostname (and optionally port) in both places; keep the same username and password, or obtain new credentials from the project maintainer and update the config accordingly.