Readme
idun
A Rust library, CLI, and terminal UI for streaming real-time EEG, IMU, and
impedance data from IDUN Guardian earbuds over Bluetooth Low Energy.
Includes an experimental local packet decoder and an optional cloud fallback
that sends raw BLE packets to the IDUN Cloud API for authoritative decoding.
All-in-one view (EEG + Accel + Gyro + Impedance)
┌──────────────────────────────────────────────────────────────────────────────┐
│ IDUN Guardian │ ● IGEB [ 90ABCDEF] │ ALL │ Bat 87 % │ Z: 5. 3 kΩ │ 12. 5 pkt/ s │
├──────────────────────────────────────────────────────────────────────────────┤
│ EEG min: - 38 max: + 42 rms: 18 µV ±500µV │
│ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ bipolar channel, rolling 4 - second window │
├──────────────────────────────────────────────────────────────────────────────┤
│ Accel x: + 0. 010 g y: + 0. 020 g z: - 0. 980 g │
│ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ red= X green= Y blue= Z │
├──────────────────────────────────────────────────────────────────────────────┤
│ Gyro x: + 0. 1 °/ s y: + 0. 3 °/ s z: - 0. 0 °/ s │
│ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ red= X green= Y blue= Z │
├──────────────────────────────────────────────────────────────────────────────┤
│ Impedance 5. 32 kΩ ( 120 pts) │
│ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ │
├──────────────────────────────────────────────────────────────────────────────┤
│ [ Tab] Pick [ 1 ] EEG [ 2 ] IMU [ 3 ] Imp [ 4 ] All [ + / - ] Scale [ a] Auto [ v] Smooth [ q] Quit │
│ MAC SIM - 00 - 11 … FW sim- 1. 0 . 0 HW sim- 3. 0 a A: + 0. 01 , + 0. 02 , - 0. 98 g │
└──────────────────────────────────────────────────────────────────────────────┘
Table of contents
Supported hardware
Model
BLE name
EEG
Impedance
IMU
Battery
Guardian Earbud 2.1a
IGEB
✓
✓
✓
✓
Guardian Earbud 3.0a
IGE - XXXXXX
✓
✓
✓
✓
Electrode configuration
The Guardian is a single earbud (not a pair) with a bipolar electrode
montage :
Electrode
Location
Role
In-ear-canal
Inside ear canal
Signal
Outer-ear
Concha / outer ear
Reference
This produces one EEG channel measuring the voltage difference between the
two electrodes. EEG and IMU data are multiplexed onto a single BLE GATT
characteristic.
Data channels
Channel
Count
Unit
Rate
Source
EEG
1
µV
250 Hz
BLE characteristic fcc4
Accelerometer
3 (X, Y, Z)
g
~52 Hz
BLE characteristic fcc4 (multiplexed)
Gyroscope
3 (X, Y, Z)
°/s
~52 Hz
BLE characteristic fcc4 (multiplexed)
Impedance
1
Ω / kΩ
on-demand
BLE characteristic fcc8
Battery
1
%
polled 60s
Standard BLE 0x2A19
API credentials
IDUN Cloud API token (optional)
The Guardian's BLE wire format is proprietary. For authoritative EEG
decoding, the IDUN Cloud WebSocket API can decode raw BLE packets server-side.
You do NOT need a token for raw BLE streaming, local experimental
decoding (--decode ), the TUI, or impedance/battery reading.
The cloud API token is only needed for authoritative sample-level EEG decoding.
Getting a token
Create an account at idun.tech
Navigate to your dashboard and generate an API token
Copy the token string (it looks like a long alphanumeric key)
Using the token
There are three ways to provide your IDUN API token:
Option 1: Environment variable (recommended)
# Add to your shell profile (~/.bashrc, ~/.zshrc, etc.)
export IDUN_API_TOKEN="your_api_token_here"
# Then use --cloud in CLI or TUI
cargo run --bin idun -- --decode --cloud
Option 2: Command-line flag
cargo run -- bin idun -- --decode --cloud --token " your_api_token_here"
Option 3: Programmatic (library usage)
use idun:: cloud:: CloudDecoder;
// Direct token
let mut decoder = CloudDecoder:: new(
" your_api_token_here" . to_string ( ) ,
" AA-BB-CC-DD-EE-FF" . to_string ( ) , // device MAC address
) ;
// Or from IDUN_API_TOKEN environment variable
let mut decoder = CloudDecoder:: from_env(
" AA-BB-CC-DD-EE-FF" . to_string ( ) ,
) . expect ( " IDUN_API_TOKEN env var must be set" ) ;
⚠️ Security : Keep your API token private. Do not commit it to version
control. Use environment variables or a secrets manager in production.
Add IDUN_API_TOKEN to your . gitignore if storing in a file.
Installation
As a library
# Cargo.toml — full build (includes TUI + local decoder):
idun = "0.0.1"
# Library only — no TUI, but keep local decoder:
idun = { version = "0.0.1", default-features = false, features = ["local-decode"] }
# Minimal — no TUI, no local decoder (cloud-only or raw passthrough):
idun = { version = "0.0.1", default-features = false }
From source
git clone https://github.com/user/idun
cd idun
cargo build -- release
System dependencies
Platform
Requirement
Install
Linux (Debian/Ubuntu)
D-Bus dev libs
sudo apt install libdbus-1-dev pkg-config
Linux (Fedora)
D-Bus dev libs
sudo dnf install dbus-devel
macOS
Core Bluetooth
Built-in (grant Bluetooth permission on first run)
Windows
WinRT Bluetooth
Built-in
Quick start (library)
use idun:: prelude:: * ;
# [ tokio ::main ]
async fn main ( ) -> anyhow:: Result < ( ) > {
let client = GuardianClient:: new( GuardianClientConfig:: default( ) ) ;
// Option A: scan and auto-connect to the first device found
let ( mut rx, handle) = client. connect ( ) . await? ;
// Option B: scan all, then pick a device
// let devices = client.scan_all().await?;
// let (mut rx, handle) = client.connect_to(devices[0].clone()).await?;
handle. start_recording ( ) . await? ;
while let Some ( event) = rx. recv ( ) . await {
match event {
GuardianEvent:: Eeg( r) => {
println! ( " EEG idx={:3} ts={:.0} ms {} bytes" ,
r. index, r. timestamp, r. raw_data. len ( ) ) ;
}
GuardianEvent:: Accelerometer( r) => {
println! ( " Accel x={:+.4} g y={:+.4} g z={:+.4} g" ,
r. sample. x, r. sample. y, r. sample. z) ;
}
GuardianEvent:: Gyroscope( r) => {
println! ( " Gyro x={:+.2} °/s y={:+.2} °/s z={:+.2} °/s" ,
r. sample. x, r. sample. y, r. sample. z) ;
}
GuardianEvent:: Impedance( r) => {
println! ( " Impedance: {:.2} kΩ ({} Ω)" ,
r. impedance_kohms, r. impedance_ohms) ;
}
GuardianEvent:: Battery( b) => {
println! ( " Battery: {} %" , b. level) ;
}
GuardianEvent:: DeviceInfo( info) => {
println! ( " Device: MAC={} FW={} HW={} " ,
info. mac_address, info. firmware_version, info. hardware_version) ;
}
GuardianEvent:: Connected( name) => {
println! ( " Connected to {name} " ) ;
}
GuardianEvent:: Disconnected => {
println! ( " Disconnected" ) ;
break ;
}
}
}
handle. stop_recording ( ) . await? ;
handle. disconnect ( ) . await? ;
Ok ( ( ) )
}
Event types
GuardianEvent variant
Payload
When
Connected( String )
Device name
BLE link established
Disconnected
—
BLE link lost
Eeg( EegReading)
index, timestamp, raw_data, samples, decode_source
Every ~80ms (20 samples @ 250 Hz)
Accelerometer( AccelerometerReading)
index, timestamp, XyzSample (g)
~52 Hz (extracted from EEG+IMU packets)
Gyroscope( GyroscopeReading)
index, timestamp, XyzSample (°/s)
~52 Hz (extracted from EEG+IMU packets)
Impedance( ImpedanceReading)
impedance_ohms, impedance_kohms
During impedance streaming
Battery( BatteryReading)
level (0–100%)
Every 60 seconds
DeviceInfo( DeviceInfo)
mac_address, firmware_version, hardware_version
Once after connect
GuardianHandle commands
handle. start_recording ( ) . await? ; // Send 'M' command → start EEG+IMU streaming
handle. stop_recording ( ) . await? ; // Send 'S' command → stop EEG+IMU streaming
handle. start_impedance ( ) . await? ; // Send 'Z' command → start impedance
handle. stop_impedance ( ) . await? ; // Send 'X' command → stop impedance
handle. led_on ( ) . await? ; // Send 'd1' config → LED on
handle. led_off ( ) . await? ; // Send 'd0' config → LED off
handle. read_battery ( ) . await? ; // Read standard BLE battery characteristic
handle. is_connected ( ) . await; // Check BLE link status
handle. disconnect ( ) . await? ; // Graceful BLE disconnect
CLI binary
# Basic EEG streaming
cargo run --bin idun
# Impedance streaming
cargo run --bin idun -- --impedance
# 60 Hz mains notch filter (Americas, Japan)
cargo run --bin idun -- --60hz
# Connect to a specific device by name or address
cargo run --bin idun -- --address "IGE-ABCDEF"
# Record to CSV file
cargo run --bin idun -- --csv recording.csv
# Experimental local EEG decoding + CSV
cargo run --bin idun -- --decode --csv decoded.csv
# Cloud fallback decoding
cargo run --bin idun -- --decode --cloud --token my-api-token
# Custom scan timeout
cargo run --bin idun -- --timeout 30
CLI flags
Flag
Description
Default
--impedance
Stream impedance instead of EEG
off
- - 60hz
Use 60 Hz notch filter
50 Hz
--address ADDR
Connect to a specific BLE address or name
auto-connect first
--csv FILE
Write data to a CSV file
off
--decode
Attempt experimental 12-bit EEG sample decoding
off
--cloud
Enable IDUN Cloud fallback when local decoding fails
off
--token TOKEN
IDUN API token (alternative to IDUN_API_TOKEN env var)
env var
--timeout SECS
BLE scan timeout in seconds
15
--help
Show help
—
Interactive commands (type + Enter while streaming)
Command
Action
q
Gracefully disconnect and quit
b
Read battery level
z
Start impedance streaming (stops EEG)
s
Stop current measurement
m
Start EEG/IMU measurement
d
Disconnect from device
Enable verbose logging
RUST_LOG = debug cargo run -- bin idun
RUST_LOG = idun=debug cargo run -- bin idun # library logs only
Terminal UI
# Real device — scan and auto-connect
cargo run --bin idun-tui
# Simulated data (no hardware needed — requires `simulate` feature)
cargo run --bin idun-tui --features simulate -- --simulate
# 60 Hz notch filter
cargo run --bin idun-tui -- --60hz
The TUI writes logs to $ TMPDIR /idun-tui.log (never to stderr, which would
corrupt the alternate-screen display).
Views
Press 1 –4 to switch between views:
Key
View
Charts shown
1
EEG
Single EEG channel (full height, with smooth overlay)
2
IMU
Accelerometer X/Y/Z + Gyroscope X/Y/Z (stacked)
3
Impedance
Impedance history (auto-scaled)
4
All
EEG + Accel + Gyro + Impedance (default)
EEG view (1 )
┌──────────────────────────────────────────────────────────────────────────────┐
│ EEG min: - 38 max: + 42 rms: 18 µV ±500µV │
│ │
│ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ bipolar EEG channel, rolling 4 - second window │
│ smooth overlay ( cyan) over raw ( dim) │
│ │
│ 0s 4s │
└──────────────────────────────────────────────────────────────────────────────┘
The EEG chart shows a 4-second rolling window at 250 Hz (1000 points).
Smooth mode (on by default, toggle with v ) draws the raw signal in a dim
colour as background, then overlays a 9-sample moving average (~36ms) in
bright cyan.
IMU view (2 )
┌──────────────────────────────────────────────────────────────────────────────┐
│ Accel x: + 0. 010 g y: + 0. 020 g z: - 0. 980 g │
│ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ red= X green= Y blue= Z ( auto- scaled Y axis) │
├──────────────────────────────────────────────────────────────────────────────┤
│ Gyro x: + 0. 1 °/ s y: + 0. 3 °/ s z: - 0. 0 °/ s │
│ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ red= X green= Y blue= Z ( auto- scaled Y axis) │
└──────────────────────────────────────────────────────────────────────────────┘
Accelerometer and gyroscope each show 3 axes (X=red, Y=green, Z=blue) with
auto-scaling Y axis. Buffer holds ~4 seconds at 52 Hz (208 points per axis).
Device picker (Tab )
┌──── ⠋ Scanning… ( 2 found) ─────────────────────────────────────────────┐
│ ● IGEB [ 90ABCDEF] ← connected │
│ ▶ IGE - 123456 [ 12345678 ] │
│ │
│ [ ↑↓] Nav [ ↵] Connect [ s] Rescan [ Esc] Close │
└──────────────────────────────────────────────────────────────────────────┘
If the device disconnects unexpectedly, the TUI clears the device list and
starts a fresh BLE scan after a 2-second delay (giving the earbud time to
resume advertising).
TUI key reference
Key
Context
Action
Tab
streaming
open device picker
1
streaming
EEG view (single bipolar channel)
2
streaming
IMU view (accel + gyro, 6 axes)
3
streaming
Impedance view
4
streaming
All-in-one view (EEG + accel + gyro + impedance)
+ / =
EEG view
zoom out (increase µV scale)
-
EEG view
zoom in (decrease µV scale)
a
EEG view
auto-scale to current peak amplitude
v
streaming
toggle smooth overlay on/off
p
streaming
pause — stop measurement
r
streaming
resume — start measurement
z
streaming
start impedance streaming (stops EEG)
c
streaming
clear all waveform buffers
s
streaming / picker
trigger a fresh BLE scan
d
streaming
disconnect current device
q / Esc
streaming
quit
↑ / ↓
picker
navigate device list
Enter
picker
connect to selected device
Esc
picker
close picker
Ctrl- C
anywhere
force quit
Y-axis scale presets (EEG)
Preset
Range
±10 µV
Very sensitive
±25 µV
±50 µV
Simulator default
±100 µV
±200 µV
±500 µV
Real device default
±1000 µV
±2000 µV
Full range
Simulator
The built-in simulator (--simulate ) generates physiologically realistic
synthetic data without hardware:
EEG signal components:
Component
Frequency
Amplitude
Description
Alpha
10 Hz
±20 µV
Dominant resting rhythm
Beta
22 Hz
±6 µV
Active/alert state
Theta
6 Hz
±10 µV
Drowsiness / meditation
Delta
2 Hz
±4 µV
Deep sleep baseline
Noise
broadband
±4 µV
Deterministic pseudo-random
Blink
~0.17 Hz
+150 µV peak
Gaussian artifact every ~6s
Jaw clench
~0.10 Hz
±80 µV
65 Hz EMG burst every ~10s
IMU simulation:
Channel
Pattern
Amplitude
Accel X
0.3 Hz sine
±0.01 g
Accel Y
0.5 Hz cosine
±0.02 g
Accel Z
-1.0 g + 0.1 Hz sine
±0.005 g (gravity-dominated)
Gyro X
0.2 Hz sine
±2.5 °/s
Gyro Y
0.3 Hz cosine
±1.8 °/s
Gyro Z
0.15 Hz sine
±0.7 °/s
Other simulated data:
Impedance: 6 kΩ baseline with slow drift + spike artifacts every ~16s
Battery: drains from 92% at ~1%/min
cargo run -- bin idun-tui -- features simulate -- --simulate
Cloud fallback decoding
When --decode and --cloud are both enabled, the CLI uses a three-level
failsafe chain for EEG packet decoding:
Raw BLE packet
│
▼
┌─────────────────────┐
│ 1. Local 12 - bit │ ← experimental decoder ( instant, no network)
│ decoder │ validity check: non- empty, not all zeros,
│ │ RMS between 0. 1 –2000 µV
└────────┬────────────┘
│ failed?
▼
┌─────────────────────┐
│ 2. IDUN Cloud API │ ← authoritative decoder ( requires API token)
│ WebSocket │ connects lazily on first local failure
│ │ sticky failure: disabled for session if fails
└────────┬────────────┘
│ unavailable?
▼
┌─────────────────────┐
│ 3. Raw packet │ ← base64- encoded raw bytes ( always available)
│ passthrough │
└─────────────────────┘
Cloud API protocol
The IDUN Cloud uses a WebSocket endpoint at wss: // ws-api.idun.cloud with
query-parameter authentication:
wss: // ws-api.idun.cloud?authorization={api_token}
Session flow:
Connect WebSocket with auth token
Send startNewRecording → receive recordingUpdate with recordingId
Send subscribeLiveStreamInsights for RAW_EEG / FILTERED_EEG / IMU
Send publishRawMeasurements with base64-encoded raw BLE packets
Receive liveStreamInsights with decoded data
Send endOngoingRecording when done
Available cloud stream types:
Type
Description
RAW_EEG
Unfiltered EEG samples
FILTERED_EEG
Bandpass-filtered EEG
IMU
Accelerometer + gyroscope
Available cloud predictions:
Type
Description
FFT
Frequency spectrum
JAW_CLENCH
Jaw clench detection
BIN_HEOG
Horizontal eye movement
QUALITY_SCORE
Signal quality metric
CALM_SCORE
Relaxation level
COGNITIVE_READINESS
Cognitive readiness score
Usage
# Cloud fallback with environment variable
export IDUN_API_TOKEN=my-api-token
cargo run --bin idun -- --decode --cloud
# Cloud fallback with inline token
cargo run --bin idun -- --decode --cloud --token my-api-token
# Cloud-only (skip local decoding attempt)
cargo run --bin idun -- --cloud --token my-api-token
Library usage
use idun:: cloud:: CloudDecoder;
let mut decoder = CloudDecoder:: new(
" my-api-token" . to_string ( ) ,
" AA-BB-CC-DD-EE-FF" . to_string ( ) , // device MAC address
) ;
// Lazy connect — only opens WebSocket when needed
decoder. connect ( ) . await? ;
// Send a raw BLE packet for cloud decoding
decoder. send_raw_packet ( & raw_ble_bytes, timestamp_ms, packet_index) . await? ;
// Poll for decoded results
if let Some ( decoded_json) = decoder. recv_decoded ( ) . await? {
println! ( " Cloud decoded: {} " , decoded_json) ;
}
// Clean shutdown
decoder. disconnect ( ) . await? ;
BLE protocol reference
GATT characteristics
UUID suffix
Full UUID
Purpose
Direction
fcc4
beffd56c-c915-48f5-930d-4c1feee0fcc4
EEG + IMU data
Notify
fcc8
beffd56c-c915-48f5-930d-4c1feee0fcc8
Impedance data
Notify
fcc9
beffd56c-c915-48f5-930d-4c1feee0fcc9
Configuration
Write
fcca
beffd56c-c915-48f5-930d-4c1feee0fcca
Commands
Write
2A19
Standard BLE SIG
Battery Level
Read
2A25
Standard BLE SIG
Serial Number / MAC ID
Read
2A26
Standard BLE SIG
Firmware Revision
Read
2A27
Standard BLE SIG
Hardware Revision
Read
Commands (written to fcca )
Byte
ASCII
Action
0x4D
M
Start EEG/IMU measurement
0x53
S
Stop EEG/IMU measurement
0x5A
Z
Start impedance streaming
0x58
X
Stop impedance streaming
Configuration (written to fcc9 )
Value
ASCII
Action
0x64 0x31
d1
LED on
0x64 0x30
d0
LED off
0x6E 0x30
n0
50 Hz notch filter (Europe, Asia)
0x6E 0x31
n1
60 Hz notch filter (Americas, Japan)
Each BLE notification on fcc4 :
byte[ 0 ] : header tag / packet type
byte[ 1 ] : sequence index ( 0 –255 , wraps around)
bytes[ 2 .. ] : packed measurement samples ( EEG and/ or IMU )
The header byte (byte[0]) likely differentiates EEG packets from IMU packets.
The Python SDK does not decode locally — it base64-encodes the entire packet
and forwards it to the IDUN Cloud for processing.
Sampling:
EEG sample rate: 250 Hz
EEG samples per BLE packet: 20
Packet index range: 0–255 (8-bit, wraps)
BLE notification interval: ~80 ms
Experimental local EEG decoder
The local decoder assumes 12-bit big-endian packed unsigned format
(3 bytes → 2 samples), similar to the Muse Classic format:
sample₀ = ( byte[ 0 ] < < 4 ) | ( byte[ 1 ] > > 4 ) // even samples
sample₁ = ( ( byte[ 1 ] & 0x0F ) < < 8 ) | byte[ 2 ] // odd samples
µV = 0. 48828125 × ( sample₁₂ − 2048 ) // mid-scale offset
⚠️ This format is speculative. The actual Guardian wire format is
proprietary and undocumented. The decoder uses a validity heuristic (non-empty,
not all zeros, RMS between 0.1–2000 µV) and falls back to cloud decoding or
raw passthrough when the heuristic rejects the result.
Experimental IMU decoder
IMU data may be packed as 6 × i16 little-endian values (accelerometer XYZ
gyroscope XYZ) in the last 12 bytes of EEG+IMU packets:
accel[ axis] = i16_LE × 0. 0000610352 // g/LSB (±2g range)
gyro[ axis] = i16_LE × 0. 0074768 // °/s/LSB (±245 dps range)
Scale factors assume a common MEMS IMU (e.g., LSM6DS3) configuration.
Impedance notifications on fcc8 are little-endian unsigned integers
(1–4 bytes depending on payload size):
impedance_ohms = u32_LE( data[ 0 .. 4 ] )
impedance_kohms = impedance_ohms / 1000. 0
Timestamp reconstruction
Since BLE notifications arrive with variable latency, timestamps are
reconstructed from the 8-bit packet index:
First packet anchors to now ( ) .
Subsequent packets compute elapsed time from the index delta:
dt = ( current_index − last_index) mod 256 × ( samples_per_packet / sample_rate)
Wrap-around (255 → 0) is handled automatically.
Architecture
User Code → GuardianClient
├── scan_all ( ) → Vec < GuardianDevice>
├── connect ( ) → ( Receiver< GuardianEvent> , GuardianHandle)
└── connect_to ( dev) → ( Receiver< GuardianEvent> , GuardianHandle)
┌── GuardianEvent:: Eeg
├── GuardianEvent:: Accelerometer
mpsc:: Receiver ◄───────── ├── GuardianEvent:: Gyroscope
( async channel) ├── GuardianEvent:: Impedance
├── GuardianEvent:: Battery
├── GuardianEvent:: DeviceInfo
├── GuardianEvent:: Connected
└── GuardianEvent:: Disconnected
GuardianHandle CloudDecoder
├── start_recording ( ) ├── connect ( )
├── stop_recording ( ) ├── send_raw_packet ( )
├── start_impedance ( ) ├── try_recv_decoded ( )
├── stop_impedance ( ) ├── recv_decoded ( )
├── led_on ( ) / led_off ( ) ├── is_connected ( )
├── read_battery ( ) └── disconnect ( )
├── is_connected ( )
└── disconnect ( )
Internal data flow
┌─────────────────┐ BLE notify ┌──────────────────┐
│ Guardian Earbud │ ──────────────────▶│ btleplug │
│ ( BLE peripheral) │ char fcc4 │ notification │
└─────────────────┘ │ stream │
└────────┬─────────┘
│
┌───────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────────┐ ┌─────────────────┐
│ parse_eeg_packet│ │ try_decode_imu_i16le│ │parse_impedance │
│ try_decode_12bit│ │ ( last 12 bytes) │ │( char fcc8) │
└───────┬────────┘ └─────────┬──────────┘ └───────┬─────────┘
│ │ │
▼ ▼ ▼
GuardianEvent:: Eeg GuardianEvent:: GuardianEvent:: Impedance
Accelerometer /
Gyroscope
All events → mpsc:: channel → user's Receiver< GuardianEvent>
Background tasks
The client spawns several tokio tasks:
Task
Purpose
Lifecycle
Notification dispatcher
Routes BLE notifications to appropriate parsers
Connected → Disconnected
Battery poller
Reads battery level every 60 seconds
Connected → Disconnected
Disconnect watcher
Monitors BLE adapter events for disconnect
Connected → Disconnected
Project layout
idun/
├── Cargo. toml # Package metadata, features, dependencies
├── Cargo. lock
├── README . md # This file
├── CHANGELOG . md # Version history
├── LICENSE # MIT license
├── . gitignore
├── src/
│ ├── lib. rs # Crate root: module declarations + prelude
│ ├── types. rs # EegReading, AccelerometerReading, GyroscopeReading,
│ │ # ImpedanceReading, BatteryReading, DeviceInfo,
│ │ # XyzSample, DecodeSource, GuardianEvent
│ ├── protocol. rs # GATT UUIDs, commands, config, sampling constants
│ ├── parse. rs # Binary decoders: EEG 12 - bit, IMU i16 LE , impedance
│ ├── guardian_client. rs # GuardianClient ( scan/ connect) + GuardianHandle
│ │ # BLE notification dispatch, disconnect watcher,
│ │ # battery polling, timestamp reconstruction
│ ├── cloud. rs # CloudDecoder: IDUN Cloud WebSocket client
│ │ # Lazy connect, session management, sticky failure
│ ├── main. rs # Headless CLI : - - decode - - cloud - - csv - - impedance
│ └── bin/
│ └── tui. rs # Full- screen TUI : EEG + IMU + impedance charts,
│ # device picker, smooth overlay, - - simulate mode
└── tests/
├── parse_tests. rs # 33 tests: EEG decoder, IMU decoder, impedance,
│ # RMS , notification parser, edge cases
├── protocol_tests. rs # 10 tests: UUID uniqueness/ SIG , command bytes, constants
├── types_tests. rs # 12 tests: all event types, clone, debug, equality
├── cloud_tests. rs # 5 tests: CloudDecoder offline, from_env, try_recv
├── client_config_tests. rs # 3 tests: GuardianClientConfig defaults and clone
└── prelude_tests. rs # 5 tests: all prelude re- exports
Configuration
let config = GuardianClientConfig {
mains_freq_60hz: false , // true for 60 Hz notch (Americas, Japan)
scan_timeout_secs: 15 , // BLE scan timeout
name_prefix: " IGE" . into ( ) , // Match "IGEB" and "IGE-XXXXXX" devices
} ;
let client = GuardianClient:: new( config) ;
Field
Type
Default
Description
mains_freq_60hz
bool
false
true → send n1 (60 Hz notch); false → send n0 (50 Hz)
scan_timeout_secs
u64
15
Abort BLE scan after this many seconds
name_prefix
String
" IGE"
Only connect to devices whose name starts with this
Dependencies
Crate
Version
Purpose
btleplug
0.11
Cross-platform BLE (scan, connect, GATT)
tokio
1.x
Async runtime (full features)
tokio-tungstenite
0.24
WebSocket client for IDUN Cloud API
uuid
1.x
GATT UUID construction
futures
0.3
Stream combinators for BLE notifications
serde + serde_json
1.x
JSON serialization for cloud protocol
base64
0.22
Encoding raw BLE packets for cloud upload
log + env_logger
0.4 / 0.11
Structured logging
anyhow
1.x
Error handling
ratatui
0.30
Terminal UI framework (optional, tui feature)
crossterm
0.29
Terminal backend (optional, tui feature)
Feature flags
Feature
Default
Effect
tui
on
Enables ratatui + crossterm deps and the idun-tui binary
local-decode
on
Enables experimental local EEG 12-bit decoder, IMU i16 LE decoder, compute_rms ( ) , and parse_notification ( ) . Without this feature, raw BLE packets are passed through undecoded and --decode is ignored with a warning. Cloud fallback still works.
simulate
off
Enables --simulate flag in the TUI for synthetic data generation without hardware
# Build without TUI (library + CLI only)
cargo build --no-default-features --features local-decode
# Build without local decoder (cloud-only or raw passthrough)
cargo build --no-default-features --features tui
# Build with everything (default)
cargo build
# Minimal build (no TUI, no local decoder — just BLE + cloud)
cargo build --no-default-features
Testing
# Run all 70 tests
cargo test
# Run with verbose output
cargo test -- --nocapture
# Run specific test suite
cargo test --test parse_tests
cargo test --test protocol_tests
cargo test --test types_tests
Test coverage
Suite
Tests
Covers
Feature-gated
parse_tests
12
Header parser, impedance parser (1/2/3/4+ bytes), edge cases
—
parse_tests:: local_decode
21
EEG 12-bit decoder (min/mid/max), IMU i16 LE (pos/neg/exact/extra), RMS, notification parser
local-decode
protocol_tests
10
UUID uniqueness/SIG compliance, command/config bytes, sampling constants, channel name
—
types_tests
12
All event types, Clone/Debug, XyzSample, DecodeSource, EEG with decoded samples
—
cloud_tests
5
CloudDecoder construction, from_env (missing/present), try_recv , CloudDecodedEeg clone
—
client_config_tests
3
Default config, custom config, config Clone
—
prelude_tests
5
All prelude re-exports compile and are accessible
—
Doc-tests
2
Library root example, CloudDecoder example
—
Total
70
Without the local-decode feature, 21 decoder-specific tests are skipped.
Comparison with the Python SDK
Feature
Python SDK (idun_guardian_sdk )
idun
BLE scanning & connection
✓ (bleak )
✓ (btleplug )
EEG/IMU streaming (raw)
✓
✓
Impedance streaming
✓
✓
Battery reading
✓
✓
Device info (MAC, FW, HW)
✓
✓
LED control
✓
✓
Notch filter config
✓
✓
Local EEG decoding
✗ (cloud only)
✓ (experimental 12-bit)
Local IMU decoding
✗ (cloud only)
✓ (experimental i16 LE)
Cloud WebSocket client
✓ (full)
✓ (decoding fallback)
Cloud HTTP API
✓ (recordings, reports)
✗
Cloud predictions (FFT, jaw clench, etc.)
✓
✗
LSL output (pylsl )
✓
✗
Real-time TUI visualization
✗
✓ (ratatui)
Multi-channel waveform display
✗
✓ (EEG + Accel XYZ + Gyro XYZ + Impedance)
Simulated data mode
✗
✓ (physiologically realistic)
Language
Python 3
Rust
Async runtime
asyncio
tokio
Cross-platform
Linux, macOS, Windows
Linux, macOS, Windows
What this crate does NOT include
Cloud HTTP API — recording management, file downloads, reports
Cloud predictions — FFT, jaw clench, BIN_HEOG, calm score, cognitive readiness
LSL streaming — Lab Streaming Layer output for integration with other tools
Recording storage — cloud-side recording persistence and retrieval
These features require IDUN API credentials and are best handled by the
official Python SDK or a separate Rust HTTP client.
Troubleshooting
No devices found during scan
Ensure the Guardian earbud is powered on and not connected to another device
Check that Bluetooth is enabled on your system
On Linux: ensure bluez is running (sudo systemctl status bluetooth )
Try increasing the scan timeout: --timeout 30
Verify the earbud name starts with IGEB or IGE -
Permission denied (Linux)
# Add your user to the bluetooth group
sudo usermod -aG bluetooth $USER
# Or run with sudo (not recommended for production)
sudo cargo run --bin idun
macOS Bluetooth permission
macOS requires Bluetooth permission for each application. On first run:
A system dialog appears: "idun" would like to use Bluetooth
Click Allow
If previously denied: System Settings → Privacy & Security → Bluetooth
EEG samples are all None
The experimental local decoder may not match the Guardian's actual wire format.
Options:
Use --cloud --token < TOKEN> for authoritative cloud decoding
Use --decode to see if the local decoder produces plausible values
Check RUST_LOG = debug output for packet hex dumps
Connection drops frequently
Move closer to the earbud (BLE range is typically 10m)
Ensure no other BLE clients are connected to the same earbud
The TUI automatically reconnects after a 2-second delay
References
IDUN Technologies — Official IDUN Guardian manufacturer
IDUN Guardian SDK — Official Python SDK (BLE + cloud API)
btleplug — Cross-platform BLE library for Rust
ratatui — Terminal UI framework for Rust
muse-rs — Similar Rust project for Muse EEG headsets (architectural reference)
Citation
If you use this software in academic work, please cite it:
@ software{ hauptmann2026idun ,
author = { Hauptmann, Eugene} ,
title = { idun: Async Rust Client for IDUN Guardian EEG Earbuds over BLE} ,
year = { 2026} ,
url = { https://github.com/eugenehp/idun} ,
version = { 0.0.1} ,
license = { MIT}
}
License
MIT