gtec 0.0.2

Rust library and TUI for g.tec Unicorn Hybrid Black 8-channel EEG headset via the Unicorn C API (runtime-loaded)
Documentation
//! Real-time Unicorn EEG terminal UI using ratatui.

use std::io;
use std::time::{Duration, Instant};

use gtec::prelude::*;

use crossterm::{
    event::{self, Event, KeyCode, KeyEventKind},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};

const DISPLAY_SAMPLES: usize = 500;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    env_logger::init();

    println!("Scanning for Unicorn devices...");
    let serials = UnicornDevice::scan(true)?;
    if serials.is_empty() {
        eprintln!("No device found.");
        return Ok(());
    }

    println!("Connecting to {}...", serials[0]);
    let mut device = UnicornDevice::open(&serials[0])?;
    let info = device.device_info()?;

    device.start_acquisition(false)?;

    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    let mut ring: Vec<Vec<f32>> = Vec::new();
    let tick_rate = Duration::from_millis(33);
    let mut last_tick = Instant::now();

    loop {
        // Read available data
        if let Ok(scans) = device.get_data(1) {
            for s in scans {
                if ring.len() >= DISPLAY_SAMPLES { ring.remove(0); }
                ring.push(s.data);
            }
        }

        terminal.draw(|f| {
            let area = f.area();
            let chunks = Layout::default()
                .direction(Direction::Vertical)
                .constraints(std::iter::once(Constraint::Length(3))
                    .chain((0..UNICORN_EEG_CHANNELS_COUNT).map(|_| Constraint::Min(3)))
                    .collect::<Vec<_>>())
                .split(area);

            let title = format!(
                " Unicorn EEG — {} | FW: {} | Scans: {} | q to quit ",
                info.serial_str(), info.firmware_version_str(), ring.len()
            );
            f.render_widget(
                Block::default().borders(Borders::ALL).title(title).style(Style::default().fg(Color::Cyan)),
                chunks[0],
            );

            let colors = [Color::Green, Color::Yellow, Color::Blue, Color::Magenta, Color::Red, Color::Cyan, Color::White, Color::LightGreen];
            for ch in 0..UNICORN_EEG_CHANNELS_COUNT {
                let data: Vec<(f64, f64)> = ring.iter().enumerate()
                    .filter_map(|(i, s)| s.get(ch).map(|&v| (i as f64, v as f64)))
                    .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 dataset = Dataset::default()
                    .name(EEG_CHANNEL_NAMES[ch])
                    .marker(symbols::Marker::Braille)
                    .graph_type(GraphType::Line)
                    .style(Style::default().fg(colors[ch]))
                    .data(&data);

                let chart = Chart::new(vec![dataset])
                    .block(Block::default().borders(Borders::ALL).title(format!(" {} ", EEG_CHANNEL_NAMES[ch])))
                    .x_axis(Axis::default().bounds([0.0, DISPLAY_SAMPLES 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(),
                        ]));

                f.render_widget(chart, chunks[1 + ch]);
            }
        })?;

        let timeout = tick_rate.checked_sub(last_tick.elapsed()).unwrap_or_default();
        if event::poll(timeout)? {
            if let Event::Key(key) = event::read()? {
                if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { break; }
            }
        }
        if last_tick.elapsed() >= tick_rate { last_tick = Instant::now(); }
    }

    device.stop_acquisition()?;
    disable_raw_mode()?;
    execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    terminal.show_cursor()?;
    println!("Bye!");
    Ok(())
}