diff --git a/Cargo.toml b/Cargo.toml index 8c2de1bc18277d46b2809a58c2c77efa82b714e7..56988286f6da94d065211d205aa4196de3fe1e83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ authors = [ "Niccolò Betto <niccolo.betto@skywarder.eu>", ] edition = "2024" -description = "Skyward Enhanced Ground Software" +edescription = "Skyward Enhanced Ground Software" license = "MIT" [dependencies] diff --git a/icons/valve_control/dark/aperture.svg b/icons/valve_control/dark/aperture.svg new file mode 100644 index 0000000000000000000000000000000000000000..75a1a4fe0ffad4b43a00481a80ed617a16dc3797 --- /dev/null +++ b/icons/valve_control/dark/aperture.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true" + xmlns="http://www.w3.org/2000/svg"> + <path + d="m 9.8075842,8.125795 c -0.047226,-0.0818 -0.1653177,-0.0818 -0.2125819,-2e-5 l -2.7091686,4.69002 c -0.047101,0.0815 0.011348,0.18406 0.1055111,0.18419 0.00288,0 0.00577,0 0.00866,0 1.7426489,0 3.3117022,-0.74298 4.4078492,-1.92938 0.03656,-0.0396 0.04318,-0.0983 0.01627,-0.14492 L 9.807592,8.125795 Z m -2.4888999,1.68471 -5.4097573,0.005 c -0.094226,8e-5 -0.1536307,0.10217 -0.1064545,0.18373 0.8246515,1.42592 2.2192034,2.4809 3.8722553,2.85359 0.052573,0.0119 0.1068068,-0.0117 0.1337538,-0.0583 l 1.6166069,-2.80007 c 0.047277,-0.0819 -0.011863,-0.18422 -0.1064042,-0.18411 z m 4.8812207,-5.80581 c -0.825356,-1.42976 -2.2234427,-2.48728 -3.8807595,-2.85911 -0.052561,-0.0118 -0.1067061,0.0117 -0.1336405,0.0584 l -1.6174875,2.80157 c -0.047252,0.0819 0.011813,0.18415 0.1063413,0.18412 l 5.4189662,-0.001 c 0.0942,-2e-5 0.153693,-0.10205 0.10658,-0.18365 z m 0.413678,1.12713 -3.2344588,0 c -0.094503,0 -0.1535677,0.10233 -0.1062909,0.18415 l 2.7089927,4.6888 c 0.04735,0.0819 0.165368,0.0815 0.212808,-3.1e-4 C 12.706741,9.120925 13,8.094715 13,7.000015 c 0,-0.62043 -0.09423,-1.2188 -0.269055,-1.7817 -0.01595,-0.0514 -0.06352,-0.0865 -0.117362,-0.0865 z M 7.0021135,1.000015 c -7.045e-4,0 -0.00142,0 -0.00213,0 -1.7423595,0 -3.311199,0.74275 -4.4073209,1.92881 -0.036558,0.0395 -0.043188,0.0983 -0.016254,0.14494 l 1.6162797,2.79947 c 0.047264,0.0819 0.1654436,0.0818 0.2126575,-9e-5 l 2.7023877,-4.6891 c 0.04695,-0.0815 -0.011498,-0.18401 -0.1056242,-0.18403 z m -5.6144011,7.87237 3.2356039,0 c 0.094503,0 0.1535552,-0.10231 0.106291,-0.18416 L 2.0185518,3.994515 c -0.047327,-0.082 -0.1653304,-0.0816 -0.2127959,3e-4 C 1.2933853,4.878505 1,5.904985 1,7.000015 c 0,0.62198 0.094717,1.22181 0.2703885,1.78596 0.01599,0.0514 0.063518,0.0864 0.1173239,0.0864 z" /> +</svg> diff --git a/icons/valve_control/dark/timing.svg b/icons/valve_control/dark/timing.svg new file mode 100644 index 0000000000000000000000000000000000000000..e267bdeeb256b7bf7670fdeb4c289da263f2372a --- /dev/null +++ b/icons/valve_control/dark/timing.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#ffffff" width="800px" height="800px" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg"> + <path + d="M 27.9999 51.9063 C 41.0546 51.9063 51.9063 41.0781 51.9063 28 C 51.9063 14.9453 41.0780 4.0937 28.0234 4.0937 C 26.7812 4.0937 26.1718 4.8437 26.1718 6.0625 L 26.1718 15.1563 C 26.1718 16.1641 26.8514 16.9844 27.8827 16.9844 C 28.9140 16.9844 29.6171 16.1641 29.6171 15.1563 L 29.6171 8.1484 C 39.9296 8.9688 47.8983 17.5 47.8983 28 C 47.8983 39.0625 39.0390 47.9219 27.9999 47.9219 C 16.9374 47.9219 8.0546 39.0625 8.0780 28 C 8.1014 23.0781 9.8593 18.6016 12.7890 15.1563 C 13.5155 14.2422 13.5624 13.1406 12.7890 12.3203 C 12.0155 11.4766 10.7030 11.5469 9.8593 12.6016 C 6.2733 16.7734 4.0937 22.1641 4.0937 28 C 4.0937 41.0781 14.9218 51.9063 27.9999 51.9063 Z M 31.7499 31.6094 C 33.6014 29.6875 33.2265 27.0625 30.9999 25.5156 L 18.6014 16.8672 C 17.4296 16.0469 16.2109 17.2656 17.0312 18.4375 L 25.6796 30.8359 C 27.2265 33.0625 29.8514 33.4609 31.7499 31.6094 Z" /> +</svg> diff --git a/icons/valve_control/light/aperture.svg b/icons/valve_control/light/aperture.svg new file mode 100644 index 0000000000000000000000000000000000000000..7975283833187be7f0c63597d21e3cfa19b0d586 --- /dev/null +++ b/icons/valve_control/light/aperture.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#000000" width="800px" height="800px" viewBox="0 0 14 14" role="img" focusable="false" aria-hidden="true" + xmlns="http://www.w3.org/2000/svg"> + <path + d="m 9.8075842,8.125795 c -0.047226,-0.0818 -0.1653177,-0.0818 -0.2125819,-2e-5 l -2.7091686,4.69002 c -0.047101,0.0815 0.011348,0.18406 0.1055111,0.18419 0.00288,0 0.00577,0 0.00866,0 1.7426489,0 3.3117022,-0.74298 4.4078492,-1.92938 0.03656,-0.0396 0.04318,-0.0983 0.01627,-0.14492 L 9.807592,8.125795 Z m -2.4888999,1.68471 -5.4097573,0.005 c -0.094226,8e-5 -0.1536307,0.10217 -0.1064545,0.18373 0.8246515,1.42592 2.2192034,2.4809 3.8722553,2.85359 0.052573,0.0119 0.1068068,-0.0117 0.1337538,-0.0583 l 1.6166069,-2.80007 c 0.047277,-0.0819 -0.011863,-0.18422 -0.1064042,-0.18411 z m 4.8812207,-5.80581 c -0.825356,-1.42976 -2.2234427,-2.48728 -3.8807595,-2.85911 -0.052561,-0.0118 -0.1067061,0.0117 -0.1336405,0.0584 l -1.6174875,2.80157 c -0.047252,0.0819 0.011813,0.18415 0.1063413,0.18412 l 5.4189662,-0.001 c 0.0942,-2e-5 0.153693,-0.10205 0.10658,-0.18365 z m 0.413678,1.12713 -3.2344588,0 c -0.094503,0 -0.1535677,0.10233 -0.1062909,0.18415 l 2.7089927,4.6888 c 0.04735,0.0819 0.165368,0.0815 0.212808,-3.1e-4 C 12.706741,9.120925 13,8.094715 13,7.000015 c 0,-0.62043 -0.09423,-1.2188 -0.269055,-1.7817 -0.01595,-0.0514 -0.06352,-0.0865 -0.117362,-0.0865 z M 7.0021135,1.000015 c -7.045e-4,0 -0.00142,0 -0.00213,0 -1.7423595,0 -3.311199,0.74275 -4.4073209,1.92881 -0.036558,0.0395 -0.043188,0.0983 -0.016254,0.14494 l 1.6162797,2.79947 c 0.047264,0.0819 0.1654436,0.0818 0.2126575,-9e-5 l 2.7023877,-4.6891 c 0.04695,-0.0815 -0.011498,-0.18401 -0.1056242,-0.18403 z m -5.6144011,7.87237 3.2356039,0 c 0.094503,0 0.1535552,-0.10231 0.106291,-0.18416 L 2.0185518,3.994515 c -0.047327,-0.082 -0.1653304,-0.0816 -0.2127959,3e-4 C 1.2933853,4.878505 1,5.904985 1,7.000015 c 0,0.62198 0.094717,1.22181 0.2703885,1.78596 0.01599,0.0514 0.063518,0.0864 0.1173239,0.0864 z" /> +</svg> diff --git a/icons/valve_control/light/timing.svg b/icons/valve_control/light/timing.svg new file mode 100644 index 0000000000000000000000000000000000000000..dd2d1e16f08a64043f4e32efed01eba0e4b47454 --- /dev/null +++ b/icons/valve_control/light/timing.svg @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> +<svg fill="#000000" width="800px" height="800px" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg"> + <path + d="M 27.9999 51.9063 C 41.0546 51.9063 51.9063 41.0781 51.9063 28 C 51.9063 14.9453 41.0780 4.0937 28.0234 4.0937 C 26.7812 4.0937 26.1718 4.8437 26.1718 6.0625 L 26.1718 15.1563 C 26.1718 16.1641 26.8514 16.9844 27.8827 16.9844 C 28.9140 16.9844 29.6171 16.1641 29.6171 15.1563 L 29.6171 8.1484 C 39.9296 8.9688 47.8983 17.5 47.8983 28 C 47.8983 39.0625 39.0390 47.9219 27.9999 47.9219 C 16.9374 47.9219 8.0546 39.0625 8.0780 28 C 8.1014 23.0781 9.8593 18.6016 12.7890 15.1563 C 13.5155 14.2422 13.5624 13.1406 12.7890 12.3203 C 12.0155 11.4766 10.7030 11.5469 9.8593 12.6016 C 6.2733 16.7734 4.0937 22.1641 4.0937 28 C 4.0937 41.0781 14.9218 51.9063 27.9999 51.9063 Z M 31.7499 31.6094 C 33.6014 29.6875 33.2265 27.0625 30.9999 25.5156 L 18.6014 16.8672 C 17.4296 16.0469 16.2109 17.2656 17.0312 18.4375 L 25.6796 30.8359 C 27.2265 33.0625 29.8514 33.4609 31.7499 31.6094 Z" /> +</svg> diff --git a/src/ui/panes/messages_viewer.rs b/src/ui/panes/messages_viewer.rs index 3d85aef8ecefb2c54be85d088e1e52fd13327540..cfee6c67ab5601d72b53852581f51c76d07c21da 100644 --- a/src/ui/panes/messages_viewer.rs +++ b/src/ui/panes/messages_viewer.rs @@ -5,24 +5,14 @@ use crate::ui::app::PaneResponse; use super::PaneBehavior; -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct MessagesViewerPane { - #[serde(skip)] - contains_pointer: bool, -} - -impl PartialEq for MessagesViewerPane { - fn eq(&self, _other: &Self) -> bool { - true - } -} +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct MessagesViewerPane; impl PaneBehavior for MessagesViewerPane { #[profiling::function] fn ui(&mut self, ui: &mut Ui) -> PaneResponse { let mut response = PaneResponse::default(); let label = ui.add_sized(ui.available_size(), Label::new("This is a label")); - self.contains_pointer = label.contains_pointer(); if label.drag_started() { response.set_drag_started(); } diff --git a/src/ui/panes/plot/source_window.rs b/src/ui/panes/plot/source_window.rs index a62d1f9e41bcb3154c7bd4197327fc62c37a7306..c3098783f3ccad53e7ebf0e8e1b4c49ef381e447 100644 --- a/src/ui/panes/plot/source_window.rs +++ b/src/ui/panes/plot/source_window.rs @@ -14,6 +14,7 @@ pub fn sources_window(ui: &mut egui::Ui, plot_settings: &mut PlotSettings) { egui::DragValue::new(&mut points_lifespan_sec) .range(5..=1800) .speed(1) + .update_while_editing(false) .suffix(" seconds"), ); res1.union(res2) diff --git a/src/ui/panes/valve_control.rs b/src/ui/panes/valve_control.rs index 4fdf47d0f52a9a540e6c7c2534deec32a5f9f12f..1e781c90d3377ad6575f55dcaf25e0a9281a7607 100644 --- a/src/ui/panes/valve_control.rs +++ b/src/ui/panes/valve_control.rs @@ -1,17 +1,323 @@ -use egui::Ui; +mod commands; +mod icons; +mod valves; + +use std::time::{Duration, Instant}; + +use egui::{ + Color32, DragValue, Frame, Label, Rect, RichText, Sense, Stroke, Ui, UiBuilder, Vec2, Widget, + vec2, +}; +use egui_extras::{Size, StripBuilder}; +use itertools::Itertools; use serde::{Deserialize, Serialize}; +use skyward_mavlink::{ + mavlink::MessageData, + orion::{ACK_TM_DATA, NACK_TM_DATA, WACK_TM_DATA}, +}; +use strum::IntoEnumIterator; +use tracing::info; -use crate::ui::app::PaneResponse; +use crate::{ + mavlink::{MavMessage, TimedMessage}, + ui::app::PaneResponse, +}; use super::PaneBehavior; -mod enums; +use commands::CommandSM; +use icons::Icon; +use valves::{Valve, ValveStateManager}; + +const DEFAULT_AUTO_REFRESH_RATE: Duration = Duration::from_secs(1); #[derive(Clone, PartialEq, Default, Serialize, Deserialize, Debug)] -pub struct ValveControlPane {} +pub struct ValveControlPane { + // INTERNAL + #[serde(skip)] + valves_state: ValveStateManager, + + // VALVE COMMANDS LIST + #[serde(skip)] + commands: Vec<CommandSM>, + + // REFRESH SETTINGS + auto_refresh: Option<Duration>, + + #[serde(skip)] + manual_refresh: bool, + + #[serde(skip)] + last_refresh: Option<Instant>, + + // UI SETTINGS + #[serde(skip)] + is_settings_window_open: bool, +} impl PaneBehavior for ValveControlPane { fn ui(&mut self, ui: &mut Ui) -> PaneResponse { - todo!() + let mut pane_response = PaneResponse::default(); + + let res = ui + .scope_builder(UiBuilder::new().sense(Sense::click_and_drag()), |ui| { + self.pane_ui()(ui); + ui.allocate_space(ui.available_size()); + }) + .response; + + // Show the menu when the user right-clicks the pane + res.context_menu(self.menu_ui()); + + // Check if the user started dragging the pane + if res.drag_started() { + pane_response.set_drag_started(); + } + + egui::Window::new("Settings") + .id(ui.auto_id_with("settings")) + .auto_sized() + .collapsible(true) + .movable(true) + .open(&mut self.is_settings_window_open) + .show(ui.ctx(), Self::window_ui(&mut self.auto_refresh)); + + pane_response + } + + fn get_message_subscriptions(&self) -> Box<dyn Iterator<Item = u32>> { + let mut subscriptions = vec![]; + if self.needs_refresh() { + // TODO + // subscriptions.push(); + } + + // Subscribe to ACK, NACK, WACK messages if any command is waiting for a response + if self.commands.iter().any(CommandSM::is_waiting_for_response) { + subscriptions.push(ACK_TM_DATA::ID); + subscriptions.push(NACK_TM_DATA::ID); + subscriptions.push(WACK_TM_DATA::ID); + } + + Box::new(subscriptions.into_iter()) + } + + fn update(&mut self, messages: &[&TimedMessage]) { + if self.needs_refresh() { + // TODO + } + + // Capture any ACK/NACK/WACK messages and update the valve state + for message in messages { + for cmd in self.commands.iter_mut() { + // intercept all ACK/NACK/WACK messages + cmd.capture_response(&message.message); + // If a response was captured, consume the command and update the valve state + if let Some((valve, parameter)) = cmd.consume_response() { + self.valves_state.set_parameter_of(valve, parameter); + } + } + + // Remove consumed commands + self.commands.retain(|cmd| !cmd.is_consumed()); + } + + self.reset_last_refresh(); + } + + fn drain_outgoing_messages(&mut self) -> Vec<MavMessage> { + let mut outgoing = vec![]; + + // Pack and send the next command + for cmd in self.commands.iter_mut() { + if let Some(message) = cmd.pack_and_wait() { + outgoing.push(message); + } + } + + outgoing + } +} + +// ┌────────────────────────┐ +// │ UI METHODS │ +// └────────────────────────┘ +impl ValveControlPane { + fn pane_ui(&mut self) -> impl FnOnce(&mut Ui) { + |ui| { + let valve_chunks = Valve::iter().chunks(3); + StripBuilder::new(ui) + .sizes(Size::remainder(), 3) + .vertical(|mut strip| { + for chunk in &valve_chunks { + strip.strip(|builder| { + builder.sizes(Size::remainder(), 3).horizontal(|mut strip| { + for valve in chunk { + strip.cell(self.valve_frame_ui(valve)); + } + }); + }); + } + }); + } + } + + fn menu_ui(&mut self) -> impl FnOnce(&mut Ui) { + |ui| { + if ui.button("Refresh now").clicked() { + self.manual_refresh = true; + ui.close_menu(); + } + if ui.button("Settings").clicked() { + self.is_settings_window_open = true; + ui.close_menu(); + } + } + } + + fn window_ui(auto_refresh_setting: &mut Option<Duration>) -> impl FnOnce(&mut Ui) { + |ui| { + // Display auto refresh setting + let mut auto_refresh = auto_refresh_setting.is_some(); + ui.horizontal(|ui| { + ui.checkbox(&mut auto_refresh, "Auto Refresh"); + if auto_refresh { + let auto_refresh_duration = + auto_refresh_setting.get_or_insert(DEFAULT_AUTO_REFRESH_RATE); + let mut auto_refresh_rate = 1. / auto_refresh_duration.as_secs_f32(); + DragValue::new(&mut auto_refresh_rate) + .speed(0.1) + .range(0.1..=10.0) + .fixed_decimals(1) + .update_while_editing(false) + .suffix(" Hz") + .ui(ui); + *auto_refresh_duration = Duration::from_secs_f32(1. / auto_refresh_rate); + } else { + *auto_refresh_setting = None; + } + }); + } + } + + fn valve_frame_ui(&self, valve: Valve) -> impl FnOnce(&mut Ui) { + move |ui| { + let valve_str = valve.to_string(); + let timing = self.valves_state.get_timing_for(valve); + let aperture = self.valves_state.get_aperture_for(valve); + + let timing_str = match timing { + valves::ParameterValue::Valid(value) => { + format!("{} [ms]", value) + } + valves::ParameterValue::Missing => "N/A".to_owned(), + valves::ParameterValue::Invalid(err_id) => { + format!("ERROR: {}", err_id) + } + }; + let aperture_str = match aperture { + valves::ParameterValue::Valid(value) => { + format!("{}", value) + } + valves::ParameterValue::Missing => "N/A".to_owned(), + valves::ParameterValue::Invalid(err_id) => { + format!("ERROR: {}", err_id) + } + }; + + let inside_frame = |ui: &mut Ui| { + let response = ui.response(); + let visuals = ui.style().interact(&response); + let text_color = visuals.text_color(); + let icon_size = Vec2::splat(17.); + + StripBuilder::new(ui) + .size(Size::exact(20.)) + .size(Size::exact(20.)) + .vertical(|mut strip| { + strip.cell(|ui| { + Label::new( + RichText::new(valve_str.to_ascii_uppercase()) + .color(text_color) + .size(16.0), + ) + .selectable(false) + .ui(ui); + }); + strip.cell(|ui| { + ui.vertical(|ui| { + ui.horizontal(|ui| { + let rect = Rect::from_min_size(ui.cursor().min, icon_size); + Icon::Timing.paint(ui, rect); + ui.allocate_rect(rect, Sense::hover()); + Label::new(format!("Timing: {}", timing_str)) + .selectable(false) + .ui(ui); + }); + ui.horizontal(|ui| { + let rect = Rect::from_min_size(ui.cursor().min, icon_size); + Icon::Aperture.paint(ui, rect); + ui.allocate_rect(rect, Sense::hover()); + Label::new(format!("Aperture: {}", aperture_str)) + .selectable(false) + .ui(ui); + }); + }); + }); + }); + }; + + ui.scope_builder( + UiBuilder::new() + .id_salt("valve_".to_owned() + &valve_str) + .sense(Sense::click()), + |ui| { + ui.set_width(200.); + ui.set_height(60.); + let response = ui.response(); + let visuals = ui.style().interact(&response); + + let fill_color = if response.hovered() { + visuals.bg_fill + } else { + visuals.bg_fill.gamma_multiply(0.3) + }; + + Frame::canvas(ui.style()) + .fill(fill_color) + .stroke(Stroke::NONE) + .inner_margin(ui.spacing().menu_margin) + .show(ui, inside_frame); + + if response.clicked() { + info!("Clicked!"); + } + }, + ); + } + } +} + +// ┌───────────────────────────┐ +// │ UTILS METHODS │ +// └───────────────────────────┘ +impl ValveControlPane { + fn needs_refresh(&self) -> bool { + // manual trigger of refresh + let manual_triggered = self.manual_refresh; + // automatic trigger of refresh + let auto_triggered = if let Some(auto_refresh) = self.auto_refresh { + self.last_refresh + .is_none_or(|last| last.elapsed() >= auto_refresh) + } else { + false + }; + + manual_triggered || auto_triggered + } + + fn reset_last_refresh(&mut self) { + self.last_refresh = Some(Instant::now()); + self.manual_refresh = false; } } diff --git a/src/ui/panes/valve_control/commands.rs b/src/ui/panes/valve_control/commands.rs new file mode 100644 index 0000000000000000000000000000000000000000..8593fd9be94a5e594de9cae0dc8a635bc81e1209 --- /dev/null +++ b/src/ui/panes/valve_control/commands.rs @@ -0,0 +1,163 @@ +use crate::mavlink::{ + ACK_TM_DATA, MavMessage, MessageData, NACK_TM_DATA, SET_ATOMIC_VALVE_TIMING_TC_DATA, + SET_VALVE_MAXIMUM_APERTURE_TC_DATA, WACK_TM_DATA, +}; + +use super::valves::{ParameterValue, Valve, ValveParameter}; + +#[derive(Debug, Clone, PartialEq)] +pub enum CommandSM { + Request(Command), + WaitingForResponse(Command), + Response((Valve, ValveParameter)), + Consumed, +} + +impl CommandSM { + pub fn pack_and_wait(&mut self) -> Option<MavMessage> { + match self { + CommandSM::Request(command) => { + let message = MavMessage::from(command.clone()); + *self = CommandSM::WaitingForResponse(command.clone()); + Some(message) + } + _ => None, + } + } + + pub fn capture_response(&mut self, message: &MavMessage) { + if let CommandSM::WaitingForResponse(Command { kind, valve }) = self { + let id = kind.message_id() as u8; + match message { + MavMessage::ACK_TM(ACK_TM_DATA { recv_msgid, .. }) if *recv_msgid == id => { + *self = CommandSM::Response((*valve, kind.to_valid_parameter())); + } + MavMessage::NACK_TM(NACK_TM_DATA { + err_id, recv_msgid, .. + }) if *recv_msgid == id => { + *self = CommandSM::Response((*valve, kind.to_invalid_parameter(*err_id))); + } + MavMessage::WACK_TM(WACK_TM_DATA { + err_id, recv_msgid, .. + }) if *recv_msgid == id => { + *self = CommandSM::Response((*valve, kind.to_invalid_parameter(*err_id))); + } + _ => {} + } + } + } + + pub fn consume_response(&mut self) -> Option<(Valve, ValveParameter)> { + match self { + CommandSM::Response((valve, parameter)) => { + let res = Some((*valve, parameter.clone())); + *self = CommandSM::Consumed; + res + } + _ => None, + } + } + + pub fn is_waiting_for_response(&self) -> bool { + matches!(self, CommandSM::WaitingForResponse(_)) + } + + pub fn is_consumed(&self) -> bool { + matches!(self, CommandSM::Consumed) + } +} + +impl From<Command> for CommandSM { + fn from(value: Command) -> Self { + CommandSM::Request(value) + } +} + +trait ControllableValves { + fn set_atomic_valve_timing(self, timing: u32) -> Command; + fn set_valve_maximum_aperture(self, aperture: f32) -> Command; +} + +impl ControllableValves for Valve { + fn set_atomic_valve_timing(self, timing: u32) -> Command { + Command { + kind: CommandKind::SetAtomicValveTiming(timing), + valve: self, + } + } + + fn set_valve_maximum_aperture(self, aperture: f32) -> Command { + Command { + kind: CommandKind::SetValveMaximumAperture(aperture), + valve: self, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Command { + kind: CommandKind, + valve: Valve, +} + +impl From<Command> for MavMessage { + fn from(value: Command) -> Self { + match value.kind { + CommandKind::SetAtomicValveTiming(timing) => { + MavMessage::SET_ATOMIC_VALVE_TIMING_TC(SET_ATOMIC_VALVE_TIMING_TC_DATA { + servo_id: value.valve.into(), + maximum_timing: timing, + }) + } + CommandKind::SetValveMaximumAperture(aperture) => { + MavMessage::SET_VALVE_MAXIMUM_APERTURE_TC(SET_VALVE_MAXIMUM_APERTURE_TC_DATA { + servo_id: value.valve.into(), + maximum_aperture: aperture, + }) + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum CommandKind { + SetAtomicValveTiming(u32), + SetValveMaximumAperture(f32), +} + +impl CommandKind { + fn message_id(&self) -> u32 { + match self { + CommandKind::SetAtomicValveTiming(_) => SET_ATOMIC_VALVE_TIMING_TC_DATA::ID, + CommandKind::SetValveMaximumAperture(_) => SET_VALVE_MAXIMUM_APERTURE_TC_DATA::ID, + } + } + + fn to_valid_parameter(&self) -> ValveParameter { + (*self).into() + } + + fn to_invalid_parameter(&self, error: u16) -> ValveParameter { + match self { + CommandKind::SetAtomicValveTiming(_) => { + ValveParameter::AtomicValveTiming(ParameterValue::Invalid(error)) + } + CommandKind::SetValveMaximumAperture(_) => { + ValveParameter::ValveMaximumAperture(ParameterValue::Invalid(error)) + } + } + } +} + +impl From<CommandKind> for ValveParameter { + fn from(value: CommandKind) -> Self { + match value { + CommandKind::SetAtomicValveTiming(timing) => { + ValveParameter::AtomicValveTiming(ParameterValue::Valid(timing)) + } + CommandKind::SetValveMaximumAperture(aperture) => { + ValveParameter::ValveMaximumAperture(ParameterValue::Valid(aperture)) + } + } + } +} diff --git a/src/ui/panes/valve_control/enums.rs b/src/ui/panes/valve_control/enums.rs deleted file mode 100644 index a1bf8fcdd341d13f6c306c69de358c3d5912122f..0000000000000000000000000000000000000000 --- a/src/ui/panes/valve_control/enums.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::fmt::Display; - -use strum_macros::EnumIter; - -use crate::mavlink::{ - MessageData, SET_ATOMIC_VALVE_TIMING_TC_DATA, SET_VALVE_MAXIMUM_APERTURE_TC_DATA, Servoslist, -}; - -#[allow(non_camel_case_types)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)] -pub enum Valve { - OxFilling, - OxRelease, - OxVenting, - N2Filling, - N2Release, - N2Quenching, - N23Way, - Main, - Nitrogen, -} - -impl From<Valve> for Servoslist { - fn from(valve: Valve) -> Servoslist { - match valve { - Valve::OxFilling => Servoslist::OX_FILLING_VALVE, - Valve::OxRelease => Servoslist::OX_RELEASE_VALVE, - Valve::OxVenting => Servoslist::OX_VENTING_VALVE, - Valve::N2Filling => Servoslist::N2_FILLING_VALVE, - Valve::N2Release => Servoslist::N2_RELEASE_VALVE, - Valve::N2Quenching => Servoslist::N2_QUENCHING_VALVE, - Valve::N23Way => Servoslist::N2_3WAY_VALVE, - Valve::Main => Servoslist::MAIN_VALVE, - Valve::Nitrogen => Servoslist::NITROGEN_VALVE, - } - } -} - -impl Display for Valve { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Valve::OxFilling => write!(f, "Oxidizer Filling"), - Valve::OxRelease => write!(f, "Oxidizer Release"), - Valve::OxVenting => write!(f, "Oxidizer Venting"), - Valve::N2Filling => write!(f, "Nitrogen Filling"), - Valve::N2Release => write!(f, "Nitrogen Release"), - Valve::N2Quenching => write!(f, "Nitrogen Quenching"), - Valve::N23Way => write!(f, "Nitrogen 3-Way"), - Valve::Main => write!(f, "Main"), - Valve::Nitrogen => write!(f, "Nitrogen"), - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)] -pub enum ValveCommands { - AtomicValveTiming, - ValveMaximumAperture, -} - -impl From<ValveCommands> for u32 { - fn from(command: ValveCommands) -> u32 { - match command { - ValveCommands::AtomicValveTiming => SET_ATOMIC_VALVE_TIMING_TC_DATA::ID, - ValveCommands::ValveMaximumAperture => SET_VALVE_MAXIMUM_APERTURE_TC_DATA::ID, - } - } -} diff --git a/src/ui/panes/valve_control/icons.rs b/src/ui/panes/valve_control/icons.rs new file mode 100644 index 0000000000000000000000000000000000000000..064fee550c449c9a1a8e7ff479068f79f942d0b5 --- /dev/null +++ b/src/ui/panes/valve_control/icons.rs @@ -0,0 +1,45 @@ +use egui::{ImageSource, Rect, Theme, Ui}; + +#[derive(Debug, Clone, Copy)] +pub enum Icon { + Aperture, + Timing, +} + +impl Icon { + fn get_image(&self, theme: Theme) -> ImageSource { + match (&self, theme) { + (Icon::Aperture, Theme::Light) => { + egui::include_image!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/icons/valve_control/light/aperture.svg" + )) + } + (Icon::Aperture, Theme::Dark) => { + egui::include_image!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/icons/valve_control/dark/aperture.svg" + )) + } + (Icon::Timing, Theme::Light) => { + egui::include_image!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/icons/valve_control/light/timing.svg" + )) + } + (Icon::Timing, Theme::Dark) => { + egui::include_image!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/icons/valve_control/dark/timing.svg" + )) + } + } + } +} + +impl Icon { + pub fn paint(&mut self, ui: &mut Ui, image_rect: Rect) { + let theme = ui.ctx().theme(); + egui::Image::new(self.get_image(theme)).paint_at(ui, image_rect); + } +} diff --git a/src/ui/panes/valve_control/valves.rs b/src/ui/panes/valve_control/valves.rs new file mode 100644 index 0000000000000000000000000000000000000000..9580dc169d8000ea01aaa283c596c18e68c12231 --- /dev/null +++ b/src/ui/panes/valve_control/valves.rs @@ -0,0 +1,139 @@ +//! Valve Control Pane +//! +//! NOTE: We assume that no more than one entity will sent messages to control valves at a time. + +use std::fmt::Display; + +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +use crate::{error::ErrInstrument, mavlink::Servoslist}; + +#[derive(Clone, Debug, PartialEq)] +pub struct ValveStateManager { + settings: Vec<(Valve, ValveParameter)>, +} + +impl Default for ValveStateManager { + fn default() -> Self { + Self::new() + } +} + +impl ValveStateManager { + pub fn new() -> Self { + let settings = Valve::iter() + .flat_map(|valve| ValveParameter::iter().map(move |parameter| (valve, parameter))) + .collect(); + Self { settings } + } + + pub fn set_parameter_of(&mut self, valve: Valve, parameter: ValveParameter) { + let (_, par) = self + .settings + .iter_mut() + .find(|(v, _)| *v == valve) + .log_unwrap(); + *par = parameter; + } + + pub fn get_timing_for(&self, valve: Valve) -> ParameterValue<u32, u16> { + for (_, par) in self.settings.iter().filter(|(v, _)| *v == valve) { + match par { + ValveParameter::AtomicValveTiming(parameter_value) => { + return parameter_value.clone(); + } + _ => continue, + }; + } + unreachable!() + } + + pub fn get_aperture_for(&self, valve: Valve) -> ParameterValue<f32, u16> { + for (_, par) in self.settings.iter().filter(|(v, _)| *v == valve) { + match par { + ValveParameter::ValveMaximumAperture(parameter_value) => { + return parameter_value.clone(); + } + _ => continue, + }; + } + unreachable!() + } +} + +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)] +pub enum Valve { + OxFilling, + OxRelease, + OxVenting, + N2Filling, + N2Release, + N2Quenching, + N23Way, + Main, + Nitrogen, +} + +impl From<Valve> for Servoslist { + fn from(valve: Valve) -> Servoslist { + match valve { + Valve::OxFilling => Servoslist::OX_FILLING_VALVE, + Valve::OxRelease => Servoslist::OX_RELEASE_VALVE, + Valve::OxVenting => Servoslist::OX_VENTING_VALVE, + Valve::N2Filling => Servoslist::N2_FILLING_VALVE, + Valve::N2Release => Servoslist::N2_RELEASE_VALVE, + Valve::N2Quenching => Servoslist::N2_QUENCHING_VALVE, + Valve::N23Way => Servoslist::N2_3WAY_VALVE, + Valve::Main => Servoslist::MAIN_VALVE, + Valve::Nitrogen => Servoslist::NITROGEN_VALVE, + } + } +} + +impl From<Valve> for u8 { + fn from(valve: Valve) -> u8 { + Servoslist::from(valve) as u8 + } +} + +impl Display for Valve { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Valve::OxFilling => write!(f, "Oxidizer Filling"), + Valve::OxRelease => write!(f, "Oxidizer Release"), + Valve::OxVenting => write!(f, "Oxidizer Venting"), + Valve::N2Filling => write!(f, "Nitrogen Filling"), + Valve::N2Release => write!(f, "Nitrogen Release"), + Valve::N2Quenching => write!(f, "Nitrogen Quenching"), + Valve::N23Way => write!(f, "Nitrogen 3-Way"), + Valve::Main => write!(f, "Main"), + Valve::Nitrogen => write!(f, "Nitrogen"), + } + } +} + +#[derive(Clone, Debug, PartialEq, EnumIter)] +pub enum ValveParameter { + AtomicValveTiming(ParameterValue<u32, u16>), + ValveMaximumAperture(ParameterValue<f32, u16>), +} + +#[derive(Clone, Debug, PartialEq, Default)] +pub enum ParameterValue<T, E> { + Valid(T), // T is the valid parameter value + #[default] + Missing, // The parameter is missing + Invalid(E), // E is the reason why the parameter is invalid +} + +impl<T: Display, E: Display> Display for ParameterValue<T, E> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParameterValue::Valid(value) => write!(f, "{}", value), + ParameterValue::Missing => write!(f, "MISSING"), + ParameterValue::Invalid(error) => write!(f, "INVALID: {}", error), + } + } +}