|
| 1 | +use crate::bluetooth::aacp::{AACPEvent, BatteryComponent, BatteryInfo, BatteryStatus, ControlCommandIdentifiers}; |
| 2 | +use crate::devices::enums::AirPodsNoiseControlMode; |
| 3 | +use crate::ui::messages::BluetoothUIMessage; |
| 4 | +use log::info; |
| 5 | +use std::collections::{HashMap, HashSet}; |
| 6 | +use tokio::sync::mpsc::UnboundedReceiver; |
| 7 | + |
| 8 | +fn battery_label(component: BatteryComponent) -> &'static str { |
| 9 | + match component { |
| 10 | + BatteryComponent::Headphone => "Headphones", |
| 11 | + BatteryComponent::Left => "L", |
| 12 | + BatteryComponent::Right => "R", |
| 13 | + BatteryComponent::Case => "Case", |
| 14 | + } |
| 15 | +} |
| 16 | + |
| 17 | +fn format_battery(info: &BatteryInfo) -> String { |
| 18 | + match info.status { |
| 19 | + BatteryStatus::Disconnected => format!("{}: disconnected", battery_label(info.component)), |
| 20 | + BatteryStatus::Charging => format!("{}: {}% (charging)", battery_label(info.component), info.level), |
| 21 | + BatteryStatus::NotCharging => format!("{}: {}%", battery_label(info.component), info.level), |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +#[derive(Default)] |
| 26 | +struct HeadlessStatus { |
| 27 | + connected: HashSet<String>, |
| 28 | + battery: HashMap<String, Vec<BatteryInfo>>, |
| 29 | + listening_mode: HashMap<String, u8>, |
| 30 | +} |
| 31 | + |
| 32 | +/// Consumes `BluetoothUIMessage` in `--no-tray` mode (otherwise only the tray/GUI read this |
| 33 | +/// channel) and logs `info!` lines for connection, battery, and noise control mode changes, |
| 34 | +/// skipping events that don't change the last-known value. |
| 35 | +pub async fn run_headless_console(mut ui_rx: UnboundedReceiver<BluetoothUIMessage>) { |
| 36 | + let mut status = HeadlessStatus::default(); |
| 37 | + while let Some(message) = ui_rx.recv().await { |
| 38 | + match message { |
| 39 | + BluetoothUIMessage::DeviceConnected(mac) => { |
| 40 | + if status.connected.insert(mac.clone()) { |
| 41 | + info!("Connected: {}", mac); |
| 42 | + } |
| 43 | + } |
| 44 | + BluetoothUIMessage::DeviceDisconnected(mac) => { |
| 45 | + if status.connected.remove(&mac) { |
| 46 | + info!("Disconnected: {}", mac); |
| 47 | + } |
| 48 | + status.battery.remove(&mac); |
| 49 | + status.listening_mode.remove(&mac); |
| 50 | + } |
| 51 | + BluetoothUIMessage::AACPUIEvent(mac, event) => match event { |
| 52 | + AACPEvent::BatteryInfo(battery_info) => { |
| 53 | + let last = status.battery.entry(mac.clone()).or_default(); |
| 54 | + for component_info in &battery_info { |
| 55 | + let changed = last |
| 56 | + .iter() |
| 57 | + .find(|b| b.component == component_info.component) |
| 58 | + .map(|b| b != component_info) |
| 59 | + .unwrap_or(true); |
| 60 | + if changed { |
| 61 | + info!("{} battery — {}", mac, format_battery(component_info)); |
| 62 | + } |
| 63 | + } |
| 64 | + *last = battery_info; |
| 65 | + } |
| 66 | + AACPEvent::ControlCommand(status_update) |
| 67 | + if status_update.identifier == ControlCommandIdentifiers::ListeningMode => |
| 68 | + { |
| 69 | + let mode = status_update |
| 70 | + .value |
| 71 | + .first() |
| 72 | + .map(AirPodsNoiseControlMode::from_byte) |
| 73 | + .unwrap_or(AirPodsNoiseControlMode::Transparency); |
| 74 | + let mode_byte = mode.to_byte(); |
| 75 | + let changed = status.listening_mode.get(&mac) != Some(&mode_byte); |
| 76 | + if changed { |
| 77 | + info!("{} noise control mode: {}", mac, mode); |
| 78 | + status.listening_mode.insert(mac, mode_byte); |
| 79 | + } |
| 80 | + } |
| 81 | + _ => {} |
| 82 | + }, |
| 83 | + BluetoothUIMessage::OpenWindow |
| 84 | + | BluetoothUIMessage::ATTNotification(_, _, _) |
| 85 | + | BluetoothUIMessage::NoOp => {} |
| 86 | + } |
| 87 | + } |
| 88 | +} |
0 commit comments