use std::{cell::OnceCell, collections::HashMap, path::PathBuf, sync::{atomic::{AtomicBool, Ordering}, Arc}, time::Duration}; use common::{ControlPacket, TelemetryPacket}; use eframe::{egui::{self, containers, Align2, Checkbox, Context, IconData, Id, ImageSource, Label, Ui}, Storage}; use image::ImageFormat; use tokio::{runtime::Runtime, sync::{mpsc, watch::{self, Receiver}}}; use egui_toast::{Toast, Toasts}; use crate::{auto::Configurable, storage::StorageManager, POWER_THRESHOLD}; pub const GUI: OnceCell = OnceCell::new(); pub fn gui(data: Receiver, toasts: mpsc::Receiver, executor: Runtime, autoconf: watch::Receiver<&'static [Configurable]>, storage: StorageManager, auto_allowed: Arc, auto_enabled: Arc) -> eframe::Result { let icon = egui::include_image!("../assets/lizard.png"); let icon = image::load_from_memory_with_format(include_bytes!("../assets/lizard.png"), ImageFormat::Png).unwrap(); let icon = IconData { width: icon.width(), height: icon.height(), rgba: icon.into_rgba8().into_raw(), }; let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([700.0, 480.0]) .with_app_id("cruisecontrol") .with_icon(icon), ..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, storage, autoconf, auto_allowed, auto_enabled))) }), ) } #[derive(Default)] pub struct GUIData { pub telemetry: Option, pub last_command: Option, } #[cfg(target_os = "linux")] pub const DEFAULT_VOLUME_THRESHOLD: f32 = 5.0; #[cfg(target_os = "windows")] pub const DEFAULT_VOLUME_THRESHOLD: f32 = 1.0; const VOLUME_THRESHOLD: Configurable = Configurable::new("Volume").range(0.0..10.).default(DEFAULT_VOLUME_THRESHOLD) .description("higher accepts less noise (better)"); const DEFAULT_TURN_GAIN: f32 = 0.3; const DEFAULT_FIRE_DISTANCE: f32 = 55.0; struct GUI { data: Receiver, toasts: mpsc::Receiver, executor: Option, selected_auto: usize, autoconf: watch::Receiver<&'static [Configurable]>, storage: StorageManager, auto_allowed: Arc, auto_enabled: Arc, } impl GUI { fn with_receivers(data: Receiver, toasts: mpsc::Receiver, executor: Runtime, storage: StorageManager, autoconf: watch::Receiver<&'static [Configurable]>, auto_allowed: Arc, auto_enabled: Arc) -> Self { Self { data, toasts, executor: Some(executor), selected_auto: 0, autoconf, storage, auto_allowed, auto_enabled, } } } // dupe from auto crate for testing const AUTO_GAP: Configurable = Configurable::new("auto minimum gap").range(0. .. 300.).default(140.) .description("distance (mm) distance measurements must instantaneously drop to indicate a detection. This should line up with the size of the smallest robot you compete against"); const AUTO_SELF_OCCLUSION: Configurable = Configurable::new("auto self occlusion").range(0. .. 200.).default(143.) .description("distance (mm) below which measurements are considered noise in the scan phase"); pub static CONFIGS: &[Configurable] = &[ AUTO_GAP, AUTO_SELF_OCCLUSION, ]; 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"); configurator(ui, &VOLUME_THRESHOLD, &self.storage); 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(); }); POWER_THRESHOLD.store(self.storage.load(&VOLUME_THRESHOLD), Ordering::Relaxed); for conf in CONFIGS { configurator(ui, conf, &self.storage); } }); 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:?}")); } ui.label(format!("auto authorized: {}", if self.auto_allowed.load(Ordering::Acquire) {"✅"} else {"❌"})); ui.label(format!("auto running: {}", if self.auto_enabled.load(Ordering::Acquire) {"✅ zoom vroom"} else {"❌"})); 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)); } } } fn configurator(ui: &mut Ui, config: &'static Configurable, map: &StorageManager) { ui.add(egui::Slider::from_get_set(config.min as f64 ..= config.max as f64, map.get_set(config)).text(config.name)); if let Some(description) = config.description { ui.label(description); } }