From 4a9a2e7b642e4d2ec2e4e1259ffee0a06e108022 Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Mon, 20 Apr 2026 14:26:27 +0530 Subject: [PATCH] linux-rust: handle disconnections and fix rename --- linux-rust/src/bluetooth/aacp.rs | 9 +-- linux-rust/src/devices/airpods.rs | 18 ++++-- linux-rust/src/main.rs | 42 +++++--------- linux-rust/src/ui/window.rs | 92 ++++++++++++++++++------------- linux-rust/src/utils.rs | 30 ++++++++-- 5 files changed, 111 insertions(+), 80 deletions(-) diff --git a/linux-rust/src/bluetooth/aacp.rs b/linux-rust/src/bluetooth/aacp.rs index 1cdb151..bed6ca8 100644 --- a/linux-rust/src/bluetooth/aacp.rs +++ b/linux-rust/src/bluetooth/aacp.rs @@ -28,7 +28,7 @@ pub mod opcodes { pub const EAR_DETECTION: u8 = 0x06; pub const CONVERSATION_AWARENESS: u8 = 0x4B; pub const INFORMATION: u8 = 0x1D; - pub const RENAME: u8 = 0x1E; + pub const RENAME: u8 = 0x1A; pub const PROXIMITY_KEYS_REQ: u8 = 0x30; pub const PROXIMITY_KEYS_RSP: u8 = 0x31; pub const STEM_PRESS: u8 = 0x19; @@ -959,9 +959,10 @@ impl AACPManager { pub async fn send_rename_packet(&self, name: &str) -> Result<()> { let name_bytes = name.as_bytes(); let size = name_bytes.len(); - let mut packet = Vec::with_capacity(5 + size); + let mut packet = Vec::with_capacity(6 + size); packet.push(opcodes::RENAME); packet.push(0x00); + packet.push(0x01); packet.push(size as u8); packet.push(0x00); packet.extend_from_slice(name_bytes); @@ -1215,8 +1216,8 @@ async fn recv_thread(manager: AACPManager, sp: Arc) { manager.receive_packet(data).await; } Err(e) => { - error!("Read error: {}", e); - debug!( + debug!("Read error: {}", e); + info!( "We have probably disconnected, clearing state variables (owns=false, connected_devices=empty, control_command_status_list=empty)." ); let mut state = manager.state.lock().await; diff --git a/linux-rust/src/devices/airpods.rs b/linux-rust/src/devices/airpods.rs index f8af095..f0e876c 100644 --- a/linux-rust/src/devices/airpods.rs +++ b/linux-rust/src/devices/airpods.rs @@ -8,9 +8,9 @@ use ksni::Handle; use log::{debug, error, info}; use serde::{Deserialize, Serialize}; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; use tokio::sync::Mutex; use tokio::time::{Duration, sleep}; +use crate::utils::get_app_settings_path; pub struct AirPodsDevice { pub mac_address: Address, @@ -25,7 +25,6 @@ impl AirPodsDevice { mac_address: Address, tray_handle: Option>, ui_tx: tokio::sync::mpsc::UnboundedSender, - stem_control: Arc, ) -> Self { info!("Creating new AirPodsDevice for {}", mac_address); let mut aacp_manager = AACPManager::new(); @@ -82,7 +81,17 @@ impl AirPodsDevice { error!("Failed to request proximity keys: {}", e); } - if stem_control.load(Ordering::Relaxed) { + let app_settings_path = get_app_settings_path(); + let settings = std::fs::read_to_string(&app_settings_path) + .ok() + .and_then(|s| serde_json::from_str::(&s).ok()); + let stem_control = settings + .clone() + .and_then(|v| v.get("stem_control").cloned()) + .and_then(|s| serde_json::from_value(s).ok()) + .unwrap_or(false); + + if stem_control { // Enable stem press detection (double and triple tap) // StemConfig bitmask for the control command: single=0x01, double=0x02, triple=0x04, long=0x08 // We want double and triple: 0x02 | 0x04 = 0x06 @@ -222,7 +231,6 @@ impl AirPodsDevice { let local_mac_events = local_mac.clone(); let ui_tx_clone = ui_tx.clone(); let command_tx_clone = command_tx.clone(); - let stem_control_clone = stem_control.clone(); tokio::spawn(async move { while let Some(event) = rx.recv().await { let event_clone = event.clone(); @@ -348,7 +356,7 @@ impl AirPodsDevice { "Received Stem Press: {:?} on {:?}", press_type, bud_type ); - if stem_control_clone.load(Ordering::Relaxed) { + if stem_control { let controller = mc_clone.lock().await; match press_type { StemPressType::DoublePress => { diff --git a/linux-rust/src/main.rs b/linux-rust/src/main.rs index c4e9eee..f43f575 100644 --- a/linux-rust/src/main.rs +++ b/linux-rust/src/main.rs @@ -19,7 +19,7 @@ use dbus::blocking::stdintf::org_freedesktop_dbus::Properties; use dbus::message::MatchRule; use devices::airpods::AirPodsDevice; use ksni::TrayMethods; -use log::{info, warn}; +use log::{debug, info, warn}; use std::collections::HashMap; use std::env; use std::sync::atomic::{AtomicBool}; @@ -44,12 +44,7 @@ struct Args { )] le_debug: bool, #[arg(long, short = 'v', help = "Show application version and exit")] - version: bool, - #[arg( - long, - help = "Disable stem press track control (use this if your environment already handles AirPods AVRCP commands natively)" - )] - no_stem_control: bool, + version: bool } fn main() -> iced::Result { @@ -88,40 +83,28 @@ fn main() -> iced::Result { Arc::new(RwLock::new(HashMap::new())); // Load stem_control initial value from settings JSON, then apply CLI override. - let app_settings_path = get_app_settings_path(); - let saved_stem_control = std::fs::read_to_string(&app_settings_path) - .ok() - .and_then(|s| serde_json::from_str::(&s).ok()) - .and_then(|v| v.get("stem_control").and_then(|b| b.as_bool())) - .unwrap_or(true); - // CLI --no-stem-control overrides the saved setting. - let stem_control_initial = if args.no_stem_control { false } else { saved_stem_control }; - let stem_control: Arc = Arc::new(AtomicBool::new(stem_control_initial)); - if args.no_tray { // Run headless without UI info!("Running in headless mode (no GUI)"); let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async_main(ui_tx, device_managers, stem_control)).unwrap(); + rt.block_on(async_main(ui_tx, device_managers)).unwrap(); Ok(()) } else { // Run with UI let device_managers_clone = device_managers.clone(); - let stem_control_clone = stem_control.clone(); std::thread::spawn(|| { let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async_main(ui_tx, device_managers_clone, stem_control_clone)) + rt.block_on(async_main(ui_tx, device_managers_clone)) .unwrap(); }); - ui::window::start_ui(ui_rx, args.start_minimized, device_managers, stem_control) + ui::window::start_ui(ui_rx, args.start_minimized, device_managers) } } async fn async_main( ui_tx: tokio::sync::mpsc::UnboundedSender, device_managers: Arc>>, - stem_control: Arc, ) -> bluer::Result<()> { let args = Args::parse(); @@ -189,7 +172,7 @@ async fn async_main( .unwrap_or_else(|| "Unknown".to_string()); info!("Found connected AirPods: {}, initializing.", name); let airpods_device = - AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx.clone(), stem_control.clone()).await; + AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx.clone()).await; let mut managers = device_managers.write().await; // let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone()); @@ -278,9 +261,6 @@ async fn async_main( let Some(is_connected) = connected_var.0.as_ref().as_u64() else { return true; }; - if is_connected == 0 { - return true; - } let proxy = conn.with_proxy("org.bluez", path, std::time::Duration::from_millis(5000)); let Ok(uuids) = proxy.get::>("org.bluez.Device1", "UUIDs") else { return true; @@ -293,7 +273,12 @@ async fn async_main( let Ok(addr) = addr_str.parse::
() else { return true; }; - + if is_connected==0 { + if let Err(e) = ui_tx.send(BluetoothUIMessage::DeviceDisconnected(addr_str.clone())) { + warn!("Failed to send DeviceConnected UI message: {:?}", e); + } + return true + } if managed_devices_mac.contains(&addr_str) { info!("Managed device connected: {}, initializing", addr_str); let type_ = devices_list.get(&addr_str).unwrap().type_.clone(); @@ -327,9 +312,8 @@ async fn async_main( let handle_clone = tray_handle.clone(); let ui_tx_clone = ui_tx.clone(); let device_managers = device_managers.clone(); - let stem_control_arc = stem_control.clone(); tokio::spawn(async move { - let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone.clone(), stem_control_arc.clone()).await; + let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone.clone()).await; let mut managers = device_managers.write().await; // let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone()); let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone()); diff --git a/linux-rust/src/ui/window.rs b/linux-rust/src/ui/window.rs index 879f6f7..4574b97 100644 --- a/linux-rust/src/ui/window.rs +++ b/linux-rust/src/ui/window.rs @@ -10,7 +10,7 @@ use crate::ui::airpods::airpods_view; use crate::ui::messages::BluetoothUIMessage; use crate::ui::nothing::nothing_view; use crate::utils::{MyTheme, get_app_settings_path, get_devices_path}; -use bluer::{Address, Session}; +use bluer::{Address}; use iced::border::Radius; use iced::overlay::menu; use iced::widget::button::Style; @@ -19,7 +19,7 @@ use iced::widget::{ Space, button, column, combo_box, container, pane_grid, row, rule, scrollable, text, text_input, toggler }; -use iced::{Background, Border, Center, Element, Font, Length, Padding, Size, Subscription, Task, Theme, daemon, window, Settings}; +use iced::{Background, Border, Center, Element, Font, Length, Padding, Size, Subscription, Task, Theme, daemon, window, Settings, Program}; use log::{debug, error}; use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; @@ -31,7 +31,7 @@ pub fn start_ui( ui_rx: UnboundedReceiver, start_minimized: bool, device_managers: Arc>>, - stem_control: Arc, + // stem_control: Arc, ) -> iced::Result { let ui_rx = Arc::new(Mutex::new(ui_rx)); @@ -42,7 +42,7 @@ pub fn start_ui( Arc::clone(&ui_rx), start_minimized, Arc::clone(&device_managers), - Arc::clone(&stem_control), + // Arc::clone(&stem_control), ) }, App::update, @@ -50,6 +50,7 @@ pub fn start_ui( ) .subscription(App::subscription) .theme(App::theme) + .title(App::title) .settings(Settings { id: Some("librepods".to_string()), fonts: vec![include_bytes!("../../assets/font/sf_pro.otf").as_slice().into()], @@ -74,7 +75,7 @@ pub struct App { device_type_state: combo_box::State, selected_device_type: Option, tray_text_mode: bool, - stem_control: Arc, + stem_control: bool, } pub struct BluetoothState { @@ -98,7 +99,7 @@ pub enum Message { ThemeSelected(MyTheme), CopyToClipboard(String), BluetoothMessage(BluetoothUIMessage), - ShowNewDialogTab, + // ShowNewDialogTab, GotPairedDevices(HashMap), StartAddDevice(String, Address), SelectDeviceType(DeviceType), @@ -127,7 +128,7 @@ impl App { ui_rx: Arc>>, start_minimized: bool, device_managers: Arc>>, - stem_control: Arc, + // stem_control: Arc, ) -> (Self, Task) { let (mut panes, first_pane) = pane_grid::State::new(Pane::Sidebar); let split = panes.split(pane_grid::Axis::Vertical, first_pane, Pane::Content); @@ -160,6 +161,11 @@ impl App { .and_then(|v| v.get("tray_text_mode").cloned()) .and_then(|ttm| serde_json::from_value(ttm).ok()) .unwrap_or(false); + let stem_control = settings + .clone() + .and_then(|v| v.get("stem_control").cloned()) + .and_then(|s| serde_json::from_value(s).ok()) + .unwrap_or(false); let bluetooth_state = BluetoothState::new(); @@ -246,7 +252,7 @@ impl App { let settings = serde_json::json!({ "theme": self.selected_theme, "tray_text_mode": self.tray_text_mode, - "stem_control": self.stem_control.load(Ordering::Relaxed), + "stem_control": self.stem_control, }); debug!( "Writing settings to {}: {}", @@ -404,7 +410,16 @@ impl App { let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg); debug!("Device disconnected: {}", mac); + self.bluetooth_state + .connected_devices + .retain(|device| device != &mac); + self.device_states.remove(&mac); + + if matches!(&self.selected_tab, Tab::Device(selected_mac) if selected_mac == &mac) { + self.selected_tab = Tab::Device("none".to_string()); + } + Task::batch(vec![wait_task]) } BluetoothUIMessage::AACPUIEvent(mac, event) => { @@ -520,11 +535,11 @@ impl App { } } } - Message::ShowNewDialogTab => { - debug!("switching to Add Device tab"); - self.selected_tab = Tab::AddDevice; - Task::perform(load_paired_devices(), Message::GotPairedDevices) - } + // Message::ShowNewDialogTab => { + // debug!("switching to Add Device tab"); + // self.selected_tab = Tab::AddDevice; + // Task::perform(load_paired_devices(), Message::GotPairedDevices) + // } Message::GotPairedDevices(map) => { self.paired_devices = map; Task::none() @@ -615,7 +630,7 @@ impl App { let settings = serde_json::json!({ "theme": self.selected_theme, "tray_text_mode": self.tray_text_mode, - "stem_control": self.stem_control.load(Ordering::Relaxed), + "stem_control": self.stem_control, }); debug!( "Writing settings to {}: {}", @@ -626,12 +641,12 @@ impl App { Task::none() } Message::StemControlChanged(is_enabled) => { - self.stem_control.store(is_enabled, Ordering::Relaxed); + self.stem_control = is_enabled; let app_settings_path = get_app_settings_path(); let settings = serde_json::json!({ "theme": self.selected_theme, "tray_text_mode": self.tray_text_mode, - "stem_control": self.stem_control.load(Ordering::Relaxed), + "stem_control": self.stem_control, }); debug!( "Writing settings to {}: {}", @@ -1040,7 +1055,7 @@ impl App { ] .spacing(12); - let stem_control_value = self.stem_control.load(Ordering::Relaxed); + let stem_control_value = self.stem_control; let stem_control_toggle = container( row![ column![ @@ -1300,25 +1315,26 @@ async fn wait_for_message(ui_rx: Arc } } } -async fn load_paired_devices() -> HashMap { - let mut devices = HashMap::new(); - let session = Session::new().await.ok().unwrap(); - let adapter = session.default_adapter().await.ok().unwrap(); - let addresses = adapter.device_addresses().await.ok().unwrap(); - for addr in addresses { - let device = adapter.device(addr).ok().unwrap(); - let paired = device.is_paired().await.ok().unwrap(); - if paired { - let name = device - .name() - .await - .ok() - .flatten() - .unwrap_or_else(|| "Unknown".to_string()); - devices.insert(name, addr); - } - } - - devices -} +// async fn load_paired_devices() -> HashMap { +// let mut devices = HashMap::new(); +// +// let session = Session::new().await.ok().unwrap(); +// let adapter = session.default_adapter().await.ok().unwrap(); +// let addresses = adapter.device_addresses().await.ok().unwrap(); +// for addr in addresses { +// let device = adapter.device(addr).ok().unwrap(); +// let paired = device.is_paired().await.ok().unwrap(); +// if paired { +// let name = device +// .name() +// .await +// .ok() +// .flatten() +// .unwrap_or_else(|| "Unknown".to_string()); +// devices.insert(name, addr); +// } +// } +// +// devices +// } diff --git a/linux-rust/src/utils.rs b/linux-rust/src/utils.rs index b97cb09..88ee466 100644 --- a/linux-rust/src/utils.rs +++ b/linux-rust/src/utils.rs @@ -15,18 +15,40 @@ pub fn get_devices_path() -> PathBuf { pub fn get_preferences_path() -> PathBuf { let config_dir = std::env::var("XDG_CONFIG_HOME") - .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); + .unwrap_or_else(|_| format!("{}/.config", std::env::var("HOME").unwrap_or_default())); PathBuf::from(config_dir) .join("librepods") .join("preferences.json") } pub fn get_app_settings_path() -> PathBuf { + let home = std::env::var("HOME").unwrap_or_default(); + let config_dir = std::env::var("XDG_CONFIG_HOME") - .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); - PathBuf::from(config_dir) + .unwrap_or_else(|_| format!("{}/.config", home)); + + let data_dir = std::env::var("XDG_DATA_HOME") + .unwrap_or_else(|_| format!("{}/.local/share", home)); + + let new_path = PathBuf::from(&config_dir) .join("librepods") - .join("app_settings.json") + .join("app_settings.json"); + + let old_path = PathBuf::from(&data_dir) + .join("app_settings.json"); + + // migrate if needed + if old_path.exists() && !new_path.exists() { + if let Some(parent) = new_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + + if std::fs::copy(&old_path, &new_path).is_ok() { + let _ = std::fs::remove_file(&old_path); + } + } + + new_path } fn e(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {