1
Fork 0
cruisecontrol/interface/src/gui.rs

157 lines
6.3 KiB
Rust

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<Context> = OnceCell::new();
pub fn gui(data: Receiver<GUIData>, toasts: mpsc::Receiver<Toast>, executor: Runtime, autoconf: watch::Receiver<&'static [Configurable]>, storage: StorageManager, auto_allowed: Arc<AtomicBool>, auto_enabled: Arc<AtomicBool>) -> 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<TelemetryPacket>,
pub last_command: Option<ControlPacket>,
}
#[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<GUIData>,
toasts: mpsc::Receiver<Toast>,
executor: Option<Runtime>,
selected_auto: usize,
autoconf: watch::Receiver<&'static [Configurable]>,
storage: StorageManager,
auto_allowed: Arc<AtomicBool>,
auto_enabled: Arc<AtomicBool>,
}
impl GUI {
fn with_receivers(data: Receiver<GUIData>, toasts: mpsc::Receiver<Toast>, executor: Runtime, storage: StorageManager, autoconf: watch::Receiver<&'static [Configurable]>, auto_allowed: Arc<AtomicBool>, auto_enabled: Arc<AtomicBool>) -> 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);
}
}