use std::{cell::OnceCell, path::PathBuf, time::Duration}; use common::{ControlPacket, TelemetryPacket}; use eframe::{egui::{self, containers, Align2, Checkbox, Context, Id, Label}, Storage}; use tokio::{runtime::Runtime, sync::{mpsc, watch::Receiver}}; use egui_toast::{Toast, Toasts}; use crate::storage_dir::storage_dir; pub const GUI: OnceCell = OnceCell::new(); pub fn gui(data: Receiver, toasts: mpsc::Receiver, executor: Runtime) -> eframe::Result { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([700.0, 480.0]) .with_app_id("cruisecontrol"), ..Default::default() }; eframe::run_native( "Cruise Control Dashboard", options, Box::new(|cc| { // This gives us image support: egui_extras::install_image_loaders(&cc.egui_ctx); Ok(Box::new(GUI::with_receivers(data, toasts, executor, cc.storage.unwrap()))) }), ) } #[derive(Default)] pub struct GUIData { pub telemetry: Option, pub last_command: Option, } #[cfg(target_os = "linux")] const DEFAULT_VOLUME_THRESHOLD: f32 = 5.0; #[cfg(target_os = "windows")] const DEFAULT_VOLUME_THRESHOLD: f32 = 1.0; const DEFAULT_TURN_GAIN: f32 = 0.3; const DEFAULT_FIRE_DISTANCE: f32 = 55.0; struct GUI { data: Receiver, toasts: mpsc::Receiver, executor: Option, volume_threshold: f32, auto_turn_gain: f32, /// mm auto_fire_distance: f32, selected_auto: usize, } impl GUI { fn with_receivers(data: Receiver, toasts: mpsc::Receiver, executor: Runtime, storage: &dyn Storage) -> Self { let volume_threshold: f32 = storage.get_string("volume_threshold").map(|s| s.parse().ok()).flatten().unwrap_or(DEFAULT_VOLUME_THRESHOLD); let auto_turn_gain: f32 = storage.get_string("auto_turn_gain").map(|s| s.parse().ok()).flatten().unwrap_or(DEFAULT_TURN_GAIN); let auto_fire_distance: f32 = storage.get_string("auto_fire_distance").map(|s| s.parse().ok()).flatten().unwrap_or(DEFAULT_FIRE_DISTANCE); let selected_auto: usize = storage.get_string("selected_auto").map(|s| s.parse().ok()).flatten().unwrap_or(0); Self { data, toasts, executor: Some(executor), volume_threshold, auto_turn_gain, auto_fire_distance, selected_auto, } } } impl eframe::App for GUI { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { let _ = GUI.set(ctx.clone()); egui::SidePanel::right(Id::new("config")).resizable(false).show(ctx, |ui| { ui.heading("configuration"); ui.add(egui::Slider::new(&mut self.volume_threshold, 0.0..=10.0).text("volume threshold")); ui.label("higher accepts less noise (better)"); ui.add(egui::Slider::new(&mut self.auto_turn_gain, 0.0..=1.0).text("auto turn kP")); ui.add(egui::Slider::new(&mut self.auto_fire_distance, 30.0..=100.0).text("auto fire distance (mm)")); ui.horizontal(|ui| { egui::ComboBox::new("auto selector", "select auto") .show_index(ui, &mut self.selected_auto, 3, |n| ["a","b","c"][n]); ui.button("refresh").clicked(); }); let storage = _frame.storage_mut().unwrap(); storage.set_string("volume_threshold", format!("{}",self.volume_threshold)); storage.set_string("auto_turn_gain", format!("{}",self.auto_turn_gain)); storage.set_string("auto_fire_distance", format!("{}",self.auto_fire_distance)); storage.set_string("selected_auto", format!("{}",self.selected_auto)); }); egui::CentralPanel::default().show(ctx, |ui| { ui.heading("Cruise Control"); if let Some(ref command) = self.data.borrow().last_command { ui.label(format!("sending {command:?}")); } if let Some(ref telem) = self.data.borrow().telemetry { ui.label(format!("Left tof: {}", if let Some(tof) = telem.sensors.tof_l {format!("✅ {tof}mm")} else {"❌".into()})); ui.label(format!("Right tof: {}", if let Some(tof) = telem.sensors.tof_r {format!("✅ {tof}mm")} else {"❌".into()})); ui.label(format!("Side tof: {}", if let Some(tof) = telem.sensors.tof_s {format!("✅ {tof}mm")} else {"❌".into()})); ui.label(format!("Gyro: {}", if telem.sensors.gyro.is_some() {"✅"} else {"❌"})); ui.label(format!("Cam: {:?}", telem.cam_state)); } else { ui.label("disconnected"); } }); let mut toasts = Toasts::new() .anchor(Align2::RIGHT_BOTTOM, (-10.0, -10.0)) // 10 units from the bottom right corner .direction(egui::Direction::BottomUp); while let Ok(toast) = self.toasts.try_recv() { toasts.add(toast); } toasts.show(ctx); if ctx.input(|i| i.viewport().close_requested()) { self.executor.take().unwrap().shutdown_timeout(Duration::from_millis(100)); } } }