diff --git a/src/main.rs b/src/main.rs index a56017be8f7c972165b0cd3e13e2d5aec39d8d26..4511578f8b31b8869603e0ec68df200d1c936f21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ mod message_broker; mod ui; mod utils; -use std::{fs::create_dir_all, sync::LazyLock}; +use std::{fs::create_dir_all, sync::LazyLock, time::Instant}; use error::ErrInstrument; use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt}; @@ -19,6 +19,7 @@ use ui::App; /// ReflectionContext singleton, used to get access to the Mavlink message definitions static MAVLINK_PROFILE: LazyLock<ReflectionContext> = LazyLock::new(ReflectionContext::new); +static APP_START_TIMESTAMP_ORIGIN: LazyLock<Instant> = LazyLock::new(Instant::now); static APP_NAME: &str = "segs"; @@ -59,6 +60,10 @@ fn main() -> Result<(), eframe::Error> { ..Default::default() }; + // Initialize the starting timestamp + let starting_time = &APP_START_TIMESTAMP_ORIGIN; + tracing::info!("Starting {} at {:?}", APP_NAME, starting_time); + // CreationContext constains information useful to initilize our app, like storage. // Storage allows to store custom data in a way that persist whan you restart the app. eframe::run_native( diff --git a/src/ui/panes/plot.rs b/src/ui/panes/plot.rs index e4616d1532f060623372344818eeb051332ab105..e3fe6b7173038296e1e9a91b13a6909c6307b4ef 100644 --- a/src/ui/panes/plot.rs +++ b/src/ui/panes/plot.rs @@ -1,26 +1,25 @@ +mod fields; mod source_window; use super::PaneBehavior; use crate::{ - MAVLINK_PROFILE, error::ErrInstrument, - mavlink::{ - MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage, - reflection::{FieldLike, IndexedField}, - }, + mavlink::{MessageData, ROCKET_FLIGHT_TM_DATA, TimedMessage}, ui::{app::PaneResponse, shortcuts::ShortcutHandler}, utils::units::UnitOfMeasure, }; use egui::{Color32, Ui, Vec2, Vec2b}; use egui_plot::{AxisHints, HPlacement, Legend, Line, PlotPoint, log_grid_spacer}; use serde::{self, Deserialize, Serialize}; -use source_window::sources_window; use std::{ hash::{DefaultHasher, Hash, Hasher}, iter::zip, time::{Duration, Instant}, }; +use fields::{XPlotField, YPlotField}; +use source_window::sources_window; + #[derive(Clone, Default, Debug, Serialize, Deserialize)] pub struct Plot2DPane { settings: PlotSettings, @@ -47,31 +46,23 @@ impl PaneBehavior for Plot2DPane { let ctrl_pressed = ui.input(|i| i.modifiers.ctrl); - let x_unit = UnitOfMeasure::from( - &self - .settings - .x_field - .field() - .unit - .clone() - .unwrap_or_default(), - ); + let x_unit = self.settings.x_field.unit(); let y_units = self .settings .y_fields .iter() - .map(|(field, _)| field.field().unit.as_ref().map(UnitOfMeasure::from)) + .map(|(field, _)| field.unit()) .collect::<Vec<_>>(); // define y_unit as the common unit of the y_fields if they are all the same - let y_unit = y_units - .iter() - .fold(y_units.first().log_unwrap(), |acc, unit| { - match (acc, unit) { - (Some(uom), Some(unit)) if uom == unit => acc, - _ => &None, + let y_unit = y_units.iter().fold(y_units.first(), |acc, unit| { + if let Some(acc) = acc { + if acc == unit { + return Some(acc); } - }); - let x_name = self.settings.x_field.field().name.clone(); + } + None + }); + let x_name = self.settings.x_field.name(); let x_axis = match x_unit { UnitOfMeasure::Time(ref time_unit) => { @@ -154,7 +145,7 @@ impl PaneBehavior for Plot2DPane { Line::new(&points[..]) .color(settings.color) .width(settings.width) - .name(&field.field().name), + .name(field.name()), ); } plot_ui @@ -195,10 +186,10 @@ impl PaneBehavior for Plot2DPane { .iter() .filter(|msg| points_lifespan > &msg.time.elapsed()) { - let x: f64 = x_field.extract_as_f64(&msg.message).log_unwrap(); + let x: f64 = x_field.extract_from_message(msg).log_unwrap(); let ys: Vec<f64> = y_fields .iter() - .map(|(field, _)| field.extract_as_f64(&msg.message).log_unwrap()) + .map(|(field, _)| field.extract_from_message(msg).log_unwrap()) .collect(); if self.line_data.len() < ys.len() { @@ -243,9 +234,9 @@ struct PlotSettings { /// The message id to plot pub(super) plot_message_id: u32, /// The field to plot on the x-axis - pub(super) x_field: IndexedField, + pub(super) x_field: XPlotField, /// The fields to plot, with their respective line settings - pub(super) y_fields: Vec<(IndexedField, LineSettings)>, + pub(super) y_fields: Vec<(YPlotField, LineSettings)>, /// Whether to show the axes of the plot pub(super) axes_visible: bool, /// Points will be shown for this duration before being removed @@ -253,15 +244,13 @@ struct PlotSettings { } impl PlotSettings { - fn add_field(&mut self, field: IndexedField) { + fn add_field(&mut self, field: YPlotField) { let line_settings = LineSettings::default(); self.y_fields.push((field, line_settings)); } fn clear_fields(&mut self) { - self.x_field = 0 - .to_mav_field(self.plot_message_id, &MAVLINK_PROFILE) - .log_unwrap(); + self.x_field = XPlotField::MsgReceiptTimestamp; self.y_fields.clear(); } @@ -282,11 +271,8 @@ impl PlotSettings { impl Default for PlotSettings { fn default() -> Self { let msg_id = ROCKET_FLIGHT_TM_DATA::ID; - let x_field = 0.to_mav_field(msg_id, &MAVLINK_PROFILE).log_unwrap(); - let y_fields = vec![( - 1.to_mav_field(msg_id, &MAVLINK_PROFILE).log_unwrap(), - LineSettings::default(), - )]; + let x_field = XPlotField::MsgReceiptTimestamp; + let y_fields = vec![]; Self { plot_message_id: msg_id, x_field, diff --git a/src/ui/panes/plot/fields.rs b/src/ui/panes/plot/fields.rs new file mode 100644 index 0000000000000000000000000000000000000000..1b62451e95665d4d4bf831e8b973f94ffe92ef19 --- /dev/null +++ b/src/ui/panes/plot/fields.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + APP_START_TIMESTAMP_ORIGIN, + mavlink::{TimedMessage, reflection::IndexedField}, + utils::units::{TimeUnits, UnitOfMeasure}, +}; + +#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)] +pub enum XPlotField { + MsgReceiptTimestamp, + Field(IndexedField), +} + +impl XPlotField { + pub fn unit(&self) -> UnitOfMeasure { + match self { + XPlotField::MsgReceiptTimestamp => UnitOfMeasure::Time(TimeUnits::Millisecond), + XPlotField::Field(field) => UnitOfMeasure::from(field.field().unit.as_ref()), + } + } + + pub fn name(&self) -> String { + match self { + XPlotField::MsgReceiptTimestamp => "receival timestamp".to_string(), + XPlotField::Field(field) => field.field().name.clone(), + } + } + + pub fn extract_from_message(&self, message: &TimedMessage) -> Result<f64, String> { + match self { + XPlotField::MsgReceiptTimestamp => { + Ok((message.time - *APP_START_TIMESTAMP_ORIGIN).as_millis() as f64) + } + XPlotField::Field(field) => field.extract_as_f64(&message.message), + } + } +} + +impl From<IndexedField> for XPlotField { + fn from(field: IndexedField) -> Self { + Self::Field(field) + } +} + +#[derive(Clone, Debug, PartialEq, Hash, Serialize, Deserialize)] +pub enum YPlotField { + Field(IndexedField), +} + +impl YPlotField { + pub fn unit(&self) -> UnitOfMeasure { + match self { + YPlotField::Field(field) => UnitOfMeasure::from(field.field().unit.as_ref()), + } + } + + pub fn name(&self) -> String { + match self { + YPlotField::Field(field) => field.field().name.clone(), + } + } + + pub fn extract_from_message(&self, message: &TimedMessage) -> Result<f64, String> { + match self { + YPlotField::Field(field) => field.extract_as_f64(&message.message), + } + } +} + +impl From<IndexedField> for YPlotField { + fn from(field: IndexedField) -> Self { + Self::Field(field) + } +} diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs index c3098783f3ccad53e7ebf0e8e1b4c49ef381e447..3fb52d1c95637aa4d1e989359b51d469d4a6a6f9 100644 --- a/src/ui/panes/plot/source_window.rs +++ b/src/ui/panes/plot/source_window.rs @@ -2,7 +2,10 @@ use std::time::Duration; use crate::{MAVLINK_PROFILE, error::ErrInstrument}; -use super::{LineSettings, PlotSettings}; +use super::{ + LineSettings, PlotSettings, + fields::{XPlotField, YPlotField}, +}; #[profiling::function] pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { @@ -50,35 +53,42 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { let fields = MAVLINK_PROFILE .get_plottable_fields(plot_settings.plot_message_id) .log_expect("Invalid message id"); + let mut x_fields = vec![XPlotField::MsgReceiptTimestamp]; + let y_fields = fields + .clone() + .into_iter() + .map(|f| f.into()) + .collect::<Vec<_>>(); + x_fields.extend(fields.into_iter().map(|f| f.into())); // get the first field that is in the list of fields or the previous if valid let x_field = &plot_settings.x_field; - let new_field_x = fields - .iter() - .any(|f| f == x_field) - .then(|| x_field.to_owned()) - .or(fields.first().map(|s| s.to_owned())); - - // if there are no fields, reset the field_x and plot_lines - let Some(new_field_x) = new_field_x else { - plot_settings.clear_fields(); - return; + let new_field_x = if x_fields.iter().any(|f| f == x_field) { + x_field.to_owned() + } else { + XPlotField::MsgReceiptTimestamp }; + // update the field_x plot_settings.x_field = new_field_x; // if fields are valid, show the combo boxes for the x_axis let x_field = &mut plot_settings.x_field; egui::ComboBox::from_label("X Axis") - .selected_text(&x_field.field().name) + .selected_text(x_field.name()) .show_ui(ui, |ui| { - for msg in fields.iter() { - ui.selectable_value(x_field, msg.to_owned(), &msg.field().name); + for msg in x_fields.iter() { + ui.selectable_value(x_field, msg.to_owned(), msg.name()); } }); + // retain only the fields that are in y_fields + plot_settings + .y_fields + .retain(|(field, _)| y_fields.iter().any(|f: &YPlotField| f == field)); + // populate the plot_lines with the first field if it is empty and there are more than 1 fields - if plot_settings.y_fields.is_empty() && fields.len() > 1 { - plot_settings.add_field(fields[1].to_owned()); + if plot_settings.y_fields.is_empty() && y_fields.len() > 1 { + plot_settings.add_field(y_fields[0].clone()); } // check how many fields are left and how many are selected @@ -95,10 +105,10 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { "Y Axis".to_owned() }; egui::ComboBox::from_label(widget_label) - .selected_text(&field.field().name) + .selected_text(field.name()) .show_ui(ui, |ui| { - for msg in fields.iter() { - ui.selectable_value(field, msg.to_owned(), &msg.field().name); + for msg in y_fields.iter() { + ui.selectable_value(field, msg.to_owned(), msg.name()); } }); ui.color_edit_button_srgba(color); @@ -114,14 +124,14 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { }); // if we have fields left, show the add button - if fields.len().saturating_sub(plot_lines_len + 1) > 0 + if y_fields.len().saturating_sub(plot_lines_len + 1) > 0 && ui .button("Add Y Axis") .on_hover_text("Add another Y axis") .clicked() { // get the first field that is not in the plot_lines - let next_field = fields + let next_field = y_fields .iter() .find(|field| !plot_settings.y_fields.iter().any(|(f, _)| f == *field)) .log_unwrap(); diff --git a/src/utils/units.rs b/src/utils/units.rs index 98267d98cf45a12f542c22d5d7d2d8366c315867..cf05bc66f4cd7ab79e430b1eaf4d8a9de0772abf 100644 --- a/src/utils/units.rs +++ b/src/utils/units.rs @@ -4,14 +4,22 @@ use std::{fmt::Display, str::FromStr}; pub enum UnitOfMeasure { Time(TimeUnits), Other(String), + Adimensional, } -impl<T: AsRef<str>> From<T> for UnitOfMeasure { - fn from(s: T) -> Self { - if let Ok(unit) = TimeUnits::from_str(s.as_ref()) { - UnitOfMeasure::Time(unit) - } else { - UnitOfMeasure::Other(s.as_ref().to_string()) +impl<T: AsRef<str>> From<Option<T>> for UnitOfMeasure { + fn from(s: Option<T>) -> Self { + let s = s.as_ref(); + match s { + Some(s) if s.as_ref().is_empty() => UnitOfMeasure::Adimensional, + Some(s) => { + if let Ok(unit) = TimeUnits::from_str(s.as_ref()) { + UnitOfMeasure::Time(unit) + } else { + UnitOfMeasure::Other(s.as_ref().to_string()) + } + } + None => UnitOfMeasure::Adimensional, } } } @@ -21,6 +29,7 @@ impl Display for UnitOfMeasure { match self { UnitOfMeasure::Time(unit) => write!(f, "{}", unit), UnitOfMeasure::Other(unit) => write!(f, "{}", unit), + UnitOfMeasure::Adimensional => write!(f, ""), } } }