use std::io;
use std::time::{Duration, Instant};
use brainvision::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;
const MAX_CHANNELS: usize = 8;
const FIXED_Y_MIN: f64 = -100.0;
const FIXED_Y_MAX: f64 = 100.0;
#[derive(Debug, Clone, Copy)]
enum ScaleMode {
Auto,
Fixed,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let host = std::env::var("BRAINVISION_HOST").unwrap_or_else(|_| "127.0.0.1".into());
let port = std::env::var("BRAINVISION_PORT")
.ok()
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(RDA_PORT_I16);
println!("Connecting to {host}:{port} ...");
let mut dev = BrainVisionDevice::connect(&host, port)?;
let info = dev.wait_for_start()?;
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?;
let mut ring: Vec<Vec<f64>> = Vec::new();
let mut scale_mode = ScaleMode::Auto;
let mut last_marker = String::from("-");
let tick = Duration::from_millis(33);
let mut last_tick = Instant::now();
loop {
match dev.next_block_resilient(2, Duration::from_millis(200)) {
Ok(Some(block)) => {
if let Some(m) = block.markers.last() {
last_marker = format!("{}:{}", m.kind, m.description);
}
let n_ch = info.channel_count as usize;
for chunk in block.samples_uv.chunks(n_ch) {
if ring.len() >= DISPLAY_SAMPLES {
ring.remove(0);
}
ring.push(chunk.to_vec());
}
}
Ok(None) => break, Err(_) => {} }
terminal.draw(|f| {
let n_ch = (info.channel_count as usize).clamp(1, MAX_CHANNELS);
let areas = Layout::default()
.direction(Direction::Vertical)
.constraints(
std::iter::once(Constraint::Length(3))
.chain((0..n_ch).map(|_| Constraint::Min(3)))
.chain(std::iter::once(Constraint::Length(2)))
.collect::<Vec<_>>(),
)
.split(f.area());
let stats = dev.stats();
f.render_widget(
Block::default()
.borders(Borders::ALL)
.title(format!(
" BrainVision RDA | {} ch @ {:.2} Hz | q=quit a=auto f=fixed ",
info.channel_count,
info.sampling_rate_hz(),
))
.style(Style::default().fg(Color::Cyan)),
areas[0],
);
let colors = [
Color::Green,
Color::Yellow,
Color::Blue,
Color::Magenta,
Color::Red,
Color::Cyan,
Color::White,
Color::LightGreen,
];
for ch in 0..n_ch {
let data: Vec<(f64, f64)> = ring
.iter()
.enumerate()
.filter_map(|(i, s)| s.get(ch).copied().map(|v| (i as f64, v)))
.collect();
let (y0, y1) = match scale_mode {
ScaleMode::Fixed => (FIXED_Y_MIN, FIXED_Y_MAX),
ScaleMode::Auto => {
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 m = (y_max - y_min).max(1.0) * 0.1;
(y_min - m, y_max + m)
}
};
let ds = Dataset::default()
.name(
info.channel_names
.get(ch)
.cloned()
.unwrap_or_else(|| format!("Ch{}", ch + 1)),
)
.marker(symbols::Marker::Braille)
.graph_type(GraphType::Line)
.style(Style::default().fg(colors[ch]))
.data(&data);
f.render_widget(
Chart::new(vec![ds])
.block(
Block::default()
.borders(Borders::ALL)
.title(format!(" Channel {} ", ch + 1)),
)
.x_axis(Axis::default().bounds([0.0, DISPLAY_SAMPLES as f64]))
.y_axis(Axis::default().bounds([y0, y1])),
areas[1 + ch],
);
}
let dt_ms = stats.last_block_dt.map(|d| d.as_millis()).unwrap_or(0);
f.render_widget(
Paragraph::new(format!(
"scale={:?} | dropped_blocks={} | last_block={:?} | dt={}ms | marker={}",
scale_mode, stats.dropped_blocks, stats.last_block, dt_ms, last_marker,
))
.block(
Block::default()
.borders(Borders::ALL)
.title(" Stream stats "),
)
.style(Style::default().fg(Color::DarkGray)),
areas[1 + n_ch],
);
})?;
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 {
match k.code {
KeyCode::Char('q') => break,
KeyCode::Char('a') => scale_mode = ScaleMode::Auto,
KeyCode::Char('f') => scale_mode = ScaleMode::Fixed,
_ => {}
}
}
}
}
if last_tick.elapsed() >= tick {
last_tick = Instant::now();
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}