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::enums::{DeviceData, DeviceInformation, DeviceType};
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 CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -218,7 +221,7 @@ pub enum BatteryComponent {
Headphone = 1,
Left = 4,
Right = 2,
Case = 8
Case = 8,
}
#[repr(u8)]
@@ -226,7 +229,7 @@ pub enum BatteryComponent {
pub enum BatteryStatus {
Charging = 1,
NotCharging = 2,
Disconnected = 4
Disconnected = 4,
}
#[repr(u8)]
@@ -235,7 +238,7 @@ pub enum EarDetectionStatus {
InEar = 0x00,
OutOfEar = 0x01,
InCase = 0x02,
Disconnected = 0x03
Disconnected = 0x03,
}
impl AudioSourceType {
@@ -291,7 +294,8 @@ pub struct AirPodsLEKeys {
pub struct AACPManagerState {
pub sender: Option<mpsc::Sender<Vec<u8>>>,
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 old_connected_devices: Vec<ConnectedDevice>,
pub connected_devices: Vec<ConnectedDevice>,
@@ -307,11 +311,10 @@ pub struct AACPManagerState {
impl AACPManagerState {
fn new() -> Self {
let devices: HashMap<String, DeviceData> =
std::fs::read_to_string(get_devices_path())
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
let devices: HashMap<String, DeviceData> = std::fs::read_to_string(get_devices_path())
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
AACPManagerState {
sender: None,
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 {
Ok(Ok(s)) => Arc::new(s),
Ok(Err(e)) => {
error!("L2CAP connect failed: {}", e);
return;
}
Err(_) => {
error!("L2CAP connect timed out");
return;
}
};
let seq_packet =
match tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await {
Ok(Ok(s)) => Arc::new(s),
Ok(Err(e)) => {
error!("L2CAP connect failed: {}", e);
return;
}
Err(_) => {
error!("L2CAP connect timed out");
return;
}
};
// Wait for connection to be fully established
let start = Instant::now();
@@ -381,7 +385,8 @@ impl AACPManager {
Ok(peer) if peer.cid != 0 => break,
Ok(_) => { /* still waiting */ }
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.");
return;
}
@@ -438,19 +443,40 @@ impl AACPManager {
let mut state = self.state.lock().await;
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;
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
if let Some(status) = state.control_command_status_list.iter().find(|s| s.identifier == identifier) {
let _ = state.control_command_subscribers.get(&identifier).unwrap().last().unwrap().send(status.value.clone());
if let Some(status) = state
.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]) {
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;
}
if packet.len() < 5 {
@@ -469,7 +495,10 @@ impl AACPManager {
}
let count = payload[2] as usize;
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;
}
let mut batteries = Vec::with_capacity(count);
@@ -495,7 +524,7 @@ impl AACPManager {
error!("Unknown battery status: {:#04x}", payload[base_index + 3]);
continue;
}
}
},
});
}
let mut state = self.state.lock().await;
@@ -520,9 +549,16 @@ impl AACPManager {
};
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;
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();
} else {
state.control_command_status_list.push(status.clone());
@@ -538,9 +574,16 @@ impl AACPManager {
if let Some(ref tx) = state.event_tx {
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 {
error!("Unknown Control Command identifier: {:#04x}", identifier_byte);
error!(
"Unknown Control Command identifier: {:#04x}",
identifier_byte
);
}
}
opcodes::EAR_DETECTION => {
@@ -570,12 +613,21 @@ impl AACPManager {
let mut state = self.state.lock().await;
state.old_ear_detection_status = state.ear_detection_status.clone();
state.ear_detection_status = statuses.clone();
if let Some(ref tx) = state.event_tx {
debug!("Sending Ear Detection event: old: {:?}, new: {:?}", state.old_ear_detection_status, statuses);
let _ = tx.send(AACPEvent::EarDetection(state.old_ear_detection_status.clone(), statuses));
debug!(
"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 => {
if packet.len() == 10 {
@@ -587,7 +639,10 @@ impl AACPManager {
}
info!("Received Conversation Awareness: {}", status);
} else {
info!("Received Conversation Awareness packet with unexpected length: {}", packet.len());
info!(
"Received Conversation Awareness packet with unexpected length: {}",
packet.len()
);
}
}
opcodes::INFORMATION => {
@@ -637,25 +692,30 @@ impl AACPManager {
};
let mut state = self.state.lock().await;
if let Some(mac) = state.airpods_mac
&& 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()));
}
&& 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()));
}
let json = serde_json::to_string(&state.devices).unwrap();
if let Some(parent) = get_devices_path().parent()
&& let Err(e) = tokio::fs::create_dir_all(&parent).await {
error!("Failed to create directory for devices: {}", e);
return;
}
&& let Err(e) = tokio::fs::create_dir_all(&parent).await
{
error!("Failed to create directory for devices: {}", e);
return;
}
if let Err(e) = tokio::fs::write(&get_devices_path(), json).await {
error!("Failed to save devices: {}", e);
}
info!("Received Information: {:?}", info);
},
}
opcodes::PROXIMITY_KEYS_RSP => {
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;
}
let key_count = payload[2] as usize;
@@ -664,65 +724,77 @@ impl AACPManager {
let mut keys = Vec::new();
for _ in 0..key_count {
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;
}
let key_type = payload[offset];
let key_length = payload[offset + 2] as usize;
offset += 4;
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;
}
let key_data = payload[offset..offset + key_length].to_vec();
keys.push((key_type, key_data));
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;
for (key_type, key_data) in &keys {
if let Some(kt) = ProximityKeyType::from_u8(*key_type)
&& 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 Some(mac) = state.airpods_mac
{
let mac_str = mac.to_string();
let device_data =
state.devices.entry(mac_str.clone()).or_insert(DeviceData {
name: mac_str.clone(),
type_: DeviceType::AirPods,
information: None,
});
match kt {
ProximityKeyType::Irk => {
match device_data.information.as_mut() {
Some(DeviceInformation::AirPods(info)) => {
info.le_keys.irk = hex::encode(key_data);
}
_ => {
error!("Device information is not AirPods for adding LE IRK.");
}
}
match kt {
ProximityKeyType::Irk => match device_data.information.as_mut() {
Some(DeviceInformation::AirPods(info)) => {
info.le_keys.irk = hex::encode(key_data);
}
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.");
}
}
_ => {
error!("Device information is not AirPods for adding LE IRK.");
}
}
},
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();
if let Some(parent) = get_devices_path().parent()
&& let Err(e) = tokio::fs::create_dir_all(&parent).await {
error!("Failed to create directory for devices: {}", e);
return;
}
&& let Err(e) = tokio::fs::create_dir_all(&parent).await
{
error!("Failed to create directory for devices: {}", e);
return;
}
if let Err(e) = tokio::fs::write(&get_devices_path(), json).await {
error!("Failed to save devices: {}", e);
}
},
}
opcodes::STEM_PRESS => info!("Received Stem Press packet."),
opcodes::AUDIO_SOURCE => {
if payload.len() < 9 {
@@ -744,12 +816,18 @@ impl AACPManager {
}
opcodes::CONNECTED_DEVICES => {
if payload.len() < 3 {
error!("Connected Devices packet too short: {}", hex::encode(payload));
error!(
"Connected Devices packet too short: {}",
hex::encode(payload)
);
return;
}
let count = payload[2] as usize;
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;
}
let mut devices = Vec::with_capacity(count);
@@ -757,17 +835,30 @@ impl AACPManager {
let base = 5 + i * 8;
let mac = format!(
"{: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 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;
state.old_connected_devices = state.connected_devices.clone();
state.connected_devices = devices.clone();
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);
}
@@ -782,7 +873,7 @@ impl AACPManager {
}
}
opcodes::EQ_DATA => {
debug!("Received EQ Data");
debug!("Received EQ Data");
}
_ => debug!("Received unknown packet with opcode {:#04x}", opcode),
}
@@ -805,17 +896,18 @@ impl AACPManager {
pub async fn send_handshake(&self) -> Result<()> {
let packet = [
0x00, 0x00, 0x04, 0x00,
0x01, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
];
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 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(0x00);
let packet = [opcode.as_slice(), data.as_slice()].concat();
@@ -833,8 +925,12 @@ impl AACPManager {
packet.extend_from_slice(name_bytes);
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 mut data = vec![identifier as u8];
for i in 0..4 {
@@ -844,10 +940,17 @@ impl AACPManager {
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 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(&[0x68, 0x00]);
@@ -879,7 +982,10 @@ impl AACPManager {
pub async fn send_hijack_request(&self, target_mac_address: &str) -> Result<()> {
let opcode = [opcodes::SMART_ROUTING, 0x00];
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(&[0x62, 0x00]);
buffer.extend_from_slice(&[0x01, 0xE5]);
@@ -907,10 +1013,18 @@ impl AACPManager {
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 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(&[0x82, 0x00]);
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<()> {
let opcode = [opcodes::SMART_ROUTING, 0x00];
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(&[0x7E, 0x00]);
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<()> {
let opcode = [opcodes::SMART_ROUTING, 0x00];
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(&[0x59, 0x00]);
buffer.extend_from_slice(&[0x01, 0xE3]);
@@ -999,10 +1119,17 @@ impl AACPManager {
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 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(&[0x4E, 0x00]);
buffer.extend_from_slice(&[0x01, 0xE5]);
@@ -1027,10 +1154,8 @@ impl AACPManager {
}
pub async fn send_some_packet(&self) -> Result<()> {
self.send_data_packet(&[
0x29, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
]).await
self.send_data_packet(&[0x29, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
.await
}
}
@@ -1049,7 +1174,9 @@ async fn recv_thread(manager: AACPManager, sp: Arc<SeqPacket>) {
}
Err(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;
state.owns = false;
state.connected_devices.clear();

View File

@@ -1,12 +1,12 @@
use bluer::l2cap::{SocketAddr, Socket, SeqPacket};
use bluer::{Address, AddressType, Result, Error};
use log::{info, error, debug};
use bluer::l2cap::{SeqPacket, Socket, SocketAddr};
use bluer::{Address, AddressType, Error, Result};
use hex;
use log::{debug, error, info};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, mpsc};
use tokio::task::JoinSet;
use tokio::time::{sleep, Duration, Instant};
use std::collections::HashMap;
use hex;
use tokio::time::{Duration, Instant, sleep};
const PSM_ATT: u16 = 0x001F;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -25,7 +25,7 @@ pub enum ATTHandles {
AirPodsLoudSoundReduction = 0x1B,
AirPodsHearingAid = 0x2A,
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)]
@@ -43,7 +43,7 @@ impl From<ATTHandles> for ATTCCCDHandles {
ATTHandles::AirPodsLoudSoundReduction => ATTCCCDHandles::LoudSoundReduction,
ATTHandles::AirPodsHearingAid => ATTCCCDHandles::HearingAid,
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 {
ATTManagerState {
sender: None,
listeners: HashMap::new()
listeners: HashMap::new(),
}
}
}
@@ -82,11 +82,15 @@ impl ATTManager {
}
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 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 {
Ok(Ok(s)) => Arc::new(s),
Ok(Err(e)) => {
@@ -95,7 +99,10 @@ impl ATTManager {
}
Err(_) => {
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(_) => {}
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.");
return Err(e.into());
}
@@ -115,7 +123,10 @@ impl ATTManager {
}
if start.elapsed() >= CONNECT_TIMEOUT {
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;
}
@@ -180,11 +191,17 @@ impl ATTManager {
if let Some(sender) = &state.sender {
sender.send(data.to_vec()).await.map_err(|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 {
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(None) => Err(Error::from(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"Response channel closed"
"Response channel closed",
))),
Err(_) => Err(Error::from(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Response timeout"
"Response timeout",
))),
}
}

View File

@@ -1,6 +1,6 @@
use std::io::Error;
use bluer::Adapter;
use log::debug;
use std::io::Error;
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();
@@ -10,15 +10,22 @@ pub(crate) async fn find_connected_airpods(adapter: &Adapter) -> bluer::Result<b
let device = adapter.device(addr)?;
if device.is_connected().await.unwrap_or(false)
&& let Ok(uuids) = device.uuids().await
&& let Some(uuids) = uuids
&& uuids.iter().any(|u| *u == target_uuid) {
return Ok(device);
}
&& let Some(uuids) = uuids
&& uuids.iter().any(|u| *u == target_uuid)
{
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 mut devices = Vec::new();
for addr in addrs {
@@ -35,5 +42,8 @@ pub async fn find_other_managed_devices(adapter: &Adapter, managed_macs: Vec<Str
return Ok(devices);
}
debug!("No other managed devices found");
Err(bluer::Error::from(Error::new(std::io::ErrorKind::NotFound, "No other managed devices found")))
}
Err(bluer::Error::from(Error::new(
std::io::ErrorKind::NotFound,
"No other managed devices found",
)))
}

View File

@@ -1,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::{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 hex;
use log::{debug, info};
use serde_json;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::Arc;
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] {
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 {
let rpa: Vec<u8> = addr.split(':')
let rpa: Vec<u8> = addr
.split(':')
.map(|s| u8::from_str_radix(s, 16).unwrap())
.collect::<Vec<_>>()
.into_iter()
@@ -38,7 +39,10 @@ fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool {
let hash_slice = &rpa[0..3];
let hash: [u8; 3] = hash_slice.try_into().unwrap();
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
}
@@ -47,20 +51,19 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
let adapter = session.default_adapter().await?;
adapter.set_powered(true).await?;
let all_devices: HashMap<String, DeviceData> =
std::fs::read_to_string(get_devices_path())
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
let all_devices: HashMap<String, DeviceData> = std::fs::read_to_string(get_devices_path())
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
let mut verified_macs: HashMap<Address, String> = HashMap::new();
let mut failed_macs: HashSet<Address> = HashSet::new();
let connecting_macs = Arc::new(Mutex::new(HashSet::<Address>::new()));
let pattern = Pattern {
data_type: 0xFF, // Manufacturer specific data
data_type: 0xFF, // Manufacturer specific data
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?;
@@ -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 {
if device_data.type_ == DeviceType::AirPods
&& let Some(DeviceInformation::AirPods(info)) = &device_data.information
&& let Ok(irk_bytes) = hex::decode(&info.le_keys.irk)
&& 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);
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;
}
}
&& let Ok(irk_bytes) = hex::decode(&info.le_keys.irk)
&& 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
);
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 {
@@ -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
&& let Some(device_data) = all_devices.get(mac)
&& let Some(DeviceInformation::AirPods(info)) = &device_data.information
&& let Ok(enc_key_bytes) = hex::decode(&info.le_keys.enc_key)
&& enc_key_bytes.len() == 16 {
matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap());
}
&& let Some(DeviceInformation::AirPods(info)) = &device_data.information
&& let Ok(enc_key_bytes) = hex::decode(&info.le_keys.enc_key)
&& enc_key_bytes.len() == 16
{
matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap());
}
if matched_airpods_mac.is_some() {
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 Some(enc_key) = &matched_enc_key
&& let Some(apple_data) = data.get(&76)
&& 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);
debug!("Decrypted data from airpods_mac {}: {}",
matched_airpods_mac.as_ref().unwrap_or(&"unknown".to_string()),
hex::encode(decrypted));
&& 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);
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;
debug!("Connection state: {}", connection_state);
if connection_state == 0x00 {
let pref_path = get_preferences_path();
let preferences: HashMap<String, HashMap<String, bool>> =
std::fs::read_to_string(&pref_path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
let auto_connect = preferences.get(matched_airpods_mac.as_ref().unwrap())
.and_then(|prefs| prefs.get("autoConnect"))
.copied()
.unwrap_or(true);
debug!("Auto-connect preference for {}: {}", matched_airpods_mac.as_ref().unwrap(), 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;
let connection_state = apple_data[10] as usize;
debug!("Connection state: {}", connection_state);
if connection_state == 0x00 {
let pref_path = get_preferences_path();
let preferences: HashMap<
String,
HashMap<String, bool>,
> = std::fs::read_to_string(&pref_path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
let auto_connect = preferences
.get(matched_airpods_mac.as_ref().unwrap())
.and_then(|prefs| prefs.get("autoConnect"))
.copied()
.unwrap_or(true);
debug!(
"Auto-connect preference for {}: {}",
matched_airpods_mac.as_ref().unwrap(),
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();
// 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);
}
}
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());
}
Err(e) => {
info!(
"Failed to execute bluetoothctl to connect to AirPods {}: {}",
matched_airpods_mac.as_ref().unwrap(),
e
);
}
}
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);
info!(
"Auto-connect is disabled for {}, not attempting to connect.",
matched_airpods_mac.as_ref().unwrap()
);
}
}
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::att::ATTManager;
use std::sync::Arc;
pub struct DeviceManagers {
att: Option<Arc<ATTManager>>,
@@ -9,16 +9,25 @@ pub struct DeviceManagers {
impl DeviceManagers {
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 {
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
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) {

View File

@@ -1,5 +1,5 @@
pub(crate) mod discovery;
pub mod aacp;
pub mod att;
pub(crate) mod discovery;
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::{AACPEvent, AACPManager, AirPodsLEKeys, ProximityKeyType};
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::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 mac_address: Address,
@@ -33,7 +33,9 @@ impl AirPodsDevice {
// att_manager.connect(mac_address).await.expect("Failed to connect ATT");
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");
@@ -61,24 +63,39 @@ impl AirPodsDevice {
}
info!("Requesting Proximity Keys: IRK and ENC_KEY");
if let Err(e) = aacp_manager.send_proximity_keys_request(
vec![ProximityKeyType::Irk, ProximityKeyType::EncKey],
).await {
if let Err(e) = aacp_manager
.send_proximity_keys_request(vec![ProximityKeyType::Irk, ProximityKeyType::EncKey])
.await
{
error!("Failed to request proximity keys: {}", e);
}
let session = bluer::Session::new().await.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 session = bluer::Session::new()
.await
.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 (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel();
aacp_manager.set_event_channel(tx).await;
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();
@@ -92,50 +109,76 @@ impl AirPodsDevice {
let mc_listener = media_controller.lock().await;
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);
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();
tokio::spawn(async move {
while let Some(value) = listening_mode_rx.recv().await {
if let Some(handle) = &tray_handle_clone {
handle.update(|tray: &mut MyTray| {
tray.listening_mode = Some(value[0]);
}).await;
handle
.update(|tray: &mut MyTray| {
tray.listening_mode = Some(value[0]);
})
.await;
}
}
});
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();
tokio::spawn(async move {
while let Some(value) = allow_off_rx.recv().await {
if let Some(handle) = &tray_handle_clone {
handle.update(|tray: &mut MyTray| {
tray.allow_off_option = Some(value[0]);
}).await;
handle
.update(|tray: &mut MyTray| {
tray.allow_off_option = Some(value[0]);
})
.await;
}
}
});
let (conversation_detect_tx, mut conversation_detect_rx) = tokio::sync::mpsc::unbounded_channel();
aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::ConversationDetectConfig, conversation_detect_tx).await;
let (conversation_detect_tx, mut conversation_detect_rx) =
tokio::sync::mpsc::unbounded_channel();
aacp_manager
.subscribe_to_control_command(
ControlCommandIdentifiers::ConversationDetectConfig,
conversation_detect_tx,
)
.await;
let tray_handle_clone = tray_handle.clone();
tokio::spawn(async move {
while let Some(value) = conversation_detect_rx.recv().await {
if let Some(handle) = &tray_handle_clone {
handle.update(|tray: &mut MyTray| {
tray.conversation_detect_enabled = Some(value[0] == 0x01);
}).await;
handle
.update(|tray: &mut MyTray| {
tray.conversation_detect_enabled = Some(value[0] == 0x01);
})
.await;
}
}
});
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();
tokio::spawn(async move {
while let Some(value) = owns_connection_rx.recv().await {
@@ -158,46 +201,62 @@ impl AirPodsDevice {
let event_clone = event.clone();
match event {
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;
debug!("Calling handle_ear_detection with old_status: {:?}, new_status: {:?}", old_status, new_status);
controller.handle_ear_detection(old_status, new_status).await;
debug!(
"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) => {
debug!("Received BatteryInfo event: {:?}", battery_info);
if let Some(handle) = &tray_handle {
handle.update(|tray: &mut MyTray| {
for b in &battery_info {
match b.component as u8 {
0x01 => {
tray.battery_headphone = Some(b.level);
tray.battery_headphone_status = Some(b.status);
handle
.update(|tray: &mut MyTray| {
for b in &battery_info {
match b.component as u8 {
0x01 => {
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");
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");
}
AACPEvent::ControlCommand(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");
}
AACPEvent::ConversationalAwareness(status) => {
@@ -208,37 +267,60 @@ impl AirPodsDevice {
AACPEvent::ConnectedDevices(old_devices, new_devices) => {
let local_mac = local_mac_events.clone();
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;
not_in_old && not_local
});
for device in new_devices_filtered {
info!("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);
info!(
"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 local_mac_clone = local_mac.clone();
let device_mac_clone = device.mac.clone();
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);
}
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);
}
});
}
}
AACPEvent::OwnershipToFalseRequest => {
info!("Received ownership to false request. Setting ownership to false and pausing media.");
let _ = command_tx_clone.send((ControlCommandIdentifiers::OwnsConnection, vec![0x00]));
info!(
"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;
controller.pause_all_media().await;
controller.deactivate_a2dp_profile().await;
}
_ => {
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");
}
}
@@ -268,5 +350,5 @@ pub struct AirPodsInformation {
pub left_serial_number: String,
pub right_serial_number: 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::devices::airpods::AirPodsInformation;
use crate::devices::nothing::NothingInformation;
use iced::widget::combo_box;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DeviceType {
AirPods,
Nothing
Nothing,
}
impl Display for DeviceType {
@@ -21,12 +20,11 @@ impl Display for DeviceType {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data")]
pub enum DeviceInformation {
AirPods(AirPodsInformation),
Nothing(NothingInformation)
Nothing(NothingInformation),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -36,7 +34,6 @@ pub struct DeviceData {
pub information: Option<DeviceInformation>,
}
#[derive(Clone, Debug)]
pub enum DeviceState {
AirPods(AirPodsState),
@@ -60,7 +57,7 @@ pub struct AirPodsState {
pub conversation_awareness_enabled: bool,
pub personalized_volume_enabled: bool,
pub allow_off_mode: bool,
pub battery: Vec<BatteryInfo>
pub battery: Vec<BatteryInfo>,
}
#[derive(Clone, Debug)]
@@ -68,7 +65,7 @@ pub enum AirPodsNoiseControlMode {
Off,
NoiseCancellation,
Transparency,
Adaptive
Adaptive,
}
impl Display for AirPodsNoiseControlMode {
@@ -115,7 +112,7 @@ pub enum NothingAncMode {
MidNoiseCancellation,
HighNoiseCancellation,
AdaptiveNoiseCancellation,
Transparency
Transparency,
}
impl Display for NothingAncMode {
@@ -152,4 +149,4 @@ impl NothingAncMode {
NothingAncMode::Off => 0x05,
}
}
}
}

View File

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

View File

@@ -1,50 +1,59 @@
mod bluetooth;
mod devices;
mod media_controller;
mod ui;
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 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 tokio::sync::mpsc::unbounded_channel;
use tokio::sync::RwLock;
use crate::bluetooth::managers::DeviceManagers;
use crate::devices::enums::DeviceData;
use crate::ui::messages::BluetoothUIMessage;
use crate::ui::tray::MyTray;
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)]
struct Args {
#[arg(long, short='d', help="Enable debug logging")]
#[arg(long, short = 'd', help = "Enable debug logging")]
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,
#[arg(long, help="Start the application minimized to tray")]
#[arg(long, help = "Start the application minimized to tray")]
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,
#[arg(long, short='v', help="Show application version and exit")]
version: bool
#[arg(long, short = 'v', help = "Show application version and exit")]
version: bool,
}
fn main() -> iced::Result {
let args = Args::parse();
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(());
}
@@ -54,23 +63,33 @@ fn main() -> iced::Result {
if wayland_display {
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();
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();
std::thread::spawn(|| {
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)
}
async fn async_main(
ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>,
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
@@ -84,10 +103,11 @@ async fn async_main(
log::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| {
log::error!("Deserialization failed: {}", e);
HashMap::new()
});
let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json)
.unwrap_or_else(|e| {
log::error!("Deserialization failed: {}", e);
HashMap::new()
});
for (mac, device_data) in devices_list.iter() {
if device_data.type_ == devices::enums::DeviceType::Nothing {
managed_devices_mac.push(mac.clone());
@@ -134,9 +154,13 @@ async fn async_main(
info!("Checking for connected devices...");
match find_connected_airpods(&adapter).await {
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);
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 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)
.set_aacp(airpods_device.aacp_manager);
drop(managers);
ui_tx.send(BluetoothUIMessage::DeviceConnected(device.address().to_string())).unwrap();
ui_tx
.send(BluetoothUIMessage::DeviceConnected(
device.address().to_string(),
))
.unwrap();
}
Err(_) => {
info!("No connected AirPods found.");
@@ -157,20 +185,29 @@ async fn async_main(
Ok(devices) => {
for device in devices {
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 ui_tx_clone = ui_tx.clone();
let device_managers = device_managers.clone();
tokio::spawn(async move {
let mut managers = device_managers.write().await;
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());
managers
.entry(addr_str.clone())
.or_insert(dev_managers)
.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)
});
@@ -178,7 +215,9 @@ async fn async_main(
}
Err(e) => {
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);
} else {
info!("No other managed devices found.");
@@ -189,28 +228,42 @@ async fn async_main(
let conn = Connection::new_system()?;
let rule = MatchRule::new_signal("org.freedesktop.DBus.Properties", "PropertiesChanged");
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_") {
return true;
}
// 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;
};
if iface != "org.bluez.Device1" {
return true;
}
let Some(connected_var) = changed.get("Connected") else { return true; };
let Some(is_connected) = connected_var.0.as_ref().as_u64() else { return true; };
let Some(connected_var) = changed.get("Connected") else {
return true;
};
let Some(is_connected) = connected_var.0.as_ref().as_u64() else {
return true;
};
if is_connected == 0 {
return true;
}
let proxy = conn.with_proxy("org.bluez", path, std::time::Duration::from_millis(5000));
let Ok(uuids) = proxy.get::<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 Ok(addr_str) = proxy.get::<String>("org.bluez.Device1", "Address") else { return true; };
let Ok(addr) = addr_str.parse::<Address>() else { return true; };
let Ok(addr_str) = proxy.get::<String>("org.bluez.Device1", "Address") else {
return true;
};
let Ok(addr) = addr_str.parse::<Address>() else {
return true;
};
if managed_devices_mac.contains(&addr_str) {
info!("Managed device connected: {}, initializing", addr_str);
@@ -227,7 +280,9 @@ async fn async_main(
.or_insert(dev_managers)
.set_att(dev.att_manager);
drop(managers);
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
ui_tx_clone
.send(BluetoothUIMessage::DeviceConnected(addr_str.clone()))
.unwrap();
});
}
return true;
@@ -236,7 +291,9 @@ async fn async_main(
if !uuids.iter().any(|u| u.to_lowercase() == target_uuid) {
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);
let handle_clone = tray_handle.clone();
let ui_tx_clone = ui_tx.clone();
@@ -251,7 +308,9 @@ async fn async_main(
.or_insert(dev_managers)
.set_aacp(airpods_device.aacp_manager);
drop(managers);
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
ui_tx_clone
.send(BluetoothUIMessage::DeviceConnected(addr_str.clone()))
.unwrap();
});
true
})?;
@@ -260,4 +319,4 @@ async fn async_main(
loop {
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 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 crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers};
use iced::Alignment::End;
use iced::border::Radius;
use iced::overlay::menu;
use iced::widget::button::Style;
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 std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use tokio::runtime::Runtime;
use crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers};
// use crate::bluetooth::att::ATTManager;
use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation, DeviceState};
use crate::ui::window::Message;
@@ -29,26 +31,20 @@ pub fn airpods_view<'a>(
let rename_input = container(
row![
Space::with_width(10),
text("Name").size(16).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}
),
text("Name").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill),
text_input(
"",
&state.device_name
)
.padding(Padding{
top: 5.0,
bottom: 5.0,
left: 10.0,
right: 10.0,
})
.style(
|theme: &Theme, _status| {
text_input("", &state.device_name)
.padding(Padding {
top: 5.0,
bottom: 5.0,
left: 10.0,
right: 10.0,
})
.style(|theme: &Theme, _status| {
text_input::Style {
background: Background::Color(Color::TRANSPARENT),
border: Default::default(),
@@ -57,56 +53,52 @@ pub fn airpods_view<'a>(
value: theme.palette().text,
selection: Default::default(),
}
}
)
.align_x(End)
.on_input({
})
.align_x(End)
.on_input({
let mac = mac.clone();
let state = state.clone();
move|new_name| {
move |new_name| {
let aacp_manager = aacp_manager_for_rename.clone();
run_async_in_thread(
{
let new_name = new_name.clone();
async move {
aacp_manager.send_rename_packet(&new_name).await.expect("Failed to send rename packet");
}
run_async_in_thread({
let new_name = new_name.clone();
async move {
aacp_manager
.send_rename_packet(&new_name)
.await
.expect("Failed to send rename packet");
}
);
});
let mut state = state.clone();
state.device_name = new_name.clone();
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
}
}
)
})
]
.align_y(Center)
.align_y(Center),
)
.padding(Padding{
top: 5.0,
bottom: 5.0,
left: 10.0,
right: 10.0,
})
.style(
|theme: &Theme| {
let mut style = container::Style::default();
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
let mut border = Border::default();
border.color = theme.palette().primary.scale_alpha(0.5);
style.border = border.rounded(16);
style
}
);
.padding(Padding {
top: 5.0,
bottom: 5.0,
left: 10.0,
right: 10.0,
})
.style(|theme: &Theme| {
let mut style = container::Style::default();
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
let mut border = Border::default();
border.color = theme.palette().primary.scale_alpha(0.5);
style.border = border.rounded(16);
style
});
let listening_mode = container(row![
text("Listening Mode").size(16).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}
),
let listening_mode = container(
row![
text("Listening Mode").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill),
{
let state_clone = state.clone();
@@ -121,78 +113,71 @@ pub fn airpods_view<'a>(
move |selected_mode| {
let aacp_manager = aacp_manager.clone();
let selected_mode_c = selected_mode.clone();
run_async_in_thread(
async move {
aacp_manager.send_control_command(
run_async_in_thread(async move {
aacp_manager
.send_control_command(
ControlCommandIdentifiers::ListeningMode,
&[selected_mode_c.to_byte()]
).await.expect("Failed to send Noise Control Mode command");
}
);
&[selected_mode_c.to_byte()],
)
.await
.expect("Failed to send Noise Control Mode command");
});
let mut state = state_clone.clone();
state.noise_control_mode = selected_mode.clone();
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
}
}
},
)
.width(Length::from(200))
.input_style(
|theme: &Theme, _status| {
text_input::Style {
background: Background::Color(theme.palette().primary.scale_alpha(0.2)),
border: Border {
width: 1.0,
color: theme.palette().text.scale_alpha(0.3),
radius: Radius::from(4.0)
},
icon: Default::default(),
placeholder: theme.palette().text,
value: theme.palette().text,
selection: Default::default(),
}
}
)
.padding(Padding{
.input_style(|theme: &Theme, _status| text_input::Style {
background: Background::Color(theme.palette().primary.scale_alpha(0.2)),
border: Border {
width: 1.0,
color: theme.palette().text.scale_alpha(0.3),
radius: Radius::from(4.0),
},
icon: Default::default(),
placeholder: theme.palette().text,
value: theme.palette().text,
selection: Default::default(),
})
.padding(Padding {
top: 5.0,
bottom: 5.0,
left: 10.0,
right: 10.0,
})
.menu_style(
|theme: &Theme| {
menu::Style {
background: Background::Color(theme.palette().background),
border: Border {
width: 1.0,
color: theme.palette().text,
radius: Radius::from(4.0)
},
text_color: theme.palette().text,
selected_text_color: theme.palette().text,
selected_background: Background::Color(theme.palette().primary.scale_alpha(0.3)),
}
}
)
.menu_style(|theme: &Theme| menu::Style {
background: Background::Color(theme.palette().background),
border: Border {
width: 1.0,
color: theme.palette().text,
radius: Radius::from(4.0),
},
text_color: theme.palette().text,
selected_text_color: theme.palette().text,
selected_background: Background::Color(
theme.palette().primary.scale_alpha(0.3),
),
})
}
]
.align_y(Center)
.align_y(Center),
)
.padding(Padding{
top: 5.0,
bottom: 5.0,
left: 18.0,
right: 18.0,
})
.style(
|theme: &Theme| {
let mut style = container::Style::default();
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
let mut border = Border::default();
border.color = theme.palette().primary.scale_alpha(0.5);
style.border = border.rounded(16);
style
}
);
.padding(Padding {
top: 5.0,
bottom: 5.0,
left: 18.0,
right: 18.0,
})
.style(|theme: &Theme| {
let mut style = container::Style::default();
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
let mut border = Border::default();
border.color = theme.palette().primary.scale_alpha(0.5);
style.border = border.rounded(16);
style
});
let mac_audio = 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 {
let info_rows = column![
row![
text("Model Number").size(16).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}
),
text("Model Number").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill),
text(airpods_info.model_number.clone()).size(16)
],
row![
text("Manufacturer").size(16).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}
),
text("Manufacturer").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill),
text(airpods_info.manufacturer.clone()).size(16)
],
row![
text("Serial Number").size(16).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}
),
text("Serial Number").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill),
button(
text(airpods_info.serial_number.clone()).size(16)
)
.style(
|theme: &Theme, _status| {
let mut style = Style::default();
style.text_color = theme.palette().text;
style.background = Some(Background::Color(Color::TRANSPARENT));
style
}
)
button(text(airpods_info.serial_number.clone()).size(16))
.style(|theme: &Theme, _status| {
let mut style = Style::default();
style.text_color = theme.palette().text;
style.background = Some(Background::Color(Color::TRANSPARENT));
style
})
.padding(0)
.on_press(Message::CopyToClipboard(airpods_info.serial_number.clone()))
],
row![
text("Left Serial Number").size(16).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}
),
text("Left Serial Number").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill),
button(
text(airpods_info.left_serial_number.clone()).size(16)
)
.style(
|theme: &Theme, _status| {
let mut style = Style::default();
style.text_color = theme.palette().text;
style.background = Some(Background::Color(Color::TRANSPARENT));
style
}
)
button(text(airpods_info.left_serial_number.clone()).size(16))
.style(|theme: &Theme, _status| {
let mut style = Style::default();
style.text_color = theme.palette().text;
style.background = Some(Background::Color(Color::TRANSPARENT));
style
})
.padding(0)
.on_press(Message::CopyToClipboard(airpods_info.left_serial_number.clone()))
.on_press(Message::CopyToClipboard(
airpods_info.left_serial_number.clone()
))
],
row![
text("Right Serial Number").size(16).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}
),
text("Right Serial Number").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill),
button(
text(airpods_info.right_serial_number.clone()).size(16)
)
.style(
|theme: &Theme, _status| {
let mut style = Style::default();
style.text_color = theme.palette().text;
style.background = Some(Background::Color(Color::TRANSPARENT));
style
}
)
button(text(airpods_info.right_serial_number.clone()).size(16))
.style(|theme: &Theme, _status| {
let mut style = Style::default();
style.text_color = theme.palette().text;
style.background = Some(Background::Color(Color::TRANSPARENT));
style
})
.padding(0)
.on_press(Message::CopyToClipboard(airpods_info.right_serial_number.clone()))
.on_press(Message::CopyToClipboard(
airpods_info.right_serial_number.clone()
))
],
row![
text("Version 1").size(16).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}
),
text("Version 1").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill),
text(airpods_info.version1.clone()).size(16)
],
row![
text("Version 2").size(16).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}
),
text("Version 2").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill),
text(airpods_info.version2.clone()).size(16)
],
row![
text("Version 3").size(16).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}
),
text("Version 3").size(16).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().text);
style
}),
Space::with_width(Length::Fill),
text(airpods_info.version3.clone()).size(16)
]
@@ -509,56 +470,53 @@ pub fn airpods_view<'a>(
.padding(8);
information_col = column![
container(
text("Device Information").size(18).style(
|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().primary);
style
}
)
).padding(Padding{
container(text("Device Information").size(18).style(|theme: &Theme| {
let mut style = text::Style::default();
style.color = Some(theme.palette().primary);
style
}))
.padding(Padding {
top: 5.0,
bottom: 5.0,
left: 18.0,
right: 18.0,
}),
container(info_rows)
.padding(Padding{
top: 5.0,
bottom: 5.0,
left: 10.0,
right: 10.0,
})
.style(
|theme: &Theme| {
let mut style = container::Style::default();
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
let mut border = Border::default();
border.color = theme.palette().primary.scale_alpha(0.5);
style.border = border.rounded(16);
style
}
)
.padding(Padding {
top: 5.0,
bottom: 5.0,
left: 10.0,
right: 10.0,
})
.style(|theme: &Theme| {
let mut style = container::Style::default();
style.background =
Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
let mut border = Border::default();
border.color = theme.palette().primary.scale_alpha(0.5);
style.border = border.rounded(16);
style
})
];
} else {
error!("Expected AirPodsInformation for device {}, got something else", mac.clone());
error!(
"Expected AirPodsInformation for device {}, got something else",
mac.clone()
);
}
}
container(
column![
rename_input,
Space::with_height(Length::from(20)),
listening_mode,
Space::with_height(Length::from(20)),
audio_settings_col,
Space::with_height(Length::from(20)),
off_listening_mode_toggle,
Space::with_height(Length::from(20)),
information_col
]
)
container(column![
rename_input,
Space::with_height(Length::from(20)),
listening_mode,
Space::with_height(Length::from(20)),
audio_settings_col,
Space::with_height(Length::from(20)),
off_listening_mode_toggle,
Space::with_height(Length::from(20)),
information_col
])
.padding(20)
.center_x(Length::Fill)
.height(Length::Fill)
@@ -572,4 +530,4 @@ where
let rt = Runtime::new().unwrap();
rt.block_on(fut);
});
}
}

View File

@@ -3,9 +3,9 @@ use crate::bluetooth::aacp::AACPEvent;
#[derive(Debug, Clone)]
pub enum BluetoothUIMessage {
OpenWindow,
DeviceConnected(String), // mac
DeviceDisconnected(String), // mac
AACPUIEvent(String, AACPEvent), // mac, event
DeviceConnected(String), // mac
DeviceDisconnected(String), // mac
AACPUIEvent(String, AACPEvent), // mac, event
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 window;
pub mod messages;
mod airpods;
mod nothing;

View File

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

View File

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

View File

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

View File

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