use std::io;
use std::time::{Duration, Instant};
use neurosky::prelude::*;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
const RAW_WINDOW: usize = 512;
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let ports = MindWaveDevice::find()?;
if ports.is_empty() {
eprintln!("No MindWave device found.");
return Ok(());
}
println!("Opening {}…", ports[0]);
let mut device = MindWaveDevice::open(&ports[0])?;
device.auto_connect()?;
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?;
let mut raw_ring: Vec<f64> = Vec::new();
let mut attn = 0u8;
let mut med = 0u8;
let mut sig = 0u8;
let mut asic: Option<AsicEeg> = None;
let mut bp = BandPowers::default();
let mut extractor = BandPowerExtractor::new(512.0, 50.0, RAW_WINDOW);
let tick = Duration::from_millis(33); let mut last_tick = Instant::now();
loop {
for pkt in device.read().unwrap_or_default() {
match pkt {
Packet::RawValue(v) => {
if raw_ring.len() >= RAW_WINDOW { raw_ring.remove(0); }
raw_ring.push(v as f64);
bp = extractor.push(v);
}
Packet::Attention(v) => attn = v,
Packet::Meditation(v) => med = v,
Packet::PoorSignal(v) => sig = v,
Packet::AsicEeg(e) => asic = Some(e),
_ => {}
}
}
terminal.draw(|f| render(f, &raw_ring, attn, med, sig, &bp, &asic))?;
let timeout = tick.checked_sub(last_tick.elapsed()).unwrap_or_default();
if event::poll(timeout)? {
if let Event::Key(k) = event::read()? {
if k.kind == KeyEventKind::Press && k.code == KeyCode::Char('q') {
break;
}
}
}
if last_tick.elapsed() >= tick { last_tick = Instant::now(); }
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}
fn render(
f: &mut Frame,
raw_ring: &[f64],
attn: u8,
med: u8,
sig: u8,
bp: &BandPowers,
asic: &Option<AsicEeg>,
) {
let area = f.area();
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(5)])
.split(area);
let sig_str = if sig == 0 { "✓ good".to_string() } else { format!("⚠ {sig}") };
f.render_widget(
Block::default()
.borders(Borders::ALL)
.title(format!(
" NeuroSky MindWave │ Attention: {attn} │ Meditation: {med} \
│ Signal: {sig_str} │ q=quit "
))
.style(Style::default().fg(Color::Cyan)),
rows[0],
);
let cols = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(62), Constraint::Percentage(38)])
.split(rows[1]);
let data: Vec<(f64, f64)> = raw_ring.iter().enumerate()
.map(|(i, &v)| (i as f64, v))
.collect();
let y_min = data.iter().map(|d| d.1).fold( f64::INFINITY, f64::min);
let y_max = data.iter().map(|d| d.1).fold(f64::NEG_INFINITY, f64::max);
let margin = (y_max - y_min).max(1.0) * 0.1;
let ds = Dataset::default()
.name("Raw EEG")
.marker(symbols::Marker::Braille)
.graph_type(GraphType::Line)
.style(Style::default().fg(Color::Green))
.data(&data);
f.render_widget(
Chart::new(vec![ds])
.block(Block::default().borders(Borders::ALL).title(" Raw EEG (512 Hz) "))
.x_axis(Axis::default().bounds([0.0, RAW_WINDOW as f64]))
.y_axis(
Axis::default()
.bounds([y_min - margin, y_max + margin])
.labels::<Vec<Line>>(vec![
format!("{:.0}", y_min - margin).into(),
format!("{:.0}", y_max + margin).into(),
]),
),
cols[0],
);
let bar_w = (cols[1].width as usize).saturating_sub(20).max(4);
let bp_n = bp.normalised();
let bands = [
("δ Delta", bp_n.delta),
("θ Theta", bp_n.theta),
("α Alpha", bp_n.alpha),
("β Beta ", bp_n.beta),
("γ Gamma", bp_n.gamma),
];
let mut lines: Vec<Line> = vec![
Line::from(Span::styled(" ── Computed from raw ──", Style::default().fg(Color::DarkGray))),
];
for (label, val) in &bands {
let filled = (*val * bar_w as f64) as usize;
let bar = "█".repeat(filled) + &"░".repeat(bar_w - filled);
lines.push(Line::from(format!(" {} │{}│ {:>3.0}%", label, bar, val * 100.0)));
}
if let Some(eeg) = asic {
lines.push(Line::from(Span::styled(
" ── ASIC hardware ──",
Style::default().fg(Color::DarkGray),
)));
let hw = eeg.as_array();
let hw_max = hw.iter().copied().max().unwrap_or(1).max(1) as f64;
let hw_names = ["δ", "θ", "αL", "αH", "βL", "βH", "γL", "γM"];
for (&name, &val) in hw_names.iter().zip(hw.iter()) {
let filled = ((val as f64 / hw_max) * (bar_w / 2) as f64) as usize;
let bar = "█".repeat(filled) + &"░".repeat(bar_w / 2 - filled);
lines.push(Line::from(format!(" {name} │{bar}│ {val}")));
}
}
f.render_widget(
Paragraph::new(lines)
.block(Block::default().borders(Borders::ALL).title(" Band Powers "))
.style(Style::default().fg(Color::Yellow)),
cols[1],
);
}