a simple (oscillo|vector|spectro)-scope for your terminal
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213use crossterm::event::{Event, KeyCode, KeyModifiers};use ratatui::{ style::Style, text::Span, widgets::{Axis, GraphType},};
use crate::input::Matrix;
use super::{update_value_f, update_value_i, DataSet, Dimension, DisplayMode, GraphConfig};
pub struct Oscilloscope { pub triggering: bool, pub falling_edge: bool, pub threshold: f64, pub depth: u32, pub peaks: bool,}
impl Default for Oscilloscope { fn default() -> Self { Oscilloscope { triggering: false, falling_edge: false, threshold: 0.0, depth: 0, peaks: true, } }}
impl DisplayMode for Oscilloscope { fn mode_str(&self) -> &'static str { "oscillo" }
fn channel_name(&self, index: usize) -> String { match index { 0 => "L".into(), 1 => "R".into(), _ => format!("{}", index), } }
fn header(&self, _: &GraphConfig) -> String { if self.triggering { format!( "{} {:.0}{} trigger", if self.falling_edge { "v" } else { "^" }, self.threshold, if self.depth > 1 { format!(":{}", self.depth) } else { "".into() }, ) } else { "live".into() } }
fn axis(&'_ self, cfg: &GraphConfig, dimension: Dimension) -> Axis<'_> { let (name, bounds) = match dimension { Dimension::X => ("time -", [0.0, cfg.samples as f64]), Dimension::Y => ("| amplitude", [-cfg.scale, cfg.scale]), }; let mut a = Axis::default(); if cfg.show_ui { // TODO don't make it necessary to check show_ui inside here a = a.title(Span::styled(name, Style::default().fg(cfg.labels_color))); } a.style(Style::default().fg(cfg.axis_color)).bounds(bounds) }
fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> { vec![DataSet::new( None, vec![(0.0, 0.0), (cfg.samples as f64, 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color, )] }
fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet> { let mut out = Vec::new();
let mut trigger_offset = 0; if self.depth == 0 { self.depth = 1 } if self.triggering { for i in 0..data[0].len() { if triggered(&data[0], i, self.threshold, self.depth, self.falling_edge) { // triggered break; } trigger_offset += 1; } }
if self.triggering { out.push(DataSet::new( Some("T".into()), vec![(0.0, self.threshold)], cfg.marker_type, GraphType::Scatter, cfg.labels_color, )); }
for (n, channel) in data.iter().enumerate().rev() { let (mut min, mut max) = (0.0, 0.0); let mut tmp = Vec::new(); for (i, sample) in channel.iter().enumerate() { if *sample < min { min = *sample }; if *sample > max { max = *sample }; if i >= trigger_offset { tmp.push(((i - trigger_offset) as f64, *sample)); } }
if self.peaks { out.push(DataSet::new( None, vec![(0.0, min), (0.0, max)], cfg.marker_type, GraphType::Scatter, cfg.palette(n), )) }
out.push(DataSet::new( Some(self.channel_name(n)), tmp, cfg.marker_type, if cfg.scatter { GraphType::Scatter } else { GraphType::Line }, cfg.palette(n), )); }
out }
fn handle(&mut self, event: Event) { if let Event::Key(key) = event { let magnitude = match key.modifiers { KeyModifiers::SHIFT => 10.0, KeyModifiers::CONTROL => 5.0, KeyModifiers::ALT => 0.2, _ => 1.0, }; match key.code { KeyCode::PageUp => { update_value_f(&mut self.threshold, 250.0, magnitude, 0.0..32768.0) } KeyCode::PageDown => { update_value_f(&mut self.threshold, -250.0, magnitude, 0.0..32768.0) } KeyCode::Char('t') => self.triggering = !self.triggering, KeyCode::Char('e') => self.falling_edge = !self.falling_edge, KeyCode::Char('p') => self.peaks = !self.peaks, KeyCode::Char('=') => update_value_i(&mut self.depth, true, 1, 1.0, 1..65535), KeyCode::Char('-') => update_value_i(&mut self.depth, false, 1, 1.0, 1..65535), KeyCode::Char('+') => update_value_i(&mut self.depth, true, 10, 1.0, 1..65535), KeyCode::Char('_') => update_value_i(&mut self.depth, false, 10, 1.0, 1..65535), KeyCode::Esc => { self.triggering = false; } _ => {} } } }}
#[allow(clippy::collapsible_else_if)] // TODO can this be made nicer?fn triggered(data: &[f64], index: usize, threshold: f64, depth: u32, falling_edge: bool) -> bool { if data.len() < index + (1 + depth as usize) { return false; } if falling_edge { if data[index] >= threshold { for i in 1..=depth as usize { if data[index + i] >= threshold { return false; } } true } else { false } } else { if data[index] <= threshold { for i in 1..=depth as usize { if data[index + i] <= threshold { return false; } } true } else { false } }}