157 lines
6.3 KiB
Rust
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);
|
|
}
|
|
}
|