diff --git a/Cargo.toml b/Cargo.toml index b02925e..ba158a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,11 +47,6 @@ puffin_egui = { version = "0.23", optional = true } rfd = "0.10" home = "0.5" # No home directories in web assembly indicatif = "0.17" - -[target.'cfg(all(target_arch = "x86_64", not(target_os = "windows")))'.dependencies] -rand = { version = "0.8", default-features = false, features = ["small_rng", "getrandom"] } - -[target.'cfg(target_os = "windows")'.dependencies] rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] } # Android dependencies diff --git a/src/data_source/serial.rs b/src/data_source/serial.rs index 71cd8b0..07376e9 100644 --- a/src/data_source/serial.rs +++ b/src/data_source/serial.rs @@ -51,7 +51,7 @@ pub enum SerialStatus { /// If `send_heartbeats` is set, regular heartbeat messages will be sent to /// the device. If no heartbeats are sent, the device will not send log /// messages. -#[cfg(not(target_arch = "wasm32"))] // TODO: serial ports on wasm? +#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] pub fn downlink_port( ctx: Option, downlink_tx: &mut Sender, @@ -142,7 +142,7 @@ pub fn find_serial_port() -> Option { /// Continuously monitors for connected USB serial devices and connects to them. /// Run in a separate thread using `spawn_downlink_monitor`. -#[cfg(not(target_arch = "wasm32"))] // TODO: serial ports on wasm? +#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] pub fn downlink_monitor( ctx: Option, serial_status_tx: Sender<(SerialStatus, Option)>, @@ -168,7 +168,7 @@ pub fn downlink_monitor( } /// Spawns `downlink_monitor` in a new thread. -#[cfg(not(target_arch = "wasm32"))] // TODO: serial ports on wasm? +#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] pub fn spawn_downlink_monitor( ctx: Option, serial_status_tx: Sender<(SerialStatus, Option)>, @@ -209,7 +209,8 @@ impl SerialDataSource { let ctx = ctx.clone(); - #[cfg(not(target_arch = "wasm32"))] // TODO: can't spawn threads on wasm + // There are no serial ports on wasm, and on android the Java side handles this. + #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] spawn_downlink_monitor(Some(ctx), serial_status_tx, downlink_tx, uplink_rx, true); let telemetry_log_path = Self::new_telemetry_log_path(); diff --git a/src/gui/panels/menu_bar.rs b/src/gui/panels/menu_bar.rs index 3c48497..f77fb3a 100644 --- a/src/gui/panels/menu_bar.rs +++ b/src/gui/panels/menu_bar.rs @@ -38,7 +38,7 @@ impl MenuBarPanel { ui.separator(); // Opening files manually is not available on web assembly - #[cfg(target_arch = "x86_64")] + #[cfg(all(not(target_arch = "wasm"), not(target_os = "android")))] if ui.selectable_label(false, "🗁 Open Log File").clicked() { if let Some(data_source) = open_log_file() { sam.data_source = Box::new(data_source); @@ -49,6 +49,7 @@ impl MenuBarPanel { ui.toggle_value(&mut sam.archive_window.open, "🗄 Flight Archive"); // Toggle archive panel + #[cfg(all(not(target_arch = "wasm"), not(target_os = "android")))] if ui.selectable_label(data_source_is_sim, "💻 Simulate").clicked() { sam.data_source = Box::new(SimulationDataSource::default()); } diff --git a/src/gui/plot.rs b/src/gui/plot.rs index 985ef08..759644e 100644 --- a/src/gui/plot.rs +++ b/src/gui/plot.rs @@ -15,6 +15,9 @@ use egui_plot::{AxisBools, Corner, Legend, Line, LineStyle, PlotBounds, VLine}; use crate::gui::*; use crate::telemetry_ext::*; +const DOWNSAMPLING_FACTOR: usize = 4; +const MAX_DOWNSAMPLING_RUNS: usize = 2; + fn plot_time(x: &Instant, data_source: &dyn DataSource) -> f64 { if let Some((first_t, _first_vs)) = data_source.vehicle_states().next() { x.duration_since(*first_t).as_secs_f64() @@ -23,12 +26,28 @@ fn plot_time(x: &Instant, data_source: &dyn DataSource) -> f64 { } } +fn downsample_points(data: &Vec<[f64; 2]>) -> Vec<[f64; 2]> { + data.chunks(DOWNSAMPLING_FACTOR * 2) + .map(|chunk| { + let (min_i, min) = chunk.iter().enumerate().min_by(|(_, x), (_, y)| x[1].total_cmp(&y[1])).unwrap(); + let (max_i, max) = chunk.iter().enumerate().max_by(|(_, x), (_, y)| x[1].total_cmp(&y[1])).unwrap(); + match (min_i, max_i) { + (i, j) if i < j => [*min, *max], + _ => [*max, *min], + } + }) + .flatten() + .collect() +} + /// Cache for a single line. struct PlotCacheLine { name: String, color: Color32, pub callback: Box Option>, - data: Vec<[f64; 2]>, + /// Increasingly downsampled plot data. The first entry is the full data, followed by + /// smaller and smaller vectors. + data: Vec>, stats: Option<(f64, f64, f64, f64)>, last_bounds: Option, last_view: Vec<[f64; 2]>, @@ -40,7 +59,7 @@ impl PlotCacheLine { name: name.to_string(), color, callback: Box::new(cb), - data: Vec::new(), + data: vec![Vec::new()], stats: None, last_bounds: None, last_view: vec![], @@ -54,17 +73,50 @@ impl PlotCacheLine { .filter_map(|(x, vs)| (self.callback)(vs).map(|y| [x, y as f64])); if keep_first > 0 { - self.data.extend(new_data) + self.data[0].extend(new_data) } else { - self.data = new_data.collect(); + self.data[0] = new_data.collect(); } + + // clear the downsample caches + self.data.truncate(1); } fn clear_cache(&mut self) { - self.data.truncate(0); + self.data.truncate(1); + self.data[0].truncate(0); + } + + fn cached_data_downsampled(&mut self, bounds: PlotBounds, view_width: f32) -> Vec<[f64; 2]> { + if self.data[0].len() == 0 { + return Vec::new(); + } + + let (xmin, xmax) = (bounds.min()[0], bounds.max()[0]); + + let mut imin = self.data[0].partition_point(|d| d[0] < xmin); + let mut imax = imin + self.data[0][imin..].partition_point(|d| d[0] < xmax); + + imin = imin.saturating_sub(1); + imax = usize::min(imax + 1, self.data[0].len() - 1); + + for level in 0..=MAX_DOWNSAMPLING_RUNS { + if level >= self.data.len() { + self.data.push(downsample_points(&self.data[level-1])); + } + + if imax - imin < (2.0 * view_width) as usize { + return self.data[level][imin..imax].to_vec(); + } else if level < MAX_DOWNSAMPLING_RUNS { + imin /= DOWNSAMPLING_FACTOR; + imax /= DOWNSAMPLING_FACTOR; + } + } + + self.data.last().unwrap()[imin..imax].to_vec() } - pub fn data_for_bounds(&mut self, bounds: PlotBounds, data_source: &dyn DataSource) -> Vec<[f64; 2]> { + pub fn data_for_bounds(&mut self, bounds: PlotBounds, data_source: &dyn DataSource, view_width: f32) -> Vec<[f64; 2]> { #[cfg(feature = "profiling")] puffin::profile_function!(); @@ -76,15 +128,7 @@ impl PlotCacheLine { } if self.last_bounds.map(|b| b != bounds).unwrap_or(true) { - let (xmin, xmax) = (bounds.min()[0], bounds.max()[0]); - - let imin = self.data.partition_point(|d| d[0] < xmin); - let imax = imin + self.data[imin..].partition_point(|d| d[0] < xmax); - - let imin = imin.saturating_sub(1); - let imax = usize::min(imax + 1, self.data.len() - 1); - - self.last_view = self.data[imin..imax].to_vec(); + self.last_view = self.cached_data_downsampled(bounds, view_width); self.last_bounds = Some(bounds); self.stats = None; } @@ -196,7 +240,13 @@ impl PlotCache { } /// Lines to be plotted - pub fn plot_lines(&mut self, bounds: PlotBounds, show_stats: bool, data_source: &dyn DataSource) -> Vec { + pub fn plot_lines( + &mut self, + bounds: PlotBounds, + show_stats: bool, + data_source: &dyn DataSource, + view_width: f32, + ) -> Vec { #[cfg(feature = "profiling")] puffin::profile_function!(); @@ -204,7 +254,7 @@ impl PlotCache { self.lines .iter_mut() .map(|pcl| { - let data = pcl.data_for_bounds(bounds, data_source); + let data = pcl.data_for_bounds(bounds, data_source, view_width); let stats = show_stats.then(|| pcl.stats()).flatten(); let legend = if let Some((mean, std_dev, min, max)) = stats { format!( @@ -374,8 +424,9 @@ impl PlotUiExt for egui::Ui { } let show_stats = shared.show_stats; + let view_width = self.max_rect().width(); let ir = plot.show(self, move |plot_ui| { - let lines = cache.plot_lines(plot_ui.plot_bounds(), show_stats, data_source); + let lines = cache.plot_lines(plot_ui.plot_bounds(), show_stats, data_source, view_width); for l in lines.into_iter() { plot_ui.line(l.width(1.2)); } diff --git a/src/gui/tabs/plot.rs b/src/gui/tabs/plot.rs index a03d999..bcd3102 100644 --- a/src/gui/tabs/plot.rs +++ b/src/gui/tabs/plot.rs @@ -90,12 +90,14 @@ impl PlotTab { let shared_plot = Rc::new(RefCell::new(SharedPlotState::new())); let orientation_plot = PlotState::new("Orientation", (Some(-180.0), Some(540.0)), shared_plot.clone()) - .line("Roll (Z) [°]", B, |vs| vs.euler_angles().map(|a| a.z)) - .line("Pitch (X) [°]", R, |vs| vs.euler_angles().map(|a| a.x)) - .line("Yaw (Y) [°]", G, |vs| vs.euler_angles().map(|a| a.y)) - .line("Roll (True) (Z) [°]", B1, |vs| vs.true_euler_angles().map(|a| a.z)) - .line("Pitch (True) (X) [°]", R1, |vs| vs.true_euler_angles().map(|a| a.x)) - .line("Yaw (True) (Y) [°]", G1, |vs| vs.true_euler_angles().map(|a| a.y)); + .line("Roll (Z) [°]", B, |vs| vs.euler_angles.map(|a| a.z)) + .line("Pitch (X) [°]", R, |vs| vs.euler_angles.map(|a| a.x)) + .line("Yaw (Y) [°]", G, |vs| vs.euler_angles.map(|a| a.y)) + .line("Angle of Attack [°]", O, |vs| vs.angle_of_attack) + .line("Roll (True) (Z) [°]", B1, |vs| vs.true_euler_angles.map(|a| a.z)) + .line("Pitch (True) (X) [°]", R1, |vs| vs.true_euler_angles.map(|a| a.x)) + .line("Yaw (True) (Y) [°]", G1, |vs| vs.true_euler_angles.map(|a| a.y)) + .line("Angle of Attack (True) [°]", O1, |vs| vs.true_angle_of_attack); let vertical_speed_plot = PlotState::new("Vert. Speed & Accel.", (None, None), shared_plot.clone()) .line("Vertical Accel [m/s²]", O1, |vs| vs.vertical_accel) diff --git a/src/simulation.rs b/src/simulation.rs index 9930fd2..2b98f43 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -1,4 +1,5 @@ use std::collections::VecDeque; +use std::f32::consts::PI; use nalgebra::{UnitQuaternion, Vector3}; use rand::distributions::Distribution; @@ -11,15 +12,12 @@ use mithril::telemetry::*; use crate::gui::windows::archive::ARCHIVE; -#[cfg(any(target_os = "windows", target_os = "android"))] type Rng = rand::rngs::StdRng; -#[cfg(not(any(target_os = "windows", target_os = "android")))] -type Rng = rand::rngs::SmallRng; const GRAVITY: f32 = 9.80665; pub const SIMULATION_TICK_MS: u32 = 1; -pub const PLOT_STEP_MS: u32 = 100; +pub const PLOT_STEP_MS: u32 = 50; #[derive(Clone, Debug, PartialEq)] pub struct SimulationSettings { @@ -109,6 +107,7 @@ pub struct SimulationState { pub(crate) accelerometer1: Option>, pub(crate) accelerometer2: Option>, pub(crate) magnetometer: Option>, + pub(crate) pressure_baro: Option, pub(crate) altitude_baro: Option, pub(crate) state_estimator: StateEstimator, @@ -455,6 +454,12 @@ impl From<&SimulationState> for VehicleState { time: ss.time, mode: Some(ss.mode), orientation: ss.state_estimator.orientation, + euler_angles: ss.state_estimator.orientation.map(|q| q.euler_angles()).map(|(r, p, y)| Vector3::new(r, p, y) * 180.0 / PI), + angle_of_attack: ss.state_estimator.orientation.map(|q| { + let up = Vector3::new(0.0, 0.0, 1.0); + let attitude = q * up; + 90.0 - up.dot(&attitude).acos().to_degrees() + }), altitude_asl: Some(ss.state_estimator.altitude_asl()), altitude_baro: ss.altitude_baro, altitude_ground_asl: Some(ss.state_estimator.altitude_ground), @@ -479,6 +484,12 @@ impl From<&SimulationState> for VehicleState { time: ss.time, mode: Some(ss.mode), orientation: ss.state_estimator.orientation, + euler_angles: ss.state_estimator.orientation.map(|q| q.euler_angles()).map(|(r, p, y)| Vector3::new(r, p, y) * 180.0 / PI), + angle_of_attack: ss.state_estimator.orientation.map(|q| { + let up = Vector3::new(0.0, 0.0, 1.0); + let attitude = q * up; + 90.0 - up.dot(&attitude).acos().to_degrees() + }), altitude_asl: Some(ss.state_estimator.altitude_asl()), // TODO altitude_baro: ss.altitude_baro, altitude_ground_asl: Some(ss.altitude_ground), @@ -503,6 +514,15 @@ impl From<&SimulationState> for VehicleState { 0 }), true_orientation: Some(ss.orientation), + true_euler_angles: { + let (r, p, y) = ss.orientation.euler_angles(); + Some(Vector3::new(r, p, y) * 180.0 / PI) + }, + true_angle_of_attack: { + let up = Vector3::new(0.0, 0.0, 1.0); + let attitude = ss.orientation * up; + Some(90.0 - up.dot(&attitude).acos().to_degrees()) + }, true_vertical_accel: Some(ss.acceleration.z), true_vertical_speed: Some(ss.velocity.z), ..Default::default()