From 26cee5c8a5ea769303ab95cae34d9fa4b44f3763 Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Thu, 23 Oct 2025 17:25:23 +0530 Subject: [PATCH] linux-rust: show battery info from LE in tray should probably have an indication when not connected --- linux-rust/src/bluetooth/le.rs | 16 +++++++- linux-rust/src/main.rs | 6 +-- linux-rust/src/ui/tray.rs | 67 ++++++++++++++-------------------- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/linux-rust/src/bluetooth/le.rs b/linux-rust/src/bluetooth/le.rs index d4303b5..8aee3d9 100644 --- a/linux-rust/src/bluetooth/le.rs +++ b/linux-rust/src/bluetooth/le.rs @@ -11,6 +11,8 @@ use futures::StreamExt; use hex; use std::time::Duration; use std::path::PathBuf; +use crate::bluetooth::aacp::BatteryStatus; +use crate::ui::tray::MyTray; fn get_proximity_keys_path() -> PathBuf { let data_dir = std::env::var("XDG_DATA_HOME") @@ -66,7 +68,7 @@ fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool { hash == computed_hash } -pub async fn start_le_monitor() -> bluer::Result<()> { +pub async fn start_le_monitor(tray_handle: Option>) -> bluer::Result<()> { let session = Session::new().await?; let adapter = session.default_adapter().await?; adapter.set_powered(true).await?; @@ -125,6 +127,7 @@ pub async fn start_le_monitor() -> bluer::Result<()> { if verified_macs.contains(&addr) { let mut events = dev.events().await?; + let tray_handle_clone = tray_handle.clone(); tokio::spawn(async move { while let Some(ev) = events.next().await { match ev { @@ -170,6 +173,17 @@ pub async fn start_le_monitor() -> bluer::Result<()> { (case_byte & 0x7F, (case_byte & 0x80) != 0) }; + if let Some(handle) = &tray_handle_clone { + handle.update(|tray: &mut MyTray| { + tray.battery_l = if left_byte == 0xff { None } else { Some(left_battery as u8) }; + tray.battery_l_status = if left_byte == 0xff { Some(BatteryStatus::Disconnected) } else if left_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) }; + tray.battery_r = if right_byte == 0xff { None } else { Some(right_battery as u8) }; + tray.battery_r_status = if right_byte == 0xff { Some(BatteryStatus::Disconnected) } else if right_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) }; + tray.battery_c = if case_byte == 0xff { None } else { Some(case_battery as u8) }; + tray.battery_c_status = if case_byte == 0xff { Some(BatteryStatus::Disconnected) } else if case_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) }; + }).await; + } + info!("Battery status: Left: {}, Right: {}, Case: {}, InEar: L:{} R:{}", if left_byte == 0xff { "disconnected".to_string() } else { format!("{}% (charging: {})", left_battery, left_charging) }, if right_byte == 0xff { "disconnected".to_string() } else { format!("{}% (charging: {})", right_battery, right_charging) }, diff --git a/linux-rust/src/main.rs b/linux-rust/src/main.rs index 6da2a6d..1c4239c 100644 --- a/linux-rust/src/main.rs +++ b/linux-rust/src/main.rs @@ -59,10 +59,10 @@ async fn main() -> bluer::Result<()> { let adapter = session.default_adapter().await?; adapter.set_powered(true).await?; - // Start LE monitor for Apple devices - tokio::spawn(async { + let le_tray_clone = tray_handle.clone(); + tokio::spawn(async move { info!("Starting LE monitor..."); - if let Err(e) = start_le_monitor().await { + if let Err(e) = start_le_monitor(le_tray_clone).await { log::error!("LE monitor error: {}", e); } }); diff --git a/linux-rust/src/ui/tray.rs b/linux-rust/src/ui/tray.rs index a354290..a4cc119 100644 --- a/linux-rust/src/ui/tray.rs +++ b/linux-rust/src/ui/tray.rs @@ -28,7 +28,7 @@ impl ksni::Tray for MyTray { "AirPods".into() } fn icon_pixmap(&self) -> Vec { - let text = if self.connected { + let text = { let mut levels: Vec = Vec::new(); if let Some(l) = self.battery_l { if self.battery_l_status != Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) { @@ -40,19 +40,17 @@ impl ksni::Tray for MyTray { levels.push(r); } } - if let Some(c) = self.battery_c { - if self.battery_c_status != Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) { - levels.push(c); - } - } + // if let Some(c) = self.battery_c { + // if self.battery_c_status != Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) { + // levels.push(c); + // } + // } let min_battery = levels.iter().min().copied(); if let Some(b) = min_battery { format!("{}", b) } else { "?".to_string() } - } else { - "".into() }; let any_bud_charging = matches!(self.battery_l_status, Some(crate::bluetooth::aacp::BatteryStatus::Charging)) || matches!(self.battery_r_status, Some(crate::bluetooth::aacp::BatteryStatus::Charging)); @@ -60,39 +58,30 @@ impl ksni::Tray for MyTray { vec![icon] } fn tool_tip(&self) -> ToolTip { - if self.connected { - let format_component = |label: &str, level: Option, status: Option| -> String { - match status { - Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) => format!("{}: -", label), - _ => { - let pct = level.map(|b| format!("{}%", b)).unwrap_or("?".to_string()); - let suffix = if status == Some(crate::bluetooth::aacp::BatteryStatus::Charging) { - "⚡" - } else { - "" - }; - format!("{}: {}{}", label, pct, suffix) - } + let format_component = |label: &str, level: Option, status: Option| -> String { + match status { + Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) => format!("{}: -", label), + _ => { + let pct = level.map(|b| format!("{}%", b)).unwrap_or("?".to_string()); + let suffix = if status == Some(crate::bluetooth::aacp::BatteryStatus::Charging) { + "⚡" + } else { + "" + }; + format!("{}: {}{}", label, pct, suffix) } - }; - - let l = format_component("L", self.battery_l, self.battery_l_status); - let r = format_component("R", self.battery_r, self.battery_r_status); - let c = format_component("C", self.battery_c, self.battery_c_status); - - ToolTip { - icon_name: "".to_string(), - icon_pixmap: vec![], - title: "Battery Status".to_string(), - description: format!("{} {} {}", l, r, c), - } - } else { - ToolTip { - icon_name: "".to_string(), - icon_pixmap: vec![], - title: "Not Connected".to_string(), - description: "Device is not connected.".to_string(), } + }; + + let l = format_component("L", self.battery_l, self.battery_l_status); + let r = format_component("R", self.battery_r, self.battery_r_status); + let c = format_component("C", self.battery_c, self.battery_c_status); + + ToolTip { + icon_name: "".to_string(), + icon_pixmap: vec![], + title: "Battery Status".to_string(), + description: format!("{} {} {}", l, r, c), } } fn menu(&self) -> Vec> {