mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-31 07:10:45 +00:00
fix(linux-rust): format and fix syntax error
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod airpods;
|
||||
pub mod enums;
|
||||
pub(crate) mod nothing;
|
||||
pub(crate) mod nothing;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mod airpods;
|
||||
pub mod messages;
|
||||
mod nothing;
|
||||
pub mod tray;
|
||||
pub mod window;
|
||||
pub mod messages;
|
||||
mod airpods;
|
||||
mod nothing;
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user