mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-01 07:39:11 +00:00
linux-rust: implement bluetooth logic with ui
finally! only copying all ui from android to first release
This commit is contained in:
@@ -37,5 +37,6 @@ pub async fn find_other_managed_devices(adapter: &Adapter, managed_macs: Vec<Str
|
||||
if !devices.is_empty() {
|
||||
return Ok(devices);
|
||||
}
|
||||
debug!("No other managed devices found");
|
||||
Err(bluer::Error::from(Error::new(std::io::ErrorKind::NotFound, "No other managed devices found")))
|
||||
}
|
||||
@@ -1,52 +1,39 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use crate::bluetooth::aacp::AACPManager;
|
||||
use crate::bluetooth::att::ATTManager;
|
||||
|
||||
pub enum BluetoothManager {
|
||||
AACP(Arc<AACPManager>),
|
||||
ATT(Arc<ATTManager>),
|
||||
}
|
||||
|
||||
pub struct DeviceManagers {
|
||||
att: Option<Arc<ATTManager>>,
|
||||
aacp: Option<Arc<AACPManager>>,
|
||||
}
|
||||
|
||||
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<String, DeviceManagers>,
|
||||
}
|
||||
|
||||
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<Arc<AACPManager>> {
|
||||
self.aacp.clone()
|
||||
}
|
||||
|
||||
pub fn get_att(&self) -> Option<Arc<ATTManager>> {
|
||||
self.att.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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::<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(|| {
|
||||
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 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 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();
|
||||
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;
|
||||
// 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));
|
||||
device_managers.insert(
|
||||
device.address().to_string(),
|
||||
Arc::from(BluetoothManager::AACP(Arc::new(airpods_device.aacp_manager))),
|
||||
);
|
||||
|
||||
let mut managers = device_managers.write().await;
|
||||
let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone());
|
||||
managers
|
||||
.entry(device.address().to_string())
|
||||
.or_insert(dev_managers)
|
||||
.set_aacp(airpods_device.aacp_manager)
|
||||
;
|
||||
drop(managers)
|
||||
}
|
||||
Err(_) => {
|
||||
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);
|
||||
let type_ = devices_list.get(&addr_str).unwrap().type_.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 {
|
||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
||||
let mut managers = device_managers.write().await;
|
||||
match type_ {
|
||||
devices::enums::DeviceType::Nothing => {
|
||||
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<BluetoothUIMessage
|
||||
match type_ {
|
||||
devices::enums::DeviceType::Nothing => {
|
||||
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<BluetoothUIMessage
|
||||
info!("AirPods connected: {}, initializing", name);
|
||||
let handle_clone = tray_handle.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 {
|
||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
||||
let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone).await;
|
||||
device_managers.insert(
|
||||
addr_str,
|
||||
Arc::from(BluetoothManager::AACP(Arc::new(airpods_device.aacp_manager))),
|
||||
);
|
||||
let mut managers = device_managers.write().await;
|
||||
let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone());
|
||||
managers
|
||||
.entry(addr_str)
|
||||
.or_insert(dev_managers)
|
||||
.set_aacp(airpods_device.aacp_manager);
|
||||
drop(managers);
|
||||
});
|
||||
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...");
|
||||
loop {
|
||||
|
||||
@@ -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<String, DeviceData>,
|
||||
state: &AirPodsState,
|
||||
state: &'a AirPodsState,
|
||||
aacp_manager: Arc<AACPManager>
|
||||
) -> 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<F>(fut: F)
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
{
|
||||
thread::spawn(move || {
|
||||
let rt = Runtime::new().unwrap();
|
||||
rt.block_on(fut);
|
||||
});
|
||||
}
|
||||
@@ -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<u8>), // mac, handle, data
|
||||
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),
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, DeviceData>,
|
||||
state: &NothingState
|
||||
state: &NothingState,
|
||||
att_manager: Arc<ATTManager>
|
||||
) -> 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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);
|
||||
// }
|
||||
@@ -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<BluetoothUIMessage>,
|
||||
start_minimized: bool,
|
||||
ui_command_tx: tokio::sync::mpsc::UnboundedSender<UICommand>,
|
||||
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
|
||||
) -> 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<Mutex<UnboundedReceiver<BluetoothUIMessage>>>,
|
||||
bluetooth_state: BluetoothState,
|
||||
ui_command_tx: tokio::sync::mpsc::UnboundedSender<UICommand>,
|
||||
paired_devices: HashMap<String, Address>,
|
||||
device_states: HashMap<String, DeviceState>,
|
||||
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
|
||||
pending_add_device: Option<(String, Address)>,
|
||||
device_type_state: combo_box::State<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)]
|
||||
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<String, Address>),
|
||||
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<BluetoothUIMessage>,
|
||||
start_minimized: bool,
|
||||
ui_command_tx: tokio::sync::mpsc::UnboundedSender<UICommand>,
|
||||
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
|
||||
) -> (Self, Task<Message>) {
|
||||
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<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 {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user