linux-rust: parse and store device info

This commit is contained in:
Kavish Devar
2025-10-28 17:42:59 +05:30
parent c5a824c384
commit fa8bc11060
2 changed files with 154 additions and 52 deletions

View File

@@ -15,10 +15,10 @@ const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const POLL_INTERVAL: Duration = Duration::from_millis(200); const POLL_INTERVAL: Duration = Duration::from_millis(200);
const HEADER_BYTES: [u8; 4] = [0x04, 0x00, 0x04, 0x00]; const HEADER_BYTES: [u8; 4] = [0x04, 0x00, 0x04, 0x00];
fn get_proximity_keys_path() -> PathBuf { fn get_devices_path() -> PathBuf {
let data_dir = std::env::var("XDG_DATA_HOME") let data_dir = std::env::var("XDG_DATA_HOME")
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default()));
PathBuf::from(data_dir).join("librepods").join("proximity_keys.json") PathBuf::from(data_dir).join("librepods").join("devices.json")
} }
pub mod opcodes { pub mod opcodes {
@@ -28,7 +28,7 @@ pub mod opcodes {
pub const CONTROL_COMMAND: u8 = 0x09; pub const CONTROL_COMMAND: u8 = 0x09;
pub const EAR_DETECTION: u8 = 0x06; pub const EAR_DETECTION: u8 = 0x06;
pub const CONVERSATION_AWARENESS: u8 = 0x4B; pub const CONVERSATION_AWARENESS: u8 = 0x4B;
pub const DEVICE_METADATA: u8 = 0x1D; pub const INFORMATION: u8 = 0x1D;
pub const RENAME: u8 = 0x1E; pub const RENAME: u8 = 0x1E;
pub const PROXIMITY_KEYS_REQ: u8 = 0x30; pub const PROXIMITY_KEYS_REQ: u8 = 0x30;
pub const PROXIMITY_KEYS_RSP: u8 = 0x31; pub const PROXIMITY_KEYS_RSP: u8 = 0x31;
@@ -242,6 +242,39 @@ pub enum AACPEvent {
OwnershipToFalseRequest, OwnershipToFalseRequest,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DeviceType {
AirPods,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LEData {
pub irk: String,
pub enc_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AirPodsInformation {
pub name: String,
pub model_number: String,
pub manufacturer: String,
pub serial_number: String,
pub version1: String,
pub version2: String,
pub hardware_revision: String,
pub updater_identifier: String,
pub left_serial_number: String,
pub right_serial_number: String,
pub version3: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceData {
pub type_: DeviceType,
pub le: LEData,
pub information: Option<AirPodsInformation>,
}
pub struct AACPManagerState { pub struct AACPManagerState {
pub sender: Option<mpsc::Sender<Vec<u8>>>, pub sender: Option<mpsc::Sender<Vec<u8>>>,
pub control_command_status_list: Vec<ControlCommandStatus>, pub control_command_status_list: Vec<ControlCommandStatus>,
@@ -255,16 +288,17 @@ pub struct AACPManagerState {
pub old_ear_detection_status: Vec<EarDetectionStatus>, pub old_ear_detection_status: Vec<EarDetectionStatus>,
pub ear_detection_status: Vec<EarDetectionStatus>, pub ear_detection_status: Vec<EarDetectionStatus>,
event_tx: Option<mpsc::UnboundedSender<AACPEvent>>, event_tx: Option<mpsc::UnboundedSender<AACPEvent>>,
proximity_keys: HashMap<ProximityKeyType, Vec<u8>>, pub devices: HashMap<String, DeviceData>,
pub airpods_mac: Option<Address>, pub airpods_mac: Option<Address>,
} }
impl AACPManagerState { impl AACPManagerState {
fn new() -> Self { fn new() -> Self {
let proximity_keys = std::fs::read_to_string(get_proximity_keys_path()) let devices: HashMap<String, DeviceData> =
.ok() std::fs::read_to_string(get_devices_path())
.and_then(|s| serde_json::from_str(&s).ok()) .ok()
.unwrap_or_default(); .and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
AACPManagerState { AACPManagerState {
sender: None, sender: None,
control_command_status_list: Vec::new(), control_command_status_list: Vec::new(),
@@ -278,7 +312,7 @@ impl AACPManagerState {
old_ear_detection_status: Vec::new(), old_ear_detection_status: Vec::new(),
ear_detection_status: Vec::new(), ear_detection_status: Vec::new(),
event_tx: None, event_tx: None,
proximity_keys, devices,
airpods_mac: None, airpods_mac: None,
} }
} }
@@ -542,7 +576,66 @@ impl AACPManager {
info!("Received Conversation Awareness packet with unexpected length: {}", packet.len()); info!("Received Conversation Awareness packet with unexpected length: {}", packet.len());
} }
} }
opcodes::DEVICE_METADATA => info!("Received Device Metadata packet."), opcodes::INFORMATION => {
if payload.len() < 6 {
error!("Information packet too short: {}", hex::encode(payload));
return;
}
let data = &payload[4..];
let mut index = 0;
while index < data.len() && data[index] != 0x00 {
index += 1;
}
let mut strings = Vec::new();
while index < data.len() {
while index < data.len() && data[index] == 0x00 {
index += 1;
}
if index >= data.len() {
break;
}
let start = index;
while index < data.len() && data[index] != 0x00 {
index += 1;
}
let str_bytes = &data[start..index];
if let Ok(s) = std::str::from_utf8(str_bytes) {
strings.push(s.to_string());
}
}
strings.remove(0); // Remove the first empty string as per comment
let info = AirPodsInformation {
name: strings.get(0).cloned().unwrap_or_default(),
model_number: strings.get(1).cloned().unwrap_or_default(),
manufacturer: strings.get(2).cloned().unwrap_or_default(),
serial_number: strings.get(3).cloned().unwrap_or_default(),
version1: strings.get(4).cloned().unwrap_or_default(),
version2: strings.get(5).cloned().unwrap_or_default(),
hardware_revision: strings.get(6).cloned().unwrap_or_default(),
updater_identifier: strings.get(7).cloned().unwrap_or_default(),
left_serial_number: strings.get(8).cloned().unwrap_or_default(),
right_serial_number: strings.get(9).cloned().unwrap_or_default(),
version3: strings.get(10).cloned().unwrap_or_default(),
};
let mut state = self.state.lock().await;
if let Some(mac) = state.airpods_mac {
if let Some(device_data) = state.devices.get_mut(&mac.to_string()) {
device_data.information = Some(info.clone());
}
}
let json = serde_json::to_string(&state.devices).unwrap();
if let Some(parent) = get_devices_path().parent() {
if 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 => { opcodes::PROXIMITY_KEYS_RSP => {
if payload.len() < 4 { if payload.len() < 4 {
error!("Proximity Keys Response packet too short: {}", hex::encode(payload)); error!("Proximity Keys Response packet too short: {}", hex::encode(payload));
@@ -572,34 +665,29 @@ impl AACPManager {
let mut state = self.state.lock().await; let mut state = self.state.lock().await;
for (key_type, key_data) in &keys { for (key_type, key_data) in &keys {
if let Some(kt) = ProximityKeyType::from_u8(*key_type) { if let Some(kt) = ProximityKeyType::from_u8(*key_type) {
state.proximity_keys.insert(kt, key_data.clone()); if let Some(mac) = state.airpods_mac {
} let mac_str = mac.to_string();
} let device_data = state.devices.entry(mac_str.clone()).or_insert(DeviceData {
type_: DeviceType::AirPods,
if let Some(mac) = state.airpods_mac { le: LEData { irk: "".to_string(), enc_key: "".to_string() },
let path = get_proximity_keys_path(); information: None,
let mut all_keys: HashMap<String, HashMap<ProximityKeyType, Vec<u8>>> = });
std::fs::read_to_string(&path) match kt {
.ok() ProximityKeyType::Irk => device_data.le.irk = hex::encode(key_data),
.and_then(|s| serde_json::from_str(&s).ok()) ProximityKeyType::EncKey => device_data.le.enc_key = hex::encode(key_data),
.unwrap_or_default(); }
all_keys.insert(mac.to_string(), state.proximity_keys.clone());
let json = serde_json::to_string(&all_keys).unwrap();
if let Some(parent) = path.parent() {
if let Err(e) = tokio::fs::create_dir_all(&parent).await {
error!("Failed to create directory for proximity keys: {}", e);
return;
} }
} }
if let Err(e) = tokio::fs::write(&path, json).await { }
error!("Failed to save proximity keys: {}", e); let json = serde_json::to_string(&state.devices).unwrap();
if let Some(parent) = get_devices_path().parent() {
if 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 {
if let Some(ref tx) = state.event_tx { error!("Failed to save devices: {}", e);
let _ = tx.send(AACPEvent::ProximityKeys(keys));
} }
}, },
opcodes::STEM_PRESS => info!("Received Stem Press packet."), opcodes::STEM_PRESS => info!("Received Stem Press packet."),

View File

@@ -1,3 +1,4 @@
use std::cmp::PartialEq;
use bluer::monitor::{Monitor, MonitorEvent, Pattern}; use bluer::monitor::{Monitor, MonitorEvent, Pattern};
use bluer::{Address, Session}; use bluer::{Address, Session};
use aes::Aes128; use aes::Aes128;
@@ -15,11 +16,12 @@ use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::bluetooth::aacp::BatteryStatus; use crate::bluetooth::aacp::BatteryStatus;
use crate::ui::tray::MyTray; use crate::ui::tray::MyTray;
use crate::bluetooth::aacp::{DeviceData, LEData, DeviceType};
fn get_proximity_keys_path() -> PathBuf { fn get_devices_path() -> PathBuf {
let data_dir = std::env::var("XDG_DATA_HOME") let data_dir = std::env::var("XDG_DATA_HOME")
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default()));
PathBuf::from(data_dir).join("librepods").join("proximity_keys.json") PathBuf::from(data_dir).join("librepods").join("devices.json")
} }
fn get_preferences_path() -> PathBuf { fn get_preferences_path() -> PathBuf {
@@ -76,13 +78,21 @@ fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool {
hash == computed_hash hash == computed_hash
} }
impl PartialEq for DeviceType {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(DeviceType::AirPods, DeviceType::AirPods) => true
}
}
}
pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> bluer::Result<()> { pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> bluer::Result<()> {
let session = Session::new().await?; let session = Session::new().await?;
let adapter = session.default_adapter().await?; let adapter = session.default_adapter().await?;
adapter.set_powered(true).await?; adapter.set_powered(true).await?;
let all_proximity_keys: HashMap<String, HashMap<ProximityKeyType, Vec<u8>>> = let all_devices: HashMap<String, DeviceData> =
std::fs::read_to_string(get_proximity_keys_path()) std::fs::read_to_string(get_devices_path())
.ok() .ok()
.and_then(|s| serde_json::from_str(&s).ok()) .and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default(); .unwrap_or_default();
@@ -130,16 +140,18 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
} else { } else {
debug!("Checking RPA for device: {}", addr_str); debug!("Checking RPA for device: {}", addr_str);
let mut found_mac = None; let mut found_mac = None;
for (airpods_mac, keys) in &all_proximity_keys { for (airpods_mac, device_data) in &all_devices {
if let Some(irk_vec) = keys.get(&ProximityKeyType::Irk) { if device_data.type_ == DeviceType::AirPods {
if irk_vec.len() == 16 { if let Ok(irk_bytes) = hex::decode(&device_data.le.irk) {
let irk: [u8; 16] = irk_vec.as_slice().try_into().unwrap(); if irk_bytes.len() == 16 {
debug!("Verifying RPA {} for airpods MAC {} with IRK {}", addr_str, airpods_mac, hex::encode(irk)); let irk: [u8; 16] = irk_bytes.as_slice().try_into().unwrap();
if verify_rpa(&addr_str, &irk) { debug!("Verifying RPA {} for airpods MAC {} with IRK {}", addr_str, airpods_mac, device_data.le.irk);
info!("Matched our device ({}) with the irk for {}", addr, airpods_mac); if verify_rpa(&addr_str, &irk) {
verified_macs.insert(addr, airpods_mac.clone()); info!("Matched our device ({}) with the irk for {}", addr, airpods_mac);
found_mac = Some(airpods_mac.clone()); verified_macs.insert(addr, airpods_mac.clone());
break; found_mac = Some(airpods_mac.clone());
break;
}
} }
} }
} }
@@ -155,10 +167,12 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
} }
if let Some(ref mac) = matched_airpods_mac { if let Some(ref mac) = matched_airpods_mac {
if let Some(keys) = all_proximity_keys.get(mac) { if let Some(device_data) = all_devices.get(mac) {
if let Some(enc_key_vec) = keys.get(&ProximityKeyType::EncKey) { if !device_data.le.enc_key.is_empty() {
if enc_key_vec.len() == 16 { if let Ok(enc_key_bytes) = hex::decode(&device_data.le.enc_key) {
matched_enc_key = Some(enc_key_vec.as_slice().try_into().unwrap()); if enc_key_bytes.len() == 16 {
matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap());
}
} }
} }
} }