Skip to main content

scope/display/
oscilloscope.rs

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