Skip to content

Commit

Permalink
Downsample plot data, cache euler_angles
Browse files Browse the repository at this point in the history
  • Loading branch information
KoffeinFlummi committed Nov 26, 2023
1 parent f113985 commit cfc1c4b
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 38 deletions.
5 changes: 0 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/data_source/serial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<egui::Context>,
downlink_tx: &mut Sender<DownlinkMessage>,
Expand Down Expand Up @@ -142,7 +142,7 @@ pub fn find_serial_port() -> Option<String> {

/// 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<egui::Context>,
serial_status_tx: Sender<(SerialStatus, Option<String>)>,
Expand All @@ -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<egui::Context>,
serial_status_tx: Sender<(SerialStatus, Option<String>)>,
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion src/gui/panels/menu_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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());
}
Expand Down
87 changes: 69 additions & 18 deletions src/gui/plot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<dyn FnMut(&VehicleState) -> Option<f32>>,
data: Vec<[f64; 2]>,
/// Increasingly downsampled plot data. The first entry is the full data, followed by
/// smaller and smaller vectors.
data: Vec<Vec<[f64; 2]>>,
stats: Option<(f64, f64, f64, f64)>,
last_bounds: Option<PlotBounds>,
last_view: Vec<[f64; 2]>,
Expand All @@ -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![],
Expand All @@ -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!();

Expand All @@ -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;
}
Expand Down Expand Up @@ -196,15 +240,21 @@ impl PlotCache {
}

/// Lines to be plotted
pub fn plot_lines(&mut self, bounds: PlotBounds, show_stats: bool, data_source: &dyn DataSource) -> Vec<Line> {
pub fn plot_lines(
&mut self,
bounds: PlotBounds,
show_stats: bool,
data_source: &dyn DataSource,
view_width: f32,
) -> Vec<Line> {
#[cfg(feature = "profiling")]
puffin::profile_function!();

self.update_caches_if_necessary(data_source);
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!(
Expand Down Expand Up @@ -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));
}
Expand Down
14 changes: 8 additions & 6 deletions src/gui/tabs/plot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 24 additions & 4 deletions src/simulation.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::VecDeque;
use std::f32::consts::PI;

use nalgebra::{UnitQuaternion, Vector3};
use rand::distributions::Distribution;
Expand All @@ -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 {
Expand Down Expand Up @@ -109,6 +107,7 @@ pub struct SimulationState {
pub(crate) accelerometer1: Option<Vector3<f32>>,
pub(crate) accelerometer2: Option<Vector3<f32>>,
pub(crate) magnetometer: Option<Vector3<f32>>,
pub(crate) pressure_baro: Option<f32>,
pub(crate) altitude_baro: Option<f32>,

pub(crate) state_estimator: StateEstimator,
Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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()
Expand Down

0 comments on commit cfc1c4b

Please sign in to comment.