linux-rust: implement bluetooth logic with ui

finally! only copying all ui from android to first release
This commit is contained in:
Kavish Devar
2025-11-08 02:12:24 +05:30
parent a2cda688d4
commit bf6630dbd1
8 changed files with 581 additions and 370 deletions

View File

@@ -37,5 +37,6 @@ pub async fn find_other_managed_devices(adapter: &Adapter, managed_macs: Vec<Str
if !devices.is_empty() { if !devices.is_empty() {
return Ok(devices); return Ok(devices);
} }
debug!("No other managed devices found");
Err(bluer::Error::from(Error::new(std::io::ErrorKind::NotFound, "No other managed devices found"))) Err(bluer::Error::from(Error::new(std::io::ErrorKind::NotFound, "No other managed devices found")))
} }

View File

@@ -1,52 +1,39 @@
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use crate::bluetooth::aacp::AACPManager; use crate::bluetooth::aacp::AACPManager;
use crate::bluetooth::att::ATTManager; use crate::bluetooth::att::ATTManager;
pub enum BluetoothManager {
AACP(Arc<AACPManager>),
ATT(Arc<ATTManager>),
}
pub struct DeviceManagers { pub struct DeviceManagers {
att: Option<Arc<ATTManager>>, att: Option<Arc<ATTManager>>,
aacp: Option<Arc<AACPManager>>, aacp: Option<Arc<AACPManager>>,
} }
impl DeviceManagers { impl DeviceManagers {
fn new() -> Self { pub fn with_aacp(aacp: AACPManager) -> Self {
Self { att: None, aacp: None }
}
fn with_aacp(aacp: AACPManager) -> Self {
Self { att: None, aacp: Some(Arc::new(aacp)) } 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 } Self { att: Some(Arc::new(att)), aacp: None }
} }
}
pub struct BluetoothDevices { // keeping the att for airpods optional as it requires changes in system bluez config
devices: HashMap<String, DeviceManagers>, pub fn with_both(aacp: AACPManager, att: ATTManager) -> Self {
} Self { att: Some(Arc::new(att)), aacp: Some(Arc::new(aacp)) }
impl BluetoothDevices {
fn new() -> Self {
Self { devices: HashMap::new() }
} }
fn add_aacp(&mut self, mac: String, manager: AACPManager) { pub fn set_aacp(&mut self, manager: AACPManager) {
self.devices self.aacp = Some(Arc::new(manager));
.entry(mac)
.or_insert_with(DeviceManagers::new)
.aacp = Some(Arc::new(manager));
} }
fn add_att(&mut self, mac: String, manager: ATTManager) { pub fn set_att(&mut self, manager: ATTManager) {
self.devices self.att = Some(Arc::new(manager));
.entry(mac) }
.or_insert_with(DeviceManagers::new)
.att = Some(Arc::new(manager)); pub fn get_aacp(&self) -> Option<Arc<AACPManager>> {
self.aacp.clone()
}
pub fn get_att(&self) -> Option<Arc<ATTManager>> {
self.att.clone()
} }
} }

View File

@@ -52,7 +52,9 @@ impl Display for DeviceState {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AirPodsState { pub struct AirPodsState {
pub device_name: String,
pub conversation_awareness_enabled: bool, pub conversation_awareness_enabled: bool,
pub personalized_volume_enabled: bool,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View File

@@ -14,16 +14,16 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use crate::bluetooth::discovery::{find_connected_airpods, find_other_managed_devices}; use crate::bluetooth::discovery::{find_connected_airpods, find_other_managed_devices};
use devices::airpods::AirPodsDevice; use devices::airpods::AirPodsDevice;
use bluer::Address; use bluer::{Address, InternalErrorKind};
use ksni::TrayMethods; use ksni::TrayMethods;
use crate::ui::tray::MyTray; use crate::ui::tray::MyTray;
use clap::Parser; use clap::Parser;
use crate::bluetooth::le::start_le_monitor; use crate::bluetooth::le::start_le_monitor;
use tokio::sync::mpsc::unbounded_channel; use tokio::sync::mpsc::unbounded_channel;
use crate::bluetooth::att::ATTHandles; use tokio::sync::RwLock;
use crate::bluetooth::managers::BluetoothManager; use crate::bluetooth::managers::DeviceManagers;
use crate::devices::enums::DeviceData; use crate::devices::enums::DeviceData;
use crate::ui::messages::{AirPodsCommand, BluetoothUIMessage, NothingCommand, UICommand}; use crate::ui::messages::BluetoothUIMessage;
use crate::utils::get_devices_path; use crate::utils::get_devices_path;
#[derive(Parser)] #[derive(Parser)]
@@ -40,28 +40,29 @@ fn main() -> iced::Result {
let args = Args::parse(); let args = Args::parse();
let log_level = if args.debug { "debug" } else { "info" }; let log_level = if args.debug { "debug" } else { "info" };
if env::var("RUST_LOG").is_err() { 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(); env_logger::init();
let (ui_tx, ui_rx) = unbounded_channel::<BluetoothUIMessage>(); let (ui_tx, ui_rx) = unbounded_channel::<BluetoothUIMessage>();
let (ui_command_tx, ui_command_rx) = unbounded_channel::<UICommand>();
let device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>> = Arc::new(RwLock::new(HashMap::new()));
let device_managers_clone = device_managers.clone();
std::thread::spawn(|| { std::thread::spawn(|| {
let rt = tokio::runtime::Runtime::new().unwrap(); 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<BluetoothUIMessage>, mut ui_command_rx: tokio::sync::mpsc::UnboundedReceiver<UICommand>) -> bluer::Result<()> { async fn async_main(
ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>,
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
) -> bluer::Result<()> {
let args = Args::parse(); let args = Args::parse();
// let mut device_command_txs: HashMap<String, tokio::sync::mpsc::UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>> = HashMap::new();
let mut device_managers: HashMap<String, Arc<BluetoothManager>> = HashMap::new();
let mut managed_devices_mac: Vec<String> = Vec::new(); // includes ony non-AirPods. AirPods handled separately. let mut managed_devices_mac: Vec<String> = Vec::new(); // includes ony non-AirPods. AirPods handled separately.
let devices_path = get_devices_path(); let devices_path = get_devices_path();
@@ -125,12 +126,15 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage
let ui_tx_clone = ui_tx.clone(); let ui_tx_clone = ui_tx.clone();
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(device.address().to_string())).unwrap(); ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(device.address().to_string())).unwrap();
let airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx_clone).await; let airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx_clone).await;
// device_command_txs.insert(device.address().to_string(), airpods_device.command_tx.unwrap());
// device_managers.insert(device.address().to_string(), Arc::new(airpods_device.aacp_manager)); let mut managers = device_managers.write().await;
device_managers.insert( let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone());
device.address().to_string(), managers
Arc::from(BluetoothManager::AACP(Arc::new(airpods_device.aacp_manager))), .entry(device.address().to_string())
); .or_insert(dev_managers)
.set_aacp(airpods_device.aacp_manager)
;
drop(managers)
} }
Err(_) => { Err(_) => {
info!("No connected AirPods found."); info!("No connected AirPods found.");
@@ -144,30 +148,37 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage
info!("Found connected managed device: {}, initializing.", addr_str); info!("Found connected managed device: {}, initializing.", addr_str);
let type_ = devices_list.get(&addr_str).unwrap().type_.clone(); let type_ = devices_list.get(&addr_str).unwrap().type_.clone();
let ui_tx_clone = ui_tx.clone(); let ui_tx_clone = ui_tx.clone();
let mut device_managers = device_managers.clone(); let device_managers = device_managers.clone();
tokio::spawn(async move { tokio::spawn(async move {
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
let mut managers = device_managers.write().await;
match type_ { match type_ {
devices::enums::DeviceType::Nothing => { devices::enums::DeviceType::Nothing => {
let dev = devices::nothing::NothingDevice::new(device.address(), ui_tx_clone).await; let dev = devices::nothing::NothingDevice::new(device.address(), ui_tx_clone).await;
device_managers.insert( let dev_managers = DeviceManagers::with_att(dev.att_manager.clone());
addr_str, managers
Arc::from(BluetoothManager::ATT(Arc::new(dev.att_manager))), .entry(addr_str)
); .or_insert(dev_managers)
.set_att(dev.att_manager);
} }
_ => {} _ => {}
} }
drop(managers)
}); });
} }
} }
Err(e) => { 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 conn = Connection::new_system()?;
let rule = MatchRule::new_signal("org.freedesktop.DBus.Properties", "PropertiesChanged"); let rule = MatchRule::new_signal("org.freedesktop.DBus.Properties", "PropertiesChanged");
let device_managers_clone = device_managers.clone();
conn.add_match(rule, move |_: (), conn, msg| { conn.add_match(rule, move |_: (), conn, msg| {
let Some(path) = msg.path() else { return true; }; let Some(path) = msg.path() else { return true; };
if !path.contains("/org/bluez/hci") || !path.contains("/dev_") { if !path.contains("/org/bluez/hci") || !path.contains("/dev_") {
@@ -198,14 +209,17 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage
match type_ { match type_ {
devices::enums::DeviceType::Nothing => { devices::enums::DeviceType::Nothing => {
let ui_tx_clone = ui_tx.clone(); let ui_tx_clone = ui_tx.clone();
let mut device_managers = device_managers.clone(); let device_managers = device_managers.clone();
tokio::spawn(async move { tokio::spawn(async move {
let mut managers = device_managers.write().await;
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone).await; let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone).await;
device_managers.insert( let dev_managers = DeviceManagers::with_att(dev.att_manager.clone());
addr_str, managers
Arc::from(BluetoothManager::ATT(Arc::new(dev.att_manager))), .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<BluetoothUIMessage
info!("AirPods connected: {}, initializing", name); info!("AirPods connected: {}, initializing", name);
let handle_clone = tray_handle.clone(); let handle_clone = tray_handle.clone();
let ui_tx_clone = ui_tx.clone(); let ui_tx_clone = ui_tx.clone();
let mut device_managers = device_managers.clone(); let device_managers = device_managers.clone();
tokio::spawn(async move { tokio::spawn(async move {
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone).await; let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone).await;
device_managers.insert( let mut managers = device_managers.write().await;
addr_str, let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone());
Arc::from(BluetoothManager::AACP(Arc::new(airpods_device.aacp_manager))), managers
); .entry(addr_str)
.or_insert(dev_managers)
.set_aacp(airpods_device.aacp_manager);
drop(managers);
}); });
true true
})?; })?;
tokio::spawn(async move {
while let Some(command) = ui_command_rx.recv().await {
match command {
UICommand::AirPods(AirPodsCommand::SetControlCommandStatus(mac, identifier, value)) => {
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..."); info!("Listening for Bluetooth connections via D-Bus...");
loop { loop {

View File

@@ -1,197 +1,407 @@
use std::collections::HashMap; use std::collections::HashMap;
use iced::widget::{button, column, container, row, text, toggler, Space}; use std::sync::Arc;
use iced::{Background, Border, Color, Length, Theme}; 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::button::Style;
use iced::widget::rule::FillMode;
use log::error; use log::error;
use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation}; use tokio::runtime::Runtime;
use crate::ui::window::{DeviceMessage, Message}; use crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers};
use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation, DeviceState};
use crate::ui::window::Message;
pub fn airpods_view<'a>( pub fn airpods_view<'a>(
mac: &str, mac: &'a str,
devices_list: &HashMap<String, DeviceData>, devices_list: &HashMap<String, DeviceData>,
state: &AirPodsState, state: &'a AirPodsState,
aacp_manager: Arc<AACPManager>
) -> iced::widget::Container<'a, Message> { ) -> 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 mut information_col = column![];
let mac = mac.to_string(); let mac = mac.to_string();
if let Some(device) = devices_list.get(mac.as_str()) { if let Some(device) = devices_list.get(mac.as_str()) {
if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information { if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information {
information_col = information_col let info_rows = column![
.push(text("Device Information").size(18).style( row![
|theme: &Theme| { text("Model Number").size(16).style(
let mut style = text::Style::default(); |theme: &Theme| {
style.color = Some(theme.palette().primary); let mut style = text::Style::default();
style style.color = Some(theme.palette().text);
} style
)) }
.push(Space::with_height(Length::from(10))) ),
.push( Space::with_width(Length::Fill),
row![ text(airpods_info.model_number.clone()).size(16)
text("Model Number").size(16).style( ],
|theme: &Theme| { row![
let mut style = text::Style::default(); text("Manufacturer").size(16).style(
style.color = Some(theme.palette().text); |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 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( .padding(0)
|theme: &Theme, _status| { .on_press(Message::CopyToClipboard(airpods_info.serial_number.clone()))
let mut style = Style::default(); ],
style.text_color = theme.palette().text; row![
style.background = Some(Background::Color(Color::TRANSPARENT)); text("Left Serial Number").size(16).style(
style |theme: &Theme| {
} let mut style = text::Style::default();
) style.color = Some(theme.palette().text);
.padding(0) style
.on_press(Message::CopyToClipboard(airpods_info.serial_number.clone())) }
] ),
) Space::with_width(Length::Fill),
.push( button(
row![ text(airpods_info.left_serial_number.clone()).size(16)
text("Left Serial Number").size(16).style( )
|theme: &Theme| { .style(
let mut style = text::Style::default(); |theme: &Theme, _status| {
style.color = Some(theme.palette().text); let mut style = Style::default();
style.text_color = theme.palette().text;
style.background = Some(Background::Color(Color::TRANSPARENT));
style style
} }
),
Space::with_width(Length::Fill),
button(
text(airpods_info.left_serial_number.clone()).size(16)
) )
.style( .padding(0)
|theme: &Theme, _status| { .on_press(Message::CopyToClipboard(airpods_info.left_serial_number.clone()))
let mut style = Style::default(); ],
style.text_color = theme.palette().text; row![
style.background = Some(Background::Color(Color::TRANSPARENT)); text("Right Serial Number").size(16).style(
style |theme: &Theme| {
} let mut style = text::Style::default();
) style.color = Some(theme.palette().text);
.padding(0) style
.on_press(Message::CopyToClipboard(airpods_info.left_serial_number.clone())) }
] ),
) Space::with_width(Length::Fill),
.push( button(
row![ text(airpods_info.right_serial_number.clone()).size(16)
text("Right Serial Number").size(16).style( )
|theme: &Theme| { .style(
let mut style = text::Style::default(); |theme: &Theme, _status| {
style.color = Some(theme.palette().text); let mut style = Style::default();
style.text_color = theme.palette().text;
style.background = Some(Background::Color(Color::TRANSPARENT));
style style
} }
),
Space::with_width(Length::Fill),
button(
text(airpods_info.right_serial_number.clone()).size(16)
) )
.style( .padding(0)
|theme: &Theme, _status| { .on_press(Message::CopyToClipboard(airpods_info.right_serial_number.clone()))
let mut style = Style::default(); ],
style.text_color = theme.palette().text; row![
style.background = Some(Background::Color(Color::TRANSPARENT)); text("Version 1").size(16).style(
style |theme: &Theme| {
} let mut style = text::Style::default();
) style.color = Some(theme.palette().text);
.padding(0) style
.on_press(Message::CopyToClipboard(airpods_info.right_serial_number.clone())) }
] ),
) Space::with_width(Length::Fill),
.push( text(airpods_info.version1.clone()).size(16)
row![ ],
text("Version 1").size(16).style( row![
|theme: &Theme| { text("Version 2").size(16).style(
let mut style = text::Style::default(); |theme: &Theme| {
style.color = Some(theme.palette().text); let mut style = text::Style::default();
style style.color = Some(theme.palette().text);
} style
), }
Space::with_width(Length::Fill), ),
text(airpods_info.version1.clone()).size(16) Space::with_width(Length::Fill),
] text(airpods_info.version2.clone()).size(16)
) ],
.push( row![
row![ text("Version 3").size(16).style(
text("Version 2").size(16).style( |theme: &Theme| {
|theme: &Theme| { let mut style = text::Style::default();
let mut style = text::Style::default(); style.color = Some(theme.palette().text);
style.color = Some(theme.palette().text); style
style }
} ),
), Space::with_width(Length::Fill),
Space::with_width(Length::Fill), text(airpods_info.version3.clone()).size(16)
text(airpods_info.version2.clone()).size(16) ]
] ]
) .spacing(4)
.push( .padding(8);
row![
text("Version 3").size(16).style( information_col = column![
|theme: &Theme| { container(
let mut style = text::Style::default(); text("Device Information").size(18).style(
style.color = Some(theme.palette().text); |theme: &Theme| {
style let mut style = text::Style::default();
} style.color = Some(theme.palette().primary);
), style
Space::with_width(Length::Fill), }
text(airpods_info.version3.clone()).size(16) )
] ).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 { } else {
error!("Expected AirPodsInformation for device {}, got something else", mac); 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( container(
column![ column![
toggler_widget, rename_input,
Space::with_height(Length::from(20)),
audio_settings_col,
Space::with_height(Length::from(10)), Space::with_height(Length::from(10)),
container(information_col) 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)
] ]
) )
.padding(20) .padding(20)
.center_x(Length::Fill) .center_x(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
} }
fn run_async_in_thread<F>(fut: F)
where
F: Future<Output = ()> + Send + 'static,
{
thread::spawn(move || {
let rt = Runtime::new().unwrap();
rt.block_on(fut);
});
}

View File

@@ -1,5 +1,4 @@
use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers}; use crate::bluetooth::aacp::AACPEvent;
use crate::devices::enums::NothingAncMode;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum BluetoothUIMessage { pub enum BluetoothUIMessage {
@@ -10,21 +9,3 @@ pub enum BluetoothUIMessage {
ATTNotification(String, u16, Vec<u8>), // mac, handle, data ATTNotification(String, u16, Vec<u8>), // mac, handle, data
NoOp NoOp
} }
#[derive(Debug, Clone)]
pub enum UICommand {
AirPods(AirPodsCommand),
Nothing(NothingCommand),
}
#[derive(Debug, Clone)]
pub enum AirPodsCommand {
SetControlCommandStatus(String, ControlCommandIdentifiers, Vec<u8>),
RenameDevice(String, String),
}
#[derive(Debug, Clone)]
pub enum NothingCommand {
SetNoiseCancellationMode(String, NothingAncMode),
}

View File

@@ -1,13 +1,16 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
use iced::{Background, Border, Length, Theme}; use iced::{Background, Border, Length, Theme};
use iced::widget::{container, text, column, row, Space, combo_box}; use iced::widget::{container, text, column, row, Space, combo_box};
use crate::bluetooth::att::ATTManager;
use crate::devices::enums::{DeviceData, DeviceInformation, NothingState}; use crate::devices::enums::{DeviceData, DeviceInformation, NothingState};
use crate::ui::window::Message; use crate::ui::window::Message;
pub fn nothing_view<'a>( pub fn nothing_view<'a>(
mac: &str, mac: &str,
devices_list: &HashMap<String, DeviceData>, devices_list: &HashMap<String, DeviceData>,
state: &NothingState state: &NothingState,
att_manager: Arc<ATTManager>
) -> iced::widget::Container<'a, Message> { ) -> iced::widget::Container<'a, Message> {
let mut information_col = iced::widget::column![]; let mut information_col = iced::widget::column![];
let mac = mac.to_string(); let mac = mac.to_string();
@@ -21,7 +24,7 @@ pub fn nothing_view<'a>(
style style
} }
)) ))
.push(iced::widget::Space::with_height(iced::Length::from(10))) .push(Space::with_height(iced::Length::from(10)))
.push( .push(
iced::widget::row![ iced::widget::row![
text("Serial Number").size(16).style( text("Serial Number").size(16).style(
@@ -31,7 +34,7 @@ pub fn nothing_view<'a>(
style style
} }
), ),
iced::widget::Space::with_width(iced::Length::Fill), Space::with_width(Length::Fill),
text(nothing_info.serial_number.clone()).size(16) text(nothing_info.serial_number.clone()).size(16)
] ]
) )
@@ -44,7 +47,7 @@ pub fn nothing_view<'a>(
style style
} }
), ),
iced::widget::Space::with_width(iced::Length::Fill), Space::with_width(Length::Fill),
text(nothing_info.firmware_version.clone()).size(16) text(nothing_info.firmware_version.clone()).size(16)
] ]
); );
@@ -75,3 +78,19 @@ pub fn nothing_view<'a>(
.center_x(Length::Fill) .center_x(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
} }
// 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);
// }

View File

@@ -1,17 +1,19 @@
use std::collections::HashMap; use std::collections::HashMap;
use iced::widget::button::Style; 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 iced::{daemon, window, Background, Border, Center, Color, Element, Length, Size, Subscription, Task, Theme};
use std::sync::Arc; use std::sync::Arc;
use bluer::{Address, Session}; use bluer::{Address, Session};
use iced::border::Radius; use iced::border::Radius;
use iced::overlay::menu; use iced::overlay::menu;
use iced::widget::rule::FillMode;
use log::{debug, error}; use log::{debug, error};
use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedReceiver;
use tokio::sync::Mutex; use tokio::sync::{Mutex, RwLock};
use crate::bluetooth::aacp::{AACPEvent}; use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers};
use crate::bluetooth::managers::DeviceManagers;
use crate::devices::enums::{AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode, NothingState}; 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::utils::{get_devices_path, get_app_settings_path, MyTheme};
use crate::ui::airpods::airpods_view; use crate::ui::airpods::airpods_view;
use crate::ui::nothing::nothing_view; use crate::ui::nothing::nothing_view;
@@ -19,12 +21,12 @@ use crate::ui::nothing::nothing_view;
pub fn start_ui( pub fn start_ui(
ui_rx: UnboundedReceiver<BluetoothUIMessage>, ui_rx: UnboundedReceiver<BluetoothUIMessage>,
start_minimized: bool, start_minimized: bool,
ui_command_tx: tokio::sync::mpsc::UnboundedSender<UICommand>, device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
) -> iced::Result { ) -> iced::Result {
daemon(App::title, App::update, App::view) daemon(App::title, App::update, App::view)
.subscription(App::subscription) .subscription(App::subscription)
.theme(App::theme) .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 { pub struct App {
@@ -35,9 +37,9 @@ pub struct App {
selected_theme: MyTheme, selected_theme: MyTheme,
ui_rx: Arc<Mutex<UnboundedReceiver<BluetoothUIMessage>>>, ui_rx: Arc<Mutex<UnboundedReceiver<BluetoothUIMessage>>>,
bluetooth_state: BluetoothState, bluetooth_state: BluetoothState,
ui_command_tx: tokio::sync::mpsc::UnboundedSender<UICommand>,
paired_devices: HashMap<String, Address>, paired_devices: HashMap<String, Address>,
device_states: HashMap<String, DeviceState>, device_states: HashMap<String, DeviceState>,
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
pending_add_device: Option<(String, Address)>, pending_add_device: Option<(String, Address)>,
device_type_state: combo_box::State<DeviceType>, device_type_state: combo_box::State<DeviceType>,
selected_device_type: Option<DeviceType>, selected_device_type: Option<DeviceType>,
@@ -55,12 +57,6 @@ impl BluetoothState {
} }
} }
#[derive(Debug, Clone)]
pub enum DeviceMessage {
ConversationAwarenessToggled(bool),
NothingAncModeSelected(NothingAncMode)
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
WindowOpened(window::Id), WindowOpened(window::Id),
@@ -70,13 +66,13 @@ pub enum Message {
ThemeSelected(MyTheme), ThemeSelected(MyTheme),
CopyToClipboard(String), CopyToClipboard(String),
BluetoothMessage(BluetoothUIMessage), BluetoothMessage(BluetoothUIMessage),
DeviceMessage(String, DeviceMessage),
ShowNewDialogTab, ShowNewDialogTab,
GotPairedDevices(HashMap<String, Address>), GotPairedDevices(HashMap<String, Address>),
StartAddDevice(String, Address), StartAddDevice(String, Address),
SelectDeviceType(DeviceType), SelectDeviceType(DeviceType),
ConfirmAddDevice, ConfirmAddDevice,
CancelAddDevice, CancelAddDevice,
StateChanged(String, DeviceState),
} }
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
@@ -96,7 +92,7 @@ impl App {
pub fn new( pub fn new(
ui_rx: UnboundedReceiver<BluetoothUIMessage>, ui_rx: UnboundedReceiver<BluetoothUIMessage>,
start_minimized: bool, start_minimized: bool,
ui_command_tx: tokio::sync::mpsc::UnboundedSender<UICommand>, device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
) -> (Self, Task<Message>) { ) -> (Self, Task<Message>) {
let (mut panes, first_pane) = pane_grid::State::new(Pane::Sidebar); let (mut panes, first_pane) = pane_grid::State::new(Pane::Sidebar);
let split = panes.split(pane_grid::Axis::Vertical, first_pane, Pane::Content); let split = panes.split(pane_grid::Axis::Vertical, first_pane, Pane::Content);
@@ -169,7 +165,6 @@ impl App {
selected_theme, selected_theme,
ui_rx, ui_rx,
bluetooth_state, bluetooth_state,
ui_command_tx,
paired_devices: HashMap::new(), paired_devices: HashMap::new(),
device_states, device_states,
pending_add_device: None, pending_add_device: None,
@@ -177,6 +172,7 @@ impl App {
DeviceType::Nothing DeviceType::Nothing
]), ]),
selected_device_type: None, selected_device_type: None,
device_managers
}, },
Task::batch(vec![open_task, wait_task]) Task::batch(vec![open_task, wait_task])
) )
@@ -217,32 +213,6 @@ impl App {
Message::CopyToClipboard(data) => { Message::CopyToClipboard(data) => {
iced::clipboard::write(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) => { Message::BluetoothMessage(ui_message) => {
match ui_message { match ui_message {
BluetoothUIMessage::NoOp => { BluetoothUIMessage::NoOp => {
@@ -312,8 +282,33 @@ impl App {
}; };
match type_ { match type_ {
Some(DeviceType::AirPods) => { 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<String, DeviceData> = 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 { 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) => { Some(DeviceType::Nothing) => {
@@ -351,7 +346,7 @@ impl App {
match event { match event {
AACPEvent::ControlCommand(status) => { AACPEvent::ControlCommand(status) => {
match status.identifier { match status.identifier {
crate::bluetooth::aacp::ControlCommandIdentifiers::ConversationDetectConfig => { ControlCommandIdentifiers::ConversationDetectConfig => {
let is_enabled = match status.value.as_slice() { let is_enabled = match status.value.as_slice() {
[0x01] => true, [0x01] => true,
[0x02] => false, [0x02] => false,
@@ -364,6 +359,19 @@ impl App {
state.conversation_awareness_enabled = is_enabled; 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); debug!("Unhandled Control Command Status: {:?}", status);
} }
@@ -440,6 +448,10 @@ impl App {
self.selected_device_type = None; self.selected_device_type = None;
Task::none() Task::none()
} }
Message::StateChanged(mac, state) => {
self.device_states.insert(mac, state);
Task::none()
}
} }
} }
@@ -577,11 +589,25 @@ impl App {
settings settings
] ]
.padding(12); .padding(12);
pane_grid::Content::new(
pane_grid::Content::new(content) 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 => { Pane::Content => {
let device_managers = self.device_managers.blocking_read();
let content = match &self.selected_tab { let content = match &self.selected_tab {
Tab::Device(id) => { Tab::Device(id) => {
if id == "none" { if id == "none" {
@@ -597,7 +623,25 @@ impl App {
match device_type { match device_type {
Some(DeviceType::AirPods) => { Some(DeviceType::AirPods) => {
if let Some(DeviceState::AirPods(state)) = device_state { 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 { } else {
container( container(
text("No state available for this AirPods device").size(16) text("No state available for this AirPods device").size(16)
@@ -608,7 +652,25 @@ impl App {
} }
Some(DeviceType::Nothing) => { Some(DeviceType::Nothing) => {
if let Some(DeviceState::Nothing(state)) = device_state { 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 { } else {
container( container(
text("No state available for this Nothing device").size(16) text("No state available for this Nothing device").size(16)