Skip to content

Commit e9470e2

Browse files
linux-rust: log device status in headless mode, clean up logger init
In --no-tray mode the BluetoothUIMessage channel (connection, battery, noise control mode) was produced but never consumed, leaving headless users with no visibility into device state. Add a small console consumer that logs each field only when it changes. Also moves logger setup into an explicit env_logger::Builder instead of mutating RUST_LOG via an unsafe env::set_var call.
1 parent 672e65a commit e9470e2

2 files changed

Lines changed: 110 additions & 14 deletions

File tree

linux-rust/src/headless.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
}

linux-rust/src/main.rs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod bluetooth;
22
mod devices;
3+
mod headless;
34
mod media_controller;
45
mod ui;
56
mod utils;
@@ -8,6 +9,7 @@ use crate::bluetooth::discovery::{find_connected_airpods, find_other_managed_dev
89
use crate::bluetooth::le::start_le_monitor;
910
use crate::bluetooth::managers::DeviceManagers;
1011
use crate::devices::enums::DeviceData;
12+
use crate::headless::run_headless_console;
1113
use crate::ui::messages::BluetoothUIMessage;
1214
use crate::ui::tray::MyTray;
1315
use crate::utils::{get_app_settings_path, get_devices_path};
@@ -47,6 +49,24 @@ struct Args {
4749
version: bool
4850
}
4951

52+
/// Default log filter: global level at `app_level`, with known-noisy external crates kept quiet
53+
/// unless the user already set `RUST_LOG` themselves, in which case that takes precedence.
54+
fn default_log_filter(debug: bool, le_debug: bool) -> String {
55+
let app_level = if debug { "debug" } else { "info" };
56+
let le_level = if le_debug { "debug" } else { "info" };
57+
format!(
58+
"{app_level},zbus=warn,winit=warn,tracing=warn,iced_wgpu=warn,wgpu_hal=warn,wgpu_core=warn,cosmic_text=warn,naga=warn,iced_winit=warn,librepods::bluetooth::le={le_level}"
59+
)
60+
}
61+
62+
fn init_logging(debug: bool, le_debug: bool) {
63+
let env = env_logger::Env::default()
64+
.filter_or("RUST_LOG", default_log_filter(debug, le_debug));
65+
env_logger::Builder::from_env(env)
66+
.format_timestamp_secs()
67+
.init();
68+
}
69+
5070
fn main() -> iced::Result {
5171
let args = Args::parse();
5272

@@ -58,24 +78,11 @@ fn main() -> iced::Result {
5878
return Ok(());
5979
}
6080

61-
let log_level = if args.debug { "debug" } else { "info" };
6281
// let wayland_display = env::var("WAYLAND_DISPLAY").is_ok();
6382
// if wayland_display && env::var("WGPU_BACKEND").is_err() {
6483
// unsafe { env::set_var("WGPU_BACKEND", "gl") };
6584
// }
66-
if env::var("RUST_LOG").is_err() {
67-
unsafe {
68-
env::set_var(
69-
"RUST_LOG",
70-
log_level.to_owned()
71-
+ &format!(
72-
",zbus=warn,winit=warn,tracing=warn,iced_wgpu=warn,wgpu_hal=warn,wgpu_core=warn,cosmic_text=warn,naga=warn,iced_winit=warn,librepods::bluetooth::le={}",
73-
if args.le_debug { "debug" } else { "info" }
74-
),
75-
)
76-
};
77-
}
78-
env_logger::init();
85+
init_logging(args.debug, args.le_debug);
7986

8087
let (ui_tx, ui_rx) = unbounded_channel::<BluetoothUIMessage>();
8188

@@ -87,6 +94,7 @@ fn main() -> iced::Result {
8794
// Run headless without UI
8895
info!("Running in headless mode (no GUI)");
8996
let rt = tokio::runtime::Runtime::new().unwrap();
97+
rt.spawn(run_headless_console(ui_rx));
9098
rt.block_on(async_main(ui_tx, device_managers)).unwrap();
9199
Ok(())
92100
} else {

0 commit comments

Comments
 (0)