mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-01 07:39:11 +00:00
linux-rust: implement basic connections
This commit is contained in:
4636
linux-rust/Cargo.lock
generated
Normal file
4636
linux-rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
linux-rust/Cargo.toml
Normal file
15
linux-rust/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "librepods-rust"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
tokio = {version = "1.47.1", features = ["full"]}
|
||||
bluer = { version = "0.17.4", features = ["full"] }
|
||||
env_logger = {version = "0.11.8", features = ["auto-color"]}
|
||||
uuid = "1.18.1"
|
||||
log = "0.4.28"
|
||||
dbus = "0.9.9"
|
||||
hex = "0.4.3"
|
||||
iced = {version = "0.13.1", features = ["tokio", "auto-detect-theme"]}
|
||||
libpulse-binding = "2.30.1"
|
||||
74
linux-rust/src/airpods.rs
Normal file
74
linux-rust/src/airpods.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use crate::bluetooth::aacp::{AACPManager, ProximityKeyType, AACPEvent};
|
||||
use crate::media_controller::MediaController;
|
||||
use bluer::Address;
|
||||
use log::{debug, info};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub struct AirPodsDevice {
|
||||
pub mac_address: Address,
|
||||
pub aacp_manager: AACPManager,
|
||||
pub media_controller: Arc<Mutex<MediaController>>,
|
||||
}
|
||||
|
||||
impl AirPodsDevice {
|
||||
pub async fn new(mac_address: Address) -> Self {
|
||||
info!("Creating new AirPodsDevice for {}", mac_address);
|
||||
let mut aacp_manager = AACPManager::new();
|
||||
aacp_manager.connect(mac_address).await;
|
||||
|
||||
info!("Sending handshake");
|
||||
aacp_manager.send_handshake().await.expect(
|
||||
"Failed to send handshake to AirPods device",
|
||||
);
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
info!("Setting feature flags");
|
||||
aacp_manager.send_set_feature_flags_packet().await.expect(
|
||||
"Failed to set feature flags",
|
||||
);
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
info!("Requesting notifications");
|
||||
aacp_manager.send_notification_request().await.expect(
|
||||
"Failed to request notifications",
|
||||
);
|
||||
|
||||
info!("Requesting Proximity Keys: IRK and ENC_KEY");
|
||||
aacp_manager.send_proximity_keys_request(
|
||||
vec![ProximityKeyType::Irk, ProximityKeyType::EncKey],
|
||||
).await.expect(
|
||||
"Failed to request proximity keys",
|
||||
);
|
||||
let media_controller = Arc::new(Mutex::new(MediaController::new(mac_address.to_string())));
|
||||
let mc_clone = media_controller.clone();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
aacp_manager.set_event_channel(tx).await;
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
AACPEvent::EarDetection(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!("Received unhandled AACP event: {:?}", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AirPodsDevice {
|
||||
mac_address,
|
||||
aacp_manager,
|
||||
media_controller,
|
||||
}
|
||||
}
|
||||
}
|
||||
658
linux-rust/src/bluetooth/aacp.rs
Normal file
658
linux-rust/src/bluetooth/aacp.rs
Normal file
@@ -0,0 +1,658 @@
|
||||
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};
|
||||
|
||||
const PSM: u16 = 0x1001;
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const POLL_INTERVAL: Duration = Duration::from_millis(200);
|
||||
const HEADER_BYTES: [u8; 4] = [0x04, 0x00, 0x04, 0x00];
|
||||
|
||||
pub mod opcodes {
|
||||
pub const SET_FEATURE_FLAGS: u8 = 0x4D;
|
||||
pub const REQUEST_NOTIFICATIONS: u8 = 0x0F;
|
||||
pub const BATTERY_INFO: u8 = 0x04;
|
||||
pub const CONTROL_COMMAND: u8 = 0x09;
|
||||
pub const EAR_DETECTION: u8 = 0x06;
|
||||
pub const CONVERSATION_AWARENESS: u8 = 0x4B;
|
||||
pub const DEVICE_METADATA: u8 = 0x1D;
|
||||
pub const RENAME: u8 = 0x1E;
|
||||
pub const PROXIMITY_KEYS_REQ: u8 = 0x30;
|
||||
pub const PROXIMITY_KEYS_RSP: u8 = 0x31;
|
||||
pub const STEM_PRESS: u8 = 0x19;
|
||||
pub const EQ_DATA: u8 = 0x53;
|
||||
pub const CONNECTED_DEVICES: u8 = 0x2E;
|
||||
pub const AUDIO_SOURCE: u8 = 0x0E;
|
||||
pub const SMART_ROUTING: u8 = 0x10;
|
||||
pub const SMART_ROUTING_RESP: u8 = 0x11;
|
||||
pub const SEND_CONNECTED_MAC: u8 = 0x14;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ControlCommandStatus {
|
||||
pub identifier: ControlCommandIdentifiers,
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ControlCommandIdentifiers {
|
||||
MicMode = 0x01,
|
||||
ButtonSendMode = 0x05,
|
||||
VoiceTrigger = 0x12,
|
||||
SingleClickMode = 0x14,
|
||||
DoubleClickMode = 0x15,
|
||||
ClickHoldMode = 0x16,
|
||||
DoubleClickInterval = 0x17,
|
||||
ClickHoldInterval = 0x18,
|
||||
ListeningModeConfigs = 0x1A,
|
||||
OneBudAncMode = 0x1B,
|
||||
CrownRotationDirection = 0x1C,
|
||||
ListeningMode = 0x0D,
|
||||
AutoAnswerMode = 0x1E,
|
||||
ChimeVolume = 0x1F,
|
||||
VolumeSwipeInterval = 0x23,
|
||||
CallManagementConfig = 0x24,
|
||||
VolumeSwipeMode = 0x25,
|
||||
AdaptiveVolumeConfig = 0x26,
|
||||
SoftwareMuteConfig = 0x27,
|
||||
ConversationDetectConfig = 0x28,
|
||||
Ssl = 0x29,
|
||||
HearingAid = 0x2C,
|
||||
AutoAncStrength = 0x2E,
|
||||
HpsGainSwipe = 0x2F,
|
||||
HrmState = 0x30,
|
||||
InCaseToneConfig = 0x31,
|
||||
SiriMultitoneConfig = 0x32,
|
||||
HearingAssistConfig = 0x33,
|
||||
AllowOffOption = 0x34,
|
||||
StemConfig = 0x39,
|
||||
SleepDetectionConfig = 0x35,
|
||||
AllowAutoConnect = 0x36,
|
||||
EarDetectionConfig = 0x0A,
|
||||
AutomaticConnectionConfig = 0x20,
|
||||
OwnsConnection = 0x06,
|
||||
}
|
||||
|
||||
impl ControlCommandIdentifiers {
|
||||
fn from_u8(value: u8) -> Option<Self> {
|
||||
match value {
|
||||
0x01 => Some(Self::MicMode),
|
||||
0x05 => Some(Self::ButtonSendMode),
|
||||
0x12 => Some(Self::VoiceTrigger),
|
||||
0x14 => Some(Self::SingleClickMode),
|
||||
0x15 => Some(Self::DoubleClickMode),
|
||||
0x16 => Some(Self::ClickHoldMode),
|
||||
0x17 => Some(Self::DoubleClickInterval),
|
||||
0x18 => Some(Self::ClickHoldInterval),
|
||||
0x1A => Some(Self::ListeningModeConfigs),
|
||||
0x1B => Some(Self::OneBudAncMode),
|
||||
0x1C => Some(Self::CrownRotationDirection),
|
||||
0x0D => Some(Self::ListeningMode),
|
||||
0x1E => Some(Self::AutoAnswerMode),
|
||||
0x1F => Some(Self::ChimeVolume),
|
||||
0x23 => Some(Self::VolumeSwipeInterval),
|
||||
0x24 => Some(Self::CallManagementConfig),
|
||||
0x25 => Some(Self::VolumeSwipeMode),
|
||||
0x26 => Some(Self::AdaptiveVolumeConfig),
|
||||
0x27 => Some(Self::SoftwareMuteConfig),
|
||||
0x28 => Some(Self::ConversationDetectConfig),
|
||||
0x29 => Some(Self::Ssl),
|
||||
0x2C => Some(Self::HearingAid),
|
||||
0x2E => Some(Self::AutoAncStrength),
|
||||
0x2F => Some(Self::HpsGainSwipe),
|
||||
0x30 => Some(Self::HrmState),
|
||||
0x31 => Some(Self::InCaseToneConfig),
|
||||
0x32 => Some(Self::SiriMultitoneConfig),
|
||||
0x33 => Some(Self::HearingAssistConfig),
|
||||
0x34 => Some(Self::AllowOffOption),
|
||||
0x39 => Some(Self::StemConfig),
|
||||
0x35 => Some(Self::SleepDetectionConfig),
|
||||
0x36 => Some(Self::AllowAutoConnect),
|
||||
0x0A => Some(Self::EarDetectionConfig),
|
||||
0x20 => Some(Self::AutomaticConnectionConfig),
|
||||
0x06 => Some(Self::OwnsConnection),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProximityKeyType {
|
||||
Irk = 0x01,
|
||||
EncKey = 0x04,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StemPressType {
|
||||
SinglePress = 0x05,
|
||||
DoublePress = 0x06,
|
||||
TriplePress = 0x07,
|
||||
LongPress = 0x08,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StemPressBudType {
|
||||
Left = 0x01,
|
||||
Right = 0x02,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AudioSourceType {
|
||||
None = 0x00,
|
||||
Call = 0x01,
|
||||
Media = 0x02,
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BatteryComponent {
|
||||
Left = 4,
|
||||
Right = 2,
|
||||
Case = 8
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BatteryStatus {
|
||||
Charging = 1,
|
||||
NotCharging = 2,
|
||||
Disconnected = 4
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EarDetectionStatus {
|
||||
InEar = 0x00,
|
||||
OutOfEar = 0x01,
|
||||
InCase = 0x02,
|
||||
Disconnected = 0x03
|
||||
}
|
||||
|
||||
impl AudioSourceType {
|
||||
fn from_u8(value: u8) -> Option<Self> {
|
||||
match value {
|
||||
0x00 => Some(Self::None),
|
||||
0x01 => Some(Self::Call),
|
||||
0x02 => Some(Self::Media),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AudioSource {
|
||||
pub mac: String,
|
||||
pub r#type: AudioSourceType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BatteryInfo {
|
||||
pub component: BatteryComponent,
|
||||
pub level: u8,
|
||||
pub status: BatteryStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ConnectedDevice {
|
||||
pub mac: String,
|
||||
pub info1: u8,
|
||||
pub info2: u8,
|
||||
pub r#type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AACPEvent {
|
||||
BatteryInfo(Vec<BatteryInfo>),
|
||||
ControlCommand(ControlCommandStatus),
|
||||
EarDetection(Vec<EarDetectionStatus>, Vec<EarDetectionStatus>),
|
||||
ConversationalAwareness(u8),
|
||||
ProximityKeys(Vec<(u8, Vec<u8>)>),
|
||||
AudioSource(AudioSource),
|
||||
ConnectedDevices(Vec<ConnectedDevice>),
|
||||
}
|
||||
|
||||
struct AACPManagerState {
|
||||
sender: Option<mpsc::Sender<Vec<u8>>>,
|
||||
control_command_status_list: Vec<ControlCommandStatus>,
|
||||
owns: bool,
|
||||
connected_devices: Vec<ConnectedDevice>,
|
||||
audio_source: Option<AudioSource>,
|
||||
battery_info: Vec<BatteryInfo>,
|
||||
pub conversational_awareness_status: u8,
|
||||
old_ear_detection_status: Vec<EarDetectionStatus>,
|
||||
ear_detection_status: Vec<EarDetectionStatus>,
|
||||
event_tx: Option<mpsc::UnboundedSender<AACPEvent>>,
|
||||
}
|
||||
|
||||
impl AACPManagerState {
|
||||
fn new() -> Self {
|
||||
AACPManagerState {
|
||||
sender: None,
|
||||
control_command_status_list: Vec::new(),
|
||||
owns: false,
|
||||
connected_devices: Vec::new(),
|
||||
audio_source: None,
|
||||
battery_info: Vec::new(),
|
||||
conversational_awareness_status: 0,
|
||||
old_ear_detection_status: Vec::new(),
|
||||
ear_detection_status: Vec::new(),
|
||||
event_tx: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AACPManager {
|
||||
state: Arc<Mutex<AACPManagerState>>,
|
||||
tasks: Arc<Mutex<JoinSet<()>>>,
|
||||
}
|
||||
|
||||
impl AACPManager {
|
||||
pub fn new() -> Self {
|
||||
AACPManager {
|
||||
state: Arc::new(Mutex::new(AACPManagerState::new())),
|
||||
tasks: Arc::new(Mutex::new(JoinSet::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self, addr: Address) {
|
||||
info!("AACPManager connecting to {} on PSM {:#06X}...", addr, PSM);
|
||||
let target_sa = SocketAddr::new(addr, AddressType::BrEdr, PSM);
|
||||
|
||||
let socket = match Socket::new_seq_packet() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to create L2CAP socket: {}", e);
|
||||
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();
|
||||
loop {
|
||||
match seq_packet.peer_addr() {
|
||||
Ok(peer) if peer.cid != 0 => break,
|
||||
Ok(_) => { /* still waiting */ }
|
||||
Err(e) => {
|
||||
if e.raw_os_error() == Some(107) { // ENOTCONN
|
||||
error!("Peer has disconnected during connection setup.");
|
||||
return;
|
||||
}
|
||||
error!("Error getting peer address: {}", e);
|
||||
}
|
||||
}
|
||||
if start.elapsed() >= CONNECT_TIMEOUT {
|
||||
error!("Timed out waiting for L2CAP connection to be fully established.");
|
||||
return;
|
||||
}
|
||||
sleep(POLL_INTERVAL).await;
|
||||
}
|
||||
|
||||
info!("L2CAP connection established with {}", addr);
|
||||
|
||||
let (tx, rx) = mpsc::channel(128);
|
||||
|
||||
let manager_clone = self.clone();
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.sender = Some(tx);
|
||||
}
|
||||
|
||||
let mut tasks = self.tasks.lock().await;
|
||||
tasks.spawn(recv_thread(manager_clone, seq_packet.clone()));
|
||||
tasks.spawn(send_thread(rx, seq_packet));
|
||||
}
|
||||
|
||||
async fn send_packet(&self, data: &[u8]) -> Result<()> {
|
||||
let state = self.state.lock().await;
|
||||
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",
|
||||
))
|
||||
})
|
||||
} else {
|
||||
error!("Cannot send packet, sender is not available.");
|
||||
Err(Error::from(std::io::Error::new(
|
||||
std::io::ErrorKind::NotConnected,
|
||||
"L2CAP stream not connected",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_data_packet(&self, data: &[u8]) -> Result<()> {
|
||||
let packet = [HEADER_BYTES.as_slice(), data].concat();
|
||||
self.send_packet(&packet).await
|
||||
}
|
||||
|
||||
pub async fn set_event_channel(&self, tx: mpsc::UnboundedSender<AACPEvent>) {
|
||||
let mut state = self.state.lock().await;
|
||||
state.event_tx = Some(tx);
|
||||
}
|
||||
|
||||
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));
|
||||
return;
|
||||
}
|
||||
if packet.len() < 5 {
|
||||
debug!("Received packet too short: {}", hex::encode(packet));
|
||||
return;
|
||||
}
|
||||
|
||||
let opcode = packet[4];
|
||||
let payload = &packet[4..];
|
||||
|
||||
match opcode {
|
||||
opcodes::BATTERY_INFO => {
|
||||
if payload.len() < 3 {
|
||||
error!("Battery Info packet too short: {}", hex::encode(payload));
|
||||
return;
|
||||
}
|
||||
let count = payload[2] as usize;
|
||||
if payload.len() < 3 + count * 5 {
|
||||
error!("Battery Info packet length mismatch: {}", hex::encode(payload));
|
||||
return;
|
||||
}
|
||||
let mut batteries = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let base_index = 3 + i * 5;
|
||||
batteries.push(BatteryInfo {
|
||||
component: match payload[base_index] {
|
||||
0x02 => BatteryComponent::Right,
|
||||
0x04 => BatteryComponent::Left,
|
||||
0x08 => BatteryComponent::Case,
|
||||
_ => {
|
||||
error!("Unknown battery component: {:#04x}", payload[base_index]);
|
||||
continue;
|
||||
}
|
||||
},
|
||||
level: payload[base_index + 2],
|
||||
status: match payload[base_index + 3] {
|
||||
0x01 => BatteryStatus::Charging,
|
||||
0x02 => BatteryStatus::NotCharging,
|
||||
0x04 => BatteryStatus::Disconnected,
|
||||
_ => {
|
||||
error!("Unknown battery status: {:#04x}", payload[base_index + 3]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let mut state = self.state.lock().await;
|
||||
state.battery_info = batteries.clone();
|
||||
if let Some(ref tx) = state.event_tx {
|
||||
let _ = tx.send(AACPEvent::BatteryInfo(batteries));
|
||||
}
|
||||
info!("Received Battery Info: {:?}", state.battery_info);
|
||||
}
|
||||
opcodes::CONTROL_COMMAND => {
|
||||
if payload.len() < 7 {
|
||||
error!("Control Command packet too short: {}", hex::encode(payload));
|
||||
return;
|
||||
}
|
||||
let identifier_byte = payload[2];
|
||||
let value_bytes = &payload[3..7];
|
||||
|
||||
let last_non_zero = value_bytes.iter().rposition(|&b| b != 0);
|
||||
let value = match last_non_zero {
|
||||
Some(i) => value_bytes[..=i].to_vec(),
|
||||
None => vec![0],
|
||||
};
|
||||
|
||||
if let Some(identifier) = ControlCommandIdentifiers::from_u8(identifier_byte) {
|
||||
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) {
|
||||
existing.value = value.clone();
|
||||
} else {
|
||||
state.control_command_status_list.push(status.clone());
|
||||
}
|
||||
if identifier == ControlCommandIdentifiers::OwnsConnection {
|
||||
state.owns = value_bytes[0] != 0;
|
||||
}
|
||||
if let Some(ref tx) = state.event_tx {
|
||||
let _ = tx.send(AACPEvent::ControlCommand(status));
|
||||
}
|
||||
info!("Received Control Command: {:?}, value: {}", identifier, hex::encode(&value));
|
||||
} else {
|
||||
error!("Unknown Control Command identifier: {:#04x}", identifier_byte);
|
||||
}
|
||||
}
|
||||
opcodes::EAR_DETECTION => {
|
||||
let primary_status = packet[6];
|
||||
let secondary_status = packet[7];
|
||||
let mut statuses = Vec::new();
|
||||
statuses.push(match primary_status {
|
||||
0x00 => EarDetectionStatus::InEar,
|
||||
0x01 => EarDetectionStatus::OutOfEar,
|
||||
0x02 => EarDetectionStatus::InCase,
|
||||
0x03 => EarDetectionStatus::Disconnected,
|
||||
_ => {
|
||||
error!("Unknown ear detection status: {:#04x}", primary_status);
|
||||
EarDetectionStatus::OutOfEar
|
||||
}
|
||||
});
|
||||
statuses.push(match secondary_status {
|
||||
0x00 => EarDetectionStatus::InEar,
|
||||
0x01 => EarDetectionStatus::OutOfEar,
|
||||
0x02 => EarDetectionStatus::InCase,
|
||||
0x03 => EarDetectionStatus::Disconnected,
|
||||
_ => {
|
||||
error!("Unknown ear detection status: {:#04x}", secondary_status);
|
||||
EarDetectionStatus::OutOfEar
|
||||
}
|
||||
});
|
||||
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));
|
||||
}
|
||||
info!("Received Ear Detection Status: {:?}", state.ear_detection_status);
|
||||
}
|
||||
opcodes::CONVERSATION_AWARENESS => {
|
||||
if packet.len() == 10 {
|
||||
let status = packet[9];
|
||||
let mut state = self.state.lock().await;
|
||||
state.conversational_awareness_status = status;
|
||||
if let Some(ref tx) = state.event_tx {
|
||||
let _ = tx.send(AACPEvent::ConversationalAwareness(status));
|
||||
}
|
||||
info!("Received Conversation Awareness: {}", status);
|
||||
} else {
|
||||
info!("Received Conversation Awareness packet with unexpected length: {}", packet.len());
|
||||
}
|
||||
}
|
||||
opcodes::DEVICE_METADATA => info!("Received Device Metadata packet."),
|
||||
opcodes::PROXIMITY_KEYS_RSP => {
|
||||
if payload.len() < 4 {
|
||||
error!("Proximity Keys Response packet too short: {}", hex::encode(payload));
|
||||
return;
|
||||
}
|
||||
let key_count = payload[2] as usize;
|
||||
debug!("Proximity Keys Response contains {} keys.", key_count);
|
||||
let mut offset = 3;
|
||||
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));
|
||||
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));
|
||||
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<_>>());
|
||||
let state = self.state.lock().await;
|
||||
if let Some(ref tx) = state.event_tx {
|
||||
let _ = tx.send(AACPEvent::ProximityKeys(keys));
|
||||
}
|
||||
},
|
||||
opcodes::STEM_PRESS => info!("Received Stem Press packet."),
|
||||
opcodes::AUDIO_SOURCE => {
|
||||
if payload.len() < 9 {
|
||||
error!("Audio Source packet too short: {}", hex::encode(payload));
|
||||
return;
|
||||
}
|
||||
let mac = format!(
|
||||
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
||||
payload[7], payload[6], payload[5], payload[4], payload[3], payload[2]
|
||||
);
|
||||
let typ = AudioSourceType::from_u8(payload[8]).unwrap_or(AudioSourceType::None);
|
||||
let audio_source = AudioSource { mac, r#type: typ };
|
||||
let mut state = self.state.lock().await;
|
||||
state.audio_source = Some(audio_source.clone());
|
||||
if let Some(ref tx) = state.event_tx {
|
||||
let _ = tx.send(AACPEvent::AudioSource(audio_source));
|
||||
}
|
||||
info!("Received Audio Source: {:?}", state.audio_source);
|
||||
}
|
||||
opcodes::CONNECTED_DEVICES => {
|
||||
if payload.len() < 3 {
|
||||
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));
|
||||
return;
|
||||
}
|
||||
let mut devices = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
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]
|
||||
);
|
||||
let info1 = payload[base + 6];
|
||||
let info2 = payload[base + 7];
|
||||
devices.push(ConnectedDevice { mac, info1, info2, r#type: None });
|
||||
}
|
||||
let mut state = self.state.lock().await;
|
||||
state.connected_devices = devices.clone();
|
||||
if let Some(ref tx) = state.event_tx {
|
||||
let _ = tx.send(AACPEvent::ConnectedDevices(devices));
|
||||
}
|
||||
info!("Received Connected Devices: {:?}", state.connected_devices);
|
||||
}
|
||||
opcodes::SMART_ROUTING_RESP => {
|
||||
info!("Received Smart Routing Response: {:?}", &payload[1..]);
|
||||
}
|
||||
opcodes::EQ_DATA => {
|
||||
debug!("Received EQ Data");
|
||||
}
|
||||
_ => debug!("Received unknown packet with opcode {:#04x}", opcode),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_notification_request(&self) -> Result<()> {
|
||||
let opcode = [opcodes::REQUEST_NOTIFICATIONS, 0x00];
|
||||
let data = [0xFF, 0xFF, 0xFF, 0xFF];
|
||||
let packet = [opcode.as_slice(), data.as_slice()].concat();
|
||||
self.send_data_packet(&packet).await
|
||||
}
|
||||
|
||||
pub async fn send_set_feature_flags_packet(&self) -> Result<()> {
|
||||
let opcode = [opcodes::SET_FEATURE_FLAGS, 0x00];
|
||||
let data = [0xD7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||
let packet = [opcode.as_slice(), data.as_slice()].concat();
|
||||
self.send_data_packet(&packet).await
|
||||
}
|
||||
|
||||
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
|
||||
];
|
||||
self.send_packet(&packet).await
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
self.send_data_packet(&packet).await
|
||||
}
|
||||
|
||||
pub async fn send_rename_packet(&self, name: &str) -> Result<()> {
|
||||
let name_bytes = name.as_bytes();
|
||||
let size = name_bytes.len();
|
||||
let mut packet = Vec::with_capacity(5 + size);
|
||||
packet.push(opcodes::RENAME);
|
||||
packet.push(0x00);
|
||||
packet.push(size as u8);
|
||||
packet.push(0x00);
|
||||
packet.extend_from_slice(name_bytes);
|
||||
self.send_data_packet(&packet).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_thread(manager: AACPManager, sp: Arc<SeqPacket>) {
|
||||
let mut buf = vec![0u8; 1024];
|
||||
loop {
|
||||
match sp.recv(&mut buf).await {
|
||||
Ok(0) => {
|
||||
info!("Remote closed the connection.");
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
let data = &buf[..n];
|
||||
debug!("Received {} bytes: {}", n, hex::encode(data));
|
||||
manager.receive_packet(data).await;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Read error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut state = manager.state.lock().await;
|
||||
state.sender = None;
|
||||
}
|
||||
|
||||
async fn send_thread(mut rx: mpsc::Receiver<Vec<u8>>, sp: Arc<SeqPacket>) {
|
||||
while let Some(data) = rx.recv().await {
|
||||
if let Err(e) = sp.send(&data).await {
|
||||
error!("Failed to send data: {}", e);
|
||||
break;
|
||||
}
|
||||
debug!("Sent {} bytes: {}", data.len(), hex::encode(&data));
|
||||
}
|
||||
info!("Send thread finished.");
|
||||
}
|
||||
20
linux-rust/src/bluetooth/discovery.rs
Normal file
20
linux-rust/src/bluetooth/discovery.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use std::io::Error;
|
||||
|
||||
pub(crate) async fn find_connected_airpods(adapter: &bluer::Adapter) -> bluer::Result<bluer::Device> {
|
||||
let target_uuid = uuid::Uuid::parse_str("74ec2172-0bad-4d01-8f77-997b2be0722a").unwrap();
|
||||
|
||||
let addrs = adapter.device_addresses().await?;
|
||||
for addr in addrs {
|
||||
let device = adapter.device(addr)?;
|
||||
if device.is_connected().await.unwrap_or(false) {
|
||||
if let Ok(uuids) = device.uuids().await {
|
||||
if let Some(uuids) = uuids {
|
||||
if uuids.iter().any(|u| *u == target_uuid) {
|
||||
return Ok(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(bluer::Error::from(Error::new(std::io::ErrorKind::NotFound, "No connected AirPods found")))
|
||||
}
|
||||
2
linux-rust/src/bluetooth/mod.rs
Normal file
2
linux-rust/src/bluetooth/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod discovery;
|
||||
pub mod aacp;
|
||||
81
linux-rust/src/main.rs
Normal file
81
linux-rust/src/main.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
mod bluetooth;
|
||||
mod airpods;
|
||||
mod media_controller;
|
||||
|
||||
use std::env;
|
||||
use log::{debug, 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 crate::bluetooth::discovery::find_connected_airpods;
|
||||
use crate::airpods::AirPodsDevice;
|
||||
use bluer::Address;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> bluer::Result<()> {
|
||||
if env::var("RUST_LOG").is_err() {
|
||||
unsafe { env::set_var("RUST_LOG", "debug"); }
|
||||
}
|
||||
|
||||
env_logger::init();
|
||||
|
||||
let session = bluer::Session::new().await?;
|
||||
let adapter = session.default_adapter().await?;
|
||||
adapter.set_powered(true).await?;
|
||||
|
||||
info!("Listening for new connections.");
|
||||
|
||||
info!("Checking for connected devices...");
|
||||
match find_connected_airpods(&adapter).await {
|
||||
Ok(device) => {
|
||||
let name = device.name().await?.unwrap_or_else(|| "Unknown".to_string());
|
||||
info!("Found connected AirPods: {}, initializing.", name);
|
||||
let _airpods_device = AirPodsDevice::new(device.address()).await;
|
||||
}
|
||||
Err(_) => {
|
||||
info!("No connected AirPods found.");
|
||||
}
|
||||
}
|
||||
|
||||
let conn = Connection::new_system()?;
|
||||
let rule = MatchRule::new_signal("org.freedesktop.DBus.Properties", "PropertiesChanged");
|
||||
conn.add_match(rule, |_: (), conn, msg| {
|
||||
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 {
|
||||
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; };
|
||||
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 target_uuid = "74ec2172-0bad-4d01-8f77-997b2be0722a";
|
||||
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 Ok(addr_str) = proxy.get::<String>("org.bluez.Device1", "Address") else { return true; };
|
||||
let Ok(addr) = addr_str.parse::<Address>() else { return true; };
|
||||
info!("AirPods connected: {}, initializing", name);
|
||||
tokio::spawn(async move {
|
||||
let _airpods_device = AirPodsDevice::new(addr).await;
|
||||
});
|
||||
true
|
||||
})?;
|
||||
|
||||
info!("Listening for Bluetooth connections via D-Bus...");
|
||||
loop {
|
||||
conn.process(std::time::Duration::from_millis(1000))?;
|
||||
}
|
||||
}
|
||||
616
linux-rust/src/media_controller.rs
Normal file
616
linux-rust/src/media_controller.rs
Normal file
@@ -0,0 +1,616 @@
|
||||
use log::{info, debug, warn, error};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use std::process::Command;
|
||||
use dbus::blocking::Connection;
|
||||
use std::time::Duration;
|
||||
use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
|
||||
use crate::bluetooth::aacp::EarDetectionStatus;
|
||||
use libpulse_binding::mainloop::standard::Mainloop;
|
||||
use libpulse_binding::context::{Context, FlagSet as ContextFlagSet};
|
||||
use libpulse_binding::operation::State as OperationState;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use libpulse_binding::def::Retval;
|
||||
use libpulse_binding::callbacks::ListResult;
|
||||
use libpulse_binding::proplist::Proplist;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct OwnedCardProfileInfo {
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct OwnedCardInfo {
|
||||
index: u32,
|
||||
proplist: Proplist,
|
||||
profiles: Vec<OwnedCardProfileInfo>,
|
||||
}
|
||||
|
||||
struct MediaControllerState {
|
||||
connected_device_mac: String,
|
||||
is_playing: bool,
|
||||
paused_by_app_services: Vec<String>,
|
||||
device_index: Option<u32>,
|
||||
cached_a2dp_profile: String,
|
||||
old_in_ear_data: Vec<bool>,
|
||||
user_played_the_media: bool,
|
||||
i_paused_the_media: bool,
|
||||
ear_detection_enabled: bool,
|
||||
disconnect_when_not_wearing: bool,
|
||||
}
|
||||
|
||||
impl MediaControllerState {
|
||||
fn new() -> Self {
|
||||
MediaControllerState {
|
||||
connected_device_mac: String::new(),
|
||||
is_playing: false,
|
||||
paused_by_app_services: Vec::new(),
|
||||
device_index: None,
|
||||
cached_a2dp_profile: String::new(),
|
||||
old_in_ear_data: vec![false, false],
|
||||
user_played_the_media: false,
|
||||
i_paused_the_media: false,
|
||||
ear_detection_enabled: true,
|
||||
disconnect_when_not_wearing: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MediaController {
|
||||
state: Arc<Mutex<MediaControllerState>>,
|
||||
}
|
||||
|
||||
impl MediaController {
|
||||
pub fn new(connected_mac: String) -> Self {
|
||||
let mut state = MediaControllerState::new();
|
||||
state.connected_device_mac = connected_mac;
|
||||
MediaController {
|
||||
state: Arc::new(Mutex::new(state)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_ear_detection(&self, old_statuses: Vec<EarDetectionStatus>, new_statuses: Vec<EarDetectionStatus>) {
|
||||
debug!("Entering handle_ear_detection with old_statuses: {:?}, new_statuses: {:?}", old_statuses, new_statuses);
|
||||
|
||||
let old_in_ear_data: Vec<bool> = old_statuses.iter().map(|s| *s == EarDetectionStatus::InEar).collect();
|
||||
let new_in_ear_data: Vec<bool> = new_statuses.iter().map(|s| *s == EarDetectionStatus::InEar).collect();
|
||||
|
||||
let in_ear = new_in_ear_data.iter().all(|&b| b);
|
||||
|
||||
let old_all_out = old_in_ear_data.iter().all(|&b| !b);
|
||||
let new_has_at_least_one_in = new_in_ear_data.iter().any(|&b| b);
|
||||
let new_all_out = new_in_ear_data.iter().all(|&b| !b);
|
||||
|
||||
debug!("Computed states: in_ear={}, old_all_out={}, new_has_at_least_one_in={}, new_all_out={}", in_ear, old_all_out, new_has_at_least_one_in, new_all_out);
|
||||
|
||||
{
|
||||
let state = self.state.lock().await;
|
||||
if !state.ear_detection_enabled {
|
||||
debug!("Ear detection disabled, skipping");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if new_has_at_least_one_in && old_all_out {
|
||||
debug!("Condition met: buds inserted, activating A2DP and checking play state");
|
||||
self.activate_a2dp_profile().await;
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
if state.is_playing {
|
||||
state.user_played_the_media = true;
|
||||
debug!("Set user_played_the_media to true as media was playing");
|
||||
}
|
||||
}
|
||||
} else if new_all_out {
|
||||
debug!("Condition met: buds removed, pausing media");
|
||||
self.pause().await;
|
||||
{
|
||||
let state = self.state.lock().await;
|
||||
if state.disconnect_when_not_wearing {
|
||||
debug!("Disconnect when not wearing enabled, deactivating A2DP");
|
||||
drop(state);
|
||||
self.deactivate_a2dp_profile().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reset_user_played = (old_in_ear_data.iter().any(|&b| !b) && new_in_ear_data.iter().all(|&b| b)) ||
|
||||
(new_in_ear_data.iter().any(|&b| !b) && old_in_ear_data.iter().all(|&b| b));
|
||||
if reset_user_played {
|
||||
debug!("Transition detected, resetting user_played_the_media");
|
||||
let mut state = self.state.lock().await;
|
||||
state.user_played_the_media = false;
|
||||
}
|
||||
|
||||
info!("Ear Detection - old_in_ear_data: {:?}, new_in_ear_data: {:?}", old_in_ear_data, new_in_ear_data);
|
||||
|
||||
let mut old_sorted = old_in_ear_data.clone();
|
||||
old_sorted.sort();
|
||||
let mut new_sorted = new_in_ear_data.clone();
|
||||
new_sorted.sort();
|
||||
if new_sorted != old_sorted {
|
||||
debug!("Ear data changed, checking resume/pause logic");
|
||||
if in_ear {
|
||||
debug!("Resuming media as buds are in ear");
|
||||
self.resume().await;
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.i_paused_the_media = false;
|
||||
}
|
||||
} else {
|
||||
if !old_all_out {
|
||||
debug!("Pausing media as buds are not fully in ear");
|
||||
self.pause().await;
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.i_paused_the_media = true;
|
||||
}
|
||||
} else {
|
||||
debug!("Playing media");
|
||||
self.resume().await;
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.i_paused_the_media = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.old_in_ear_data = new_in_ear_data;
|
||||
debug!("Updated old_in_ear_data to {:?}", state.old_in_ear_data);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn activate_a2dp_profile(&self) {
|
||||
debug!("Entering activate_a2dp_profile");
|
||||
let state = self.state.lock().await;
|
||||
|
||||
if state.connected_device_mac.is_empty() {
|
||||
warn!("Connected device MAC is empty, cannot activate A2DP profile");
|
||||
return;
|
||||
}
|
||||
|
||||
let device_index = state.device_index;
|
||||
let mac = state.connected_device_mac.clone();
|
||||
drop(state);
|
||||
|
||||
let mut current_device_index = device_index;
|
||||
|
||||
if current_device_index.is_none() {
|
||||
warn!("Device index not found, trying to get it.");
|
||||
current_device_index = self.get_audio_device_index(&mac).await;
|
||||
if let Some(idx) = current_device_index {
|
||||
let mut state = self.state.lock().await;
|
||||
state.device_index = Some(idx);
|
||||
} else {
|
||||
warn!("Could not get device index. Cannot activate A2DP profile.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.is_a2dp_profile_available().await {
|
||||
warn!("A2DP profile not available, attempting to restart WirePlumber");
|
||||
if self.restart_wire_plumber().await {
|
||||
let mut state = self.state.lock().await;
|
||||
state.device_index = self.get_audio_device_index(&state.connected_device_mac).await;
|
||||
debug!("Updated device_index after WirePlumber restart: {:?}", state.device_index);
|
||||
if !self.is_a2dp_profile_available().await {
|
||||
error!("A2DP profile still not available after WirePlumber restart");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
error!("Could not restart WirePlumber, A2DP profile unavailable");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let preferred_profile = self.get_preferred_a2dp_profile().await;
|
||||
if preferred_profile.is_empty() {
|
||||
error!("No suitable A2DP profile found");
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Activating A2DP profile for AirPods: {}", preferred_profile);
|
||||
let state = self.state.lock().await;
|
||||
let device_index = state.device_index;
|
||||
drop(state);
|
||||
|
||||
if let Some(idx) = device_index {
|
||||
let profile_name = preferred_profile.clone();
|
||||
let success = tokio::task::spawn_blocking(move || {
|
||||
set_card_profile_sync(idx, &profile_name)
|
||||
}).await.unwrap_or(false);
|
||||
|
||||
if success {
|
||||
info!("Successfully activated A2DP profile: {}", preferred_profile);
|
||||
} else {
|
||||
warn!("Failed to activate A2DP profile: {}", preferred_profile);
|
||||
}
|
||||
} else {
|
||||
error!("Device index not available for activating profile.");
|
||||
}
|
||||
}
|
||||
|
||||
async fn pause(&self) {
|
||||
debug!("Pausing playback");
|
||||
let paused_services = tokio::task::spawn_blocking(|| {
|
||||
debug!("Listing DBus names for media players");
|
||||
let conn = Connection::new_session().unwrap();
|
||||
let proxy = conn.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(5));
|
||||
let (names,): (Vec<String>,) = proxy.method_call("org.freedesktop.DBus", "ListNames", ()).unwrap();
|
||||
let mut paused_services = Vec::new();
|
||||
|
||||
for service in names {
|
||||
if service.starts_with("org.mpris.MediaPlayer2.") {
|
||||
debug!("Checking playback status for service: {}", service);
|
||||
let proxy = conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5));
|
||||
if let Ok(playback_status) = proxy.get::<String>("org.mpris.MediaPlayer2.Player", "PlaybackStatus") {
|
||||
if playback_status == "Playing" {
|
||||
debug!("Service {} is playing, attempting to pause", service);
|
||||
if proxy.method_call::<(), _, &str, &str>("org.mpris.MediaPlayer2.Player", "Pause", ()).is_ok() {
|
||||
info!("Paused playback for: {}", service);
|
||||
paused_services.push(service);
|
||||
} else {
|
||||
debug!("Failed to pause service: {}", service);
|
||||
error!("Failed to pause {}", service);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
paused_services
|
||||
}).await.unwrap();
|
||||
|
||||
if !paused_services.is_empty() {
|
||||
debug!("Paused services: {:?}", paused_services);
|
||||
info!("Paused {} media player(s) via DBus", paused_services.len());
|
||||
let mut state = self.state.lock().await;
|
||||
state.paused_by_app_services = paused_services;
|
||||
} else {
|
||||
debug!("No playing media players found");
|
||||
info!("No playing media players found to pause");
|
||||
}
|
||||
}
|
||||
|
||||
async fn resume(&self) {
|
||||
debug!("Entering resume method");
|
||||
debug!("Resuming playback");
|
||||
let state = self.state.lock().await;
|
||||
let services = state.paused_by_app_services.clone();
|
||||
drop(state);
|
||||
|
||||
if services.is_empty() {
|
||||
debug!("No services to resume");
|
||||
info!("No services to resume");
|
||||
return;
|
||||
}
|
||||
|
||||
let resumed_count = tokio::task::spawn_blocking(move || {
|
||||
let conn = Connection::new_session().unwrap();
|
||||
let mut resumed_count = 0;
|
||||
for service in services {
|
||||
debug!("Attempting to resume service: {}", service);
|
||||
let proxy = conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5));
|
||||
if proxy.method_call::<(), _, &str, &str>("org.mpris.MediaPlayer2.Player", "Play", ()).is_ok() {
|
||||
info!("Resumed playback for: {}", service);
|
||||
resumed_count += 1;
|
||||
} else {
|
||||
debug!("Failed to resume service: {}", service);
|
||||
warn!("Failed to resume {}", service);
|
||||
}
|
||||
}
|
||||
resumed_count
|
||||
}).await.unwrap();
|
||||
|
||||
if resumed_count > 0 {
|
||||
debug!("Resumed {} services", resumed_count);
|
||||
info!("Resumed {} media player(s) via DBus", resumed_count);
|
||||
let mut state = self.state.lock().await;
|
||||
state.paused_by_app_services.clear();
|
||||
} else {
|
||||
debug!("Failed to resume any services");
|
||||
error!("Failed to resume any media players via DBus");
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_a2dp_profile_available(&self) -> bool {
|
||||
debug!("Entering is_a2dp_profile_available");
|
||||
let state = self.state.lock().await;
|
||||
let device_index = state.device_index;
|
||||
drop(state);
|
||||
|
||||
let index = match device_index {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
debug!("Device index is None, returning false");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut mainloop = Mainloop::new().unwrap();
|
||||
let mut context = Context::new(&mut mainloop, "LibrePods-is_a2dp_profile_available").unwrap();
|
||||
context.connect(None, ContextFlagSet::NOAUTOSPAWN, None).unwrap();
|
||||
loop {
|
||||
match mainloop.iterate(false) {
|
||||
_ if context.get_state() == libpulse_binding::context::State::Ready => break,
|
||||
_ if context.get_state() == libpulse_binding::context::State::Failed || context.get_state() == libpulse_binding::context::State::Terminated => return false,
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
let introspector = context.introspect();
|
||||
let card_info_list = Rc::new(RefCell::new(None));
|
||||
let op = introspector.get_card_info_list({
|
||||
let card_info_list = card_info_list.clone();
|
||||
let mut list = Vec::new();
|
||||
move |result| {
|
||||
match result {
|
||||
ListResult::Item(item) => {
|
||||
let profiles = item.profiles.iter().map(|p| OwnedCardProfileInfo {
|
||||
name: p.name.as_ref().map(|n| n.to_string()),
|
||||
}).collect();
|
||||
list.push(OwnedCardInfo {
|
||||
index: item.index,
|
||||
proplist: item.proplist.clone(),
|
||||
profiles,
|
||||
});
|
||||
},
|
||||
ListResult::End => *card_info_list.borrow_mut() = Some(list.clone()),
|
||||
ListResult::Error => *card_info_list.borrow_mut() = None,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while op.get_state() == OperationState::Running {
|
||||
mainloop.iterate(false);
|
||||
}
|
||||
mainloop.quit(Retval(0));
|
||||
|
||||
if let Some(list) = card_info_list.borrow().as_ref() {
|
||||
if let Some(card) = list.iter().find(|c| c.index == index) {
|
||||
let available = card.profiles.iter().any(|p| {
|
||||
p.name.as_ref().map_or(false, |name| {
|
||||
name.starts_with("a2dp-sink")
|
||||
})
|
||||
});
|
||||
debug!("A2DP profile available: {}", available);
|
||||
return available;
|
||||
}
|
||||
}
|
||||
debug!("A2DP profile not available");
|
||||
false
|
||||
}).await.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn get_preferred_a2dp_profile(&self) -> String {
|
||||
debug!("Entering get_preferred_a2dp_profile");
|
||||
let state = self.state.lock().await;
|
||||
let device_index = state.device_index;
|
||||
let cached_profile = state.cached_a2dp_profile.clone();
|
||||
drop(state);
|
||||
|
||||
let index = match device_index {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
debug!("Device index is None, returning empty string");
|
||||
return String::new();
|
||||
}
|
||||
};
|
||||
|
||||
if !cached_profile.is_empty() && self.is_profile_available(index, &cached_profile).await {
|
||||
debug!("Using cached A2DP profile: {}", cached_profile);
|
||||
return cached_profile;
|
||||
}
|
||||
|
||||
let profiles_to_check = vec!["a2dp-sink-sbc_xq", "a2dp-sink-sbc", "a2dp-sink"];
|
||||
for profile in profiles_to_check {
|
||||
debug!("Checking availability of profile: {}", profile);
|
||||
if self.is_profile_available(index, profile).await {
|
||||
debug!("Selected profile: {}", profile);
|
||||
info!("Selected best available A2DP profile: {}", profile);
|
||||
let mut state = self.state.lock().await;
|
||||
state.cached_a2dp_profile = profile.to_string();
|
||||
return profile.to_string();
|
||||
}
|
||||
}
|
||||
debug!("No suitable profile found");
|
||||
String::new()
|
||||
}
|
||||
|
||||
async fn is_profile_available(&self, card_index: u32, profile: &str) -> bool {
|
||||
debug!("Entering is_profile_available for card index: {}, profile: {}", card_index, profile);
|
||||
let profile_name = profile.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut mainloop = Mainloop::new().unwrap();
|
||||
let mut context = Context::new(&mut mainloop, "LibrePods-is_profile_available").unwrap();
|
||||
context.connect(None, ContextFlagSet::NOAUTOSPAWN, None).unwrap();
|
||||
loop {
|
||||
match mainloop.iterate(false) {
|
||||
_ if context.get_state() == libpulse_binding::context::State::Ready => break,
|
||||
_ if context.get_state() == libpulse_binding::context::State::Failed || context.get_state() == libpulse_binding::context::State::Terminated => return false,
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
let introspector = context.introspect();
|
||||
let card_info_list = Rc::new(RefCell::new(None));
|
||||
let op = introspector.get_card_info_list({
|
||||
let card_info_list = card_info_list.clone();
|
||||
let mut list = Vec::new();
|
||||
move |result| {
|
||||
match result {
|
||||
ListResult::Item(item) => {
|
||||
let profiles = item.profiles.iter().map(|p| OwnedCardProfileInfo {
|
||||
name: p.name.as_ref().map(|n| n.to_string()),
|
||||
}).collect();
|
||||
list.push(OwnedCardInfo {
|
||||
index: item.index,
|
||||
proplist: item.proplist.clone(),
|
||||
profiles,
|
||||
});
|
||||
},
|
||||
ListResult::End => *card_info_list.borrow_mut() = Some(list.clone()),
|
||||
ListResult::Error => *card_info_list.borrow_mut() = None,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while op.get_state() == OperationState::Running {
|
||||
mainloop.iterate(false);
|
||||
}
|
||||
mainloop.quit(Retval(0));
|
||||
|
||||
if let Some(list) = card_info_list.borrow().as_ref() {
|
||||
if let Some(card) = list.iter().find(|c| c.index == card_index) {
|
||||
let available = card.profiles.iter().any(|p| p.name.as_ref().map_or(false, |n| n == &profile_name));
|
||||
debug!("Profile {} available: {}", profile_name, available);
|
||||
return available;
|
||||
}
|
||||
}
|
||||
debug!("Profile {} not available", profile_name);
|
||||
false
|
||||
}).await.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn restart_wire_plumber(&self) -> bool {
|
||||
debug!("Entering restart_wire_plumber");
|
||||
info!("Restarting WirePlumber to rediscover A2DP profiles");
|
||||
let result = Command::new("systemctl")
|
||||
.args(&["--user", "restart", "wireplumber"])
|
||||
.output();
|
||||
|
||||
match result {
|
||||
Ok(output) if output.status.success() => {
|
||||
info!("WirePlumber restarted successfully");
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
error!("Failed to restart WirePlumber. Do you use wireplumber?");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_audio_device_index(&self, mac: &str) -> Option<u32> {
|
||||
debug!("Entering get_audio_device_index for MAC: {}", mac);
|
||||
if mac.is_empty() {
|
||||
debug!("MAC is empty, returning None");
|
||||
return None;
|
||||
}
|
||||
let mac_clone = mac.to_string();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut mainloop = Mainloop::new().unwrap();
|
||||
let mut context = Context::new(&mut mainloop, "LibrePods-get_audio_device_index").unwrap();
|
||||
context.connect(None, ContextFlagSet::NOAUTOSPAWN, None).unwrap();
|
||||
|
||||
loop {
|
||||
match mainloop.iterate(false) {
|
||||
_ if context.get_state() == libpulse_binding::context::State::Ready => break,
|
||||
_ if context.get_state() == libpulse_binding::context::State::Failed || context.get_state() == libpulse_binding::context::State::Terminated => return None,
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
let introspector = context.introspect();
|
||||
let card_info_list = Rc::new(RefCell::new(None));
|
||||
let op = introspector.get_card_info_list({
|
||||
let card_info_list = card_info_list.clone();
|
||||
let mut list = Vec::new();
|
||||
move |result| {
|
||||
match result {
|
||||
ListResult::Item(item) => {
|
||||
let profiles = item.profiles.iter().map(|p| OwnedCardProfileInfo {
|
||||
name: p.name.as_ref().map(|n| n.to_string()),
|
||||
}).collect();
|
||||
list.push(OwnedCardInfo {
|
||||
index: item.index,
|
||||
proplist: item.proplist.clone(),
|
||||
profiles,
|
||||
});
|
||||
},
|
||||
ListResult::End => *card_info_list.borrow_mut() = Some(list.clone()),
|
||||
ListResult::Error => *card_info_list.borrow_mut() = None,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while op.get_state() == OperationState::Running {
|
||||
mainloop.iterate(false);
|
||||
}
|
||||
mainloop.quit(Retval(0));
|
||||
|
||||
if let Some(list) = card_info_list.borrow().as_ref() {
|
||||
for card in list {
|
||||
let props = &card.proplist;
|
||||
if let Some(device_string) = props.get_str("device.string") {
|
||||
if device_string.contains(&mac_clone) {
|
||||
info!("Found audio device index for MAC {}: {}", mac_clone, card.index);
|
||||
return Some(card.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error!("No matching Bluetooth card found for MAC address: {}", mac_clone);
|
||||
None
|
||||
}).await.unwrap_or(None)
|
||||
}
|
||||
|
||||
pub async fn deactivate_a2dp_profile(&self) {
|
||||
debug!("Entering deactivate_a2dp_profile");
|
||||
let mut state = self.state.lock().await;
|
||||
|
||||
if state.device_index.is_none() {
|
||||
state.device_index = self.get_audio_device_index(&state.connected_device_mac).await;
|
||||
}
|
||||
|
||||
if state.connected_device_mac.is_empty() || state.device_index.is_none() {
|
||||
warn!("Connected device MAC or index is empty, cannot deactivate A2DP profile");
|
||||
return;
|
||||
}
|
||||
let device_index = state.device_index.unwrap();
|
||||
drop(state);
|
||||
|
||||
info!("Deactivating A2DP profile for AirPods by setting to off");
|
||||
|
||||
let success = tokio::task::spawn_blocking(move || {
|
||||
set_card_profile_sync(device_index, "off")
|
||||
}).await.unwrap_or(false);
|
||||
|
||||
if success {
|
||||
info!("Successfully deactivated A2DP profile");
|
||||
} else {
|
||||
warn!("Failed to deactivate A2DP profile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_card_profile_sync(card_index: u32, profile_name: &str) -> bool {
|
||||
let mut mainloop = Mainloop::new().unwrap();
|
||||
let mut context = Context::new(&mut mainloop, "LibrePods-set_card_profile").unwrap();
|
||||
context.connect(None, ContextFlagSet::NOAUTOSPAWN, None).unwrap();
|
||||
|
||||
loop {
|
||||
match mainloop.iterate(false) {
|
||||
_ if context.get_state() == libpulse_binding::context::State::Ready => break,
|
||||
_ if context.get_state() == libpulse_binding::context::State::Failed || context.get_state() == libpulse_binding::context::State::Terminated => return false,
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
let mut introspector = context.introspect();
|
||||
let op = introspector.set_card_profile_by_index(card_index, profile_name, None);
|
||||
|
||||
while op.get_state() == OperationState::Running {
|
||||
mainloop.iterate(false);
|
||||
}
|
||||
mainloop.quit(Retval(0));
|
||||
|
||||
true
|
||||
}
|
||||
Reference in New Issue
Block a user