scope/display/
oscilloscope.rs

1use crossterm::event::{Event, KeyModifiers, KeyCode};
2use ratatui::{widgets::{Axis, GraphType}, style::Style, text::Span};
3
4use crate::input::Matrix;
5
6use super::{update_value_f, update_value_i, DataSet, Dimension, DisplayMode, GraphConfig};
7
8#[derive(Default)]
9pub struct Oscilloscope {
10	pub triggering: bool,
11	pub falling_edge: bool,
12	pub threshold: f64,
13	pub depth: u32,
14	pub peaks: bool,
15}
16
17impl DisplayMode for Oscilloscope {
18	fn mode_str(&self) -> &'static str {
19		"oscillo"
20	}
21
22	fn channel_name(&self, index: usize) -> String {
23		match index {
24			0 => "L".into(),
25			1 => "R".into(),
26			_ => format!("{}", index),
27		}
28	}
29
30	fn header(&self, _: &GraphConfig) -> String {
31		if self.triggering {
32			format!(
33				"{} {:.0}{} trigger",
34				if self.falling_edge { "v" } else { "^" },
35				self.threshold,
36				if self.depth > 1 { format!(":{}", self.depth) } else { "".into() },
37			)
38		} else {
39			"live".into()
40		}
41	}
42
43	fn axis(&self, cfg: &GraphConfig, dimension: Dimension) -> Axis {
44		let (name, bounds) = match dimension {
45			Dimension::X => ("time -", [0.0, cfg.samples as f64]),
46			Dimension::Y => ("| amplitude", [-cfg.scale, cfg.scale]),
47		};
48		let mut a = Axis::default();
49		if cfg.show_ui { // TODO don't make it necessary to check show_ui inside here
50			a = a.title(Span::styled(name, Style::default().fg(cfg.labels_color)));
51		}
52		a.style(Style::default().fg(cfg.axis_color)).bounds(bounds)
53	}
54
55	fn references(&self, cfg: &GraphConfig) -> Vec<DataSet> {
56		vec![
57			DataSet::new(None, vec![(0.0, 0.0), (cfg.samples as f64, 0.0)], cfg.marker_type, GraphType::Line, cfg.axis_color),
58		]
59	}
60
61	fn process(&mut self, cfg: &GraphConfig, data: &Matrix<f64>) -> Vec<DataSet> {
62		let mut out = Vec::new();
63
64		let mut trigger_offset = 0;
65		if self.depth == 0 { self.depth = 1 }
66		if self.triggering {
67			for i in 0..data[0].len() {
68				if triggered(&data[0], i, self.threshold, self.depth, self.falling_edge) { // triggered
69					break;
70				}
71				trigger_offset += 1;
72			}
73		}
74
75		if self.triggering {
76			out.push(DataSet::new(Some("T".into()), vec![(0.0, self.threshold)], cfg.marker_type, GraphType::Scatter, cfg.labels_color));
77		}
78
79		for (n, channel) in data.iter().enumerate().rev() {
80			let (mut min, mut max) = (0.0, 0.0);
81			let mut tmp = Vec::new();
82			for (i, sample) in channel.iter().enumerate() {
83				if *sample < min { min = *sample };
84				if *sample > max { max = *sample };
85				if i >= trigger_offset {
86					tmp.push(((i - trigger_offset) as f64, *sample));
87				}
88			}
89
90			if self.peaks {
91				out.push(DataSet::new(
92					None,
93					vec![(0.0, min), (0.0, max)],
94					cfg.marker_type,
95					GraphType::Scatter,
96					cfg.palette(n)
97				))
98			}
99
100			out.push(DataSet::new(
101				Some(self.channel_name(n)),
102				tmp,
103				cfg.marker_type,
104				if cfg.scatter { GraphType::Scatter } else { GraphType::Line },
105				cfg.palette(n),
106			));
107		}
108
109		out
110	}
111
112	fn handle(&mut self, event: Event) {
113		if let Event::Key(key) = event {
114			let magnitude = match key.modifiers {
115				KeyModifiers::SHIFT => 10.0,
116				KeyModifiers::CONTROL => 5.0,
117				KeyModifiers::ALT => 0.2,
118				_ => 1.0,
119			};
120			match key.code {
121				KeyCode::PageUp   => update_value_f(&mut self.threshold, 250.0, magnitude, 0.0..32768.0),
122				KeyCode::PageDown => update_value_f(&mut self.threshold, -250.0, magnitude, 0.0..32768.0),
123				KeyCode::Char('t') => self.triggering   = !self.triggering,
124				KeyCode::Char('e') => self.falling_edge = !self.falling_edge,
125				KeyCode::Char('p') => self.peaks        = !self.peaks,
126				KeyCode::Char('=') => update_value_i(&mut self.depth, true, 1, 1.0, 1..65535),
127				KeyCode::Char('-') => update_value_i(&mut self.depth, false, 1, 1.0, 1..65535),
128				KeyCode::Char('+') => update_value_i(&mut self.depth, true, 10, 1.0, 1..65535),
129				KeyCode::Char('_') => update_value_i(&mut self.depth, false, 10, 1.0, 1..65535),
130				KeyCode::Esc => {
131					self.triggering = false;
132				},
133				_ => {}
134			}
135		}
136	}
137}
138
139#[allow(clippy::collapsible_else_if)] // TODO can this be made nicer?
140fn triggered(data: &[f64], index: usize, threshold: f64, depth: u32, falling_edge:bool) -> bool {
141	if data.len() < index + (1+depth as usize) { return false; }
142	if falling_edge {
143		if data[index] >= threshold {
144			for i in 1..=depth as usize {
145				if data[index+i] >= threshold {
146					return false;
147				}
148			}
149			true
150		} else {
151			false
152		}
153	} else {
154		if data[index] <= threshold {
155			for i in 1..=depth as usize {
156				if data[index+i] <= threshold {
157					return false;
158				}
159			}
160			true
161		} else {
162			false
163		}
164	}
165}