1
Fork 0

Compare commits

...

3 commits

Author SHA1 Message Date
706ed9db03
adapt to windows audio quirks (virtualbox ones at least) 2025-03-06 17:09:37 -05:00
bb974c2015
send tof status to gui 2025-03-06 12:17:13 -05:00
79b22b3cb7
added basic control gui
tried fltk, didn't like it, using egui
2025-03-06 11:49:33 -05:00
5 changed files with 3212 additions and 26 deletions

View file

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
/// -1..1
pub type Speed = f32;
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ControlPacket {
/// Forward, Clockwise
Twist(Speed, Speed),

3092
interface/Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -13,3 +13,7 @@ serde = "1.0.217"
tokio = { version = "1.43.0", features = ["full"] }
common = {path = "../common"}
heapless = "0.7.0"
eframe = "0.30"
egui_extras = { version = "0.30", features = ["default", "image"] }
egui-toast = "0.16.0"

78
interface/src/gui.rs Normal file
View file

@ -0,0 +1,78 @@
use std::cell::OnceCell;
use common::{ControlPacket, TelemetryPacket};
use eframe::egui::{self, containers, Align2, Checkbox, Context, Label};
use tokio::sync::{mpsc, watch::Receiver};
use egui_toast::{Toast, Toasts};
pub const GUI: OnceCell<Context> = OnceCell::new();
pub fn gui(data: Receiver<GUIData>, toasts: mpsc::Receiver<Toast>) -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
..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)))
}),
)
}
#[derive(Default)]
pub struct GUIData {
pub telemetry: Option<TelemetryPacket>,
pub last_command: Option<ControlPacket>,
}
struct GUI {
data: Receiver<GUIData>,
toasts: mpsc::Receiver<Toast>,
}
impl GUI {
fn with_receivers(data: Receiver<GUIData>, toasts: mpsc::Receiver<Toast>) -> Self {
Self {
data,
toasts,
}
}
}
impl eframe::App for GUI {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let _ = GUI.set(ctx.clone());
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);
}
}

View file

@ -1,19 +1,29 @@
#![feature(iter_collect_into)]
use std::{ops::ControlFlow, result, sync::Arc, thread::sleep, time::Duration};
use std::{ops::ControlFlow, result, sync::Arc, thread::{self, sleep}, time::Duration};
use anyhow::{Context, Ok, Result};
use common::{ControlPacket, TelemetryPacket};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use egui_toast::{Toast, ToastKind};
use gui::{gui, GUIData, GUI};
use pitch_detection::{detector::{mcleod::McLeodDetector, PitchDetector}, utils};
use rust_music_theory::note::{Note, NoteLetter, Pitch, Tuning};
use tokio::{io::{AsyncReadExt, AsyncWriteExt, BufWriter, WriteHalf}, net::{tcp::{OwnedReadHalf, OwnedWriteHalf}, TcpStream}, spawn, sync::{self, mpsc, RwLock}, time::timeout};
use tokio::{io::{AsyncReadExt, AsyncWriteExt, BufWriter, WriteHalf}, net::{tcp::{OwnedReadHalf, OwnedWriteHalf}, TcpStream}, spawn, sync::{self, mpsc, watch, RwLock}, time::timeout};
mod gui;
fn main() -> Result<()> {
// assumes pulseaudio system with f32 samples and 2204 sample packets
dbg!(cpal::available_hosts());
let host = cpal::default_host();
#[cfg(target_os = "windows")]
let Some(device) = host.devices().unwrap().filter(|device| device.supported_input_configs().unwrap().count() != 0).next() else {
panic!("no input devices")
};
#[cfg(target_os = "linux")]
let device = host.devices().unwrap().find(|d|d.name().unwrap() == "pulse").context("no pulse")?;
let config = device.default_input_config()?;
dbg!(config.sample_format());
@ -23,7 +33,10 @@ fn main() -> Result<()> {
const POWER_THRESHOLD: f32 = 5.0;
const CLARITY_THRESHOLD: f32 = 0.85;
#[cfg(target_os = "linux")]
const PACKET_LEN: usize = 2204;
#[cfg(target_os = "windows")]
const PACKET_LEN: usize = 512;
let stream = device.build_input_stream(&config.into(),
move | data: &[f32], _: &_| {
@ -45,26 +58,36 @@ fn main() -> Result<()> {
//let latest_telem = Arc::new(RwLock::new(None));
let control = executor.block_on(async {
let cruisecontrol = TcpStream::connect("192.168.1.2:1234").await?;
println!("connected");
cruisecontrol.set_nodelay(true)?;
let (telem, control) = cruisecontrol.into_split();
tokio::spawn(telemetry_handler(telem));
Ok(control)
})?;
let (toast_sender, toasts) = mpsc::channel(5);
let (data_sender, gui_data) =watch::channel(GUIData::default());
executor.block_on(controller(notes, control)).unwrap();
thread::spawn(move || {
let control = executor.block_on(async {
toast_sender.send(Toast::new().text("connecting to bot").kind(ToastKind::Info)).await.unwrap();
let cruisecontrol = TcpStream::connect("192.168.1.2:1234").await?;
println!("connected");
toast_sender.send(Toast::new().text("connected").kind(ToastKind::Success)).await.unwrap();
cruisecontrol.set_nodelay(true)?;
let (telem, control) = cruisecontrol.into_split();
tokio::spawn(telemetry_handler(telem, data_sender.clone()));
Ok(control)
}).unwrap();
executor.block_on(controller(notes, control, data_sender.clone())).unwrap()
});
println!("launching gui");
gui(gui_data, toasts).unwrap();
drop(stream);
Ok(())
}
async fn telemetry_handler(mut telem: OwnedReadHalf) -> Result<()> {
async fn telemetry_handler(mut telem: OwnedReadHalf, gui: watch::Sender<GUIData>) -> Result<()> {
let mut buf = vec![0; 2048];
loop {
let len = telem.read_u32().await.context("bad length")? as usize;
@ -74,10 +97,14 @@ async fn telemetry_handler(mut telem: OwnedReadHalf) -> Result<()> {
println!("telem: {telem:?}");
gui.send_modify(|gui| {
gui.telemetry = Some(telem);
});
GUI.get().map(|c| c.request_repaint());
}
}
async fn controller(mut notes: mpsc::Receiver<(Option<pitch_detection::Pitch<f32>>, f32)>, controller: OwnedWriteHalf) -> Result<()> {
async fn controller(mut notes: mpsc::Receiver<(Option<pitch_detection::Pitch<f32>>, f32)>, controller: OwnedWriteHalf, gui: watch::Sender<GUIData>) -> Result<()> {
let mut controller = BufWriter::new(controller);
//send_packet(&mut controller, ControlPacket::Arm(true)).await?;
//println!("armed flipper");
@ -105,10 +132,13 @@ async fn controller(mut notes: mpsc::Receiver<(Option<pitch_detection::Pitch<f32
}
}
send_packet(&mut controller, control).await?;
send_packet(&mut controller, control.clone()).await?;
gui.send_modify(|gui| gui.last_command = Some(control));
} else {
send_packet(&mut controller, ControlPacket::Twist(0.0, 0.0)).await?;
gui.send_modify(|gui| gui.last_command = Some(ControlPacket::Stop));
}
GUI.get().map(|c| c.request_repaint());
}
}