Skip to content

Commit 6f0323e

Browse files
committed
linux-rust: parse single battery of AirPods Max
1 parent 4737cbf commit 6f0323e

File tree

5 files changed

+75
-47
lines changed

5 files changed

+75
-47
lines changed

linux-rust/src/bluetooth/aacp.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ pub enum AudioSourceType {
215215
#[repr(u8)]
216216
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217217
pub enum BatteryComponent {
218+
Headphone = 1,
218219
Left = 4,
219220
Right = 2,
220221
Case = 8
@@ -476,6 +477,7 @@ impl AACPManager {
476477
let base_index = 3 + i * 5;
477478
batteries.push(BatteryInfo {
478479
component: match payload[base_index] {
480+
0x01 => BatteryComponent::Headphone,
479481
0x02 => BatteryComponent::Right,
480482
0x04 => BatteryComponent::Left,
481483
0x08 => BatteryComponent::Case,

linux-rust/src/devices/airpods.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ impl AirPodsDevice {
170170
handle.update(|tray: &mut MyTray| {
171171
for b in &battery_info {
172172
match b.component as u8 {
173+
0x01 => {
174+
tray.battery_headphone = Some(b.level);
175+
tray.battery_headphone_status = Some(b.status);
176+
}
173177
0x02 => {
174178
tray.battery_r = Some(b.level);
175179
tray.battery_r_status = Some(b.status);

linux-rust/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ async fn async_main(
102102
} else {
103103
let tray = MyTray {
104104
conversation_detect_enabled: None,
105+
battery_headphone: None,
106+
battery_headphone_status: None,
105107
battery_l: None,
106108
battery_l_status: None,
107109
battery_r: None,

linux-rust/src/ui/tray.rs

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,26 @@ use ab_glyph::{Font, ScaleFont};
44
use ksni::{Icon, ToolTip};
55
use tokio::sync::mpsc::UnboundedSender;
66

7-
use crate::bluetooth::aacp::ControlCommandIdentifiers;
7+
use crate::bluetooth::aacp::{BatteryStatus, ControlCommandIdentifiers};
88
use crate::ui::messages::BluetoothUIMessage;
99
use crate::utils::get_app_settings_path;
1010

1111
#[derive(Debug)]
12-
pub(crate) struct MyTray {
13-
pub(crate) conversation_detect_enabled: Option<bool>,
14-
pub(crate) battery_l: Option<u8>,
15-
pub(crate) battery_l_status: Option<crate::bluetooth::aacp::BatteryStatus>,
16-
pub(crate) battery_r: Option<u8>,
17-
pub(crate) battery_r_status: Option<crate::bluetooth::aacp::BatteryStatus>,
18-
pub(crate) battery_c: Option<u8>,
19-
pub(crate) battery_c_status: Option<crate::bluetooth::aacp::BatteryStatus>,
20-
pub(crate) connected: bool,
21-
pub(crate) listening_mode: Option<u8>,
22-
pub(crate) allow_off_option: Option<u8>,
23-
pub(crate) command_tx: Option<UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>>,
24-
pub(crate) ui_tx: Option<UnboundedSender<BluetoothUIMessage>>,
12+
pub struct MyTray {
13+
pub conversation_detect_enabled: Option<bool>,
14+
pub battery_headphone: Option<u8>,
15+
pub battery_headphone_status: Option<BatteryStatus>,
16+
pub battery_l: Option<u8>,
17+
pub battery_l_status: Option<BatteryStatus>,
18+
pub battery_r: Option<u8>,
19+
pub battery_r_status: Option<BatteryStatus>,
20+
pub battery_c: Option<u8>,
21+
pub battery_c_status: Option<BatteryStatus>,
22+
pub connected: bool,
23+
pub listening_mode: Option<u8>,
24+
pub allow_off_option: Option<u8>,
25+
pub command_tx: Option<UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>>,
26+
pub ui_tx: Option<UnboundedSender<BluetoothUIMessage>>,
2527
}
2628

2729
impl ksni::Tray for MyTray {
@@ -34,30 +36,36 @@ impl ksni::Tray for MyTray {
3436
fn icon_pixmap(&self) -> Vec<Icon> {
3537
let text = {
3638
let mut levels: Vec<u8> = Vec::new();
37-
if let Some(l) = self.battery_l {
38-
if self.battery_l_status != Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) {
39-
levels.push(l);
39+
if let Some(h) = self.battery_headphone {
40+
if self.battery_headphone_status != Some(BatteryStatus::Disconnected) {
41+
levels.push(h);
4042
}
41-
}
42-
if let Some(r) = self.battery_r {
43-
if self.battery_r_status != Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) {
44-
levels.push(r);
43+
} else {
44+
if let Some(l) = self.battery_l {
45+
if self.battery_l_status != Some(BatteryStatus::Disconnected) {
46+
levels.push(l);
47+
}
48+
}
49+
if let Some(r) = self.battery_r {
50+
if self.battery_r_status != Some(BatteryStatus::Disconnected) {
51+
levels.push(r);
52+
}
4553
}
54+
// if let Some(c) = self.battery_c {
55+
// if self.battery_c_status != Some(BatteryStatus::Disconnected) {
56+
// levels.push(c);
57+
// }
58+
// }
4659
}
47-
// if let Some(c) = self.battery_c {
48-
// if self.battery_c_status != Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) {
49-
// levels.push(c);
50-
// }
51-
// }
5260
let min_battery = levels.iter().min().copied();
5361
if let Some(b) = min_battery {
5462
format!("{}", b)
5563
} else {
5664
"?".to_string()
5765
}
5866
};
59-
let any_bud_charging = matches!(self.battery_l_status, Some(crate::bluetooth::aacp::BatteryStatus::Charging))
60-
|| matches!(self.battery_r_status, Some(crate::bluetooth::aacp::BatteryStatus::Charging));
67+
let any_bud_charging = matches!(self.battery_l_status, Some(BatteryStatus::Charging))
68+
|| matches!(self.battery_r_status, Some(BatteryStatus::Charging));
6169
let app_settings_path = get_app_settings_path();
6270
let settings = std::fs::read_to_string(&app_settings_path)
6371
.ok()
@@ -70,12 +78,12 @@ impl ksni::Tray for MyTray {
7078
vec![icon]
7179
}
7280
fn tool_tip(&self) -> ToolTip {
73-
let format_component = |label: &str, level: Option<u8>, status: Option<crate::bluetooth::aacp::BatteryStatus>| -> String {
81+
let format_component = |label: &str, level: Option<u8>, status: Option<BatteryStatus>| -> String {
7482
match status {
75-
Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) => format!("{}: -", label),
83+
Some(BatteryStatus::Disconnected) => format!("{}: -", label),
7684
_ => {
7785
let pct = level.map(|b| format!("{}%", b)).unwrap_or("?".to_string());
78-
let suffix = if status == Some(crate::bluetooth::aacp::BatteryStatus::Charging) {
86+
let suffix = if status == Some(BatteryStatus::Charging) {
7987
"⚡"
8088
} else {
8189
""

linux-rust/src/ui/window.rs

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -610,22 +610,34 @@ impl App {
610610
match self.device_states.get(mac) {
611611
Some(DeviceState::AirPods(state)) => {
612612
let b = &state.battery;
613-
let left = b.iter().find(|x| x.component == BatteryComponent::Left)
614-
.map(|x| x.level).unwrap_or_default();
615-
let right = b.iter().find(|x| x.component == BatteryComponent::Right)
616-
.map(|x| x.level).unwrap_or_default();
617-
let case = b.iter().find(|x| x.component == BatteryComponent::Case)
618-
.map(|x| x.level).unwrap_or_default();
619-
let left_charging = b.iter().find(|x| x.component == BatteryComponent::Left)
620-
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
621-
let right_charging = b.iter().find(|x| x.component == BatteryComponent::Right)
622-
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
623-
let case_charging = b.iter().find(|x| x.component == BatteryComponent::Case)
624-
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
625-
format!(
626-
"\u{1018E5} {}%{} \u{1018E8} {}%{} \u{100E6C} {}%{}",
627-
left, if left_charging {"\u{1002E6}"} else {""}, right, if right_charging {"\u{1002E6}"} else {""}, case, if case_charging {"\u{1002E6}"} else {""}
628-
)
613+
let headphone = b.iter().find(|x| x.component == BatteryComponent::Headphone)
614+
.map(|x| x.level);
615+
// if headphones is not None, use only that
616+
if let Some(level) = headphone {
617+
let charging = b.iter().find(|x| x.component == BatteryComponent::Headphone)
618+
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
619+
format!(
620+
"􀺹 {}%{}",
621+
level, if charging {"\u{1002E6}"} else {""}
622+
)
623+
} else {
624+
let left = b.iter().find(|x| x.component == BatteryComponent::Left)
625+
.map(|x| x.level).unwrap_or_default();
626+
let right = b.iter().find(|x| x.component == BatteryComponent::Right)
627+
.map(|x| x.level).unwrap_or_default();
628+
let case = b.iter().find(|x| x.component == BatteryComponent::Case)
629+
.map(|x| x.level).unwrap_or_default();
630+
let left_charging = b.iter().find(|x| x.component == BatteryComponent::Left)
631+
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
632+
let right_charging = b.iter().find(|x| x.component == BatteryComponent::Right)
633+
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
634+
let case_charging = b.iter().find(|x| x.component == BatteryComponent::Case)
635+
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
636+
format!(
637+
"\u{1018E5} {}%{} \u{1018E8} {}%{} \u{100E6C} {}%{}",
638+
left, if left_charging {"\u{1002E6}"} else {""}, right, if right_charging {"\u{1002E6}"} else {""}, case, if case_charging {"\u{1002E6}"} else {""}
639+
)
640+
}
629641
}
630642
_ => "Connected".to_string(),
631643
}

0 commit comments

Comments
 (0)