Compare commits
3 commits
971c57fa13
...
706ed9db03
Author | SHA1 | Date | |
---|---|---|---|
706ed9db03 | |||
bb974c2015 | |||
79b22b3cb7 |
5 changed files with 3212 additions and 26 deletions
|
@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||||
/// -1..1
|
/// -1..1
|
||||||
pub type Speed = f32;
|
pub type Speed = f32;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub enum ControlPacket {
|
pub enum ControlPacket {
|
||||||
/// Forward, Clockwise
|
/// Forward, Clockwise
|
||||||
Twist(Speed, Speed),
|
Twist(Speed, Speed),
|
||||||
|
|
3092
interface/Cargo.lock
generated
3092
interface/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -13,3 +13,7 @@ serde = "1.0.217"
|
||||||
tokio = { version = "1.43.0", features = ["full"] }
|
tokio = { version = "1.43.0", features = ["full"] }
|
||||||
common = {path = "../common"}
|
common = {path = "../common"}
|
||||||
heapless = "0.7.0"
|
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
78
interface/src/gui.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,29 @@
|
||||||
#![feature(iter_collect_into)]
|
#![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 anyhow::{Context, Ok, Result};
|
||||||
use common::{ControlPacket, TelemetryPacket};
|
use common::{ControlPacket, TelemetryPacket};
|
||||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
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 pitch_detection::{detector::{mcleod::McLeodDetector, PitchDetector}, utils};
|
||||||
use rust_music_theory::note::{Note, NoteLetter, Pitch, Tuning};
|
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<()> {
|
fn main() -> Result<()> {
|
||||||
// assumes pulseaudio system with f32 samples and 2204 sample packets
|
// assumes pulseaudio system with f32 samples and 2204 sample packets
|
||||||
dbg!(cpal::available_hosts());
|
dbg!(cpal::available_hosts());
|
||||||
let host = cpal::default_host();
|
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 device = host.devices().unwrap().find(|d|d.name().unwrap() == "pulse").context("no pulse")?;
|
||||||
|
|
||||||
|
|
||||||
let config = device.default_input_config()?;
|
let config = device.default_input_config()?;
|
||||||
|
|
||||||
dbg!(config.sample_format());
|
dbg!(config.sample_format());
|
||||||
|
@ -23,7 +33,10 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
const POWER_THRESHOLD: f32 = 5.0;
|
const POWER_THRESHOLD: f32 = 5.0;
|
||||||
const CLARITY_THRESHOLD: f32 = 0.85;
|
const CLARITY_THRESHOLD: f32 = 0.85;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
const PACKET_LEN: usize = 2204;
|
const PACKET_LEN: usize = 2204;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const PACKET_LEN: usize = 512;
|
||||||
|
|
||||||
let stream = device.build_input_stream(&config.into(),
|
let stream = device.build_input_stream(&config.into(),
|
||||||
move | data: &[f32], _: &_| {
|
move | data: &[f32], _: &_| {
|
||||||
|
@ -45,26 +58,36 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
//let latest_telem = Arc::new(RwLock::new(None));
|
//let latest_telem = Arc::new(RwLock::new(None));
|
||||||
|
|
||||||
let control = executor.block_on(async {
|
let (toast_sender, toasts) = mpsc::channel(5);
|
||||||
let cruisecontrol = TcpStream::connect("192.168.1.2:1234").await?;
|
let (data_sender, gui_data) =watch::channel(GUIData::default());
|
||||||
println!("connected");
|
|
||||||
cruisecontrol.set_nodelay(true)?;
|
|
||||||
let (telem, control) = cruisecontrol.into_split();
|
|
||||||
|
|
||||||
tokio::spawn(telemetry_handler(telem));
|
|
||||||
|
|
||||||
Ok(control)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
|
|
||||||
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);
|
drop(stream);
|
||||||
|
|
||||||
Ok(())
|
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];
|
let mut buf = vec![0; 2048];
|
||||||
loop {
|
loop {
|
||||||
let len = telem.read_u32().await.context("bad length")? as usize;
|
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:?}");
|
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);
|
let mut controller = BufWriter::new(controller);
|
||||||
//send_packet(&mut controller, ControlPacket::Arm(true)).await?;
|
//send_packet(&mut controller, ControlPacket::Arm(true)).await?;
|
||||||
//println!("armed flipper");
|
//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 {
|
} else {
|
||||||
send_packet(&mut controller, ControlPacket::Twist(0.0, 0.0)).await?;
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue