diff --git a/linux-rust/src/bluetooth/aacp.rs b/linux-rust/src/bluetooth/aacp.rs index 612cce2..78b79d3 100644 --- a/linux-rust/src/bluetooth/aacp.rs +++ b/linux-rust/src/bluetooth/aacp.rs @@ -215,6 +215,7 @@ pub enum AudioSourceType { #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BatteryComponent { + Headphone = 1, Left = 4, Right = 2, Case = 8 @@ -476,6 +477,7 @@ impl AACPManager { let base_index = 3 + i * 5; batteries.push(BatteryInfo { component: match payload[base_index] { + 0x01 => BatteryComponent::Headphone, 0x02 => BatteryComponent::Right, 0x04 => BatteryComponent::Left, 0x08 => BatteryComponent::Case, diff --git a/linux-rust/src/devices/airpods.rs b/linux-rust/src/devices/airpods.rs index 0d774d3..e42aaf8 100644 --- a/linux-rust/src/devices/airpods.rs +++ b/linux-rust/src/devices/airpods.rs @@ -170,6 +170,10 @@ impl AirPodsDevice { handle.update(|tray: &mut MyTray| { for b in &battery_info { match b.component as u8 { + 0x01 => { + tray.battery_headphone = Some(b.level); + tray.battery_headphone_status = Some(b.status); + } 0x02 => { tray.battery_r = Some(b.level); tray.battery_r_status = Some(b.status); diff --git a/linux-rust/src/main.rs b/linux-rust/src/main.rs index 4a507a3..4f284f4 100644 --- a/linux-rust/src/main.rs +++ b/linux-rust/src/main.rs @@ -102,6 +102,8 @@ async fn async_main( } else { let tray = MyTray { conversation_detect_enabled: None, + battery_headphone: None, + battery_headphone_status: None, battery_l: None, battery_l_status: None, battery_r: None, diff --git a/linux-rust/src/ui/tray.rs b/linux-rust/src/ui/tray.rs index 3d244d2..af97130 100644 --- a/linux-rust/src/ui/tray.rs +++ b/linux-rust/src/ui/tray.rs @@ -4,24 +4,26 @@ use ab_glyph::{Font, ScaleFont}; use ksni::{Icon, ToolTip}; use tokio::sync::mpsc::UnboundedSender; -use crate::bluetooth::aacp::ControlCommandIdentifiers; +use crate::bluetooth::aacp::{BatteryStatus, ControlCommandIdentifiers}; use crate::ui::messages::BluetoothUIMessage; use crate::utils::get_app_settings_path; #[derive(Debug)] -pub(crate) struct MyTray { - pub(crate) conversation_detect_enabled: Option, - pub(crate) battery_l: Option, - pub(crate) battery_l_status: Option, - pub(crate) battery_r: Option, - pub(crate) battery_r_status: Option, - pub(crate) battery_c: Option, - pub(crate) battery_c_status: Option, - pub(crate) connected: bool, - pub(crate) listening_mode: Option, - pub(crate) allow_off_option: Option, - pub(crate) command_tx: Option)>>, - pub(crate) ui_tx: Option>, +pub struct MyTray { + pub conversation_detect_enabled: Option, + pub battery_headphone: Option, + pub battery_headphone_status: Option, + pub battery_l: Option, + pub battery_l_status: Option, + pub battery_r: Option, + pub battery_r_status: Option, + pub battery_c: Option, + pub battery_c_status: Option, + pub connected: bool, + pub listening_mode: Option, + pub allow_off_option: Option, + pub command_tx: Option)>>, + pub ui_tx: Option>, } impl ksni::Tray for MyTray { @@ -34,21 +36,27 @@ impl ksni::Tray for MyTray { fn icon_pixmap(&self) -> Vec { 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) { - levels.push(l); + if let Some(h) = self.battery_headphone { + if self.battery_headphone_status != Some(BatteryStatus::Disconnected) { + levels.push(h); } - } - if let Some(r) = self.battery_r { - if self.battery_r_status != Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) { - levels.push(r); + } else { + if let Some(l) = self.battery_l { + if self.battery_l_status != Some(BatteryStatus::Disconnected) { + levels.push(l); + } } + if let Some(r) = self.battery_r { + if self.battery_r_status != Some(BatteryStatus::Disconnected) { + levels.push(r); + } + } + // if let Some(c) = self.battery_c { + // if self.battery_c_status != Some(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) @@ -56,8 +64,8 @@ impl ksni::Tray for MyTray { "?".to_string() } }; - 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)); + let any_bud_charging = matches!(self.battery_l_status, Some(BatteryStatus::Charging)) + || matches!(self.battery_r_status, Some(BatteryStatus::Charging)); let app_settings_path = get_app_settings_path(); let settings = std::fs::read_to_string(&app_settings_path) .ok() @@ -70,12 +78,12 @@ impl ksni::Tray for MyTray { vec![icon] } fn tool_tip(&self) -> ToolTip { - let format_component = |label: &str, level: Option, status: Option| -> String { + let format_component = |label: &str, level: Option, status: Option| -> String { match status { - Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) => format!("{}: -", label), + Some(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) { + let suffix = if status == Some(BatteryStatus::Charging) { "⚡" } else { "" diff --git a/linux-rust/src/ui/window.rs b/linux-rust/src/ui/window.rs index 2bca822..45f4933 100644 --- a/linux-rust/src/ui/window.rs +++ b/linux-rust/src/ui/window.rs @@ -610,22 +610,34 @@ impl App { match self.device_states.get(mac) { Some(DeviceState::AirPods(state)) => { let b = &state.battery; - let left = b.iter().find(|x| x.component == BatteryComponent::Left) - .map(|x| x.level).unwrap_or_default(); - let right = b.iter().find(|x| x.component == BatteryComponent::Right) - .map(|x| x.level).unwrap_or_default(); - let case = b.iter().find(|x| x.component == BatteryComponent::Case) - .map(|x| x.level).unwrap_or_default(); - let left_charging = b.iter().find(|x| x.component == BatteryComponent::Left) - .map(|x| x.status == BatteryStatus::Charging).unwrap_or(false); - let right_charging = b.iter().find(|x| x.component == BatteryComponent::Right) - .map(|x| x.status == BatteryStatus::Charging).unwrap_or(false); - let case_charging = b.iter().find(|x| x.component == BatteryComponent::Case) - .map(|x| x.status == BatteryStatus::Charging).unwrap_or(false); - format!( - "\u{1018E5} {}%{} \u{1018E8} {}%{} \u{100E6C} {}%{}", - left, if left_charging {"\u{1002E6}"} else {""}, right, if right_charging {"\u{1002E6}"} else {""}, case, if case_charging {"\u{1002E6}"} else {""} - ) + let headphone = b.iter().find(|x| x.component == BatteryComponent::Headphone) + .map(|x| x.level); + // if headphones is not None, use only that + if let Some(level) = headphone { + let charging = b.iter().find(|x| x.component == BatteryComponent::Headphone) + .map(|x| x.status == BatteryStatus::Charging).unwrap_or(false); + format!( + "􀺹 {}%{}", + level, if charging {"\u{1002E6}"} else {""} + ) + } else { + let left = b.iter().find(|x| x.component == BatteryComponent::Left) + .map(|x| x.level).unwrap_or_default(); + let right = b.iter().find(|x| x.component == BatteryComponent::Right) + .map(|x| x.level).unwrap_or_default(); + let case = b.iter().find(|x| x.component == BatteryComponent::Case) + .map(|x| x.level).unwrap_or_default(); + let left_charging = b.iter().find(|x| x.component == BatteryComponent::Left) + .map(|x| x.status == BatteryStatus::Charging).unwrap_or(false); + let right_charging = b.iter().find(|x| x.component == BatteryComponent::Right) + .map(|x| x.status == BatteryStatus::Charging).unwrap_or(false); + let case_charging = b.iter().find(|x| x.component == BatteryComponent::Case) + .map(|x| x.status == BatteryStatus::Charging).unwrap_or(false); + format!( + "\u{1018E5} {}%{} \u{1018E8} {}%{} \u{100E6C} {}%{}", + left, if left_charging {"\u{1002E6}"} else {""}, right, if right_charging {"\u{1002E6}"} else {""}, case, if case_charging {"\u{1002E6}"} else {""} + ) + } } _ => "Connected".to_string(), }