fix(linux-rust): format and fix syntax error

This commit is contained in:
doprz
2025-12-12 21:52:32 -06:00
committed by Kavish Devar
parent 902b12a227
commit c852b726de
19 changed files with 2047 additions and 1391 deletions

View File

@@ -1,16 +1,19 @@
use bluer::{l2cap::{SocketAddr, Socket, SeqPacket}, Address, AddressType, Result, Error};
use std::time::Duration;
use log::{info, error, debug};
use std::sync::Arc;
use tokio::sync::{Mutex, mpsc};
use tokio::task::JoinSet;
use tokio::time::{sleep, Instant};
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json;
use crate::devices::airpods::AirPodsInformation; use crate::devices::airpods::AirPodsInformation;
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType}; use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
use crate::utils::get_devices_path; use crate::utils::get_devices_path;
use bluer::{
Address, AddressType, Error, Result,
l2cap::{SeqPacket, Socket, SocketAddr},
};
use log::{debug, error, info};
use serde::{Deserialize, Serialize};
use serde_json;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{Mutex, mpsc};
use tokio::task::JoinSet;
use tokio::time::{Instant, sleep};
const PSM: u16 = 0x1001; const PSM: u16 = 0x1001;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -218,7 +221,7 @@ pub enum BatteryComponent {
Headphone = 1, Headphone = 1,
Left = 4, Left = 4,
Right = 2, Right = 2,
Case = 8 Case = 8,
} }
#[repr(u8)] #[repr(u8)]
@@ -226,7 +229,7 @@ pub enum BatteryComponent {
pub enum BatteryStatus { pub enum BatteryStatus {
Charging = 1, Charging = 1,
NotCharging = 2, NotCharging = 2,
Disconnected = 4 Disconnected = 4,
} }
#[repr(u8)] #[repr(u8)]
@@ -235,7 +238,7 @@ pub enum EarDetectionStatus {
InEar = 0x00, InEar = 0x00,
OutOfEar = 0x01, OutOfEar = 0x01,
InCase = 0x02, InCase = 0x02,
Disconnected = 0x03 Disconnected = 0x03,
} }
impl AudioSourceType { impl AudioSourceType {
@@ -291,7 +294,8 @@ pub struct AirPodsLEKeys {
pub struct AACPManagerState { pub struct AACPManagerState {
pub sender: Option<mpsc::Sender<Vec<u8>>>, pub sender: Option<mpsc::Sender<Vec<u8>>>,
pub control_command_status_list: Vec<ControlCommandStatus>, pub control_command_status_list: Vec<ControlCommandStatus>,
pub control_command_subscribers: HashMap<ControlCommandIdentifiers, Vec<mpsc::UnboundedSender<Vec<u8>>>>, pub control_command_subscribers:
HashMap<ControlCommandIdentifiers, Vec<mpsc::UnboundedSender<Vec<u8>>>>,
pub owns: bool, pub owns: bool,
pub old_connected_devices: Vec<ConnectedDevice>, pub old_connected_devices: Vec<ConnectedDevice>,
pub connected_devices: Vec<ConnectedDevice>, pub connected_devices: Vec<ConnectedDevice>,
@@ -307,11 +311,10 @@ pub struct AACPManagerState {
impl AACPManagerState { impl AACPManagerState {
fn new() -> Self { fn new() -> Self {
let devices: HashMap<String, DeviceData> = let devices: HashMap<String, DeviceData> = std::fs::read_to_string(get_devices_path())
std::fs::read_to_string(get_devices_path()) .ok()
.ok() .and_then(|s| serde_json::from_str(&s).ok())
.and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_default();
.unwrap_or_default();
AACPManagerState { AACPManagerState {
sender: None, sender: None,
control_command_status_list: Vec::new(), control_command_status_list: Vec::new(),
@@ -362,17 +365,18 @@ impl AACPManager {
} }
}; };
let seq_packet = match tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await { let seq_packet =
Ok(Ok(s)) => Arc::new(s), match tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await {
Ok(Err(e)) => { Ok(Ok(s)) => Arc::new(s),
error!("L2CAP connect failed: {}", e); Ok(Err(e)) => {
return; error!("L2CAP connect failed: {}", e);
} return;
Err(_) => { }
error!("L2CAP connect timed out"); Err(_) => {
return; error!("L2CAP connect timed out");
} return;
}; }
};
// Wait for connection to be fully established // Wait for connection to be fully established
let start = Instant::now(); let start = Instant::now();
@@ -381,7 +385,8 @@ impl AACPManager {
Ok(peer) if peer.cid != 0 => break, Ok(peer) if peer.cid != 0 => break,
Ok(_) => { /* still waiting */ } Ok(_) => { /* still waiting */ }
Err(e) => { Err(e) => {
if e.raw_os_error() == Some(107) { // ENOTCONN if e.raw_os_error() == Some(107) {
// ENOTCONN
error!("Peer has disconnected during connection setup."); error!("Peer has disconnected during connection setup.");
return; return;
} }
@@ -438,19 +443,40 @@ impl AACPManager {
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
state.event_tx = Some(tx); state.event_tx = Some(tx);
} }
pub async fn subscribe_to_control_command(&self, identifier: ControlCommandIdentifiers, tx: mpsc::UnboundedSender<Vec<u8>>) { pub async fn subscribe_to_control_command(
&self,
identifier: ControlCommandIdentifiers,
tx: mpsc::UnboundedSender<Vec<u8>>,
) {
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
state.control_command_subscribers.entry(identifier).or_default().push(tx); state
.control_command_subscribers
.entry(identifier)
.or_default()
.push(tx);
// send initial value if available // send initial value if available
if let Some(status) = state.control_command_status_list.iter().find(|s| s.identifier == identifier) { if let Some(status) = state
let _ = state.control_command_subscribers.get(&identifier).unwrap().last().unwrap().send(status.value.clone()); .control_command_status_list
.iter()
.find(|s| s.identifier == identifier)
{
let _ = state
.control_command_subscribers
.get(&identifier)
.unwrap()
.last()
.unwrap()
.send(status.value.clone());
} }
} }
pub async fn receive_packet(&self, packet: &[u8]) { pub async fn receive_packet(&self, packet: &[u8]) {
if !packet.starts_with(&HEADER_BYTES) { if !packet.starts_with(&HEADER_BYTES) {
debug!("Received packet does not start with expected header: {}", hex::encode(packet)); debug!(
"Received packet does not start with expected header: {}",
hex::encode(packet)
);
return; return;
} }
if packet.len() < 5 { if packet.len() < 5 {
@@ -469,7 +495,10 @@ impl AACPManager {
} }
let count = payload[2] as usize; let count = payload[2] as usize;
if payload.len() < 3 + count * 5 { if payload.len() < 3 + count * 5 {
error!("Battery Info packet length mismatch: {}", hex::encode(payload)); error!(
"Battery Info packet length mismatch: {}",
hex::encode(payload)
);
return; return;
} }
let mut batteries = Vec::with_capacity(count); let mut batteries = Vec::with_capacity(count);
@@ -495,7 +524,7 @@ impl AACPManager {
error!("Unknown battery status: {:#04x}", payload[base_index + 3]); error!("Unknown battery status: {:#04x}", payload[base_index + 3]);
continue; continue;
} }
} },
}); });
} }
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
@@ -520,9 +549,16 @@ impl AACPManager {
}; };
if let Some(identifier) = ControlCommandIdentifiers::from_u8(identifier_byte) { if let Some(identifier) = ControlCommandIdentifiers::from_u8(identifier_byte) {
let status = ControlCommandStatus { identifier, value: value.clone() }; let status = ControlCommandStatus {
identifier,
value: value.clone(),
};
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
if let Some(existing) = state.control_command_status_list.iter_mut().find(|s| s.identifier == identifier) { if let Some(existing) = state
.control_command_status_list
.iter_mut()
.find(|s| s.identifier == identifier)
{
existing.value = value.clone(); existing.value = value.clone();
} else { } else {
state.control_command_status_list.push(status.clone()); state.control_command_status_list.push(status.clone());
@@ -538,9 +574,16 @@ impl AACPManager {
if let Some(ref tx) = state.event_tx { if let Some(ref tx) = state.event_tx {
let _ = tx.send(AACPEvent::ControlCommand(status)); let _ = tx.send(AACPEvent::ControlCommand(status));
} }
info!("Received Control Command: {:?}, value: {}", identifier, hex::encode(&value)); info!(
"Received Control Command: {:?}, value: {}",
identifier,
hex::encode(&value)
);
} else { } else {
error!("Unknown Control Command identifier: {:#04x}", identifier_byte); error!(
"Unknown Control Command identifier: {:#04x}",
identifier_byte
);
} }
} }
opcodes::EAR_DETECTION => { opcodes::EAR_DETECTION => {
@@ -570,12 +613,21 @@ impl AACPManager {
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
state.old_ear_detection_status = state.ear_detection_status.clone(); state.old_ear_detection_status = state.ear_detection_status.clone();
state.ear_detection_status = statuses.clone(); state.ear_detection_status = statuses.clone();
if let Some(ref tx) = state.event_tx { if let Some(ref tx) = state.event_tx {
debug!("Sending Ear Detection event: old: {:?}, new: {:?}", state.old_ear_detection_status, statuses); debug!(
let _ = tx.send(AACPEvent::EarDetection(state.old_ear_detection_status.clone(), statuses)); "Sending Ear Detection event: old: {:?}, new: {:?}",
state.old_ear_detection_status, statuses
);
let _ = tx.send(AACPEvent::EarDetection(
state.old_ear_detection_status.clone(),
statuses,
));
} }
info!("Received Ear Detection Status: {:?}", state.ear_detection_status); info!(
"Received Ear Detection Status: {:?}",
state.ear_detection_status
);
} }
opcodes::CONVERSATION_AWARENESS => { opcodes::CONVERSATION_AWARENESS => {
if packet.len() == 10 { if packet.len() == 10 {
@@ -587,7 +639,10 @@ impl AACPManager {
} }
info!("Received Conversation Awareness: {}", status); info!("Received Conversation Awareness: {}", status);
} else { } else {
info!("Received Conversation Awareness packet with unexpected length: {}", packet.len()); info!(
"Received Conversation Awareness packet with unexpected length: {}",
packet.len()
);
} }
} }
opcodes::INFORMATION => { opcodes::INFORMATION => {
@@ -637,25 +692,30 @@ impl AACPManager {
}; };
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
if let Some(mac) = state.airpods_mac if let Some(mac) = state.airpods_mac
&& let Some(device_data) = state.devices.get_mut(&mac.to_string()) { && let Some(device_data) = state.devices.get_mut(&mac.to_string())
device_data.name = info.name.clone(); {
device_data.information = Some(DeviceInformation::AirPods(info.clone())); device_data.name = info.name.clone();
} device_data.information = Some(DeviceInformation::AirPods(info.clone()));
}
let json = serde_json::to_string(&state.devices).unwrap(); let json = serde_json::to_string(&state.devices).unwrap();
if let Some(parent) = get_devices_path().parent() if let Some(parent) = get_devices_path().parent()
&& let Err(e) = tokio::fs::create_dir_all(&parent).await { && let Err(e) = tokio::fs::create_dir_all(&parent).await
error!("Failed to create directory for devices: {}", e); {
return; error!("Failed to create directory for devices: {}", e);
} return;
}
if let Err(e) = tokio::fs::write(&get_devices_path(), json).await { if let Err(e) = tokio::fs::write(&get_devices_path(), json).await {
error!("Failed to save devices: {}", e); error!("Failed to save devices: {}", e);
} }
info!("Received Information: {:?}", info); info!("Received Information: {:?}", info);
}, }
opcodes::PROXIMITY_KEYS_RSP => { opcodes::PROXIMITY_KEYS_RSP => {
if payload.len() < 4 { if payload.len() < 4 {
error!("Proximity Keys Response packet too short: {}", hex::encode(payload)); error!(
"Proximity Keys Response packet too short: {}",
hex::encode(payload)
);
return; return;
} }
let key_count = payload[2] as usize; let key_count = payload[2] as usize;
@@ -664,65 +724,77 @@ impl AACPManager {
let mut keys = Vec::new(); let mut keys = Vec::new();
for _ in 0..key_count { for _ in 0..key_count {
if offset + 3 >= payload.len() { if offset + 3 >= payload.len() {
error!("Proximity Keys Response packet too short while parsing keys: {}", hex::encode(payload)); error!(
"Proximity Keys Response packet too short while parsing keys: {}",
hex::encode(payload)
);
return; return;
} }
let key_type = payload[offset]; let key_type = payload[offset];
let key_length = payload[offset + 2] as usize; let key_length = payload[offset + 2] as usize;
offset += 4; offset += 4;
if offset + key_length > payload.len() { if offset + key_length > payload.len() {
error!("Proximity Keys Response packet too short for key data: {}", hex::encode(payload)); error!(
"Proximity Keys Response packet too short for key data: {}",
hex::encode(payload)
);
return; return;
} }
let key_data = payload[offset..offset + key_length].to_vec(); let key_data = payload[offset..offset + key_length].to_vec();
keys.push((key_type, key_data)); keys.push((key_type, key_data));
offset += key_length; offset += key_length;
} }
info!("Received Proximity Keys Response: {:?}", keys.iter().map(|(kt, kd)| (kt, hex::encode(kd))).collect::<Vec<_>>()); info!(
"Received Proximity Keys Response: {:?}",
keys.iter()
.map(|(kt, kd)| (kt, hex::encode(kd)))
.collect::<Vec<_>>()
);
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
for (key_type, key_data) in &keys { for (key_type, key_data) in &keys {
if let Some(kt) = ProximityKeyType::from_u8(*key_type) if let Some(kt) = ProximityKeyType::from_u8(*key_type)
&& let Some(mac) = state.airpods_mac { && let Some(mac) = state.airpods_mac
let mac_str = mac.to_string(); {
let device_data = state.devices.entry(mac_str.clone()).or_insert(DeviceData { let mac_str = mac.to_string();
let device_data =
state.devices.entry(mac_str.clone()).or_insert(DeviceData {
name: mac_str.clone(), name: mac_str.clone(),
type_: DeviceType::AirPods, type_: DeviceType::AirPods,
information: None, information: None,
}); });
match kt { match kt {
ProximityKeyType::Irk => { ProximityKeyType::Irk => match device_data.information.as_mut() {
match device_data.information.as_mut() { Some(DeviceInformation::AirPods(info)) => {
Some(DeviceInformation::AirPods(info)) => { info.le_keys.irk = hex::encode(key_data);
info.le_keys.irk = hex::encode(key_data);
}
_ => {
error!("Device information is not AirPods for adding LE IRK.");
}
}
} }
ProximityKeyType::EncKey => { _ => {
match device_data.information.as_mut() { error!("Device information is not AirPods for adding LE IRK.");
Some(DeviceInformation::AirPods(info)) => {
info.le_keys.enc_key = hex::encode(key_data);
}
_ => {
error!("Device information is not AirPods for adding LE encryption key.");
}
}
} }
} },
ProximityKeyType::EncKey => match device_data.information.as_mut() {
Some(DeviceInformation::AirPods(info)) => {
info.le_keys.enc_key = hex::encode(key_data);
}
_ => {
error!(
"Device information is not AirPods for adding LE encryption key."
);
}
},
} }
}
} }
let json = serde_json::to_string(&state.devices).unwrap(); let json = serde_json::to_string(&state.devices).unwrap();
if let Some(parent) = get_devices_path().parent() if let Some(parent) = get_devices_path().parent()
&& let Err(e) = tokio::fs::create_dir_all(&parent).await { && let Err(e) = tokio::fs::create_dir_all(&parent).await
error!("Failed to create directory for devices: {}", e); {
return; error!("Failed to create directory for devices: {}", e);
} return;
}
if let Err(e) = tokio::fs::write(&get_devices_path(), json).await { if let Err(e) = tokio::fs::write(&get_devices_path(), json).await {
error!("Failed to save devices: {}", e); error!("Failed to save devices: {}", e);
} }
}, }
opcodes::STEM_PRESS => info!("Received Stem Press packet."), opcodes::STEM_PRESS => info!("Received Stem Press packet."),
opcodes::AUDIO_SOURCE => { opcodes::AUDIO_SOURCE => {
if payload.len() < 9 { if payload.len() < 9 {
@@ -744,12 +816,18 @@ impl AACPManager {
} }
opcodes::CONNECTED_DEVICES => { opcodes::CONNECTED_DEVICES => {
if payload.len() < 3 { if payload.len() < 3 {
error!("Connected Devices packet too short: {}", hex::encode(payload)); error!(
"Connected Devices packet too short: {}",
hex::encode(payload)
);
return; return;
} }
let count = payload[2] as usize; let count = payload[2] as usize;
if payload.len() < 3 + count * 8 { if payload.len() < 3 + count * 8 {
error!("Connected Devices packet length mismatch: {}", hex::encode(payload)); error!(
"Connected Devices packet length mismatch: {}",
hex::encode(payload)
);
return; return;
} }
let mut devices = Vec::with_capacity(count); let mut devices = Vec::with_capacity(count);
@@ -757,17 +835,30 @@ impl AACPManager {
let base = 5 + i * 8; let base = 5 + i * 8;
let mac = format!( let mac = format!(
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
payload[base], payload[base + 1], payload[base + 2], payload[base + 3], payload[base + 4], payload[base + 5] payload[base],
payload[base + 1],
payload[base + 2],
payload[base + 3],
payload[base + 4],
payload[base + 5]
); );
let info1 = payload[base + 6]; let info1 = payload[base + 6];
let info2 = payload[base + 7]; let info2 = payload[base + 7];
devices.push(ConnectedDevice { mac, info1, info2, r#type: None }); devices.push(ConnectedDevice {
mac,
info1,
info2,
r#type: None,
});
} }
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
state.old_connected_devices = state.connected_devices.clone(); state.old_connected_devices = state.connected_devices.clone();
state.connected_devices = devices.clone(); state.connected_devices = devices.clone();
if let Some(ref tx) = state.event_tx { if let Some(ref tx) = state.event_tx {
let _ = tx.send(AACPEvent::ConnectedDevices(state.old_connected_devices.clone(), devices)); let _ = tx.send(AACPEvent::ConnectedDevices(
state.old_connected_devices.clone(),
devices,
));
} }
info!("Received Connected Devices: {:?}", state.connected_devices); info!("Received Connected Devices: {:?}", state.connected_devices);
} }
@@ -782,7 +873,7 @@ impl AACPManager {
} }
} }
opcodes::EQ_DATA => { opcodes::EQ_DATA => {
debug!("Received EQ Data"); debug!("Received EQ Data");
} }
_ => debug!("Received unknown packet with opcode {:#04x}", opcode), _ => debug!("Received unknown packet with opcode {:#04x}", opcode),
} }
@@ -805,17 +896,18 @@ impl AACPManager {
pub async fn send_handshake(&self) -> Result<()> { pub async fn send_handshake(&self) -> Result<()> {
let packet = [ let packet = [
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
]; ];
self.send_packet(&packet).await self.send_packet(&packet).await
} }
pub async fn send_proximity_keys_request(&self, key_types: Vec<ProximityKeyType>) -> Result<()> { pub async fn send_proximity_keys_request(
&self,
key_types: Vec<ProximityKeyType>,
) -> Result<()> {
let opcode = [opcodes::PROXIMITY_KEYS_REQ, 0x00]; let opcode = [opcodes::PROXIMITY_KEYS_REQ, 0x00];
let mut data = Vec::with_capacity( 2); let mut data = Vec::with_capacity(2);
data.push(key_types.iter().fold(0u8, |acc, kt| acc | (*kt as u8))); data.push(key_types.iter().fold(0u8, |acc, kt| acc | (*kt as u8)));
data.push(0x00); data.push(0x00);
let packet = [opcode.as_slice(), data.as_slice()].concat(); let packet = [opcode.as_slice(), data.as_slice()].concat();
@@ -833,8 +925,12 @@ impl AACPManager {
packet.extend_from_slice(name_bytes); packet.extend_from_slice(name_bytes);
self.send_data_packet(&packet).await self.send_data_packet(&packet).await
} }
pub async fn send_control_command(&self, identifier: ControlCommandIdentifiers, value: &[u8]) -> Result<()> { pub async fn send_control_command(
&self,
identifier: ControlCommandIdentifiers,
value: &[u8],
) -> Result<()> {
let opcode = [opcodes::CONTROL_COMMAND, 0x00]; let opcode = [opcodes::CONTROL_COMMAND, 0x00];
let mut data = vec![identifier as u8]; let mut data = vec![identifier as u8];
for i in 0..4 { for i in 0..4 {
@@ -844,10 +940,17 @@ impl AACPManager {
self.send_data_packet(&packet).await self.send_data_packet(&packet).await
} }
pub async fn send_media_information_new_device(&self, self_mac_address: &str, target_mac_address: &str) -> Result<()> { pub async fn send_media_information_new_device(
&self,
self_mac_address: &str,
target_mac_address: &str,
) -> Result<()> {
let opcode = [opcodes::SMART_ROUTING, 0x00]; let opcode = [opcodes::SMART_ROUTING, 0x00];
let mut buffer = Vec::with_capacity(112); let mut buffer = Vec::with_capacity(112);
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); let target_mac_bytes: Vec<u8> = target_mac_address
.split(':')
.map(|s| u8::from_str_radix(s, 16).unwrap())
.collect();
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>()); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
buffer.extend_from_slice(&[0x68, 0x00]); buffer.extend_from_slice(&[0x68, 0x00]);
@@ -879,7 +982,10 @@ impl AACPManager {
pub async fn send_hijack_request(&self, target_mac_address: &str) -> Result<()> { pub async fn send_hijack_request(&self, target_mac_address: &str) -> Result<()> {
let opcode = [opcodes::SMART_ROUTING, 0x00]; let opcode = [opcodes::SMART_ROUTING, 0x00];
let mut buffer = Vec::with_capacity(106); let mut buffer = Vec::with_capacity(106);
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); let target_mac_bytes: Vec<u8> = target_mac_address
.split(':')
.map(|s| u8::from_str_radix(s, 16).unwrap())
.collect();
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>()); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
buffer.extend_from_slice(&[0x62, 0x00]); buffer.extend_from_slice(&[0x62, 0x00]);
buffer.extend_from_slice(&[0x01, 0xE5]); buffer.extend_from_slice(&[0x01, 0xE5]);
@@ -907,10 +1013,18 @@ impl AACPManager {
self.send_data_packet(&packet).await self.send_data_packet(&packet).await
} }
pub async fn send_media_information(&self, self_mac_address: &str, target_mac_address: &str, streaming_state: bool) -> Result<()> { pub async fn send_media_information(
&self,
self_mac_address: &str,
target_mac_address: &str,
streaming_state: bool,
) -> Result<()> {
let opcode = [opcodes::SMART_ROUTING, 0x00]; let opcode = [opcodes::SMART_ROUTING, 0x00];
let mut buffer = Vec::with_capacity(138); let mut buffer = Vec::with_capacity(138);
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); let target_mac_bytes: Vec<u8> = target_mac_address
.split(':')
.map(|s| u8::from_str_radix(s, 16).unwrap())
.collect();
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>()); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
buffer.extend_from_slice(&[0x82, 0x00]); buffer.extend_from_slice(&[0x82, 0x00]);
buffer.extend_from_slice(&[0x01, 0xE5, 0x4A]); buffer.extend_from_slice(&[0x01, 0xE5, 0x4A]);
@@ -943,7 +1057,10 @@ impl AACPManager {
pub async fn send_smart_routing_show_ui(&self, target_mac_address: &str) -> Result<()> { pub async fn send_smart_routing_show_ui(&self, target_mac_address: &str) -> Result<()> {
let opcode = [opcodes::SMART_ROUTING, 0x00]; let opcode = [opcodes::SMART_ROUTING, 0x00];
let mut buffer = Vec::with_capacity(134); let mut buffer = Vec::with_capacity(134);
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); let target_mac_bytes: Vec<u8> = target_mac_address
.split(':')
.map(|s| u8::from_str_radix(s, 16).unwrap())
.collect();
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>()); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
buffer.extend_from_slice(&[0x7E, 0x00]); buffer.extend_from_slice(&[0x7E, 0x00]);
buffer.extend_from_slice(&[0x01, 0xE6, 0x5B]); buffer.extend_from_slice(&[0x01, 0xE6, 0x5B]);
@@ -976,7 +1093,10 @@ impl AACPManager {
pub async fn send_hijack_reversed(&self, target_mac_address: &str) -> Result<()> { pub async fn send_hijack_reversed(&self, target_mac_address: &str) -> Result<()> {
let opcode = [opcodes::SMART_ROUTING, 0x00]; let opcode = [opcodes::SMART_ROUTING, 0x00];
let mut buffer = Vec::with_capacity(97); let mut buffer = Vec::with_capacity(97);
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); let target_mac_bytes: Vec<u8> = target_mac_address
.split(':')
.map(|s| u8::from_str_radix(s, 16).unwrap())
.collect();
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>()); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
buffer.extend_from_slice(&[0x59, 0x00]); buffer.extend_from_slice(&[0x59, 0x00]);
buffer.extend_from_slice(&[0x01, 0xE3]); buffer.extend_from_slice(&[0x01, 0xE3]);
@@ -999,10 +1119,17 @@ impl AACPManager {
self.send_data_packet(&packet).await self.send_data_packet(&packet).await
} }
pub async fn send_add_tipi_device(&self, self_mac_address: &str, target_mac_address: &str) -> Result<()> { pub async fn send_add_tipi_device(
&self,
self_mac_address: &str,
target_mac_address: &str,
) -> Result<()> {
let opcode = [opcodes::SMART_ROUTING, 0x00]; let opcode = [opcodes::SMART_ROUTING, 0x00];
let mut buffer = Vec::with_capacity(86); let mut buffer = Vec::with_capacity(86);
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); let target_mac_bytes: Vec<u8> = target_mac_address
.split(':')
.map(|s| u8::from_str_radix(s, 16).unwrap())
.collect();
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>()); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
buffer.extend_from_slice(&[0x4E, 0x00]); buffer.extend_from_slice(&[0x4E, 0x00]);
buffer.extend_from_slice(&[0x01, 0xE5]); buffer.extend_from_slice(&[0x01, 0xE5]);
@@ -1027,10 +1154,8 @@ impl AACPManager {
} }
pub async fn send_some_packet(&self) -> Result<()> { pub async fn send_some_packet(&self) -> Result<()> {
self.send_data_packet(&[ self.send_data_packet(&[0x29, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
0x29, 0x00, .await
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
]).await
} }
} }
@@ -1049,7 +1174,9 @@ async fn recv_thread(manager: AACPManager, sp: Arc<SeqPacket>) {
} }
Err(e) => { Err(e) => {
error!("Read error: {}", e); error!("Read error: {}", e);
debug!("We have probably disconnected, clearing state variables (owns=false, connected_devices=empty, control_command_status_list=empty)."); debug!(
"We have probably disconnected, clearing state variables (owns=false, connected_devices=empty, control_command_status_list=empty)."
);
let mut state = manager.state.lock().await; let mut state = manager.state.lock().await;
state.owns = false; state.owns = false;
state.connected_devices.clear(); state.connected_devices.clear();

View File

@@ -1,12 +1,12 @@
use bluer::l2cap::{SocketAddr, Socket, SeqPacket}; use bluer::l2cap::{SeqPacket, Socket, SocketAddr};
use bluer::{Address, AddressType, Result, Error}; use bluer::{Address, AddressType, Error, Result};
use log::{info, error, debug}; use hex;
use log::{debug, error, info};
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{Mutex, mpsc}; use tokio::sync::{Mutex, mpsc};
use tokio::task::JoinSet; use tokio::task::JoinSet;
use tokio::time::{sleep, Duration, Instant}; use tokio::time::{Duration, Instant, sleep};
use std::collections::HashMap;
use hex;
const PSM_ATT: u16 = 0x001F; const PSM_ATT: u16 = 0x001F;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -25,7 +25,7 @@ pub enum ATTHandles {
AirPodsLoudSoundReduction = 0x1B, AirPodsLoudSoundReduction = 0x1B,
AirPodsHearingAid = 0x2A, AirPodsHearingAid = 0x2A,
NothingEverything = 0x8002, NothingEverything = 0x8002,
NothingEverythingRead = 0x8005 // for some reason, and not the same as the write handle NothingEverythingRead = 0x8005, // for some reason, and not the same as the write handle
} }
#[repr(u16)] #[repr(u16)]
@@ -43,7 +43,7 @@ impl From<ATTHandles> for ATTCCCDHandles {
ATTHandles::AirPodsLoudSoundReduction => ATTCCCDHandles::LoudSoundReduction, ATTHandles::AirPodsLoudSoundReduction => ATTCCCDHandles::LoudSoundReduction,
ATTHandles::AirPodsHearingAid => ATTCCCDHandles::HearingAid, ATTHandles::AirPodsHearingAid => ATTCCCDHandles::HearingAid,
ATTHandles::NothingEverything => panic!("No CCCD for NothingEverything handle"), // we don't request it ATTHandles::NothingEverything => panic!("No CCCD for NothingEverything handle"), // we don't request it
ATTHandles::NothingEverythingRead => panic!("No CCD for NothingEverythingRead handle") // it sends notifications without CCCD ATTHandles::NothingEverythingRead => panic!("No CCD for NothingEverythingRead handle"), // it sends notifications without CCCD
} }
} }
} }
@@ -57,7 +57,7 @@ impl ATTManagerState {
fn new() -> Self { fn new() -> Self {
ATTManagerState { ATTManagerState {
sender: None, sender: None,
listeners: HashMap::new() listeners: HashMap::new(),
} }
} }
} }
@@ -82,11 +82,15 @@ impl ATTManager {
} }
pub async fn connect(&mut self, addr: Address) -> Result<()> { pub async fn connect(&mut self, addr: Address) -> Result<()> {
info!("ATTManager connecting to {} on PSM {:#06X}...", addr, PSM_ATT); info!(
"ATTManager connecting to {} on PSM {:#06X}...",
addr, PSM_ATT
);
let target_sa = SocketAddr::new(addr, AddressType::BrEdr, PSM_ATT); let target_sa = SocketAddr::new(addr, AddressType::BrEdr, PSM_ATT);
let socket = Socket::new_seq_packet()?; let socket = Socket::new_seq_packet()?;
let seq_packet_result = tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await; let seq_packet_result =
tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await;
let seq_packet = match seq_packet_result { let seq_packet = match seq_packet_result {
Ok(Ok(s)) => Arc::new(s), Ok(Ok(s)) => Arc::new(s),
Ok(Err(e)) => { Ok(Err(e)) => {
@@ -95,7 +99,10 @@ impl ATTManager {
} }
Err(_) => { Err(_) => {
error!("L2CAP connect timed out"); error!("L2CAP connect timed out");
return Err(Error::from(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connection timeout"))); return Err(Error::from(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Connection timeout",
)));
} }
}; };
@@ -106,7 +113,8 @@ impl ATTManager {
Ok(peer) if peer.cid != 0 => break, Ok(peer) if peer.cid != 0 => break,
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
if e.raw_os_error() == Some(107) { // ENOTCONN if e.raw_os_error() == Some(107) {
// ENOTCONN
error!("Peer has disconnected during connection setup."); error!("Peer has disconnected during connection setup.");
return Err(e.into()); return Err(e.into());
} }
@@ -115,7 +123,10 @@ impl ATTManager {
} }
if start.elapsed() >= CONNECT_TIMEOUT { if start.elapsed() >= CONNECT_TIMEOUT {
error!("Timed out waiting for L2CAP connection to be fully established."); error!("Timed out waiting for L2CAP connection to be fully established.");
return Err(Error::from(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connection timeout"))); return Err(Error::from(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Connection timeout",
)));
} }
sleep(POLL_INTERVAL).await; sleep(POLL_INTERVAL).await;
} }
@@ -180,11 +191,17 @@ impl ATTManager {
if let Some(sender) = &state.sender { if let Some(sender) = &state.sender {
sender.send(data.to_vec()).await.map_err(|e| { sender.send(data.to_vec()).await.map_err(|e| {
error!("Failed to send packet to channel: {}", e); error!("Failed to send packet to channel: {}", e);
Error::from(std::io::Error::new(std::io::ErrorKind::NotConnected, "L2CAP send channel closed")) Error::from(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"L2CAP send channel closed",
))
}) })
} else { } else {
error!("Cannot send packet, sender is not available."); error!("Cannot send packet, sender is not available.");
Err(Error::from(std::io::Error::new(std::io::ErrorKind::NotConnected, "L2CAP stream not connected"))) Err(Error::from(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"L2CAP stream not connected",
)))
} }
} }
@@ -195,11 +212,11 @@ impl ATTManager {
Ok(Some(resp)) => Ok(resp), Ok(Some(resp)) => Ok(resp),
Ok(None) => Err(Error::from(std::io::Error::new( Ok(None) => Err(Error::from(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof, std::io::ErrorKind::UnexpectedEof,
"Response channel closed" "Response channel closed",
))), ))),
Err(_) => Err(Error::from(std::io::Error::new( Err(_) => Err(Error::from(std::io::Error::new(
std::io::ErrorKind::TimedOut, std::io::ErrorKind::TimedOut,
"Response timeout" "Response timeout",
))), ))),
} }
} }

View File

@@ -1,6 +1,6 @@
use std::io::Error;
use bluer::Adapter; use bluer::Adapter;
use log::debug; use log::debug;
use std::io::Error;
pub(crate) async fn find_connected_airpods(adapter: &Adapter) -> bluer::Result<bluer::Device> { pub(crate) async fn find_connected_airpods(adapter: &Adapter) -> bluer::Result<bluer::Device> {
let target_uuid = uuid::Uuid::parse_str("74ec2172-0bad-4d01-8f77-997b2be0722a").unwrap(); let target_uuid = uuid::Uuid::parse_str("74ec2172-0bad-4d01-8f77-997b2be0722a").unwrap();
@@ -10,15 +10,22 @@ pub(crate) async fn find_connected_airpods(adapter: &Adapter) -> bluer::Result<b
let device = adapter.device(addr)?; let device = adapter.device(addr)?;
if device.is_connected().await.unwrap_or(false) if device.is_connected().await.unwrap_or(false)
&& let Ok(uuids) = device.uuids().await && let Ok(uuids) = device.uuids().await
&& let Some(uuids) = uuids && let Some(uuids) = uuids
&& uuids.iter().any(|u| *u == target_uuid) { && uuids.iter().any(|u| *u == target_uuid)
return Ok(device); {
} return Ok(device);
}
} }
Err(bluer::Error::from(Error::new(std::io::ErrorKind::NotFound, "No connected AirPods found"))) Err(bluer::Error::from(Error::new(
std::io::ErrorKind::NotFound,
"No connected AirPods found",
)))
} }
pub async fn find_other_managed_devices(adapter: &Adapter, managed_macs: Vec<String>) -> bluer::Result<Vec<bluer::Device>> { pub async fn find_other_managed_devices(
adapter: &Adapter,
managed_macs: Vec<String>,
) -> bluer::Result<Vec<bluer::Device>> {
let addrs = adapter.device_addresses().await?; let addrs = adapter.device_addresses().await?;
let mut devices = Vec::new(); let mut devices = Vec::new();
for addr in addrs { for addr in addrs {
@@ -35,5 +42,8 @@ pub async fn find_other_managed_devices(adapter: &Adapter, managed_macs: Vec<Str
return Ok(devices); return Ok(devices);
} }
debug!("No other managed devices found"); 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,20 +1,20 @@
use crate::bluetooth::aacp::BatteryStatus;
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
use crate::ui::tray::MyTray;
use crate::utils::{ah, get_devices_path, get_preferences_path};
use aes::Aes128;
use aes::cipher::generic_array::GenericArray;
use aes::cipher::{BlockDecrypt, KeyInit};
use bluer::monitor::{Monitor, MonitorEvent, Pattern}; use bluer::monitor::{Monitor, MonitorEvent, Pattern};
use bluer::{Address, Session}; use bluer::{Address, Session};
use aes::Aes128;
use aes::cipher::{KeyInit, BlockDecrypt};
use aes::cipher::generic_array::GenericArray;
use std::collections::{HashMap, HashSet};
use log::{info, debug};
use serde_json;
use futures::StreamExt; use futures::StreamExt;
use hex; use hex;
use log::{debug, info};
use serde_json;
use std::collections::{HashMap, HashSet};
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::bluetooth::aacp::BatteryStatus;
use crate::ui::tray::MyTray;
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
use crate::utils::{get_devices_path, get_preferences_path, ah};
fn decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] { fn decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
let cipher = Aes128::new(&GenericArray::from(*key)); let cipher = Aes128::new(&GenericArray::from(*key));
@@ -24,7 +24,8 @@ fn decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
} }
fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool { fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool {
let rpa: Vec<u8> = addr.split(':') let rpa: Vec<u8> = addr
.split(':')
.map(|s| u8::from_str_radix(s, 16).unwrap()) .map(|s| u8::from_str_radix(s, 16).unwrap())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.into_iter() .into_iter()
@@ -38,7 +39,10 @@ fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool {
let hash_slice = &rpa[0..3]; let hash_slice = &rpa[0..3];
let hash: [u8; 3] = hash_slice.try_into().unwrap(); let hash: [u8; 3] = hash_slice.try_into().unwrap();
let computed_hash = ah(irk, &prand); let computed_hash = ah(irk, &prand);
debug!("Verifying RPA: addr={}, hash={:?}, computed_hash={:?}", addr, hash, computed_hash); debug!(
"Verifying RPA: addr={}, hash={:?}, computed_hash={:?}",
addr, hash, computed_hash
);
hash == computed_hash hash == computed_hash
} }
@@ -47,20 +51,19 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
let adapter = session.default_adapter().await?; let adapter = session.default_adapter().await?;
adapter.set_powered(true).await?; adapter.set_powered(true).await?;
let all_devices: HashMap<String, DeviceData> = let all_devices: HashMap<String, DeviceData> = std::fs::read_to_string(get_devices_path())
std::fs::read_to_string(get_devices_path()) .ok()
.ok() .and_then(|s| serde_json::from_str(&s).ok())
.and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_default();
.unwrap_or_default();
let mut verified_macs: HashMap<Address, String> = HashMap::new(); let mut verified_macs: HashMap<Address, String> = HashMap::new();
let mut failed_macs: HashSet<Address> = HashSet::new(); let mut failed_macs: HashSet<Address> = HashSet::new();
let connecting_macs = Arc::new(Mutex::new(HashSet::<Address>::new())); let connecting_macs = Arc::new(Mutex::new(HashSet::<Address>::new()));
let pattern = Pattern { let pattern = Pattern {
data_type: 0xFF, // Manufacturer specific data data_type: 0xFF, // Manufacturer specific data
start_position: 0, start_position: 0,
content: vec![0x4C, 0x00], // Apple manufacturer ID (76) in LE content: vec![0x4C, 0x00], // Apple manufacturer ID (76) in LE
}; };
let mm = adapter.monitor().await?; let mm = adapter.monitor().await?;
@@ -99,17 +102,24 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
for (airpods_mac, device_data) in &all_devices { for (airpods_mac, device_data) in &all_devices {
if device_data.type_ == DeviceType::AirPods if device_data.type_ == DeviceType::AirPods
&& let Some(DeviceInformation::AirPods(info)) = &device_data.information && let Some(DeviceInformation::AirPods(info)) = &device_data.information
&& let Ok(irk_bytes) = hex::decode(&info.le_keys.irk) && let Ok(irk_bytes) = hex::decode(&info.le_keys.irk)
&& irk_bytes.len() == 16 { && irk_bytes.len() == 16
let irk: [u8; 16] = irk_bytes.as_slice().try_into().unwrap(); {
debug!("Verifying RPA {} for airpods MAC {} with IRK {}", addr_str, airpods_mac, info.le_keys.irk); let irk: [u8; 16] = irk_bytes.as_slice().try_into().unwrap();
if verify_rpa(&addr_str, &irk) { debug!(
info!("Matched our device ({}) with the irk for {}", addr, airpods_mac); "Verifying RPA {} for airpods MAC {} with IRK {}",
verified_macs.insert(addr, airpods_mac.clone()); addr_str, airpods_mac, info.le_keys.irk
found_mac = Some(airpods_mac.clone()); );
break; if verify_rpa(&addr_str, &irk) {
} info!(
} "Matched our device ({}) with the irk for {}",
addr, airpods_mac
);
verified_macs.insert(addr, airpods_mac.clone());
found_mac = Some(airpods_mac.clone());
break;
}
}
} }
if let Some(mac) = found_mac { if let Some(mac) = found_mac {
@@ -123,11 +133,12 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
if let Some(ref mac) = matched_airpods_mac if let Some(ref mac) = matched_airpods_mac
&& let Some(device_data) = all_devices.get(mac) && let Some(device_data) = all_devices.get(mac)
&& let Some(DeviceInformation::AirPods(info)) = &device_data.information && let Some(DeviceInformation::AirPods(info)) = &device_data.information
&& let Ok(enc_key_bytes) = hex::decode(&info.le_keys.enc_key) && let Ok(enc_key_bytes) = hex::decode(&info.le_keys.enc_key)
&& enc_key_bytes.len() == 16 { && enc_key_bytes.len() == 16
matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap()); {
} matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap());
}
if matched_airpods_mac.is_some() { if matched_airpods_mac.is_some() {
let mut events = dev.events().await?; let mut events = dev.events().await?;
@@ -140,115 +151,221 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
if let bluer::DeviceProperty::ManufacturerData(data) = prop { if let bluer::DeviceProperty::ManufacturerData(data) = prop {
if let Some(enc_key) = &matched_enc_key if let Some(enc_key) = &matched_enc_key
&& let Some(apple_data) = data.get(&76) && let Some(apple_data) = data.get(&76)
&& apple_data.len() > 20 { && apple_data.len() > 20
let last_16: [u8; 16] = apple_data[apple_data.len() - 16..].try_into().unwrap(); {
let decrypted = decrypt(enc_key, &last_16); let last_16: [u8; 16] =
debug!("Decrypted data from airpods_mac {}: {}", apple_data[apple_data.len() - 16..].try_into().unwrap();
matched_airpods_mac.as_ref().unwrap_or(&"unknown".to_string()), let decrypted = decrypt(enc_key, &last_16);
hex::encode(decrypted)); debug!(
"Decrypted data from airpods_mac {}: {}",
matched_airpods_mac
.as_ref()
.unwrap_or(&"unknown".to_string()),
hex::encode(decrypted)
);
let connection_state = apple_data[10] as usize; let connection_state = apple_data[10] as usize;
debug!("Connection state: {}", connection_state); debug!("Connection state: {}", connection_state);
if connection_state == 0x00 { if connection_state == 0x00 {
let pref_path = get_preferences_path(); let pref_path = get_preferences_path();
let preferences: HashMap<String, HashMap<String, bool>> = let preferences: HashMap<
std::fs::read_to_string(&pref_path) String,
.ok() HashMap<String, bool>,
.and_then(|s| serde_json::from_str(&s).ok()) > = std::fs::read_to_string(&pref_path)
.unwrap_or_default(); .ok()
let auto_connect = preferences.get(matched_airpods_mac.as_ref().unwrap()) .and_then(|s| serde_json::from_str(&s).ok())
.and_then(|prefs| prefs.get("autoConnect")) .unwrap_or_default();
.copied() let auto_connect = preferences
.unwrap_or(true); .get(matched_airpods_mac.as_ref().unwrap())
debug!("Auto-connect preference for {}: {}", matched_airpods_mac.as_ref().unwrap(), auto_connect); .and_then(|prefs| prefs.get("autoConnect"))
if auto_connect { .copied()
let real_address = Address::from_str(&addr_str).unwrap(); .unwrap_or(true);
let mut cm = connecting_macs_clone.lock().await; debug!(
if cm.contains(&real_address) { "Auto-connect preference for {}: {}",
info!("Already connecting to {}, skipping duplicate attempt.", matched_airpods_mac.as_ref().unwrap()); matched_airpods_mac.as_ref().unwrap(),
return; auto_connect
);
if auto_connect {
let real_address =
Address::from_str(&addr_str).unwrap();
let mut cm = connecting_macs_clone.lock().await;
if cm.contains(&real_address) {
info!(
"Already connecting to {}, skipping duplicate attempt.",
matched_airpods_mac.as_ref().unwrap()
);
return;
}
cm.insert(real_address);
// let adapter_clone = adapter_monitor_clone.clone();
// let real_device = adapter_clone.device(real_address).unwrap();
info!(
"AirPods are disconnected, attempting to connect to {}",
matched_airpods_mac.as_ref().unwrap()
);
// if let Err(e) = real_device.connect().await {
// info!("Failed to connect to AirPods {}: {}", matched_airpods_mac.as_ref().unwrap(), e);
// } else {
// info!("Successfully connected to AirPods {}", matched_airpods_mac.as_ref().unwrap());
// }
// call bluetoothctl connect <mac> for now, I don't know why bluer connect isn't working
let output =
tokio::process::Command::new("bluetoothctl")
.arg("connect")
.arg(matched_airpods_mac.as_ref().unwrap())
.output()
.await;
match output {
Ok(output) => {
if output.status.success() {
info!(
"Successfully connected to AirPods {}",
matched_airpods_mac
.as_ref()
.unwrap()
);
cm.remove(&real_address);
} else {
let stderr = String::from_utf8_lossy(
&output.stderr,
);
info!(
"Failed to connect to AirPods {}: {}",
matched_airpods_mac
.as_ref()
.unwrap(),
stderr
);
} }
cm.insert(real_address); }
// let adapter_clone = adapter_monitor_clone.clone(); Err(e) => {
// let real_device = adapter_clone.device(real_address).unwrap(); info!(
info!("AirPods are disconnected, attempting to connect to {}", matched_airpods_mac.as_ref().unwrap()); "Failed to execute bluetoothctl to connect to AirPods {}: {}",
// if let Err(e) = real_device.connect().await { matched_airpods_mac.as_ref().unwrap(),
// info!("Failed to connect to AirPods {}: {}", matched_airpods_mac.as_ref().unwrap(), e); e
// } else { );
// info!("Successfully connected to AirPods {}", matched_airpods_mac.as_ref().unwrap());
// }
// call bluetoothctl connect <mac> for now, I don't know why bluer connect isn't working
let output = tokio::process::Command::new("bluetoothctl")
.arg("connect")
.arg(matched_airpods_mac.as_ref().unwrap())
.output()
.await;
match output {
Ok(output) => {
if output.status.success() {
info!("Successfully connected to AirPods {}", matched_airpods_mac.as_ref().unwrap());
cm.remove(&real_address);
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
info!("Failed to connect to AirPods {}: {}", matched_airpods_mac.as_ref().unwrap(), stderr);
}
}
Err(e) => {
info!("Failed to execute bluetoothctl to connect to AirPods {}: {}", matched_airpods_mac.as_ref().unwrap(), e);
}
}
info!("Auto-connect is disabled for {}, not attempting to connect.", matched_airpods_mac.as_ref().unwrap());
} }
} }
info!(
let status = apple_data[5] as usize; "Auto-connect is disabled for {}, not attempting to connect.",
let primary_left = (status >> 5) & 0x01 == 1; matched_airpods_mac.as_ref().unwrap()
let this_in_case = (status >> 6) & 0x01 == 1; );
let xor_factor = primary_left ^ this_in_case;
let is_left_in_ear = if xor_factor { (status & 0x02) != 0 } else { (status & 0x08) != 0 };
let is_right_in_ear = if xor_factor { (status & 0x08) != 0 } else { (status & 0x02) != 0 };
let is_flipped = !primary_left;
let left_byte_index = if is_flipped { 2 } else { 1 };
let right_byte_index = if is_flipped { 1 } else { 2 };
let left_byte = decrypted[left_byte_index] as i32;
let right_byte = decrypted[right_byte_index] as i32;
let case_byte = decrypted[3] as i32;
let (left_battery, left_charging) = if left_byte == 0xff {
(0, false)
} else {
(left_byte & 0x7F, (left_byte & 0x80) != 0)
};
let (right_battery, right_charging) = if right_byte == 0xff {
(0, false)
} else {
(right_byte & 0x7F, (right_byte & 0x80) != 0)
};
let (case_battery, case_charging) = if case_byte == 0xff {
(0, false)
} else {
(case_byte & 0x7F, (case_byte & 0x80) != 0)
};
if let Some(handle) = &tray_handle_clone {
handle.update(|tray: &mut MyTray| {
tray.battery_l = if left_byte == 0xff { None } else { Some(left_battery as u8) };
tray.battery_l_status = if left_byte == 0xff { Some(BatteryStatus::Disconnected) } else if left_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) };
tray.battery_r = if right_byte == 0xff { None } else { Some(right_battery as u8) };
tray.battery_r_status = if right_byte == 0xff { Some(BatteryStatus::Disconnected) } else if right_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) };
tray.battery_c = if case_byte == 0xff { None } else { Some(case_battery as u8) };
tray.battery_c_status = if case_byte == 0xff { Some(BatteryStatus::Disconnected) } else if case_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) };
}).await;
}
debug!("Battery status: Left: {}, Right: {}, Case: {}, InEar: L:{} R:{}",
if left_byte == 0xff { "disconnected".to_string() } else { format!("{}% (charging: {})", left_battery, left_charging) },
if right_byte == 0xff { "disconnected".to_string() } else { format!("{}% (charging: {})", right_battery, right_charging) },
if case_byte == 0xff { "disconnected".to_string() } else { format!("{}% (charging: {})", case_battery, case_charging) },
is_left_in_ear, is_right_in_ear);
} }
}
let status = apple_data[5] as usize;
let primary_left = (status >> 5) & 0x01 == 1;
let this_in_case = (status >> 6) & 0x01 == 1;
let xor_factor = primary_left ^ this_in_case;
let is_left_in_ear = if xor_factor {
(status & 0x02) != 0
} else {
(status & 0x08) != 0
};
let is_right_in_ear = if xor_factor {
(status & 0x08) != 0
} else {
(status & 0x02) != 0
};
let is_flipped = !primary_left;
let left_byte_index = if is_flipped { 2 } else { 1 };
let right_byte_index = if is_flipped { 1 } else { 2 };
let left_byte = decrypted[left_byte_index] as i32;
let right_byte = decrypted[right_byte_index] as i32;
let case_byte = decrypted[3] as i32;
let (left_battery, left_charging) = if left_byte == 0xff {
(0, false)
} else {
(left_byte & 0x7F, (left_byte & 0x80) != 0)
};
let (right_battery, right_charging) = if right_byte == 0xff
{
(0, false)
} else {
(right_byte & 0x7F, (right_byte & 0x80) != 0)
};
let (case_battery, case_charging) = if case_byte == 0xff {
(0, false)
} else {
(case_byte & 0x7F, (case_byte & 0x80) != 0)
};
if let Some(handle) = &tray_handle_clone {
handle
.update(|tray: &mut MyTray| {
tray.battery_l = if left_byte == 0xff {
None
} else {
Some(left_battery as u8)
};
tray.battery_l_status = if left_byte == 0xff {
Some(BatteryStatus::Disconnected)
} else if left_charging {
Some(BatteryStatus::Charging)
} else {
Some(BatteryStatus::NotCharging)
};
tray.battery_r = if right_byte == 0xff {
None
} else {
Some(right_battery as u8)
};
tray.battery_r_status = if right_byte == 0xff {
Some(BatteryStatus::Disconnected)
} else if right_charging {
Some(BatteryStatus::Charging)
} else {
Some(BatteryStatus::NotCharging)
};
tray.battery_c = if case_byte == 0xff {
None
} else {
Some(case_battery as u8)
};
tray.battery_c_status = if case_byte == 0xff {
Some(BatteryStatus::Disconnected)
} else if case_charging {
Some(BatteryStatus::Charging)
} else {
Some(BatteryStatus::NotCharging)
};
})
.await;
}
debug!(
"Battery status: Left: {}, Right: {}, Case: {}, InEar: L:{} R:{}",
if left_byte == 0xff {
"disconnected".to_string()
} else {
format!(
"{}% (charging: {})",
left_battery, left_charging
)
},
if right_byte == 0xff {
"disconnected".to_string()
} else {
format!(
"{}% (charging: {})",
right_battery, right_charging
)
},
if case_byte == 0xff {
"disconnected".to_string()
} else {
format!(
"{}% (charging: {})",
case_battery, case_charging
)
},
is_left_in_ear,
is_right_in_ear
);
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::bluetooth::aacp::AACPManager; use crate::bluetooth::aacp::AACPManager;
use crate::bluetooth::att::ATTManager; use crate::bluetooth::att::ATTManager;
use std::sync::Arc;
pub struct DeviceManagers { pub struct DeviceManagers {
att: Option<Arc<ATTManager>>, att: Option<Arc<ATTManager>>,
@@ -9,16 +9,25 @@ pub struct DeviceManagers {
impl DeviceManagers { impl DeviceManagers {
pub fn with_aacp(aacp: AACPManager) -> Self { pub fn with_aacp(aacp: AACPManager) -> Self {
Self { att: None, aacp: Some(Arc::new(aacp)) } Self {
att: None,
aacp: Some(Arc::new(aacp)),
}
} }
pub 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,
}
} }
// keeping the att for airpods optional as it requires changes in system bluez config // keeping the att for airpods optional as it requires changes in system bluez config
pub fn with_both(aacp: AACPManager, att: ATTManager) -> Self { pub fn with_both(aacp: AACPManager, att: ATTManager) -> Self {
Self { att: Some(Arc::new(att)), aacp: Some(Arc::new(aacp)) } Self {
att: Some(Arc::new(att)),
aacp: Some(Arc::new(aacp)),
}
} }
pub fn set_aacp(&mut self, manager: AACPManager) { pub fn set_aacp(&mut self, manager: AACPManager) {

View File

@@ -1,5 +1,5 @@
pub(crate) mod discovery;
pub mod aacp; pub mod aacp;
pub mod att; pub mod att;
pub(crate) mod discovery;
pub mod le; pub mod le;
pub mod managers; pub mod managers;

View File

@@ -1,15 +1,15 @@
use crate::bluetooth::aacp::{AACPManager, ProximityKeyType, AACPEvent, AirPodsLEKeys};
use crate::bluetooth::aacp::ControlCommandIdentifiers; use crate::bluetooth::aacp::ControlCommandIdentifiers;
use crate::bluetooth::aacp::{AACPEvent, AACPManager, AirPodsLEKeys, ProximityKeyType};
use crate::media_controller::MediaController; use crate::media_controller::MediaController;
use bluer::Address;
use log::{debug, info, error};
use std::sync::Arc;
use ksni::Handle;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use tokio::time::{sleep, Duration};
use crate::ui::tray::MyTray;
use crate::ui::messages::BluetoothUIMessage; use crate::ui::messages::BluetoothUIMessage;
use crate::ui::tray::MyTray;
use bluer::Address;
use ksni::Handle;
use log::{debug, error, info};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::time::{Duration, sleep};
pub struct AirPodsDevice { pub struct AirPodsDevice {
pub mac_address: Address, pub mac_address: Address,
@@ -33,7 +33,9 @@ impl AirPodsDevice {
// att_manager.connect(mac_address).await.expect("Failed to connect ATT"); // att_manager.connect(mac_address).await.expect("Failed to connect ATT");
if let Some(handle) = &tray_handle { if let Some(handle) = &tray_handle {
handle.update(|tray: &mut MyTray| tray.connected = true).await; handle
.update(|tray: &mut MyTray| tray.connected = true)
.await;
} }
info!("Sending handshake"); info!("Sending handshake");
@@ -61,24 +63,39 @@ impl AirPodsDevice {
} }
info!("Requesting Proximity Keys: IRK and ENC_KEY"); info!("Requesting Proximity Keys: IRK and ENC_KEY");
if let Err(e) = aacp_manager.send_proximity_keys_request( if let Err(e) = aacp_manager
vec![ProximityKeyType::Irk, ProximityKeyType::EncKey], .send_proximity_keys_request(vec![ProximityKeyType::Irk, ProximityKeyType::EncKey])
).await { .await
{
error!("Failed to request proximity keys: {}", e); error!("Failed to request proximity keys: {}", e);
} }
let session = bluer::Session::new().await.expect("Failed to get bluer session"); let session = bluer::Session::new()
let adapter = session.default_adapter().await.expect("Failed to get default adapter"); .await
let local_mac = adapter.address().await.expect("Failed to get adapter address").to_string(); .expect("Failed to get bluer session");
let adapter = session
.default_adapter()
.await
.expect("Failed to get default adapter");
let local_mac = adapter
.address()
.await
.expect("Failed to get adapter address")
.to_string();
let media_controller = Arc::new(Mutex::new(MediaController::new(mac_address.to_string(), local_mac.clone()))); let media_controller = Arc::new(Mutex::new(MediaController::new(
mac_address.to_string(),
local_mac.clone(),
)));
let mc_clone = media_controller.clone(); let mc_clone = media_controller.clone();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel(); let (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel();
aacp_manager.set_event_channel(tx).await; aacp_manager.set_event_channel(tx).await;
if let Some(handle) = &tray_handle { if let Some(handle) = &tray_handle {
handle.update(|tray: &mut MyTray| tray.command_tx = Some(command_tx.clone())).await; handle
.update(|tray: &mut MyTray| tray.command_tx = Some(command_tx.clone()))
.await;
} }
let aacp_manager_clone = aacp_manager.clone(); let aacp_manager_clone = aacp_manager.clone();
@@ -92,50 +109,76 @@ impl AirPodsDevice {
let mc_listener = media_controller.lock().await; let mc_listener = media_controller.lock().await;
let aacp_manager_clone_listener = aacp_manager.clone(); let aacp_manager_clone_listener = aacp_manager.clone();
mc_listener.start_playback_listener(aacp_manager_clone_listener, command_tx.clone()).await; mc_listener
.start_playback_listener(aacp_manager_clone_listener, command_tx.clone())
.await;
drop(mc_listener); drop(mc_listener);
let (listening_mode_tx, mut listening_mode_rx) = tokio::sync::mpsc::unbounded_channel(); let (listening_mode_tx, mut listening_mode_rx) = tokio::sync::mpsc::unbounded_channel();
aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::ListeningMode, listening_mode_tx).await; aacp_manager
.subscribe_to_control_command(
ControlCommandIdentifiers::ListeningMode,
listening_mode_tx,
)
.await;
let tray_handle_clone = tray_handle.clone(); let tray_handle_clone = tray_handle.clone();
tokio::spawn(async move { tokio::spawn(async move {
while let Some(value) = listening_mode_rx.recv().await { while let Some(value) = listening_mode_rx.recv().await {
if let Some(handle) = &tray_handle_clone { if let Some(handle) = &tray_handle_clone {
handle.update(|tray: &mut MyTray| { handle
tray.listening_mode = Some(value[0]); .update(|tray: &mut MyTray| {
}).await; tray.listening_mode = Some(value[0]);
})
.await;
} }
} }
}); });
let (allow_off_tx, mut allow_off_rx) = tokio::sync::mpsc::unbounded_channel(); let (allow_off_tx, mut allow_off_rx) = tokio::sync::mpsc::unbounded_channel();
aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::AllowOffOption, allow_off_tx).await; aacp_manager
.subscribe_to_control_command(ControlCommandIdentifiers::AllowOffOption, allow_off_tx)
.await;
let tray_handle_clone = tray_handle.clone(); let tray_handle_clone = tray_handle.clone();
tokio::spawn(async move { tokio::spawn(async move {
while let Some(value) = allow_off_rx.recv().await { while let Some(value) = allow_off_rx.recv().await {
if let Some(handle) = &tray_handle_clone { if let Some(handle) = &tray_handle_clone {
handle.update(|tray: &mut MyTray| { handle
tray.allow_off_option = Some(value[0]); .update(|tray: &mut MyTray| {
}).await; tray.allow_off_option = Some(value[0]);
})
.await;
} }
} }
}); });
let (conversation_detect_tx, mut conversation_detect_rx) = tokio::sync::mpsc::unbounded_channel(); let (conversation_detect_tx, mut conversation_detect_rx) =
aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::ConversationDetectConfig, conversation_detect_tx).await; tokio::sync::mpsc::unbounded_channel();
aacp_manager
.subscribe_to_control_command(
ControlCommandIdentifiers::ConversationDetectConfig,
conversation_detect_tx,
)
.await;
let tray_handle_clone = tray_handle.clone(); let tray_handle_clone = tray_handle.clone();
tokio::spawn(async move { tokio::spawn(async move {
while let Some(value) = conversation_detect_rx.recv().await { while let Some(value) = conversation_detect_rx.recv().await {
if let Some(handle) = &tray_handle_clone { if let Some(handle) = &tray_handle_clone {
handle.update(|tray: &mut MyTray| { handle
tray.conversation_detect_enabled = Some(value[0] == 0x01); .update(|tray: &mut MyTray| {
}).await; tray.conversation_detect_enabled = Some(value[0] == 0x01);
})
.await;
} }
} }
}); });
let (owns_connection_tx, mut owns_connection_rx) = tokio::sync::mpsc::unbounded_channel(); let (owns_connection_tx, mut owns_connection_rx) = tokio::sync::mpsc::unbounded_channel();
aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::OwnsConnection, owns_connection_tx).await; aacp_manager
.subscribe_to_control_command(
ControlCommandIdentifiers::OwnsConnection,
owns_connection_tx,
)
.await;
let mc_clone_owns = media_controller.clone(); let mc_clone_owns = media_controller.clone();
tokio::spawn(async move { tokio::spawn(async move {
while let Some(value) = owns_connection_rx.recv().await { while let Some(value) = owns_connection_rx.recv().await {
@@ -158,46 +201,62 @@ impl AirPodsDevice {
let event_clone = event.clone(); let event_clone = event.clone();
match event { match event {
AACPEvent::EarDetection(old_status, new_status) => { AACPEvent::EarDetection(old_status, new_status) => {
debug!("Received EarDetection event: old_status={:?}, new_status={:?}", old_status, new_status); debug!(
"Received EarDetection event: old_status={:?}, new_status={:?}",
old_status, new_status
);
let controller = mc_clone.lock().await; let controller = mc_clone.lock().await;
debug!("Calling handle_ear_detection with old_status: {:?}, new_status: {:?}", old_status, new_status); debug!(
controller.handle_ear_detection(old_status, new_status).await; "Calling handle_ear_detection with old_status: {:?}, new_status: {:?}",
old_status, new_status
);
controller
.handle_ear_detection(old_status, new_status)
.await;
} }
AACPEvent::BatteryInfo(battery_info) => { AACPEvent::BatteryInfo(battery_info) => {
debug!("Received BatteryInfo event: {:?}", battery_info); debug!("Received BatteryInfo event: {:?}", battery_info);
if let Some(handle) = &tray_handle { if let Some(handle) = &tray_handle {
handle.update(|tray: &mut MyTray| { handle
for b in &battery_info { .update(|tray: &mut MyTray| {
match b.component as u8 { for b in &battery_info {
0x01 => { match b.component as u8 {
tray.battery_headphone = Some(b.level); 0x01 => {
tray.battery_headphone_status = Some(b.status); tray.battery_headphone = Some(b.level);
tray.battery_headphone_status = Some(b.status);
}
0x02 => {
tray.battery_r = Some(b.level);
tray.battery_r_status = Some(b.status);
}
0x04 => {
tray.battery_l = Some(b.level);
tray.battery_l_status = Some(b.status);
}
0x08 => {
tray.battery_c = Some(b.level);
tray.battery_c_status = Some(b.status);
}
_ => {}
} }
0x02 => {
tray.battery_r = Some(b.level);
tray.battery_r_status = Some(b.status);
}
0x04 => {
tray.battery_l = Some(b.level);
tray.battery_l_status = Some(b.status);
}
0x08 => {
tray.battery_c = Some(b.level);
tray.battery_c_status = Some(b.status);
}
_ => {}
} }
} })
}).await; .await;
} }
debug!("Updated tray with new battery info"); debug!("Updated tray with new battery info");
let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(mac_address.to_string(), event_clone)); let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(
mac_address.to_string(),
event_clone,
));
debug!("Sent BatteryInfo event to UI"); debug!("Sent BatteryInfo event to UI");
} }
AACPEvent::ControlCommand(status) => { AACPEvent::ControlCommand(status) => {
debug!("Received ControlCommand event: {:?}", status); debug!("Received ControlCommand event: {:?}", status);
let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(mac_address.to_string(), event_clone)); let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(
mac_address.to_string(),
event_clone,
));
debug!("Sent ControlCommand event to UI"); debug!("Sent ControlCommand event to UI");
} }
AACPEvent::ConversationalAwareness(status) => { AACPEvent::ConversationalAwareness(status) => {
@@ -208,37 +267,60 @@ impl AirPodsDevice {
AACPEvent::ConnectedDevices(old_devices, new_devices) => { AACPEvent::ConnectedDevices(old_devices, new_devices) => {
let local_mac = local_mac_events.clone(); let local_mac = local_mac_events.clone();
let new_devices_filtered = new_devices.iter().filter(|new_device| { let new_devices_filtered = new_devices.iter().filter(|new_device| {
let not_in_old = old_devices.iter().all(|old_device| old_device.mac != new_device.mac); let not_in_old = old_devices
.iter()
.all(|old_device| old_device.mac != new_device.mac);
let not_local = new_device.mac != local_mac; let not_local = new_device.mac != local_mac;
not_in_old && not_local not_in_old && not_local
}); });
for device in new_devices_filtered { for device in new_devices_filtered {
info!("New connected device: {}, info1: {}, info2: {}", device.mac, device.info1, device.info2); info!(
info!("Sending new Tipi packet for device {}, and sending media info to the device", device.mac); "New connected device: {}, info1: {}, info2: {}",
device.mac, device.info1, device.info2
);
info!(
"Sending new Tipi packet for device {}, and sending media info to the device",
device.mac
);
let aacp_manager_clone = aacp_manager_clone_events.clone(); let aacp_manager_clone = aacp_manager_clone_events.clone();
let local_mac_clone = local_mac.clone(); let local_mac_clone = local_mac.clone();
let device_mac_clone = device.mac.clone(); let device_mac_clone = device.mac.clone();
tokio::spawn(async move { tokio::spawn(async move {
if let Err(e) = aacp_manager_clone.send_media_information_new_device(&local_mac_clone, &device_mac_clone).await { if let Err(e) = aacp_manager_clone
.send_media_information_new_device(
&local_mac_clone,
&device_mac_clone,
)
.await
{
error!("Failed to send media info new device: {}", e); error!("Failed to send media info new device: {}", e);
} }
if let Err(e) = aacp_manager_clone.send_add_tipi_device(&local_mac_clone, &device_mac_clone).await { if let Err(e) = aacp_manager_clone
.send_add_tipi_device(&local_mac_clone, &device_mac_clone)
.await
{
error!("Failed to send add tipi device: {}", e); error!("Failed to send add tipi device: {}", e);
} }
}); });
} }
} }
AACPEvent::OwnershipToFalseRequest => { AACPEvent::OwnershipToFalseRequest => {
info!("Received ownership to false request. Setting ownership to false and pausing media."); info!(
let _ = command_tx_clone.send((ControlCommandIdentifiers::OwnsConnection, vec![0x00])); "Received ownership to false request. Setting ownership to false and pausing media."
);
let _ = command_tx_clone
.send((ControlCommandIdentifiers::OwnsConnection, vec![0x00]));
let controller = mc_clone.lock().await; let controller = mc_clone.lock().await;
controller.pause_all_media().await; controller.pause_all_media().await;
controller.deactivate_a2dp_profile().await; controller.deactivate_a2dp_profile().await;
} }
_ => { _ => {
debug!("Received unhandled AACP event: {:?}", event); debug!("Received unhandled AACP event: {:?}", event);
let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(mac_address.to_string(), event_clone)); let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(
mac_address.to_string(),
event_clone,
));
debug!("Sent unhandled AACP event to UI"); debug!("Sent unhandled AACP event to UI");
} }
} }
@@ -268,5 +350,5 @@ pub struct AirPodsInformation {
pub left_serial_number: String, pub left_serial_number: String,
pub right_serial_number: String, pub right_serial_number: String,
pub version3: String, pub version3: String,
pub le_keys: AirPodsLEKeys pub le_keys: AirPodsLEKeys,
} }

View File

@@ -1,15 +1,14 @@
use std::fmt::Display;
use iced::widget::combo_box;
use serde::{Deserialize, Serialize};
use crate::bluetooth::aacp::BatteryInfo; use crate::bluetooth::aacp::BatteryInfo;
use crate::devices::airpods::AirPodsInformation; use crate::devices::airpods::AirPodsInformation;
use crate::devices::nothing::NothingInformation; use crate::devices::nothing::NothingInformation;
use iced::widget::combo_box;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(PartialEq)]
pub enum DeviceType { pub enum DeviceType {
AirPods, AirPods,
Nothing Nothing,
} }
impl Display for DeviceType { impl Display for DeviceType {
@@ -21,12 +20,11 @@ impl Display for DeviceType {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data")] #[serde(tag = "kind", content = "data")]
pub enum DeviceInformation { pub enum DeviceInformation {
AirPods(AirPodsInformation), AirPods(AirPodsInformation),
Nothing(NothingInformation) Nothing(NothingInformation),
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -36,7 +34,6 @@ pub struct DeviceData {
pub information: Option<DeviceInformation>, pub information: Option<DeviceInformation>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum DeviceState { pub enum DeviceState {
AirPods(AirPodsState), AirPods(AirPodsState),
@@ -60,7 +57,7 @@ pub struct AirPodsState {
pub conversation_awareness_enabled: bool, pub conversation_awareness_enabled: bool,
pub personalized_volume_enabled: bool, pub personalized_volume_enabled: bool,
pub allow_off_mode: bool, pub allow_off_mode: bool,
pub battery: Vec<BatteryInfo> pub battery: Vec<BatteryInfo>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -68,7 +65,7 @@ pub enum AirPodsNoiseControlMode {
Off, Off,
NoiseCancellation, NoiseCancellation,
Transparency, Transparency,
Adaptive Adaptive,
} }
impl Display for AirPodsNoiseControlMode { impl Display for AirPodsNoiseControlMode {
@@ -115,7 +112,7 @@ pub enum NothingAncMode {
MidNoiseCancellation, MidNoiseCancellation,
HighNoiseCancellation, HighNoiseCancellation,
AdaptiveNoiseCancellation, AdaptiveNoiseCancellation,
Transparency Transparency,
} }
impl Display for NothingAncMode { impl Display for NothingAncMode {
@@ -152,4 +149,4 @@ impl NothingAncMode {
NothingAncMode::Off => 0x05, NothingAncMode::Off => 0x05,
} }
} }
} }

View File

@@ -1,3 +1,3 @@
pub mod airpods; pub mod airpods;
pub mod enums; pub mod enums;
pub(crate) mod nothing; pub(crate) mod nothing;

View File

@@ -1,167 +1,179 @@
use std::collections::HashMap;
use std::time::Duration;
use bluer::Address;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use tokio::time::sleep;
use crate::bluetooth::att::{ATTHandles, ATTManager}; use crate::bluetooth::att::{ATTHandles, ATTManager};
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType}; use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
use crate::ui::messages::BluetoothUIMessage; use crate::ui::messages::BluetoothUIMessage;
use crate::utils::get_devices_path; use crate::utils::get_devices_path;
use bluer::Address;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::time::sleep;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NothingInformation{ pub struct NothingInformation {
pub serial_number: String, pub serial_number: String,
pub firmware_version: String pub firmware_version: String,
} }
pub struct NothingDevice{ pub struct NothingDevice {
pub att_manager: ATTManager, pub att_manager: ATTManager,
pub information: NothingInformation pub information: NothingInformation,
} }
impl NothingDevice{ impl NothingDevice {
pub async fn new( pub async fn new(
mac_address: Address, mac_address: Address,
ui_tx: mpsc::UnboundedSender<BluetoothUIMessage> ui_tx: mpsc::UnboundedSender<BluetoothUIMessage>,
) -> Self { ) -> Self {
let mut att_manager = ATTManager::new(); let mut att_manager = ATTManager::new();
att_manager.connect(mac_address).await.expect("Failed to connect"); att_manager
.connect(mac_address)
.await
.expect("Failed to connect");
let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>(); let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>();
att_manager.register_listener( att_manager
ATTHandles::NothingEverythingRead, .register_listener(ATTHandles::NothingEverythingRead, tx)
tx .await;
).await;
let devices: HashMap<String, DeviceData> = let devices: HashMap<String, DeviceData> = std::fs::read_to_string(get_devices_path())
std::fs::read_to_string(get_devices_path()) .ok()
.ok() .and_then(|s| serde_json::from_str(&s).ok())
.and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_default();
.unwrap_or_default();
let device_key = mac_address.to_string(); let device_key = mac_address.to_string();
let information = if let Some(device_data) = devices.get(&device_key) { let information = if let Some(device_data) = devices.get(&device_key) {
let info = device_data.information.clone(); let info = device_data.information.clone();
if let Some(DeviceInformation::Nothing(ref nothing_info)) = info { if let Some(DeviceInformation::Nothing(ref nothing_info)) = info {
nothing_info.clone() nothing_info.clone()
} else { } else {
NothingInformation{ NothingInformation {
serial_number: String::new(), serial_number: String::new(),
firmware_version: String::new() firmware_version: String::new(),
} }
} }
} else { } else {
NothingInformation{ NothingInformation {
serial_number: String::new(), serial_number: String::new(),
firmware_version: String::new() firmware_version: String::new(),
} }
}; };
// Request version information // Request version information
att_manager.write( att_manager
ATTHandles::NothingEverything, .write(
&[ ATTHandles::NothingEverything,
0x55, 0x20, &[
0x01, 0x42, 0x55, 0x20, 0x01, 0x42, 0xC0, 0x00, 0x00, 0x00, 0x00,
0xC0, 0x00, 0x00, // something, idk
0x00, 0x00, ],
0x00, 0x00 // something, idk )
] .await
).await.expect("Failed to write"); .expect("Failed to write");
sleep(Duration::from_millis(100)).await; sleep(Duration::from_millis(100)).await;
// Request serial number // Request serial number
att_manager.write( att_manager
ATTHandles::NothingEverything, .write(
&[ ATTHandles::NothingEverything,
0x55, 0x20, &[0x55, 0x20, 0x01, 0x06, 0xC0, 0x00, 0x00, 0x13, 0x00, 0x00],
0x01, 0x06, )
0xC0, 0x00, .await
0x00, 0x13, .expect("Failed to write");
0x00, 0x00
]
).await.expect("Failed to write");
// let ui_tx_clone = ui_tx.clone(); // let ui_tx_clone = ui_tx.clone();
let information_l = information.clone(); let information_l = information.clone();
tokio::spawn(async move { tokio::spawn(async move {
while let Some(data) = rx.recv().await { while let Some(data) = rx.recv().await {
if data.starts_with(&[ if data.starts_with(&[0x55, 0x20, 0x01, 0x42, 0x40]) {
0x55, 0x20,
0x01, 0x42, 0x40
]) {
let firmware_version = String::from_utf8_lossy(&data[8..]).to_string(); let firmware_version = String::from_utf8_lossy(&data[8..]).to_string();
info!("Received firmware version from Nothing device {}: {}", mac_address, firmware_version); info!(
let new_information = NothingInformation{ "Received firmware version from Nothing device {}: {}",
mac_address, firmware_version
);
let new_information = NothingInformation {
serial_number: information_l.serial_number.clone(), serial_number: information_l.serial_number.clone(),
firmware_version: firmware_version.clone() firmware_version: firmware_version.clone(),
}; };
let mut new_devices = devices.clone(); let mut new_devices = devices.clone();
new_devices.insert( new_devices.insert(
device_key.clone(), device_key.clone(),
DeviceData{ DeviceData {
name: devices.get(&device_key) name: devices
.get(&device_key)
.map(|d| d.name.clone()) .map(|d| d.name.clone())
.unwrap_or("Nothing Device".to_string()), .unwrap_or("Nothing Device".to_string()),
type_: devices.get(&device_key) type_: devices
.get(&device_key)
.map(|d| d.type_.clone()) .map(|d| d.type_.clone())
.unwrap_or(DeviceType::Nothing), .unwrap_or(DeviceType::Nothing),
information: Some(DeviceInformation::Nothing(new_information)), information: Some(DeviceInformation::Nothing(new_information)),
} },
); );
let json = serde_json::to_string(&new_devices).unwrap(); let json = serde_json::to_string(&new_devices).unwrap();
std::fs::write(get_devices_path(), json).expect("Failed to write devices file"); std::fs::write(get_devices_path(), json).expect("Failed to write devices file");
} else if data.starts_with( } else if data.starts_with(&[0x55, 0x20, 0x01, 0x06, 0x40]) {
&[ let serial_number_start_position = data
0x55, 0x20, .iter()
0x01, 0x06, 0x40 .position(|&b| b == "S".as_bytes()[0])
] .unwrap_or(8);
) { let serial_number_end = data
let serial_number_start_position = data.iter().position(|&b| b == "S".as_bytes()[0]).unwrap_or(8); .iter()
let serial_number_end = data.iter()
.skip(serial_number_start_position) .skip(serial_number_start_position)
.position(|&b| b == 0x0A) .position(|&b| b == 0x0A)
.map(|pos| pos + serial_number_start_position) .map(|pos| pos + serial_number_start_position)
.unwrap_or(data.len()); .unwrap_or(data.len());
if data.get(serial_number_start_position + 1) == Some(&"H".as_bytes()[0]) { if data.get(serial_number_start_position + 1) == Some(&"H".as_bytes()[0]) {
let serial_number = String::from_utf8_lossy( let serial_number = String::from_utf8_lossy(
&data[serial_number_start_position..serial_number_end] &data[serial_number_start_position..serial_number_end],
).to_string(); )
info!("Received serial number from Nothing device {}: {}", mac_address, serial_number); .to_string();
let new_information = NothingInformation{ info!(
"Received serial number from Nothing device {}: {}",
mac_address, serial_number
);
let new_information = NothingInformation {
serial_number: serial_number.clone(), serial_number: serial_number.clone(),
firmware_version: information_l.firmware_version.clone() firmware_version: information_l.firmware_version.clone(),
}; };
let mut new_devices = devices.clone(); let mut new_devices = devices.clone();
new_devices.insert( new_devices.insert(
device_key.clone(), device_key.clone(),
DeviceData{ DeviceData {
name: devices.get(&device_key) name: devices
.get(&device_key)
.map(|d| d.name.clone()) .map(|d| d.name.clone())
.unwrap_or("Nothing Device".to_string()), .unwrap_or("Nothing Device".to_string()),
type_: devices.get(&device_key) type_: devices
.get(&device_key)
.map(|d| d.type_.clone()) .map(|d| d.type_.clone())
.unwrap_or(DeviceType::Nothing), .unwrap_or(DeviceType::Nothing),
information: Some(DeviceInformation::Nothing(new_information)), information: Some(DeviceInformation::Nothing(new_information)),
} },
); );
let json = serde_json::to_string(&new_devices).unwrap(); let json = serde_json::to_string(&new_devices).unwrap();
std::fs::write(get_devices_path(), json).expect("Failed to write devices file"); std::fs::write(get_devices_path(), json)
.expect("Failed to write devices file");
} else { } else {
debug!("Serial number format unexpected from Nothing device {}: {:?}", mac_address, data); debug!(
"Serial number format unexpected from Nothing device {}: {:?}",
mac_address, data
);
} }
} }
debug!("Received data from (Nothing) device {}, data: {:?}", mac_address, data); debug!(
"Received data from (Nothing) device {}, data: {:?}",
mac_address, data
);
} }
}); });
NothingDevice{ NothingDevice {
att_manager, att_manager,
information information,
} }
} }
} }

View File

@@ -1,50 +1,59 @@
mod bluetooth; mod bluetooth;
mod devices;
mod media_controller; mod media_controller;
mod ui; mod ui;
mod utils; mod utils;
mod devices;
use std::env;
use log::info;
use dbus::blocking::Connection;
use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
use dbus::message::MatchRule;
use dbus::arg::{RefArg, Variant};
use std::collections::HashMap;
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 bluer::{Address, InternalErrorKind};
use ksni::TrayMethods;
use crate::ui::tray::MyTray;
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::RwLock;
use crate::bluetooth::managers::DeviceManagers; use crate::bluetooth::managers::DeviceManagers;
use crate::devices::enums::DeviceData; use crate::devices::enums::DeviceData;
use crate::ui::messages::BluetoothUIMessage; use crate::ui::messages::BluetoothUIMessage;
use crate::ui::tray::MyTray;
use crate::utils::get_devices_path; use crate::utils::get_devices_path;
use bluer::{Address, InternalErrorKind};
use clap::Parser;
use dbus::arg::{RefArg, Variant};
use dbus::blocking::Connection;
use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
use dbus::message::MatchRule;
use devices::airpods::AirPodsDevice;
use ksni::TrayMethods;
use log::info;
use std::collections::HashMap;
use std::env;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio::sync::mpsc::unbounded_channel;
#[derive(Parser)] #[derive(Parser)]
struct Args { struct Args {
#[arg(long, short='d', help="Enable debug logging")] #[arg(long, short = 'd', help = "Enable debug logging")]
debug: bool, debug: bool,
#[arg(long, help="Disable system tray, useful if your environment doesn't support AppIndicator or StatusNotifier")] #[arg(
long,
help = "Disable system tray, useful if your environment doesn't support AppIndicator or StatusNotifier"
)]
no_tray: bool, no_tray: bool,
#[arg(long, help="Start the application minimized to tray")] #[arg(long, help = "Start the application minimized to tray")]
start_minimized: bool, start_minimized: bool,
#[arg(long, help="Enable Bluetooth LE debug logging. Only use when absolutely necessary; this produces a lot of logs.")] #[arg(
long,
help = "Enable Bluetooth LE debug logging. Only use when absolutely necessary; this produces a lot of logs."
)]
le_debug: bool, le_debug: bool,
#[arg(long, short='v', help="Show application version and exit")] #[arg(long, short = 'v', help = "Show application version and exit")]
version: bool version: bool,
} }
fn main() -> iced::Result { fn main() -> iced::Result {
let args = Args::parse(); let args = Args::parse();
if args.version { if args.version {
println!("You are running LibrePods version {}", env!("CARGO_PKG_VERSION")); println!(
"You are running LibrePods version {}",
env!("CARGO_PKG_VERSION")
);
return Ok(()); return Ok(());
} }
@@ -54,23 +63,33 @@ fn main() -> iced::Result {
if wayland_display { if wayland_display {
unsafe { env::set_var("WGPU_BACKEND", "gl") }; unsafe { env::set_var("WGPU_BACKEND", "gl") };
} }
unsafe { env::set_var("RUST_LOG", log_level.to_owned() + &format!(",winit=warn,tracing=warn,iced_wgpu=warn,wgpu_hal=warn,wgpu_core=warn,cosmic_text=warn,naga=warn,iced_winit=warn,librepods_rust::bluetooth::le={}", if args.le_debug { "debug" } else { "warn" })) }; unsafe {
env::set_var(
"RUST_LOG",
log_level.to_owned()
+ &format!(
",winit=warn,tracing=warn,iced_wgpu=warn,wgpu_hal=warn,wgpu_core=warn,cosmic_text=warn,naga=warn,iced_winit=warn,librepods_rust::bluetooth::le={}",
if args.le_debug { "debug" } else { "warn" }
),
)
};
} }
env_logger::init(); env_logger::init();
let (ui_tx, ui_rx) = unbounded_channel::<BluetoothUIMessage>(); let (ui_tx, ui_rx) = unbounded_channel::<BluetoothUIMessage>();
let device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>> = Arc::new(RwLock::new(HashMap::new())); let device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>> =
Arc::new(RwLock::new(HashMap::new()));
let device_managers_clone = device_managers.clone(); 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, device_managers_clone)).unwrap(); rt.block_on(async_main(ui_tx, device_managers_clone))
.unwrap();
}); });
ui::window::start_ui(ui_rx, args.start_minimized, device_managers) ui::window::start_ui(ui_rx, args.start_minimized, device_managers)
} }
async fn async_main( async fn async_main(
ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>, ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>,
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>, device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
@@ -84,10 +103,11 @@ async fn async_main(
log::error!("Failed to read devices file: {}", e); log::error!("Failed to read devices file: {}", e);
"{}".to_string() "{}".to_string()
}); });
let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json).unwrap_or_else(|e| { let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json)
log::error!("Deserialization failed: {}", e); .unwrap_or_else(|e| {
HashMap::new() log::error!("Deserialization failed: {}", e);
}); HashMap::new()
});
for (mac, device_data) in devices_list.iter() { for (mac, device_data) in devices_list.iter() {
if device_data.type_ == devices::enums::DeviceType::Nothing { if device_data.type_ == devices::enums::DeviceType::Nothing {
managed_devices_mac.push(mac.clone()); managed_devices_mac.push(mac.clone());
@@ -134,9 +154,13 @@ async fn async_main(
info!("Checking for connected devices..."); info!("Checking for connected devices...");
match find_connected_airpods(&adapter).await { match find_connected_airpods(&adapter).await {
Ok(device) => { Ok(device) => {
let name = device.name().await?.unwrap_or_else(|| "Unknown".to_string()); let name = device
.name()
.await?
.unwrap_or_else(|| "Unknown".to_string());
info!("Found connected AirPods: {}, initializing.", name); info!("Found connected AirPods: {}, initializing.", name);
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;
let mut managers = device_managers.write().await; let mut managers = device_managers.write().await;
// let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone()); // let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone());
@@ -146,7 +170,11 @@ async fn async_main(
.or_insert(dev_managers) .or_insert(dev_managers)
.set_aacp(airpods_device.aacp_manager); .set_aacp(airpods_device.aacp_manager);
drop(managers); drop(managers);
ui_tx.send(BluetoothUIMessage::DeviceConnected(device.address().to_string())).unwrap(); ui_tx
.send(BluetoothUIMessage::DeviceConnected(
device.address().to_string(),
))
.unwrap();
} }
Err(_) => { Err(_) => {
info!("No connected AirPods found."); info!("No connected AirPods found.");
@@ -157,20 +185,29 @@ async fn async_main(
Ok(devices) => { Ok(devices) => {
for device in devices { for device in devices {
let addr_str = device.address().to_string(); let addr_str = device.address().to_string();
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 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; let mut managers = device_managers.write().await;
if type_ == devices::enums::DeviceType::Nothing { if type_ == devices::enums::DeviceType::Nothing {
let dev = devices::nothing::NothingDevice::new(device.address(), ui_tx_clone.clone()).await; let dev = devices::nothing::NothingDevice::new(
device.address(),
ui_tx_clone.clone(),
)
.await;
let dev_managers = DeviceManagers::with_att(dev.att_manager.clone()); let dev_managers = DeviceManagers::with_att(dev.att_manager.clone());
managers managers
.entry(addr_str.clone()) .entry(addr_str.clone())
.or_insert(dev_managers) .or_insert(dev_managers)
.set_att(dev.att_manager); .set_att(dev.att_manager);
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str)).unwrap(); ui_tx_clone
.send(BluetoothUIMessage::DeviceConnected(addr_str))
.unwrap();
} }
drop(managers) drop(managers)
}); });
@@ -178,7 +215,9 @@ async fn async_main(
} }
Err(e) => { Err(e) => {
log::debug!("type of error: {:?}", e.kind); log::debug!("type of error: {:?}", e.kind);
if e.kind != bluer::ErrorKind::Internal(InternalErrorKind::Io(std::io::ErrorKind::NotFound)) { if e.kind
!= bluer::ErrorKind::Internal(InternalErrorKind::Io(std::io::ErrorKind::NotFound))
{
log::error!("Error finding other managed devices: {}", e); log::error!("Error finding other managed devices: {}", e);
} else { } else {
info!("No other managed devices found."); info!("No other managed devices found.");
@@ -189,28 +228,42 @@ async fn async_main(
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");
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_") {
return true; return true;
} }
// debug!("PropertiesChanged signal for path: {}", path); // debug!("PropertiesChanged signal for path: {}", path);
let Ok((iface, changed, _)) = msg.read3::<String, HashMap<String, Variant<Box<dyn RefArg>>>, Vec<String>>() else { let Ok((iface, changed, _)) =
msg.read3::<String, HashMap<String, Variant<Box<dyn RefArg>>>, Vec<String>>()
else {
return true; return true;
}; };
if iface != "org.bluez.Device1" { if iface != "org.bluez.Device1" {
return true; return true;
} }
let Some(connected_var) = changed.get("Connected") else { return true; }; let Some(connected_var) = changed.get("Connected") else {
let Some(is_connected) = connected_var.0.as_ref().as_u64() else { return true; }; return true;
};
let Some(is_connected) = connected_var.0.as_ref().as_u64() else {
return true;
};
if is_connected == 0 { if is_connected == 0 {
return true; return true;
} }
let proxy = conn.with_proxy("org.bluez", path, std::time::Duration::from_millis(5000)); let proxy = conn.with_proxy("org.bluez", path, std::time::Duration::from_millis(5000));
let Ok(uuids) = proxy.get::<Vec<String>>("org.bluez.Device1", "UUIDs") else { return true; }; let Ok(uuids) = proxy.get::<Vec<String>>("org.bluez.Device1", "UUIDs") else {
return true;
};
let target_uuid = "74ec2172-0bad-4d01-8f77-997b2be0722a"; let target_uuid = "74ec2172-0bad-4d01-8f77-997b2be0722a";
let Ok(addr_str) = proxy.get::<String>("org.bluez.Device1", "Address") else { return true; }; let Ok(addr_str) = proxy.get::<String>("org.bluez.Device1", "Address") else {
let Ok(addr) = addr_str.parse::<Address>() else { return true; }; return true;
};
let Ok(addr) = addr_str.parse::<Address>() else {
return true;
};
if managed_devices_mac.contains(&addr_str) { if managed_devices_mac.contains(&addr_str) {
info!("Managed device connected: {}, initializing", addr_str); info!("Managed device connected: {}, initializing", addr_str);
@@ -227,7 +280,9 @@ async fn async_main(
.or_insert(dev_managers) .or_insert(dev_managers)
.set_att(dev.att_manager); .set_att(dev.att_manager);
drop(managers); drop(managers);
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); ui_tx_clone
.send(BluetoothUIMessage::DeviceConnected(addr_str.clone()))
.unwrap();
}); });
} }
return true; return true;
@@ -236,7 +291,9 @@ async fn async_main(
if !uuids.iter().any(|u| u.to_lowercase() == target_uuid) { if !uuids.iter().any(|u| u.to_lowercase() == target_uuid) {
return true; return true;
} }
let name = proxy.get::<String>("org.bluez.Device1", "Name").unwrap_or_else(|_| "Unknown".to_string()); let name = proxy
.get::<String>("org.bluez.Device1", "Name")
.unwrap_or_else(|_| "Unknown".to_string());
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();
@@ -251,7 +308,9 @@ async fn async_main(
.or_insert(dev_managers) .or_insert(dev_managers)
.set_aacp(airpods_device.aacp_manager); .set_aacp(airpods_device.aacp_manager);
drop(managers); drop(managers);
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); ui_tx_clone
.send(BluetoothUIMessage::DeviceConnected(addr_str.clone()))
.unwrap();
}); });
true true
})?; })?;
@@ -260,4 +319,4 @@ async fn async_main(
loop { loop {
conn.process(std::time::Duration::from_millis(1000))?; conn.process(std::time::Duration::from_millis(1000))?;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,18 @@
use std::collections::HashMap; use crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers};
use std::sync::Arc;
use std::thread;
use iced::widget::{button, column, combo_box, container, row, rule, text, text_input, toggler, Rule, Space};
use iced::{Background, Border, Center, Color, Length, Padding, Theme};
use iced::Alignment::End; use iced::Alignment::End;
use iced::border::Radius; use iced::border::Radius;
use iced::overlay::menu; use iced::overlay::menu;
use iced::widget::button::Style; use iced::widget::button::Style;
use iced::widget::rule::FillMode; use iced::widget::rule::FillMode;
use iced::widget::{
Rule, Space, button, column, combo_box, container, row, rule, text, text_input, toggler,
};
use iced::{Background, Border, Center, Color, Length, Padding, Theme};
use log::error; use log::error;
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers};
// use crate::bluetooth::att::ATTManager; // use crate::bluetooth::att::ATTManager;
use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation, DeviceState}; use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation, DeviceState};
use crate::ui::window::Message; use crate::ui::window::Message;
@@ -29,26 +31,20 @@ pub fn airpods_view<'a>(
let rename_input = container( let rename_input = container(
row![ row![
Space::with_width(10), Space::with_width(10),
text("Name").size(16).style( text("Name").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_input( text_input("", &state.device_name)
"", .padding(Padding {
&state.device_name top: 5.0,
) bottom: 5.0,
.padding(Padding{ left: 10.0,
top: 5.0, right: 10.0,
bottom: 5.0, })
left: 10.0, .style(|theme: &Theme, _status| {
right: 10.0,
})
.style(
|theme: &Theme, _status| {
text_input::Style { text_input::Style {
background: Background::Color(Color::TRANSPARENT), background: Background::Color(Color::TRANSPARENT),
border: Default::default(), border: Default::default(),
@@ -57,56 +53,52 @@ pub fn airpods_view<'a>(
value: theme.palette().text, value: theme.palette().text,
selection: Default::default(), selection: Default::default(),
} }
} })
) .align_x(End)
.align_x(End) .on_input({
.on_input({
let mac = mac.clone(); let mac = mac.clone();
let state = state.clone(); let state = state.clone();
move|new_name| { move |new_name| {
let aacp_manager = aacp_manager_for_rename.clone(); let aacp_manager = aacp_manager_for_rename.clone();
run_async_in_thread( run_async_in_thread({
{ let new_name = new_name.clone();
let new_name = new_name.clone(); async move {
async move { aacp_manager
aacp_manager.send_rename_packet(&new_name).await.expect("Failed to send rename packet"); .send_rename_packet(&new_name)
} .await
.expect("Failed to send rename packet");
} }
); });
let mut state = state.clone(); let mut state = state.clone();
state.device_name = new_name.clone(); state.device_name = new_name.clone();
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state)) Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
} }
} })
)
] ]
.align_y(Center) .align_y(Center),
) )
.padding(Padding{ .padding(Padding {
top: 5.0, top: 5.0,
bottom: 5.0, bottom: 5.0,
left: 10.0, left: 10.0,
right: 10.0, right: 10.0,
}) })
.style( .style(|theme: &Theme| {
|theme: &Theme| { let mut style = container::Style::default();
let mut style = container::Style::default(); style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); let mut border = Border::default();
let mut border = Border::default(); border.color = theme.palette().primary.scale_alpha(0.5);
border.color = theme.palette().primary.scale_alpha(0.5); style.border = border.rounded(16);
style.border = border.rounded(16); style
style });
}
);
let listening_mode = container(row![ let listening_mode = container(
text("Listening Mode").size(16).style( row![
|theme: &Theme| { text("Listening Mode").size(16).style(|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),
{ {
let state_clone = state.clone(); let state_clone = state.clone();
@@ -121,78 +113,71 @@ pub fn airpods_view<'a>(
move |selected_mode| { move |selected_mode| {
let aacp_manager = aacp_manager.clone(); let aacp_manager = aacp_manager.clone();
let selected_mode_c = selected_mode.clone(); let selected_mode_c = selected_mode.clone();
run_async_in_thread( run_async_in_thread(async move {
async move { aacp_manager
aacp_manager.send_control_command( .send_control_command(
ControlCommandIdentifiers::ListeningMode, ControlCommandIdentifiers::ListeningMode,
&[selected_mode_c.to_byte()] &[selected_mode_c.to_byte()],
).await.expect("Failed to send Noise Control Mode command"); )
} .await
); .expect("Failed to send Noise Control Mode command");
});
let mut state = state_clone.clone(); let mut state = state_clone.clone();
state.noise_control_mode = selected_mode.clone(); state.noise_control_mode = selected_mode.clone();
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state)) Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
} }
} },
) )
.width(Length::from(200)) .width(Length::from(200))
.input_style( .input_style(|theme: &Theme, _status| text_input::Style {
|theme: &Theme, _status| { background: Background::Color(theme.palette().primary.scale_alpha(0.2)),
text_input::Style { border: Border {
background: Background::Color(theme.palette().primary.scale_alpha(0.2)), width: 1.0,
border: Border { color: theme.palette().text.scale_alpha(0.3),
width: 1.0, radius: Radius::from(4.0),
color: theme.palette().text.scale_alpha(0.3), },
radius: Radius::from(4.0) icon: Default::default(),
}, placeholder: theme.palette().text,
icon: Default::default(), value: theme.palette().text,
placeholder: theme.palette().text, selection: Default::default(),
value: theme.palette().text, })
selection: Default::default(), .padding(Padding {
}
}
)
.padding(Padding{
top: 5.0, top: 5.0,
bottom: 5.0, bottom: 5.0,
left: 10.0, left: 10.0,
right: 10.0, right: 10.0,
}) })
.menu_style( .menu_style(|theme: &Theme| menu::Style {
|theme: &Theme| { background: Background::Color(theme.palette().background),
menu::Style { border: Border {
background: Background::Color(theme.palette().background), width: 1.0,
border: Border { color: theme.palette().text,
width: 1.0, radius: Radius::from(4.0),
color: theme.palette().text, },
radius: Radius::from(4.0) text_color: theme.palette().text,
}, selected_text_color: theme.palette().text,
text_color: theme.palette().text, selected_background: Background::Color(
selected_text_color: theme.palette().text, theme.palette().primary.scale_alpha(0.3),
selected_background: Background::Color(theme.palette().primary.scale_alpha(0.3)), ),
} })
}
)
} }
] ]
.align_y(Center) .align_y(Center),
) )
.padding(Padding{ .padding(Padding {
top: 5.0, top: 5.0,
bottom: 5.0, bottom: 5.0,
left: 18.0, left: 18.0,
right: 18.0, right: 18.0,
}) })
.style( .style(|theme: &Theme| {
|theme: &Theme| { let mut style = container::Style::default();
let mut style = container::Style::default(); style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); let mut border = Border::default();
let mut border = Border::default(); border.color = theme.palette().primary.scale_alpha(0.5);
border.color = theme.palette().primary.scale_alpha(0.5); style.border = border.rounded(16);
style.border = border.rounded(16); style
style });
}
);
let mac_audio = mac.clone(); let mac_audio = mac.clone();
let mac_information = mac.clone(); let mac_information = mac.clone();
@@ -381,126 +366,102 @@ pub fn airpods_view<'a>(
if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information { if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information {
let info_rows = column![ let info_rows = column![
row![ row![
text("Model Number").size(16).style( text("Model Number").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.model_number.clone()).size(16) text(airpods_info.model_number.clone()).size(16)
], ],
row![ row![
text("Manufacturer").size(16).style( text("Manufacturer").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.manufacturer.clone()).size(16) text(airpods_info.manufacturer.clone()).size(16)
], ],
row![ row![
text("Serial Number").size(16).style( text("Serial Number").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),
button( button(text(airpods_info.serial_number.clone()).size(16))
text(airpods_info.serial_number.clone()).size(16) .style(|theme: &Theme, _status| {
) let mut style = Style::default();
.style( style.text_color = theme.palette().text;
|theme: &Theme, _status| { style.background = Some(Background::Color(Color::TRANSPARENT));
let mut style = Style::default(); style
style.text_color = theme.palette().text; })
style.background = Some(Background::Color(Color::TRANSPARENT));
style
}
)
.padding(0) .padding(0)
.on_press(Message::CopyToClipboard(airpods_info.serial_number.clone())) .on_press(Message::CopyToClipboard(airpods_info.serial_number.clone()))
], ],
row![ row![
text("Left Serial Number").size(16).style( text("Left Serial Number").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),
button( button(text(airpods_info.left_serial_number.clone()).size(16))
text(airpods_info.left_serial_number.clone()).size(16) .style(|theme: &Theme, _status| {
) let mut style = Style::default();
.style( style.text_color = theme.palette().text;
|theme: &Theme, _status| { style.background = Some(Background::Color(Color::TRANSPARENT));
let mut style = Style::default(); style
style.text_color = theme.palette().text; })
style.background = Some(Background::Color(Color::TRANSPARENT));
style
}
)
.padding(0) .padding(0)
.on_press(Message::CopyToClipboard(airpods_info.left_serial_number.clone())) .on_press(Message::CopyToClipboard(
airpods_info.left_serial_number.clone()
))
], ],
row![ row![
text("Right Serial Number").size(16).style( text("Right Serial Number").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),
button( button(text(airpods_info.right_serial_number.clone()).size(16))
text(airpods_info.right_serial_number.clone()).size(16) .style(|theme: &Theme, _status| {
) let mut style = Style::default();
.style( style.text_color = theme.palette().text;
|theme: &Theme, _status| { style.background = Some(Background::Color(Color::TRANSPARENT));
let mut style = Style::default(); style
style.text_color = theme.palette().text; })
style.background = Some(Background::Color(Color::TRANSPARENT));
style
}
)
.padding(0) .padding(0)
.on_press(Message::CopyToClipboard(airpods_info.right_serial_number.clone())) .on_press(Message::CopyToClipboard(
airpods_info.right_serial_number.clone()
))
], ],
row![ row![
text("Version 1").size(16).style( text("Version 1").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.version1.clone()).size(16) text(airpods_info.version1.clone()).size(16)
], ],
row![ row![
text("Version 2").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.version2.clone()).size(16) text(airpods_info.version2.clone()).size(16)
], ],
row![ row![
text("Version 3").size(16).style( text("Version 3").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.version3.clone()).size(16)
] ]
@@ -509,56 +470,53 @@ pub fn airpods_view<'a>(
.padding(8); .padding(8);
information_col = column![ information_col = column![
container( container(text("Device Information").size(18).style(|theme: &Theme| {
text("Device Information").size(18).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().primary); }))
style .padding(Padding {
}
)
).padding(Padding{
top: 5.0, top: 5.0,
bottom: 5.0, bottom: 5.0,
left: 18.0, left: 18.0,
right: 18.0, right: 18.0,
}), }),
container(info_rows) container(info_rows)
.padding(Padding{ .padding(Padding {
top: 5.0, top: 5.0,
bottom: 5.0, bottom: 5.0,
left: 10.0, left: 10.0,
right: 10.0, right: 10.0,
}) })
.style( .style(|theme: &Theme| {
|theme: &Theme| { let mut style = container::Style::default();
let mut style = container::Style::default(); style.background =
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
let mut border = Border::default(); let mut border = Border::default();
border.color = theme.palette().primary.scale_alpha(0.5); border.color = theme.palette().primary.scale_alpha(0.5);
style.border = border.rounded(16); style.border = border.rounded(16);
style style
} })
)
]; ];
} else { } else {
error!("Expected AirPodsInformation for device {}, got something else", mac.clone()); error!(
"Expected AirPodsInformation for device {}, got something else",
mac.clone()
);
} }
} }
container( container(column![
column![ rename_input,
rename_input, Space::with_height(Length::from(20)),
Space::with_height(Length::from(20)), listening_mode,
listening_mode, Space::with_height(Length::from(20)),
Space::with_height(Length::from(20)), audio_settings_col,
audio_settings_col, Space::with_height(Length::from(20)),
Space::with_height(Length::from(20)), off_listening_mode_toggle,
off_listening_mode_toggle, Space::with_height(Length::from(20)),
Space::with_height(Length::from(20)), information_col
information_col ])
]
)
.padding(20) .padding(20)
.center_x(Length::Fill) .center_x(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
@@ -572,4 +530,4 @@ where
let rt = Runtime::new().unwrap(); let rt = Runtime::new().unwrap();
rt.block_on(fut); rt.block_on(fut);
}); });
} }

View File

@@ -3,9 +3,9 @@ use crate::bluetooth::aacp::AACPEvent;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum BluetoothUIMessage { pub enum BluetoothUIMessage {
OpenWindow, OpenWindow,
DeviceConnected(String), // mac DeviceConnected(String), // mac
DeviceDisconnected(String), // mac DeviceDisconnected(String), // mac
AACPUIEvent(String, AACPEvent), // mac, event AACPUIEvent(String, AACPEvent), // mac, event
ATTNotification(String, u16, Vec<u8>), // mac, handle, data ATTNotification(String, u16, Vec<u8>), // mac, handle, data
NoOp NoOp,
} }

View File

@@ -1,5 +1,5 @@
mod airpods;
pub mod messages;
mod nothing;
pub mod tray; pub mod tray;
pub mod window; pub mod window;
pub mod messages;
mod airpods;
mod nothing;

View File

@@ -1,72 +1,62 @@
use std::collections::HashMap; use crate::bluetooth::att::{ATTHandles, ATTManager};
use std::sync::Arc; use crate::devices::enums::{DeviceData, DeviceInformation, DeviceState, NothingState};
use iced::{Background, Border, Length, Theme}; use crate::ui::window::Message;
use iced::widget::{container, text, column, row, Space};
use iced::widget::combo_box;
use iced::border::Radius; use iced::border::Radius;
use iced::overlay::menu; use iced::overlay::menu;
use iced::widget::combo_box;
use iced::widget::text_input; use iced::widget::text_input;
use tokio::runtime::Runtime; use iced::widget::{Space, column, container, row, text};
use iced::{Background, Border, Length, Theme};
use std::collections::HashMap;
use std::sync::Arc;
use std::thread; use std::thread;
use crate::bluetooth::att::{ATTManager, ATTHandles}; use tokio::runtime::Runtime;
use crate::devices::enums::{DeviceData, DeviceInformation, NothingState, DeviceState};
use crate::ui::window::Message;
pub fn nothing_view<'a>( pub fn nothing_view<'a>(
mac: &'a str, mac: &'a str,
devices_list: &HashMap<String, DeviceData>, devices_list: &HashMap<String, DeviceData>,
state: &'a NothingState, state: &'a NothingState,
att_manager: Arc<ATTManager> 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();
if let Some(device) = devices_list.get(mac.as_str()) if let Some(device) = devices_list.get(mac.as_str())
&& let Some(DeviceInformation::Nothing(ref nothing_info)) = device.information { && let Some(DeviceInformation::Nothing(ref nothing_info)) = device.information
information_col = information_col {
.push(text("Device Information").size(18).style( information_col = information_col
|theme: &Theme| { .push(text("Device Information").size(18).style(|theme: &Theme| {
let mut style = text::Style::default(); let mut style = text::Style::default();
style.color = Some(theme.palette().primary); style.color = Some(theme.palette().primary);
style style
} }))
)) .push(Space::with_height(iced::Length::from(10)))
.push(Space::with_height(iced::Length::from(10))) .push(iced::widget::row![
.push( text("Serial Number").size(16).style(|theme: &Theme| {
iced::widget::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),
text(nothing_info.serial_number.clone()).size(16)
]
)
.push(
iced::widget::row![
text("Firmware Version").size(16).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}
),
Space::with_width(Length::Fill),
text(nothing_info.firmware_version.clone()).size(16)
]
);
}
let noise_control_mode = container(row![
text("Noise Control Mode").size(16).style(
|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),
text(nothing_info.serial_number.clone()).size(16)
])
.push(iced::widget::row![
text("Firmware Version").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill),
text(nothing_info.firmware_version.clone()).size(16)
]);
}
let noise_control_mode = container(
row![
text("Noise Control Mode").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill), Space::with_width(Length::Fill),
{ {
let state_clone = state.clone(); let state_clone = state.clone();
@@ -81,110 +71,110 @@ pub fn nothing_view<'a>(
let att_manager = att_manager_clone.clone(); let att_manager = att_manager_clone.clone();
let selected_mode_c = selected_mode.clone(); let selected_mode_c = selected_mode.clone();
let mac_s = mac.clone(); let mac_s = mac.clone();
run_async_in_thread( run_async_in_thread(async move {
async move { if let Err(e) = att_manager
if let Err(e) = att_manager.write( .write(
ATTHandles::NothingEverything, ATTHandles::NothingEverything,
&[ &[
0x55, 0x55,
0x60, 0x01, 0x60,
0x0F, 0xF0, 0x01,
0x03, 0x00, 0x0F,
0x00, 0x01, 0xF0,
selected_mode_c.to_byte(), 0x00, 0x03,
0x00, 0x00 0x00,
] 0x00,
).await { 0x01,
log::error!("Failed to set noise cancellation mode for device {}: {}", mac_s, e); selected_mode_c.to_byte(),
} 0x00,
0x00,
0x00,
],
)
.await
{
log::error!(
"Failed to set noise cancellation mode for device {}: {}",
mac_s,
e
);
} }
); });
let mut state = state_clone.clone(); let mut state = state_clone.clone();
state.anc_mode = selected_mode.clone(); state.anc_mode = selected_mode.clone();
Message::StateChanged(mac.to_string(), DeviceState::Nothing(state)) Message::StateChanged(mac.to_string(), DeviceState::Nothing(state))
} }
} },
) )
.width(Length::from(200)) .width(Length::from(200))
.input_style( .input_style(|theme: &Theme, _status| text_input::Style {
|theme: &Theme, _status| { background: Background::Color(theme.palette().primary.scale_alpha(0.2)),
text_input::Style { border: Border {
background: Background::Color(theme.palette().primary.scale_alpha(0.2)), width: 1.0,
border: Border { color: theme.palette().text.scale_alpha(0.3),
width: 1.0, radius: Radius::from(4.0),
color: theme.palette().text.scale_alpha(0.3), },
radius: Radius::from(4.0) icon: Default::default(),
}, placeholder: theme.palette().text,
icon: Default::default(), value: theme.palette().text,
placeholder: theme.palette().text, selection: Default::default(),
value: theme.palette().text, })
selection: Default::default(), .padding(iced::Padding {
}
}
)
.padding(iced::Padding{
top: 5.0, top: 5.0,
bottom: 5.0, bottom: 5.0,
left: 10.0, left: 10.0,
right: 10.0, right: 10.0,
}) })
.menu_style( .menu_style(|theme: &Theme| menu::Style {
|theme: &Theme| { background: Background::Color(theme.palette().background),
menu::Style { border: Border {
background: Background::Color(theme.palette().background), width: 1.0,
border: Border { color: theme.palette().text,
width: 1.0, radius: Radius::from(4.0),
color: theme.palette().text, },
radius: Radius::from(4.0) text_color: theme.palette().text,
}, selected_text_color: theme.palette().text,
text_color: theme.palette().text, selected_background: Background::Color(
selected_text_color: theme.palette().text, theme.palette().primary.scale_alpha(0.3),
selected_background: Background::Color(theme.palette().primary.scale_alpha(0.3)), ),
} })
}
)
} }
] ]
.align_y(iced::Alignment::Center) .align_y(iced::Alignment::Center),
) )
.padding(iced::Padding{ .padding(iced::Padding {
top: 5.0, top: 5.0,
bottom: 5.0, bottom: 5.0,
left: 18.0, left: 18.0,
right: 18.0, right: 18.0,
}) })
.style( .style(|theme: &Theme| {
|theme: &Theme| { let mut style = container::Style::default();
let mut style = container::Style::default(); style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); let mut border = Border::default();
let mut border = Border::default(); border.color = theme.palette().primary.scale_alpha(0.5);
border.color = theme.palette().primary.scale_alpha(0.5); style.border = border.rounded(16);
style.border = border.rounded(16); style
style });
}
);
container( container(column![
column![ noise_control_mode,
noise_control_mode, Space::with_height(Length::from(20)),
Space::with_height(Length::from(20)), container(information_col)
container(information_col) .style(|theme: &Theme| {
.style( let mut style = container::Style::default();
|theme: &Theme| { style.background =
let mut style = container::Style::default(); Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); let mut border = Border::default();
let mut border = Border::default(); border.color = theme.palette().text;
border.color = theme.palette().text; style.border = border.rounded(20);
style.border = border.rounded(20); style
style })
} .padding(20)
) ])
.padding(20) .padding(20)
] .center_x(Length::Fill)
) .height(Length::Fill)
.padding(20)
.center_x(Length::Fill)
.height(Length::Fill)
} }
fn run_async_in_thread<F>(fut: F) fn run_async_in_thread<F>(fut: F)

View File

@@ -42,13 +42,15 @@ impl ksni::Tray for MyTray {
} }
} else { } else {
if let Some(l) = self.battery_l if let Some(l) = self.battery_l
&& self.battery_l_status != Some(BatteryStatus::Disconnected) { && self.battery_l_status != Some(BatteryStatus::Disconnected)
levels.push(l); {
} levels.push(l);
}
if let Some(r) = self.battery_r if let Some(r) = self.battery_r
&& self.battery_r_status != Some(BatteryStatus::Disconnected) { && self.battery_r_status != Some(BatteryStatus::Disconnected)
levels.push(r); {
} levels.push(r);
}
// if let Some(c) = self.battery_c { // if let Some(c) = self.battery_c {
// if self.battery_c_status != Some(BatteryStatus::Disconnected) { // if self.battery_c_status != Some(BatteryStatus::Disconnected) {
// levels.push(c); // levels.push(c);
@@ -68,7 +70,8 @@ impl ksni::Tray for MyTray {
let settings = std::fs::read_to_string(&app_settings_path) let settings = std::fs::read_to_string(&app_settings_path)
.ok() .ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok()); .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
let text_mode = settings.clone() let text_mode = settings
.clone()
.and_then(|v| v.get("tray_text_mode").cloned()) .and_then(|v| v.get("tray_text_mode").cloned())
.and_then(|ttm| serde_json::from_value(ttm).ok()) .and_then(|ttm| serde_json::from_value(ttm).ok())
.unwrap_or(false); .unwrap_or(false);
@@ -76,20 +79,21 @@ impl ksni::Tray for MyTray {
vec![icon] vec![icon]
} }
fn tool_tip(&self) -> ToolTip { fn tool_tip(&self) -> ToolTip {
let format_component = |label: &str, level: Option<u8>, status: Option<BatteryStatus>| -> String { let format_component =
match status { |label: &str, level: Option<u8>, status: Option<BatteryStatus>| -> String {
Some(BatteryStatus::Disconnected) => format!("{}: -", label), match status {
_ => { Some(BatteryStatus::Disconnected) => format!("{}: -", label),
let pct = level.map(|b| format!("{}%", b)).unwrap_or("?".to_string()); _ => {
let suffix = if status == Some(BatteryStatus::Charging) { let pct = level.map(|b| format!("{}%", b)).unwrap_or("?".to_string());
"" let suffix = if status == Some(BatteryStatus::Charging) {
} else { ""
"" } else {
}; ""
format!("{}: {}{}", label, pct, suffix) };
format!("{}: {}{}", label, pct, suffix)
}
} }
} };
};
let l = format_component("L", self.battery_l, self.battery_l_status); let l = format_component("L", self.battery_l, self.battery_l_status);
let r = format_component("R", self.battery_r, self.battery_r_status); let r = format_component("R", self.battery_r, self.battery_r_status);
@@ -119,9 +123,10 @@ impl ksni::Tray for MyTray {
("Adaptive", 0x04), ("Adaptive", 0x04),
] ]
}; };
let selected = self.listening_mode.and_then(|mode| { let selected = self
options.iter().position(|&(_, val)| val == mode) .listening_mode
}).unwrap_or(0); .and_then(|mode| options.iter().position(|&(_, val)| val == mode))
.unwrap_or(0);
let options_clone = options.clone(); let options_clone = options.clone();
vec![ vec![
StandardItem { StandardItem {
@@ -133,19 +138,26 @@ impl ksni::Tray for MyTray {
} }
}), }),
..Default::default() ..Default::default()
}.into(), }
.into(),
RadioGroup { RadioGroup {
selected, selected,
select: Box::new(move |this: &mut Self, current| { select: Box::new(move |this: &mut Self, current| {
if let Some(tx) = &this.command_tx { if let Some(tx) = &this.command_tx {
let value = options_clone.get(current).map(|&(_, val)| val).unwrap_or(0x02); let value = options_clone
.get(current)
.map(|&(_, val)| val)
.unwrap_or(0x02);
let _ = tx.send((ControlCommandIdentifiers::ListeningMode, vec![value])); let _ = tx.send((ControlCommandIdentifiers::ListeningMode, vec![value]));
} }
}), }),
options: options.into_iter().map(|(label, _)| RadioItem { options: options
label: label.into(), .into_iter()
..Default::default() .map(|(label, _)| RadioItem {
}).collect(), label: label.into(),
..Default::default()
})
.collect(),
..Default::default() ..Default::default()
} }
.into(), .into(),
@@ -156,12 +168,16 @@ impl ksni::Tray for MyTray {
enabled: self.conversation_detect_enabled.is_some(), enabled: self.conversation_detect_enabled.is_some(),
activate: Box::new(|this: &mut Self| { activate: Box::new(|this: &mut Self| {
if let Some(tx) = &this.command_tx if let Some(tx) = &this.command_tx
&& let Some(is_enabled) = this.conversation_detect_enabled { && let Some(is_enabled) = this.conversation_detect_enabled
let new_state = !is_enabled; {
let value = if !new_state { 0x02 } else { 0x01 }; let new_state = !is_enabled;
let _ = tx.send((ControlCommandIdentifiers::ConversationDetectConfig, vec![value])); let value = if !new_state { 0x02 } else { 0x01 };
this.conversation_detect_enabled = Some(new_state); let _ = tx.send((
} ControlCommandIdentifiers::ConversationDetectConfig,
vec![value],
));
this.conversation_detect_enabled = Some(new_state);
}
}), }),
..Default::default() ..Default::default()
} }
@@ -226,7 +242,8 @@ fn generate_icon(text: &str, text_mode: bool, charging: bool) -> Icon {
let dist = (dx * dx + dy * dy).sqrt(); let dist = (dx * dx + dy * dy).sqrt();
if dist > inner_radius && dist <= outer_radius { if dist > inner_radius && dist <= outer_radius {
let angle = dy.atan2(dx); let angle = dy.atan2(dx);
let angle_from_top = (angle + std::f32::consts::PI / 2.0).rem_euclid(2.0 * std::f32::consts::PI); let angle_from_top =
(angle + std::f32::consts::PI / 2.0).rem_euclid(2.0 * std::f32::consts::PI);
if angle_from_top <= percentage * 2.0 * std::f32::consts::PI { if angle_from_top <= percentage * 2.0 * std::f32::consts::PI {
img.put_pixel(x, y, Rgba([0u8, 255u8, 0u8, 255u8])); img.put_pixel(x, y, Rgba([0u8, 255u8, 0u8, 255u8]));
} }
@@ -281,4 +298,4 @@ fn generate_icon(text: &str, text_mode: bool, charging: bool) -> Icon {
height: height as i32, height: height as i32,
data, data,
} }
} }

View File

@@ -1,22 +1,33 @@
use std::collections::HashMap; use crate::bluetooth::aacp::{
use iced::widget::button::Style; AACPEvent, BatteryComponent, BatteryStatus, ControlCommandIdentifiers,
use iced::widget::{button, column, container, pane_grid, text, Space, combo_box, row, text_input, scrollable, vertical_rule, rule, toggler}; };
use iced::{daemon, window, Background, Border, Center, Element, Font, Length, Padding, Size, Subscription, Task, Theme}; use crate::bluetooth::managers::DeviceManagers;
use std::sync::Arc; use crate::devices::enums::{
AirPodsNoiseControlMode, AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode,
NothingState,
};
use crate::ui::airpods::airpods_view;
use crate::ui::messages::BluetoothUIMessage;
use crate::ui::nothing::nothing_view;
use crate::utils::{MyTheme, get_app_settings_path, get_devices_path};
use bluer::{Address, Session}; use bluer::{Address, Session};
use iced::border::Radius; use iced::border::Radius;
use iced::overlay::menu; use iced::overlay::menu;
use iced::widget::button::Style;
use iced::widget::rule::FillMode; use iced::widget::rule::FillMode;
use iced::widget::{
Space, button, column, combo_box, container, pane_grid, row, rule, scrollable, text,
text_input, toggler, vertical_rule,
};
use iced::{
Background, Border, Center, Element, Font, Length, Padding, Size, Subscription, Task, Theme,
daemon, window,
};
use log::{debug, error}; use log::{debug, error};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedReceiver;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers, BatteryComponent, BatteryStatus};
use crate::bluetooth::managers::DeviceManagers;
use crate::devices::enums::{AirPodsNoiseControlMode, AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode, NothingState};
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;
pub fn start_ui( pub fn start_ui(
ui_rx: UnboundedReceiver<BluetoothUIMessage>, ui_rx: UnboundedReceiver<BluetoothUIMessage>,
@@ -45,11 +56,11 @@ pub struct App {
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>,
tray_text_mode: bool tray_text_mode: bool,
} }
pub struct BluetoothState { pub struct BluetoothState {
connected_devices: Vec<String> connected_devices: Vec<String>,
} }
impl BluetoothState { impl BluetoothState {
@@ -76,14 +87,14 @@ pub enum Message {
ConfirmAddDevice, ConfirmAddDevice,
CancelAddDevice, CancelAddDevice,
StateChanged(String, DeviceState), StateChanged(String, DeviceState),
TrayTextModeChanged(bool) // yes, I know I should add all settings to a struct, but I'm lazy TrayTextModeChanged(bool), // yes, I know I should add all settings to a struct, but I'm lazy
} }
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Tab { pub enum Tab {
Device(String), Device(String),
Settings, Settings,
AddDevice AddDevice,
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@@ -104,10 +115,7 @@ impl App {
let ui_rx = Arc::new(Mutex::new(ui_rx)); let ui_rx = Arc::new(Mutex::new(ui_rx));
let wait_task = Task::perform( let wait_task = Task::perform(wait_for_message(Arc::clone(&ui_rx)), |msg| msg);
wait_for_message(Arc::clone(&ui_rx)),
|msg| msg,
);
let (window, open_task) = if start_minimized { let (window, open_task) = if start_minimized {
(None, Task::none()) (None, Task::none())
@@ -123,11 +131,13 @@ impl App {
let settings = std::fs::read_to_string(&app_settings_path) let settings = std::fs::read_to_string(&app_settings_path)
.ok() .ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok()); .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
let selected_theme = settings.clone() let selected_theme = settings
.clone()
.and_then(|v| v.get("theme").cloned()) .and_then(|v| v.get("theme").cloned())
.and_then(|t| serde_json::from_value(t).ok()) .and_then(|t| serde_json::from_value(t).ok())
.unwrap_or(MyTheme::Dark); .unwrap_or(MyTheme::Dark);
let tray_text_mode = settings.clone() let tray_text_mode = settings
.clone()
.and_then(|v| v.get("tray_text_mode").cloned()) .and_then(|v| v.get("tray_text_mode").cloned())
.and_then(|ttm| serde_json::from_value(ttm).ok()) .and_then(|ttm| serde_json::from_value(ttm).ok())
.unwrap_or(false); .unwrap_or(false);
@@ -141,7 +151,6 @@ impl App {
// ("28:2D:7F:C2:05:5B".to_string(), dummy_device_state), // ("28:2D:7F:C2:05:5B".to_string(), dummy_device_state),
// ]); // ]);
let device_states = HashMap::new(); let device_states = HashMap::new();
( (
Self { Self {
@@ -178,14 +187,12 @@ impl App {
paired_devices: HashMap::new(), paired_devices: HashMap::new(),
device_states, device_states,
pending_add_device: None, pending_add_device: None,
device_type_state: combo_box::State::new(vec![ device_type_state: combo_box::State::new(vec![DeviceType::Nothing]),
DeviceType::Nothing
]),
selected_device_type: None, selected_device_type: None,
device_managers, device_managers,
tray_text_mode tray_text_mode,
}, },
Task::batch(vec![open_task, wait_task]) Task::batch(vec![open_task, wait_task]),
) )
} }
@@ -217,54 +224,44 @@ impl App {
self.selected_theme = theme; self.selected_theme = theme;
let app_settings_path = get_app_settings_path(); let app_settings_path = get_app_settings_path();
let settings = serde_json::json!({"theme": self.selected_theme, "tray_text_mode": self.tray_text_mode}); let settings = serde_json::json!({"theme": self.selected_theme, "tray_text_mode": self.tray_text_mode});
debug!("Writing settings to {}: {}", app_settings_path.to_str().unwrap() , settings); debug!(
"Writing settings to {}: {}",
app_settings_path.to_str().unwrap(),
settings
);
std::fs::write(app_settings_path, settings.to_string()).ok(); std::fs::write(app_settings_path, settings.to_string()).ok();
Task::none() Task::none()
} }
Message::CopyToClipboard(data) => { Message::CopyToClipboard(data) => iced::clipboard::write(data),
iced::clipboard::write(data)
}
Message::BluetoothMessage(ui_message) => { Message::BluetoothMessage(ui_message) => {
match ui_message { match ui_message {
BluetoothUIMessage::NoOp => { BluetoothUIMessage::NoOp => {
let ui_rx = Arc::clone(&self.ui_rx); let ui_rx = Arc::clone(&self.ui_rx);
Task::perform( Task::perform(wait_for_message(ui_rx), |msg| msg)
wait_for_message(ui_rx),
|msg| msg,
)
} }
BluetoothUIMessage::OpenWindow => { BluetoothUIMessage::OpenWindow => {
let ui_rx = Arc::clone(&self.ui_rx); let ui_rx = Arc::clone(&self.ui_rx);
let wait_task = Task::perform( let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg);
wait_for_message(ui_rx),
|msg| msg,
);
debug!("Opening main window..."); debug!("Opening main window...");
if let Some(window_id) = self.window { if let Some(window_id) = self.window {
Task::batch(vec![ Task::batch(vec![window::gain_focus(window_id), wait_task])
window::gain_focus(window_id),
wait_task,
])
} else { } else {
let mut settings = window::Settings::default(); let mut settings = window::Settings::default();
settings.min_size = Some(Size::new(400.0, 300.0)); settings.min_size = Some(Size::new(400.0, 300.0));
settings.icon = window::icon::from_file("../../assets/icon.png").ok(); settings.icon = window::icon::from_file("../../assets/icon.png").ok();
let (new_window_task, open_task) = window::open(settings); let (new_window_task, open_task) = window::open(settings);
self.window = Some(new_window_task); self.window = Some(new_window_task);
Task::batch(vec![ Task::batch(vec![open_task.map(Message::WindowOpened), wait_task])
open_task.map(Message::WindowOpened),
wait_task,
])
} }
} }
BluetoothUIMessage::DeviceConnected(mac) => { BluetoothUIMessage::DeviceConnected(mac) => {
let ui_rx = Arc::clone(&self.ui_rx); let ui_rx = Arc::clone(&self.ui_rx);
let wait_task = Task::perform( let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg);
wait_for_message(ui_rx), debug!(
|msg| msg, "Device connected: {}. Adding to connected devices list",
mac
); );
debug!("Device connected: {}. Adding to connected devices list", mac);
let mut already_connected = false; let mut already_connected = false;
for device in &self.bluetooth_state.connected_devices { for device in &self.bluetooth_state.connected_devices {
if device == &mac { if device == &mac {
@@ -281,14 +278,16 @@ impl App {
// })); // }));
let type_ = { let type_ = {
let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| { let devices_json = std::fs::read_to_string(get_devices_path())
error!("Failed to read devices file: {}", e); .unwrap_or_else(|e| {
"{}".to_string() 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); let devices_list: HashMap<String, DeviceData> =
HashMap::new() serde_json::from_str(&devices_json).unwrap_or_else(|e| {
}); error!("Deserialization failed: {}", e);
HashMap::new()
});
devices_list.get(&mac).map(|d| d.type_.clone()) devices_list.get(&mac).map(|d| d.type_.clone())
}; };
match type_ { match type_ {
@@ -300,15 +299,20 @@ impl App {
let state = aacp_manager_state.blocking_lock(); let state = aacp_manager_state.blocking_lock();
debug!("AACP manager found for AirPods device {}", mac); debug!("AACP manager found for AirPods device {}", mac);
let device_name = { let device_name = {
let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| { let devices_json = std::fs::read_to_string(get_devices_path())
error!("Failed to read devices file: {}", e); .unwrap_or_else(|e| {
"{}".to_string() 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); let devices_list: HashMap<String, DeviceData> =
HashMap::new() serde_json::from_str(&devices_json).unwrap_or_else(|e| {
}); error!("Deserialization failed: {}", e);
devices_list.get(&mac).map(|d| d.name.clone()).unwrap_or_else(|| "Unknown Device".to_string()) 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 {
device_name, device_name,
@@ -351,136 +355,144 @@ impl App {
})); }));
} }
Some(DeviceType::Nothing) => { Some(DeviceType::Nothing) => {
self.device_states.insert(mac.clone(), DeviceState::Nothing(NothingState { self.device_states.insert(
anc_mode: NothingAncMode::Off, mac.clone(),
anc_mode_state: combo_box::State::new(vec![ DeviceState::Nothing(NothingState {
NothingAncMode::Off, anc_mode: NothingAncMode::Off,
NothingAncMode::Transparency, anc_mode_state: combo_box::State::new(vec![
NothingAncMode::AdaptiveNoiseCancellation, NothingAncMode::Off,
NothingAncMode::LowNoiseCancellation, NothingAncMode::Transparency,
NothingAncMode::MidNoiseCancellation, NothingAncMode::AdaptiveNoiseCancellation,
NothingAncMode::HighNoiseCancellation NothingAncMode::LowNoiseCancellation,
]), NothingAncMode::MidNoiseCancellation,
})); NothingAncMode::HighNoiseCancellation,
]),
}),
);
} }
_ => {} _ => {}
} }
Task::batch(vec![ Task::batch(vec![wait_task])
wait_task,
])
} }
BluetoothUIMessage::DeviceDisconnected(mac) => { BluetoothUIMessage::DeviceDisconnected(mac) => {
let ui_rx = Arc::clone(&self.ui_rx); let ui_rx = Arc::clone(&self.ui_rx);
let wait_task = Task::perform( let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg);
wait_for_message(ui_rx),
|msg| msg,
);
debug!("Device disconnected: {}", mac); debug!("Device disconnected: {}", mac);
self.device_states.remove(&mac); self.device_states.remove(&mac);
Task::batch(vec![ Task::batch(vec![wait_task])
wait_task,
])
} }
BluetoothUIMessage::AACPUIEvent(mac, event) => { BluetoothUIMessage::AACPUIEvent(mac, event) => {
let ui_rx = Arc::clone(&self.ui_rx); let ui_rx = Arc::clone(&self.ui_rx);
let wait_task = Task::perform( let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg);
wait_for_message(ui_rx),
|msg| msg,
);
debug!("AACP UI Event for {}: {:?}", mac, event); debug!("AACP UI Event for {}: {:?}", mac, event);
match event { match event {
AACPEvent::ControlCommand(status) => { AACPEvent::ControlCommand(status) => match status.identifier {
match status.identifier { ControlCommandIdentifiers::ListeningMode => {
ControlCommandIdentifiers::ListeningMode => { let mode = status
let mode = status.value.first().map(AirPodsNoiseControlMode::from_byte).unwrap_or(AirPodsNoiseControlMode::Transparency); .value
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { .first()
state.noise_control_mode = mode; .map(AirPodsNoiseControlMode::from_byte)
} .unwrap_or(AirPodsNoiseControlMode::Transparency);
} if let Some(DeviceState::AirPods(state)) =
ControlCommandIdentifiers::ConversationDetectConfig => { self.device_states.get_mut(&mac)
let is_enabled = match status.value.as_slice() { {
[0x01] => true, state.noise_control_mode = mode;
[0x02] => false,
_ => {
error!("Unknown Conversation Detect Config value: {:?}", status.value);
false
}
};
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
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;
}
}
ControlCommandIdentifiers::AllowOffOption => {
let is_enabled = match status.value.as_slice() {
[0x01] => true,
[0x02] => false,
_ => {
error!("Unknown Allow Off Option value: {:?}", status.value);
false
}
};
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
state.allow_off_mode = is_enabled;
state.noise_control_state = combo_box::State::new(
{
let mut modes = vec![
AirPodsNoiseControlMode::Transparency,
AirPodsNoiseControlMode::NoiseCancellation,
AirPodsNoiseControlMode::Adaptive
];
if is_enabled {
modes.insert(0, AirPodsNoiseControlMode::Off);
}
modes
}
);
}
}
_ => {
debug!("Unhandled Control Command Status: {:?}", status);
} }
} }
} ControlCommandIdentifiers::ConversationDetectConfig => {
let is_enabled = match status.value.as_slice() {
[0x01] => true,
[0x02] => false,
_ => {
error!(
"Unknown Conversation Detect Config value: {:?}",
status.value
);
false
}
};
if let Some(DeviceState::AirPods(state)) =
self.device_states.get_mut(&mac)
{
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;
}
}
ControlCommandIdentifiers::AllowOffOption => {
let is_enabled = match status.value.as_slice() {
[0x01] => true,
[0x02] => false,
_ => {
error!(
"Unknown Allow Off Option value: {:?}",
status.value
);
false
}
};
if let Some(DeviceState::AirPods(state)) =
self.device_states.get_mut(&mac)
{
state.allow_off_mode = is_enabled;
state.noise_control_state = combo_box::State::new({
let mut modes = vec![
AirPodsNoiseControlMode::Transparency,
AirPodsNoiseControlMode::NoiseCancellation,
AirPodsNoiseControlMode::Adaptive,
];
if is_enabled {
modes.insert(0, AirPodsNoiseControlMode::Off);
}
modes
});
}
}
_ => {
debug!("Unhandled Control Command Status: {:?}", status);
}
},
AACPEvent::BatteryInfo(battery_info) => { AACPEvent::BatteryInfo(battery_info) => {
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { if let Some(DeviceState::AirPods(state)) =
self.device_states.get_mut(&mac)
{
state.battery = battery_info; state.battery = battery_info;
debug!("Updated battery info for {}: {:?}", mac, state.battery); debug!("Updated battery info for {}: {:?}", mac, state.battery);
} }
} }
_ => {} _ => {}
} }
Task::batch(vec![ Task::batch(vec![wait_task])
wait_task,
])
} }
BluetoothUIMessage::ATTNotification(mac, handle, value) => { BluetoothUIMessage::ATTNotification(mac, handle, value) => {
debug!("ATT Notification for {}: handle=0x{:04X}, value={:?}", mac, handle, value); debug!(
"ATT Notification for {}: handle=0x{:04X}, value={:?}",
mac, handle, value
);
// TODO: Handle Nothing's ANC Mode changes here // TODO: Handle Nothing's ANC Mode changes here
let ui_rx = Arc::clone(&self.ui_rx); let ui_rx = Arc::clone(&self.ui_rx);
let wait_task = Task::perform( let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg);
wait_for_message(ui_rx), Task::batch(vec![wait_task])
|msg| msg,
);
Task::batch(vec![
wait_task,
])
} }
} }
} }
@@ -504,30 +516,35 @@ impl App {
} }
Message::ConfirmAddDevice => { Message::ConfirmAddDevice => {
if let Some((name, addr)) = self.pending_add_device.take() if let Some((name, addr)) = self.pending_add_device.take()
&& let Some(type_) = self.selected_device_type.take() { && let Some(type_) = self.selected_device_type.take()
let devices_path = get_devices_path(); {
let devices_json = std::fs::read_to_string(&devices_path).unwrap_or_else(|e| { let devices_path = get_devices_path();
error!("Failed to read devices file: {}", e); let devices_json = std::fs::read_to_string(&devices_path).unwrap_or_else(|e| {
"{}".to_string() error!("Failed to read devices file: {}", e);
}); "{}".to_string()
let mut devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json).unwrap_or_else(|e| { });
let mut devices_list: HashMap<String, DeviceData> =
serde_json::from_str(&devices_json).unwrap_or_else(|e| {
error!("Deserialization failed: {}", e); error!("Deserialization failed: {}", e);
HashMap::new() HashMap::new()
}); });
devices_list.insert(addr.to_string(), DeviceData { devices_list.insert(
addr.to_string(),
DeviceData {
name, name,
type_: type_.clone(), type_: type_.clone(),
information: None information: None,
}); },
let updated_json = serde_json::to_string(&devices_list).unwrap_or_else(|e| { );
error!("Serialization failed: {}", e); let updated_json = serde_json::to_string(&devices_list).unwrap_or_else(|e| {
"{}".to_string() error!("Serialization failed: {}", e);
}); "{}".to_string()
if let Err(e) = std::fs::write(&devices_path, updated_json) { });
error!("Failed to write devices file: {}", e); if let Err(e) = std::fs::write(&devices_path, updated_json) {
} error!("Failed to write devices file: {}", e);
self.selected_tab = Tab::Device(addr.to_string());
} }
self.selected_tab = Tab::Device(addr.to_string());
}
Task::none() Task::none()
} }
Message::CancelAddDevice => { Message::CancelAddDevice => {
@@ -539,39 +556,44 @@ impl App {
self.device_states.insert(mac.clone(), state); self.device_states.insert(mac.clone(), state);
// if airpods, update the noise control state combo box based on allow off mode // if airpods, update the noise control state combo box based on allow off mode
let type_ = { let type_ = {
let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| { let devices_json =
error!("Failed to read devices file: {}", e); std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| {
"{}".to_string() 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); let devices_list: HashMap<String, DeviceData> =
HashMap::new() serde_json::from_str(&devices_json).unwrap_or_else(|e| {
}); error!("Deserialization failed: {}", e);
HashMap::new()
});
devices_list.get(&mac).map(|d| d.type_.clone()) devices_list.get(&mac).map(|d| d.type_.clone())
}; };
if let Some(DeviceType::AirPods) = type_ if let Some(DeviceType::AirPods) = type_
&& let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { && let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac)
state.noise_control_state = combo_box::State::new( {
{ state.noise_control_state = combo_box::State::new({
let mut modes = vec![ let mut modes = vec![
AirPodsNoiseControlMode::Transparency, AirPodsNoiseControlMode::Transparency,
AirPodsNoiseControlMode::NoiseCancellation, AirPodsNoiseControlMode::NoiseCancellation,
AirPodsNoiseControlMode::Adaptive AirPodsNoiseControlMode::Adaptive,
]; ];
if state.allow_off_mode { if state.allow_off_mode {
modes.insert(0, AirPodsNoiseControlMode::Off); modes.insert(0, AirPodsNoiseControlMode::Off);
} }
modes modes
} });
); }
}
Task::none() Task::none()
} }
Message::TrayTextModeChanged(is_enabled) => { Message::TrayTextModeChanged(is_enabled) => {
self.tray_text_mode = is_enabled; self.tray_text_mode = is_enabled;
let app_settings_path = get_app_settings_path(); let app_settings_path = get_app_settings_path();
let settings = serde_json::json!({"theme": self.selected_theme, "tray_text_mode": self.tray_text_mode}); let settings = serde_json::json!({"theme": self.selected_theme, "tray_text_mode": self.tray_text_mode});
debug!("Writing settings to {}: {}", app_settings_path.to_str().unwrap() , settings); debug!(
"Writing settings to {}: {}",
app_settings_path.to_str().unwrap(),
settings
);
std::fs::write(app_settings_path, settings.to_string()).ok(); std::fs::write(app_settings_path, settings.to_string()).ok();
Task::none() Task::none()
} }
@@ -583,10 +605,11 @@ impl App {
error!("Failed to read devices file: {}", e); error!("Failed to read devices file: {}", e);
"{}".to_string() "{}".to_string()
}); });
let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json).unwrap_or_else(|e| { let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json)
error!("Deserialization failed: {}", e); .unwrap_or_else(|e| {
HashMap::new() error!("Deserialization failed: {}", e);
}); HashMap::new()
});
let pane_grid = pane_grid::PaneGrid::new(&self.panes, |_pane_id, pane, _is_maximized| { let pane_grid = pane_grid::PaneGrid::new(&self.panes, |_pane_id, pane, _is_maximized| {
match pane { match pane {
Pane::Sidebar => { Pane::Sidebar => {
@@ -766,7 +789,7 @@ impl App {
] ]
) )
} }
Pane::Content => { Pane::Content => {
let device_managers = self.device_managers.blocking_read(); let device_managers = self.device_managers.blocking_read();
let content = match &self.selected_tab { let content = match &self.selected_tab {
@@ -783,7 +806,7 @@ impl App {
debug!("Rendering device view for {}: type={:?}, state={:?}", id, device_type, device_state); debug!("Rendering device view for {}: type={:?}, state={:?}", id, device_type, device_state);
match device_type { match device_type {
Some(DeviceType::AirPods) => { Some(DeviceType::AirPods) => {
device_state.as_ref().and_then(|state| { device_state.as_ref().and_then(|state| {
match state { match state {
DeviceState::AirPods(state) => { DeviceState::AirPods(state) => {
@@ -1023,7 +1046,7 @@ impl App {
); );
} }
item_col = item_col.push(row(row_elements).align_y(Center)); item_col = item_col.push(row(row_elements).align_y(Center));
if let Some((_, pending_addr)) = &self.pending_add_device if let Some((_, pending_addr)) = &self.pending_add_device
&& pending_addr == &device.1 { && pending_addr == &device.1 {
item_col = item_col.push( item_col = item_col.push(
@@ -1101,7 +1124,6 @@ impl App {
.width(Length::Fill) .width(Length::Fill)
); );
} }
list_col = list_col.push( list_col = list_col.push(
container(item_col) container(item_col)
.padding(8) .padding(8)
@@ -1158,9 +1180,7 @@ impl App {
} }
} }
async fn wait_for_message( async fn wait_for_message(ui_rx: Arc<Mutex<UnboundedReceiver<BluetoothUIMessage>>>) -> Message {
ui_rx: Arc<Mutex<UnboundedReceiver<BluetoothUIMessage>>>,
) -> Message {
let mut rx = ui_rx.lock().await; let mut rx = ui_rx.lock().await;
match rx.recv().await { match rx.recv().await {
Some(msg) => Message::BluetoothMessage(msg), Some(msg) => Message::BluetoothMessage(msg),
@@ -1180,7 +1200,12 @@ async fn load_paired_devices() -> HashMap<String, Address> {
let device = adapter.device(addr).ok().unwrap(); let device = adapter.device(addr).ok().unwrap();
let paired = device.is_paired().await.ok().unwrap(); let paired = device.is_paired().await.ok().unwrap();
if paired { if paired {
let name = device.name().await.ok().flatten().unwrap_or_else(|| "Unknown".to_string()); let name = device
.name()
.await
.ok()
.flatten()
.unwrap_or_else(|| "Unknown".to_string());
devices.insert(name, addr); devices.insert(name, addr);
} }
} }

View File

@@ -1,6 +1,6 @@
use aes::Aes128;
use aes::cipher::generic_array::GenericArray; use aes::cipher::generic_array::GenericArray;
use aes::cipher::{BlockEncrypt, KeyInit}; use aes::cipher::{BlockEncrypt, KeyInit};
use aes::Aes128;
use iced::Theme; use iced::Theme;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
@@ -8,19 +8,25 @@ use std::path::PathBuf;
pub fn get_devices_path() -> PathBuf { pub fn get_devices_path() -> PathBuf {
let data_dir = std::env::var("XDG_DATA_HOME") let data_dir = std::env::var("XDG_DATA_HOME")
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default()));
PathBuf::from(data_dir).join("librepods").join("devices.json") PathBuf::from(data_dir)
.join("librepods")
.join("devices.json")
} }
pub fn get_preferences_path() -> PathBuf { pub fn get_preferences_path() -> PathBuf {
let config_dir = std::env::var("XDG_CONFIG_HOME") let config_dir = std::env::var("XDG_CONFIG_HOME")
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default()));
PathBuf::from(config_dir).join("librepods").join("preferences.json") PathBuf::from(config_dir)
.join("librepods")
.join("preferences.json")
} }
pub fn get_app_settings_path() -> PathBuf { pub fn get_app_settings_path() -> PathBuf {
let config_dir = std::env::var("XDG_CONFIG_HOME") let config_dir = std::env::var("XDG_CONFIG_HOME")
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default()));
PathBuf::from(config_dir).join("librepods").join("app_settings.json") PathBuf::from(config_dir)
.join("librepods")
.join("app_settings.json")
} }
fn e(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] { fn e(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
@@ -127,4 +133,4 @@ impl From<MyTheme> for Theme {
MyTheme::Ferra => Theme::Ferra, MyTheme::Ferra => Theme::Ferra,
} }
} }
} }