diff --git a/linux-rust/src/bluetooth/discovery.rs b/linux-rust/src/bluetooth/discovery.rs index b6d0e17..a0d4c78 100644 --- a/linux-rust/src/bluetooth/discovery.rs +++ b/linux-rust/src/bluetooth/discovery.rs @@ -37,5 +37,6 @@ pub async fn find_other_managed_devices(adapter: &Adapter, managed_macs: Vec), - ATT(Arc), -} - pub struct DeviceManagers { att: Option>, aacp: Option>, } impl DeviceManagers { - fn new() -> Self { - Self { att: None, aacp: None } - } - - fn with_aacp(aacp: AACPManager) -> Self { + pub fn with_aacp(aacp: AACPManager) -> Self { Self { att: None, aacp: Some(Arc::new(aacp)) } } - fn with_att(att: ATTManager) -> Self { + pub fn with_att(att: ATTManager) -> Self { Self { att: Some(Arc::new(att)), aacp: None } } -} -pub struct BluetoothDevices { - devices: HashMap, -} - -impl BluetoothDevices { - fn new() -> Self { - Self { devices: HashMap::new() } + // keeping the att for airpods optional as it requires changes in system bluez config + pub fn with_both(aacp: AACPManager, att: ATTManager) -> Self { + Self { att: Some(Arc::new(att)), aacp: Some(Arc::new(aacp)) } } - fn add_aacp(&mut self, mac: String, manager: AACPManager) { - self.devices - .entry(mac) - .or_insert_with(DeviceManagers::new) - .aacp = Some(Arc::new(manager)); + pub fn set_aacp(&mut self, manager: AACPManager) { + self.aacp = Some(Arc::new(manager)); } - fn add_att(&mut self, mac: String, manager: ATTManager) { - self.devices - .entry(mac) - .or_insert_with(DeviceManagers::new) - .att = Some(Arc::new(manager)); + pub fn set_att(&mut self, manager: ATTManager) { + self.att = Some(Arc::new(manager)); + } + + pub fn get_aacp(&self) -> Option> { + self.aacp.clone() + } + + pub fn get_att(&self) -> Option> { + self.att.clone() } } diff --git a/linux-rust/src/devices/enums.rs b/linux-rust/src/devices/enums.rs index 846d436..76c9ff3 100644 --- a/linux-rust/src/devices/enums.rs +++ b/linux-rust/src/devices/enums.rs @@ -52,7 +52,9 @@ impl Display for DeviceState { #[derive(Clone, Debug)] pub struct AirPodsState { + pub device_name: String, pub conversation_awareness_enabled: bool, + pub personalized_volume_enabled: bool, } #[derive(Clone, Debug)] diff --git a/linux-rust/src/main.rs b/linux-rust/src/main.rs index a2af5c1..d68e9af 100644 --- a/linux-rust/src/main.rs +++ b/linux-rust/src/main.rs @@ -14,16 +14,16 @@ use std::collections::HashMap; use std::sync::Arc; use crate::bluetooth::discovery::{find_connected_airpods, find_other_managed_devices}; use devices::airpods::AirPodsDevice; -use bluer::Address; +use bluer::{Address, InternalErrorKind}; use ksni::TrayMethods; use crate::ui::tray::MyTray; use clap::Parser; use crate::bluetooth::le::start_le_monitor; use tokio::sync::mpsc::unbounded_channel; -use crate::bluetooth::att::ATTHandles; -use crate::bluetooth::managers::BluetoothManager; +use tokio::sync::RwLock; +use crate::bluetooth::managers::DeviceManagers; use crate::devices::enums::DeviceData; -use crate::ui::messages::{AirPodsCommand, BluetoothUIMessage, NothingCommand, UICommand}; +use crate::ui::messages::BluetoothUIMessage; use crate::utils::get_devices_path; #[derive(Parser)] @@ -40,28 +40,29 @@ fn main() -> iced::Result { let args = Args::parse(); let log_level = if args.debug { "debug" } else { "info" }; if env::var("RUST_LOG").is_err() { - unsafe { env::set_var("RUST_LOG", log_level.to_owned() + ",iced_wgpu=off,wgpu_hal=off,wgpu_core=off,librepods_rust::bluetooth::le=off,cosmic_text=off,naga=off,iced_winit=off") }; + unsafe { env::set_var("RUST_LOG", log_level.to_owned() + ",winit=warn,tracing=warn,,iced_wgpu=warn,wgpu_hal=warn,wgpu_core=warn,librepods_rust::bluetooth::le=warn,cosmic_text=warn,naga=warn,iced_winit=warn") }; } env_logger::init(); let (ui_tx, ui_rx) = unbounded_channel::(); - let (ui_command_tx, ui_command_rx) = unbounded_channel::(); + let device_managers: Arc>> = Arc::new(RwLock::new(HashMap::new())); + let device_managers_clone = device_managers.clone(); std::thread::spawn(|| { let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async_main(ui_tx, ui_command_rx)).unwrap(); + rt.block_on(async_main(ui_tx, device_managers_clone)).unwrap(); }); - ui::window::start_ui(ui_rx, args.start_minimized, ui_command_tx) + ui::window::start_ui(ui_rx, args.start_minimized, device_managers) } -async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender, mut ui_command_rx: tokio::sync::mpsc::UnboundedReceiver) -> bluer::Result<()> { +async fn async_main( + ui_tx: tokio::sync::mpsc::UnboundedSender, + device_managers: Arc>>, +) -> bluer::Result<()> { let args = Args::parse(); - // let mut device_command_txs: HashMap)>> = HashMap::new(); - let mut device_managers: HashMap> = HashMap::new(); - let mut managed_devices_mac: Vec = Vec::new(); // includes ony non-AirPods. AirPods handled separately. let devices_path = get_devices_path(); @@ -125,12 +126,15 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender { info!("No connected AirPods found."); @@ -144,30 +148,37 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender { let dev = devices::nothing::NothingDevice::new(device.address(), ui_tx_clone).await; - device_managers.insert( - addr_str, - Arc::from(BluetoothManager::ATT(Arc::new(dev.att_manager))), - ); + let dev_managers = DeviceManagers::with_att(dev.att_manager.clone()); + managers + .entry(addr_str) + .or_insert(dev_managers) + .set_att(dev.att_manager); } _ => {} } + drop(managers) }); } } Err(e) => { - log::error!("Error finding connected managed devices: {}", e); + log::debug!("type of error: {:?}", e.kind); + if e.kind != bluer::ErrorKind::Internal(InternalErrorKind::Io(std::io::ErrorKind::NotFound)) { + log::error!("Error finding other managed devices: {}", e); + } else { + info!("No other managed devices found."); + } } } let conn = Connection::new_system()?; let rule = MatchRule::new_signal("org.freedesktop.DBus.Properties", "PropertiesChanged"); - let device_managers_clone = device_managers.clone(); conn.add_match(rule, move |_: (), conn, msg| { let Some(path) = msg.path() else { return true; }; if !path.contains("/org/bluez/hci") || !path.contains("/dev_") { @@ -198,14 +209,17 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender { let ui_tx_clone = ui_tx.clone(); - let mut device_managers = device_managers.clone(); + let device_managers = device_managers.clone(); tokio::spawn(async move { + let mut managers = device_managers.write().await; ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone).await; - device_managers.insert( - addr_str, - Arc::from(BluetoothManager::ATT(Arc::new(dev.att_manager))), - ); + let dev_managers = DeviceManagers::with_att(dev.att_manager.clone()); + managers + .entry(addr_str) + .or_insert(dev_managers) + .set_att(dev.att_manager); + drop(managers); }); } _ => {} @@ -220,85 +234,20 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender { - if let Some(manager) = device_managers_clone.get(&mac) { - match manager.as_ref() { - BluetoothManager::AACP(manager) => { - log::debug!("Sending control command to device {}: {:?} = {:?}", mac, identifier, value); - if let Err(e) = manager.send_control_command(identifier, value.as_ref()).await { - log::error!("Failed to send control command to device {}: {}", mac, e); - } - } - _ => { - log::warn!("AACP not available for {}", mac); - } - } - } else { - log::warn!("No manager for device {}", mac); - } - } - UICommand::AirPods(AirPodsCommand::RenameDevice(mac, new_name)) => { - if let Some(manager) = device_managers_clone.get(&mac) { - match manager.as_ref() { - BluetoothManager::AACP(manager) => { - log::debug!("Renaming device {} to {}", mac, new_name); - if let Err(e) = manager.send_rename_packet(&new_name).await { - log::error!("Failed to rename device {}: {}", mac, e); - } - } - _ => { - log::warn!("AACP not available for {}", mac); - } - } - } else { - log::warn!("No manager for device {}", mac); - } - } - UICommand::Nothing(NothingCommand::SetNoiseCancellationMode(mac, mode)) => { - if let Some(manager) = device_managers_clone.get(&mac) { - match manager.as_ref() { - BluetoothManager::ATT(manager) => { - log::debug!("Setting noise cancellation mode for device {}: {:?}", mac, mode); - if let Err(e) = manager.write( - ATTHandles::NothingEverything, - &[ - 0x55, - 0x60, 0x01, - 0x0F, 0xF0, - 0x03, 0x00, - 0x00, 0x01, // the 0x00 is an incremental counter, but it works without it - mode.to_byte(), 0x00, - 0x00, 0x00 // these both bytes were something random, 0 works too - ] - ).await { - log::error!("Failed to set noise cancellation mode for device {}: {}", mac, e); - } - } - _ => { - log::warn!("Nothing manager not available for {}", mac); - } - } - } else { - log::warn!("No manager for device {}", mac); - } - } - } - } - }); info!("Listening for Bluetooth connections via D-Bus..."); loop { diff --git a/linux-rust/src/ui/airpods.rs b/linux-rust/src/ui/airpods.rs index ed45660..84bb6e4 100644 --- a/linux-rust/src/ui/airpods.rs +++ b/linux-rust/src/ui/airpods.rs @@ -1,197 +1,407 @@ use std::collections::HashMap; -use iced::widget::{button, column, container, row, text, toggler, Space}; -use iced::{Background, Border, Color, Length, Theme}; +use std::sync::Arc; +use std::thread; +use iced::widget::{button, column, container, row, rule, text, text_input, toggler, Rule, Space}; +use iced::{Background, Border, Center, Color, Length, Padding, Theme}; +use iced::Alignment::End; +use iced::border::Radius; use iced::widget::button::Style; +use iced::widget::rule::FillMode; use log::error; -use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation}; -use crate::ui::window::{DeviceMessage, Message}; +use tokio::runtime::Runtime; +use crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers}; +use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation, DeviceState}; +use crate::ui::window::Message; pub fn airpods_view<'a>( - mac: &str, + mac: &'a str, devices_list: &HashMap, - state: &AirPodsState, + state: &'a AirPodsState, + aacp_manager: Arc ) -> iced::widget::Container<'a, Message> { + + // order: name, noise control, press and hold config, call controls (not sure if why it might be needed, adding it just in case), audio (personalized volume, conversational awareness, adaptive audio slider), connection settings, microphone, head gestures (not adding this), off listening mode, device information + + let aacp_manager_for_rename = aacp_manager.clone(); + let rename_input = container( + row![ + Space::with_width(10), + text("Name").size(16).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + } + ), + Space::with_width(Length::Fill), + text_input( + "", + &state.device_name + ) + .padding(Padding{ + top: 5.0, + bottom: 5.0, + left: 10.0, + right: 10.0, + }) + .style( + |theme: &Theme, _status| { + text_input::Style { + background: Background::Color(Color::TRANSPARENT), + border: Default::default(), + icon: Default::default(), + placeholder: theme.palette().text.scale_alpha(0.7), + value: theme.palette().text, + selection: Default::default(), + } + } + ) + .align_x(End) + .on_input( move |new_name| { + let aacp_manager = aacp_manager_for_rename.clone(); + run_async_in_thread( + { + let new_name = new_name.clone(); + async move { + aacp_manager.send_rename_packet(&new_name).await.expect("Failed to send rename packet"); + } + } + ); + let mut state = state.clone(); + state.device_name = new_name.clone(); + Message::StateChanged(mac.to_string(), DeviceState::AirPods(state)) + } + ) + ] + .align_y(Center) + ) + .padding(Padding{ + top: 5.0, + bottom: 5.0, + left: 10.0, + right: 10.0, + }) + .style( + |theme: &Theme| { + let mut style = container::Style::default(); + style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); + let mut border = Border::default(); + border.color = theme.palette().primary.scale_alpha(0.5); + style.border = border.rounded(16); + style + } + ); + + let audio_settings_col = column![ + container( + text("Audio Settings").size(18).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().primary); + style + } + ) + ) + .padding(Padding{ + top: 5.0, + bottom: 5.0, + left: 18.0, + right: 18.0, + }), + + container( + column![ + { + let aacp_manager_pv = aacp_manager.clone(); + row![ + column![ + text("Personalized Volume").size(16), + text("Adjusts the volume in response to your environment.").size(12).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text.scale_alpha(0.7)); + style + } + ) + ], + Space::with_width(Length::Fill), + toggler(state.personalized_volume_enabled) + .on_toggle(move |is_enabled| { + let aacp_manager = aacp_manager_pv.clone(); + run_async_in_thread( + async move { + aacp_manager.send_control_command( + ControlCommandIdentifiers::AdaptiveVolumeConfig, + if is_enabled { &[0x01] } else { &[0x02] } + ).await.expect("Failed to send Personalized Volume command"); + } + ); + let mut state = state.clone(); + state.personalized_volume_enabled = is_enabled; + Message::StateChanged(mac.to_string(), DeviceState::AirPods(state)) + }) + .spacing(0) + .size(20) + ] + .align_y(Center) + }, + Rule::horizontal(8).style( + |theme: &Theme| { + rule::Style { + color: theme.palette().text, + width: 1, + radius: Radius::from(12), + fill_mode: FillMode::Full + } + } + ), + { + let aacp_manager_conv_detect = aacp_manager.clone(); + row![ + column![ + text("Conversation Awareness").size(16), + text("Lowers the volume of your audio when it detects that you are speaking.").size(12).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text.scale_alpha(0.7)); + style + } + ) + ], + Space::with_width(Length::Fill), + toggler(state.conversation_awareness_enabled) + .on_toggle(move |is_enabled| { + let aacp_manager = aacp_manager_conv_detect.clone(); + run_async_in_thread( + async move { + aacp_manager.send_control_command( + ControlCommandIdentifiers::ConversationDetectConfig, + if is_enabled { &[0x01] } else { &[0x02] } + ).await.expect("Failed to send Conversation Awareness command"); + } + ); + let mut state = state.clone(); + state.conversation_awareness_enabled = is_enabled; + Message::StateChanged(mac.to_string(), DeviceState::AirPods(state)) + }) + .spacing(0) + .size(20) + ] + .align_y(Center) + } + ] + .spacing(4) + .padding(8) + ) + .padding(Padding{ + top: 5.0, + bottom: 5.0, + left: 10.0, + right: 10.0, + }) + .style( + |theme: &Theme| { + let mut style = container::Style::default(); + style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); + let mut border = Border::default(); + border.color = theme.palette().primary.scale_alpha(0.5); + style.border = border.rounded(16); + style + } + ) + ]; + let mut information_col = column![]; let mac = mac.to_string(); if let Some(device) = devices_list.get(mac.as_str()) { if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information { - information_col = information_col - .push(text("Device Information").size(18).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().primary); - style - } - )) - .push(Space::with_height(Length::from(10))) - .push( - row![ - text("Model Number").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); + let info_rows = column![ + row![ + text("Model Number").size(16).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + } + ), + Space::with_width(Length::Fill), + text(airpods_info.model_number.clone()).size(16) + ], + row![ + text("Manufacturer").size(16).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + } + ), + Space::with_width(Length::Fill), + text(airpods_info.manufacturer.clone()).size(16) + ], + row![ + text("Serial Number").size(16).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + } + ), + Space::with_width(Length::Fill), + button( + text(airpods_info.serial_number.clone()).size(16) + ) + .style( + |theme: &Theme, _status| { + let mut style = Style::default(); + style.text_color = theme.palette().text; + style.background = Some(Background::Color(Color::TRANSPARENT)); style } - ), - Space::with_width(Length::Fill), - text(airpods_info.model_number.clone()).size(16) - ] - ) - .push( - row![ - text("Manufacturer").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), - Space::with_width(Length::Fill), - text(airpods_info.manufacturer.clone()).size(16) - ] - ) - .push( - row![ - text("Serial Number").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), - Space::with_width(Length::Fill), - button( - text(airpods_info.serial_number.clone()).size(16) ) - .style( - |theme: &Theme, _status| { - let mut style = Style::default(); - style.text_color = theme.palette().text; - style.background = Some(Background::Color(Color::TRANSPARENT)); - style - } - ) - .padding(0) - .on_press(Message::CopyToClipboard(airpods_info.serial_number.clone())) - ] - ) - .push( - row![ - text("Left Serial Number").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); + .padding(0) + .on_press(Message::CopyToClipboard(airpods_info.serial_number.clone())) + ], + row![ + text("Left Serial Number").size(16).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + } + ), + Space::with_width(Length::Fill), + button( + text(airpods_info.left_serial_number.clone()).size(16) + ) + .style( + |theme: &Theme, _status| { + let mut style = Style::default(); + style.text_color = theme.palette().text; + style.background = Some(Background::Color(Color::TRANSPARENT)); style } - ), - Space::with_width(Length::Fill), - button( - text(airpods_info.left_serial_number.clone()).size(16) ) - .style( - |theme: &Theme, _status| { - let mut style = Style::default(); - style.text_color = theme.palette().text; - style.background = Some(Background::Color(Color::TRANSPARENT)); - style - } - ) - .padding(0) - .on_press(Message::CopyToClipboard(airpods_info.left_serial_number.clone())) - ] - ) - .push( - row![ - text("Right Serial Number").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); + .padding(0) + .on_press(Message::CopyToClipboard(airpods_info.left_serial_number.clone())) + ], + row![ + text("Right Serial Number").size(16).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + } + ), + Space::with_width(Length::Fill), + button( + text(airpods_info.right_serial_number.clone()).size(16) + ) + .style( + |theme: &Theme, _status| { + let mut style = Style::default(); + style.text_color = theme.palette().text; + style.background = Some(Background::Color(Color::TRANSPARENT)); style } - ), - Space::with_width(Length::Fill), - button( - text(airpods_info.right_serial_number.clone()).size(16) ) - .style( - |theme: &Theme, _status| { - let mut style = Style::default(); - style.text_color = theme.palette().text; - style.background = Some(Background::Color(Color::TRANSPARENT)); - style - } - ) - .padding(0) - .on_press(Message::CopyToClipboard(airpods_info.right_serial_number.clone())) - ] - ) - .push( - row![ - text("Version 1").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), - Space::with_width(Length::Fill), - text(airpods_info.version1.clone()).size(16) - ] - ) - .push( - row![ - text("Version 2").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), - Space::with_width(Length::Fill), - text(airpods_info.version2.clone()).size(16) - ] - ) - .push( - row![ - text("Version 3").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), - Space::with_width(Length::Fill), - text(airpods_info.version3.clone()).size(16) - ] - ); + .padding(0) + .on_press(Message::CopyToClipboard(airpods_info.right_serial_number.clone())) + ], + row![ + text("Version 1").size(16).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + } + ), + Space::with_width(Length::Fill), + text(airpods_info.version1.clone()).size(16) + ], + row![ + text("Version 2").size(16).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + } + ), + Space::with_width(Length::Fill), + text(airpods_info.version2.clone()).size(16) + ], + row![ + text("Version 3").size(16).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + } + ), + Space::with_width(Length::Fill), + text(airpods_info.version3.clone()).size(16) + ] + ] + .spacing(4) + .padding(8); + + information_col = column![ + container( + text("Device Information").size(18).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().primary); + style + } + ) + ).padding(Padding{ + top: 5.0, + bottom: 5.0, + left: 18.0, + right: 18.0, + }), + container(info_rows) + .padding(Padding{ + top: 5.0, + bottom: 5.0, + left: 10.0, + right: 10.0, + }) + .style( + |theme: &Theme| { + let mut style = container::Style::default(); + style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); + let mut border = Border::default(); + border.color = theme.palette().primary.scale_alpha(0.5); + style.border = border.rounded(16); + style + } + ) + ]; } else { error!("Expected AirPodsInformation for device {}, got something else", mac); } } - let toggler_widget = toggler(state.conversation_awareness_enabled) - .label("Conversation Awareness") - .on_toggle(move |is_enabled| Message::DeviceMessage(mac.to_string(), DeviceMessage::ConversationAwarenessToggled(is_enabled))); - container( column![ - toggler_widget, + rename_input, + Space::with_height(Length::from(20)), + audio_settings_col, Space::with_height(Length::from(10)), - container(information_col) - .style( - |theme: &Theme| { - let mut style = container::Style::default(); - style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); - let mut border = Border::default(); - border.color = theme.palette().text; - style.border = border.rounded(20); - style - } - ) - .padding(20) + information_col ] ) .padding(20) .center_x(Length::Fill) .height(Length::Fill) } + +fn run_async_in_thread(fut: F) +where + F: Future + Send + 'static, +{ + thread::spawn(move || { + let rt = Runtime::new().unwrap(); + rt.block_on(fut); + }); +} \ No newline at end of file diff --git a/linux-rust/src/ui/messages.rs b/linux-rust/src/ui/messages.rs index 8bb8689..9d5c828 100644 --- a/linux-rust/src/ui/messages.rs +++ b/linux-rust/src/ui/messages.rs @@ -1,5 +1,4 @@ -use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers}; -use crate::devices::enums::NothingAncMode; +use crate::bluetooth::aacp::AACPEvent; #[derive(Debug, Clone)] pub enum BluetoothUIMessage { @@ -9,22 +8,4 @@ pub enum BluetoothUIMessage { AACPUIEvent(String, AACPEvent), // mac, event ATTNotification(String, u16, Vec), // mac, handle, data NoOp -} - -#[derive(Debug, Clone)] -pub enum UICommand { - AirPods(AirPodsCommand), - Nothing(NothingCommand), -} - -#[derive(Debug, Clone)] -pub enum AirPodsCommand { - SetControlCommandStatus(String, ControlCommandIdentifiers, Vec), - RenameDevice(String, String), -} - -#[derive(Debug, Clone)] -pub enum NothingCommand { - SetNoiseCancellationMode(String, NothingAncMode), -} - +} \ No newline at end of file diff --git a/linux-rust/src/ui/nothing.rs b/linux-rust/src/ui/nothing.rs index 6237247..5c6dd6b 100644 --- a/linux-rust/src/ui/nothing.rs +++ b/linux-rust/src/ui/nothing.rs @@ -1,13 +1,16 @@ use std::collections::HashMap; +use std::sync::Arc; use iced::{Background, Border, Length, Theme}; use iced::widget::{container, text, column, row, Space, combo_box}; +use crate::bluetooth::att::ATTManager; use crate::devices::enums::{DeviceData, DeviceInformation, NothingState}; use crate::ui::window::Message; pub fn nothing_view<'a>( mac: &str, devices_list: &HashMap, - state: &NothingState + state: &NothingState, + att_manager: Arc ) -> iced::widget::Container<'a, Message> { let mut information_col = iced::widget::column![]; let mac = mac.to_string(); @@ -21,7 +24,7 @@ pub fn nothing_view<'a>( style } )) - .push(iced::widget::Space::with_height(iced::Length::from(10))) + .push(Space::with_height(iced::Length::from(10))) .push( iced::widget::row![ text("Serial Number").size(16).style( @@ -31,7 +34,7 @@ pub fn nothing_view<'a>( style } ), - iced::widget::Space::with_width(iced::Length::Fill), + Space::with_width(Length::Fill), text(nothing_info.serial_number.clone()).size(16) ] ) @@ -44,7 +47,7 @@ pub fn nothing_view<'a>( style } ), - iced::widget::Space::with_width(iced::Length::Fill), + Space::with_width(Length::Fill), text(nothing_info.firmware_version.clone()).size(16) ] ); @@ -74,4 +77,20 @@ pub fn nothing_view<'a>( .padding(20) .center_x(Length::Fill) .height(Length::Fill) -} \ No newline at end of file +} + + +// if let Err(e) = manager.write( +// ATTHandles::NothingEverything, +// &[ +// 0x55, +// 0x60, 0x01, +// 0x0F, 0xF0, +// 0x03, 0x00, +// 0x00, 0x01, // the 0x00 is an incremental counter, but it works without it +// mode.to_byte(), 0x00, +// 0x00, 0x00 // these both bytes were something random, 0 works too +// ] +// ).await { +// log::error!("Failed to set noise cancellation mode for device {}: {}", mac, e); +// } \ No newline at end of file diff --git a/linux-rust/src/ui/window.rs b/linux-rust/src/ui/window.rs index b0cc6b8..5bba6ea 100644 --- a/linux-rust/src/ui/window.rs +++ b/linux-rust/src/ui/window.rs @@ -1,17 +1,19 @@ use std::collections::HashMap; use iced::widget::button::Style; -use iced::widget::{button, column, container, pane_grid, text, Space, combo_box, row, text_input, scrollable}; +use iced::widget::{button, column, container, pane_grid, text, Space, combo_box, row, text_input, scrollable, vertical_rule, rule}; use iced::{daemon, window, Background, Border, Center, Color, Element, Length, Size, Subscription, Task, Theme}; use std::sync::Arc; use bluer::{Address, Session}; use iced::border::Radius; use iced::overlay::menu; +use iced::widget::rule::FillMode; use log::{debug, error}; use tokio::sync::mpsc::UnboundedReceiver; -use tokio::sync::Mutex; -use crate::bluetooth::aacp::{AACPEvent}; +use tokio::sync::{Mutex, RwLock}; +use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers}; +use crate::bluetooth::managers::DeviceManagers; use crate::devices::enums::{AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode, NothingState}; -use crate::ui::messages::{AirPodsCommand, BluetoothUIMessage, NothingCommand, UICommand}; +use crate::ui::messages::BluetoothUIMessage; use crate::utils::{get_devices_path, get_app_settings_path, MyTheme}; use crate::ui::airpods::airpods_view; use crate::ui::nothing::nothing_view; @@ -19,12 +21,12 @@ use crate::ui::nothing::nothing_view; pub fn start_ui( ui_rx: UnboundedReceiver, start_minimized: bool, - ui_command_tx: tokio::sync::mpsc::UnboundedSender, + device_managers: Arc>>, ) -> iced::Result { daemon(App::title, App::update, App::view) .subscription(App::subscription) .theme(App::theme) - .run_with(move || App::new(ui_rx, start_minimized, ui_command_tx)) + .run_with(move || App::new(ui_rx, start_minimized, device_managers)) } pub struct App { @@ -35,9 +37,9 @@ pub struct App { selected_theme: MyTheme, ui_rx: Arc>>, bluetooth_state: BluetoothState, - ui_command_tx: tokio::sync::mpsc::UnboundedSender, paired_devices: HashMap, device_states: HashMap, + device_managers: Arc>>, pending_add_device: Option<(String, Address)>, device_type_state: combo_box::State, selected_device_type: Option, @@ -55,12 +57,6 @@ impl BluetoothState { } } -#[derive(Debug, Clone)] -pub enum DeviceMessage { - ConversationAwarenessToggled(bool), - NothingAncModeSelected(NothingAncMode) -} - #[derive(Debug, Clone)] pub enum Message { WindowOpened(window::Id), @@ -70,13 +66,13 @@ pub enum Message { ThemeSelected(MyTheme), CopyToClipboard(String), BluetoothMessage(BluetoothUIMessage), - DeviceMessage(String, DeviceMessage), ShowNewDialogTab, GotPairedDevices(HashMap), StartAddDevice(String, Address), SelectDeviceType(DeviceType), ConfirmAddDevice, CancelAddDevice, + StateChanged(String, DeviceState), } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -96,7 +92,7 @@ impl App { pub fn new( ui_rx: UnboundedReceiver, start_minimized: bool, - ui_command_tx: tokio::sync::mpsc::UnboundedSender, + device_managers: 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); @@ -169,7 +165,6 @@ impl App { selected_theme, ui_rx, bluetooth_state, - ui_command_tx, paired_devices: HashMap::new(), device_states, pending_add_device: None, @@ -177,6 +172,7 @@ impl App { DeviceType::Nothing ]), selected_device_type: None, + device_managers }, Task::batch(vec![open_task, wait_task]) ) @@ -217,32 +213,6 @@ impl App { Message::CopyToClipboard(data) => { iced::clipboard::write(data) } - Message::DeviceMessage(mac, device_msg) => { - match device_msg { - DeviceMessage::ConversationAwarenessToggled(is_enabled) => { - if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { - state.conversation_awareness_enabled = is_enabled; - let value = if is_enabled { 0x01 } else { 0x02 }; - let _ = self.ui_command_tx.send(UICommand::AirPods(AirPodsCommand::SetControlCommandStatus( - mac, - crate::bluetooth::aacp::ControlCommandIdentifiers::ConversationDetectConfig, - vec![value], - ))); - } - Task::none() - } - DeviceMessage::NothingAncModeSelected(mode) => { - if let Some(DeviceState::Nothing(state)) = self.device_states.get_mut(&mac) { - state.anc_mode = mode.clone(); - let _ = self.ui_command_tx.send(UICommand::Nothing(NothingCommand::SetNoiseCancellationMode( - mac, - mode, - ))); - } - Task::none() - } - } - } Message::BluetoothMessage(ui_message) => { match ui_message { BluetoothUIMessage::NoOp => { @@ -312,8 +282,33 @@ impl App { }; match type_ { Some(DeviceType::AirPods) => { + let device_managers = self.device_managers.blocking_read(); + let device_manager = device_managers.get(&mac).unwrap(); + let aacp_manager = device_manager.get_aacp().unwrap(); + let aacp_manager_state = aacp_manager.state.clone(); + let state = aacp_manager_state.blocking_lock(); + debug!("AACP manager found for AirPods device {}", mac); + let device_name = { + let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| { + error!("Failed to read devices file: {}", e); + "{}".to_string() + }); + let devices_list: HashMap = serde_json::from_str(&devices_json).unwrap_or_else(|e| { + error!("Deserialization failed: {}", e); + HashMap::new() + }); + devices_list.get(&mac).map(|d| d.name.clone()).unwrap_or_else(|| "Unknown Device".to_string()) + }; self.device_states.insert(mac.clone(), DeviceState::AirPods(AirPodsState { - conversation_awareness_enabled: false, + device_name, + conversation_awareness_enabled: state.control_command_status_list.iter().any(|status| { + status.identifier == ControlCommandIdentifiers::ConversationDetectConfig && + matches!(status.value.as_slice(), [0x01]) + }), + personalized_volume_enabled: state.control_command_status_list.iter().any(|status| { + status.identifier == ControlCommandIdentifiers::AdaptiveVolumeConfig && + matches!(status.value.as_slice(), [0x01]) + }), })); } Some(DeviceType::Nothing) => { @@ -351,7 +346,7 @@ impl App { match event { AACPEvent::ControlCommand(status) => { match status.identifier { - crate::bluetooth::aacp::ControlCommandIdentifiers::ConversationDetectConfig => { + ControlCommandIdentifiers::ConversationDetectConfig => { let is_enabled = match status.value.as_slice() { [0x01] => true, [0x02] => false, @@ -364,6 +359,19 @@ impl App { state.conversation_awareness_enabled = is_enabled; } } + ControlCommandIdentifiers::AdaptiveVolumeConfig => { + let is_enabled = match status.value.as_slice() { + [0x01] => true, + [0x02] => false, + _ => { + error!("Unknown Adaptive Volume Config value: {:?}", status.value); + false + } + }; + if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { + state.personalized_volume_enabled = is_enabled; + } + } _ => { debug!("Unhandled Control Command Status: {:?}", status); } @@ -440,6 +448,10 @@ impl App { self.selected_device_type = None; Task::none() } + Message::StateChanged(mac, state) => { + self.device_states.insert(mac, state); + Task::none() + } } } @@ -577,11 +589,25 @@ impl App { settings ] .padding(12); - - pane_grid::Content::new(content) + pane_grid::Content::new( + row![ + content, + vertical_rule(1).style( + |theme: &Theme| { + rule::Style{ + color: theme.palette().primary.scale_alpha(0.2), + width: 2, + radius: Radius::from(8.0), + fill_mode: FillMode::Full + } + } + ) + ] + ) } Pane::Content => { + let device_managers = self.device_managers.blocking_read(); let content = match &self.selected_tab { Tab::Device(id) => { if id == "none" { @@ -597,7 +623,25 @@ impl App { match device_type { Some(DeviceType::AirPods) => { if let Some(DeviceState::AirPods(state)) = device_state { - airpods_view(id, &devices_list, state) + if let Some(device_managers) = device_managers.get(id) { + if let Some(aacp_manager) = device_managers.get_aacp() { + airpods_view(id, &devices_list, state, aacp_manager.clone()) + } else { + error!("No AACP manager found for AirPods device {}", id); + container( + text("No valid AACP manager found for this AirPods device").size(16) + ) + .center_x(Length::Fill) + .center_y(Length::Fill) + } + } else { + error!("No manager found for AirPods device {}", id); + container( + text("No manager found for this AirPods device").size(16) + ) + .center_x(Length::Fill) + .center_y(Length::Fill) + } } else { container( text("No state available for this AirPods device").size(16) @@ -608,7 +652,25 @@ impl App { } Some(DeviceType::Nothing) => { if let Some(DeviceState::Nothing(state)) = device_state { - nothing_view(id, &devices_list, state) + if let Some(device_managers) = device_managers.get(id) { + if let Some(att_manager) = device_managers.get_att() { + nothing_view(id, &devices_list, state, att_manager.clone()) + } else { + error!("No ATT manager found for Nothing device {}", id); + container( + text("No valid ATT manager found for this Nothing device").size(16) + ) + .center_x(Length::Fill) + .center_y(Length::Fill) + } + } else { + error!("No manager found for Nothing device {}", id); + container( + text("No manager found for this Nothing device").size(16) + ) + .center_x(Length::Fill) + .center_y(Length::Fill) + } } else { container( text("No state available for this Nothing device").size(16)