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.3kΩ │ 12.5pkt/s │
├──────────────────────────────────────────────────────────────────────────────┤
│ EEG min:-38 max:+42 rms:18 µV ±500µV │
│ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ bipolar channel, rolling 4-second window │
├──────────────────────────────────────────────────────────────────────────────┤
│ Accel x:+0.010g y:+0.020g z:-0.980g │
│ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 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.0a A:+0.01,+0.02,-0.98g │
└──────────────────────────────────────────────────────────────────────────────┘
Table of contents
- Supported hardware
- Installation
- Quick start (library)
- CLI binary
- Terminal UI
- Cloud fallback decoding
- BLE protocol reference
- Architecture
- Project layout
- Configuration
- Dependencies
- Testing
- Comparison with the Python SDK
- References
- License
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.)
# Then use --cloud in CLI or TUI
Option 2: Command-line flag
Option 3: Programmatic (library usage)
use CloudDecoder;
// Direct token
let mut decoder = new;
// Or from IDUN_API_TOKEN environment variable
let mut decoder = from_env.expect;
⚠️ 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_TOKENto your.gitignoreif storing in a file.
Installation
As a library
# Cargo.toml — full build (includes TUI + local decoder):
= "0.0.1"
# Library only — no TUI, but keep local decoder:
= { = "0.0.1", = false, = ["local-decode"] }
# Minimal — no TUI, no local decoder (cloud-only or raw passthrough):
= { = "0.0.1", = 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 *;
async
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.010g y:+0.020g z:-0.980g │
│ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ 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→ receiverecordingUpdatewithrecordingId - Send
subscribeLiveStreamInsightsforRAW_EEG/FILTERED_EEG/IMU - Send
publishRawMeasurementswith base64-encoded raw BLE packets - Receive
liveStreamInsightswith decoded data - Send
endOngoingRecordingwhen 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 CloudDecoder;
let mut decoder = new;
// Lazy connect — only opens WebSocket when needed
decoder.connect.await?;
// Send a raw BLE packet for cloud decoding
decoder.send_raw_packet.await?;
// Poll for decoded results
if let Some = decoder.recv_decoded.await?
// 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) |
EEG+IMU packet format
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 format
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 ;
let client = new;
| 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
bluezis running (sudo systemctl status bluetooth) - Try increasing the scan timeout:
--timeout 30 - Verify the earbud name starts with
IGEBorIGE-
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
--decodeto see if the local decoder produces plausible values - Check
RUST_LOG=debugoutput 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: