diff --git a/Cargo.toml b/Cargo.toml index 032aa92..7ab7684 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ console_error_panic_hook = "0.1.7" log = "0.4.22" [workspace] -members = ["nothing", "src-tauri"] +members = ["nothing", "src-egui", "src-tauri"] [profile.release] lto = true diff --git a/nothing/src/connect.rs b/nothing/src/connect.rs index 9c5651a..3d7fe15 100644 --- a/nothing/src/connect.rs +++ b/nothing/src/connect.rs @@ -20,9 +20,9 @@ async fn find_address( .find(|&addr| match addr.0 { [a, b, c, _, _, _] => a == address[0] && b == address[1] && c == address[2], }) - .ok_or_else(|| { + .ok_or( "Couldn't find any Ear devices connected. Make sure you're paired with your Ear." - })?; + )?; Ok(*ear_address) } @@ -39,5 +39,6 @@ pub async fn connect(address: [u8; 3], channel: u8) -> Result { + if let Err(err) = result { + eprintln!("Error reading serial response: {}", err); + break; + } + read_total += chunk_size; + } + () = tokio::time::sleep(Duration::from_secs(1)) => { + eprintln!("Timeout when reading serial response."); + eprintln!("Only got {} bytes: {:?}", read_total, &buf[..read_total]); + break; + } + }; + } + let serial_length = 16; + + const EAR_2_SERIAL_OFFSET: usize = 37; + const EAR_2024_SERIAL_OFFSET: usize = 31; + + let mut serial = String::from_utf8_lossy(&buf[EAR_2_SERIAL_OFFSET..EAR_2_SERIAL_OFFSET+serial_length]); + if serial.contains(',') { + serial = String::from_utf8_lossy(&buf[EAR_2024_SERIAL_OFFSET..EAR_2024_SERIAL_OFFSET+serial_length]); + } Ok(Self { address: stream.peer_addr()?.addr.to_string(), diff --git a/src-egui/Cargo.toml b/src-egui/Cargo.toml new file mode 100644 index 0000000..6cc486a --- /dev/null +++ b/src-egui/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "nothing-linux-egui" +version = "0.1.0" +edition = "2021" + +[dependencies] +eframe = "0.29.1" +nothing = { path = "../nothing" } +tokio = { version = "1.42.0", features = ["full"] } diff --git a/src-egui/src/async_worker.rs b/src-egui/src/async_worker.rs new file mode 100644 index 0000000..5b5abe2 --- /dev/null +++ b/src-egui/src/async_worker.rs @@ -0,0 +1,65 @@ +use eframe::egui; +use nothing::{anc::AncMode, nothing_ear_2::Ear2}; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +pub enum EarCmd { + AncMode(AncMode), + LowLatency(bool), + InEarDetection(bool), +} + +pub enum EarResponse { + DeviceInfo(DeviceInfo), + Error(String), +} + +pub struct DeviceInfo { + pub address: String, + pub firmware_version: String, + pub serial_number: String, +} + +pub async fn async_worker( + mut rx: UnboundedReceiver, + tx: UnboundedSender, + ctx: egui::Context, +) { + let send_response = |response: EarResponse| async { + tx.send(response).expect("sending EarResponse"); + ctx.request_repaint(); + }; + let ear_2 = match Ear2::new().await { + Ok(ear_2) => { + send_response(EarResponse::DeviceInfo(DeviceInfo { + address: ear_2.address.clone(), + firmware_version: ear_2.firmware_version.clone(), + serial_number: ear_2.serial_number.clone(), + })) + .await; + ear_2 + } + Err(err) => { + send_response(EarResponse::Error(err.to_string())).await; + return; + } + }; + while let Some(cmd) = rx.recv().await { + match cmd { + EarCmd::AncMode(anc_mode) => { + if let Err(err) = ear_2.set_anc(anc_mode).await { + send_response(EarResponse::Error(err.to_string())).await; + } + } + EarCmd::LowLatency(mode) => { + if let Err(err) = ear_2.set_low_latency(mode).await { + send_response(EarResponse::Error(err.to_string())).await; + } + } + EarCmd::InEarDetection(mode) => { + if let Err(err) = ear_2.set_in_ear_detection(mode).await { + send_response(EarResponse::Error(err.to_string())).await; + } + } + } + } +} diff --git a/src-egui/src/main.rs b/src-egui/src/main.rs new file mode 100644 index 0000000..20920ed --- /dev/null +++ b/src-egui/src/main.rs @@ -0,0 +1,261 @@ +use eframe::egui; + +use async_worker::{async_worker, DeviceInfo, EarCmd, EarResponse}; +use nothing::anc::AncMode; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +mod async_worker; + +fn main() -> eframe::Result { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([200.0, 400.0]), + ..Default::default() + }; + + let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel(); + let (response_tx, response_rx) = tokio::sync::mpsc::unbounded_channel(); + + eframe::run_native( + "nothing-linux", + options, + Box::new(|cc| { + let ctx = cc.egui_ctx.clone(); + + std::thread::spawn(|| { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + rt.block_on(async { + async_worker(cmd_rx, response_tx, ctx).await; + }); + }); + + Ok(Box::new(MyApp::new(cmd_tx, response_rx))) + }), + ) +} + +#[derive(PartialEq, Eq)] +pub enum UiAnc { + On, + Transparency, + Off, + Unknown, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum UiAncMode { + High, + Mid, + Low, + Adaptive, +} + +impl UiAncMode { + fn label(&self) -> &str { + match self { + UiAncMode::High => "High", + UiAncMode::Mid => "Mid", + UiAncMode::Low => "Low", + UiAncMode::Adaptive => "Adaptive", + } + } +} + +impl From<&UiAncMode> for AncMode { + fn from(value: &UiAncMode) -> Self { + match value { + UiAncMode::High => AncMode::High, + UiAncMode::Mid => AncMode::Mid, + UiAncMode::Low => AncMode::Low, + UiAncMode::Adaptive => AncMode::Adaptive, + } + } +} + +#[derive(PartialEq, Eq)] +enum UiLowLatency { + On, + Off, + Unknown, +} + +#[derive(PartialEq, Eq)] +enum UiInEarDetection { + On, + Off, + Unknown, +} + +struct MyApp { + tx: UnboundedSender, + rx: UnboundedReceiver, + device_info: Option, + error: Option, + anc: UiAnc, + anc_mode: UiAncMode, + low_latency_mode: UiLowLatency, + in_ear_detection_mode: UiInEarDetection, +} + +impl MyApp { + fn new(tx: UnboundedSender, rx: UnboundedReceiver) -> Self { + Self { + tx, + rx, + device_info: None, + error: None, + anc: UiAnc::Unknown, + anc_mode: UiAncMode::Adaptive, + low_latency_mode: UiLowLatency::Unknown, + in_ear_detection_mode: UiInEarDetection::Unknown, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + while let Ok(response) = self.rx.try_recv() { + match response { + EarResponse::DeviceInfo(device_info) => { + self.device_info = Some(device_info); + } + EarResponse::Error(err) => { + self.error = Some(err); + } + } + } + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("Nothing Ear Manager"); + if let Some(err) = &self.error { + ui.colored_label(ctx.style().visuals.error_fg_color, err); + } + ui.separator(); + if let Some(device_info) = &self.device_info { + egui::Grid::new("DeviceInfo").num_columns(2).show(ui, |ui| { + ui.label("Address:"); + ui.label(&device_info.address); + ui.end_row(); + + ui.label("Firmware:"); + ui.label(&device_info.firmware_version); + ui.end_row(); + + ui.label("Serial:"); + ui.label(&device_info.serial_number); + ui.end_row(); + }); + } else { + ui.label("Waiting for device..."); + return; + } + ui.separator(); + egui::Grid::new("Anc").num_columns(2).show(ui, |ui| { + ui.label("ANC:"); + ui.vertical(|ui| { + if ui.radio_value(&mut self.anc, UiAnc::Off, "Off").clicked() { + self.tx + .send(EarCmd::AncMode(AncMode::Off)) + .expect("sending EarCmd"); + } + if ui + .radio_value(&mut self.anc, UiAnc::Transparency, "Transparency") + .clicked() + { + self.tx + .send(EarCmd::AncMode(AncMode::Transparency)) + .expect("sending EarCmd"); + } + if ui.radio_value(&mut self.anc, UiAnc::On, "On").clicked() { + self.tx + .send(EarCmd::AncMode((&self.anc_mode).into())) + .expect("sending EarCmd"); + } + }); + ui.end_row(); + }); + ui.separator(); + ui.add_enabled_ui(self.anc == UiAnc::On, |ui| { + egui::Grid::new("AncMode").num_columns(2).show(ui, |ui| { + ui.label("ANC Mode:"); + ui.vertical(|ui| { + for mode in [ + UiAncMode::High, + UiAncMode::Mid, + UiAncMode::Low, + UiAncMode::Adaptive, + ] { + if ui + .radio_value(&mut self.anc_mode, mode, mode.label()) + .clicked() + { + self.tx + .send(EarCmd::AncMode((&mode).into())) + .expect("sending EarCmd"); + } + } + }); + ui.end_row(); + }); + }); + ui.separator(); + egui::Grid::new("LowLatency").num_columns(2).show(ui, |ui| { + ui.label("Low Latency:"); + ui.vertical(|ui| { + if ui + .radio_value(&mut self.low_latency_mode, UiLowLatency::Off, "Off") + .clicked() + { + self.tx + .send(EarCmd::LowLatency(false)) + .expect("sending EarCmd"); + } + if ui + .radio_value(&mut self.low_latency_mode, UiLowLatency::On, "On") + .clicked() + { + self.tx + .send(EarCmd::LowLatency(true)) + .expect("sending EarCmd"); + } + }); + ui.end_row(); + }); + ui.separator(); + egui::Grid::new("InEarDetection") + .num_columns(2) + .show(ui, |ui| { + ui.label("In Ear Detection:"); + ui.vertical(|ui| { + if ui + .radio_value( + &mut self.in_ear_detection_mode, + UiInEarDetection::Off, + "Off", + ) + .clicked() + { + self.tx + .send(EarCmd::InEarDetection(false)) + .expect("sending EarCmd"); + } + if ui + .radio_value( + &mut self.in_ear_detection_mode, + UiInEarDetection::On, + "On", + ) + .clicked() + { + self.tx + .send(EarCmd::InEarDetection(true)) + .expect("sending EarCmd"); + } + }); + ui.end_row(); + }); + }); + } +}