132 lines
5.2 KiB
Rust
132 lines
5.2 KiB
Rust
|
|
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<Context> = OnceCell::new();
|
|
|
|
pub fn gui(data: Receiver<GUIData>, toasts: mpsc::Receiver<Toast>, 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<TelemetryPacket>,
|
|
pub last_command: Option<ControlPacket>,
|
|
}
|
|
|
|
#[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<GUIData>,
|
|
toasts: mpsc::Receiver<Toast>,
|
|
executor: Option<Runtime>,
|
|
volume_threshold: f32,
|
|
auto_turn_gain: f32,
|
|
/// mm
|
|
auto_fire_distance: f32,
|
|
selected_auto: usize,
|
|
}
|
|
|
|
impl GUI {
|
|
fn with_receivers(data: Receiver<GUIData>, toasts: mpsc::Receiver<Toast>, 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));
|
|
}
|
|
}
|
|
}
|