linux-rust: add skeleton for other devices

This commit is contained in:
Kavish Devar
2025-11-10 13:32:47 +05:30
parent 934df2419a
commit a2cda688d4
20 changed files with 1449 additions and 338 deletions
+28 -39
View File
@@ -8,6 +8,8 @@ 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;
const PSM: u16 = 0x1001;
@@ -280,45 +282,11 @@ pub enum AACPEvent {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DeviceType {
AirPods,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LEData {
pub struct AirPodsLEKeys {
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)]
#[serde(tag = "kind", content = "data")]
pub enum DeviceInformation {
AirPods(AirPodsInformation),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceData {
pub name: String,
pub type_: DeviceType,
pub le: LEData,
pub information: Option<DeviceInformation>,
}
pub struct AACPManagerState {
pub sender: Option<mpsc::Sender<Vec<u8>>>,
pub control_command_status_list: Vec<ControlCommandStatus>,
@@ -647,7 +615,7 @@ impl AACPManager {
strings.push(s.to_string());
}
}
strings.remove(0); // Remove the first empty string as per comment
strings.remove(0);
let info = AirPodsInformation {
name: strings.get(0).cloned().unwrap_or_default(),
model_number: strings.get(1).cloned().unwrap_or_default(),
@@ -660,6 +628,10 @@ impl AACPManager {
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(),
le_keys: AirPodsLEKeys {
irk: "".to_string(),
enc_key: "".to_string(),
},
};
let mut state = self.state.lock().await;
if let Some(mac) = state.airpods_mac {
@@ -715,12 +687,29 @@ impl AACPManager {
let device_data = state.devices.entry(mac_str.clone()).or_insert(DeviceData {
name: mac_str.clone(),
type_: DeviceType::AirPods,
le: LEData { irk: "".to_string(), enc_key: "".to_string() },
information: None,
});
match kt {
ProximityKeyType::Irk => device_data.le.irk = hex::encode(key_data),
ProximityKeyType::EncKey => device_data.le.enc_key = hex::encode(key_data),
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.");
}
}
}
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.");
}
}
}
}
}
}
+34 -21
View File
@@ -16,29 +16,34 @@ const OPCODE_READ_REQUEST: u8 = 0x0A;
const OPCODE_WRITE_REQUEST: u8 = 0x12;
const OPCODE_HANDLE_VALUE_NTF: u8 = 0x1B;
const OPCODE_WRITE_RESPONSE: u8 = 0x13;
const RESPONSE_TIMEOUT: u64 = 5000;
#[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ATTHandles {
Transparency = 0x18,
LoudSoundReduction = 0x1B,
HearingAid = 0x2A,
AirPodsTransparency = 0x18,
AirPodsLoudSoundReduction = 0x1B,
AirPodsHearingAid = 0x2A,
NothingEverything = 0x8002,
NothingEverythingRead = 0x8005 // for some reason, and not the same as the write handle
}
#[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ATTCCCDHandles {
Transparency = ATTHandles::Transparency as u16 + 1,
LoudSoundReduction = ATTHandles::LoudSoundReduction as u16 + 1,
HearingAid = ATTHandles::HearingAid as u16 + 1,
Transparency = ATTHandles::AirPodsTransparency as u16 + 1,
LoudSoundReduction = ATTHandles::AirPodsLoudSoundReduction as u16 + 1,
HearingAid = ATTHandles::AirPodsHearingAid as u16 + 1,
}
impl From<ATTHandles> for ATTCCCDHandles {
fn from(handle: ATTHandles) -> Self {
match handle {
ATTHandles::Transparency => ATTCCCDHandles::Transparency,
ATTHandles::LoudSoundReduction => ATTCCCDHandles::LoudSoundReduction,
ATTHandles::HearingAid => ATTCCCDHandles::HearingAid,
ATTHandles::AirPodsTransparency => ATTCCCDHandles::Transparency,
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
}
}
}
@@ -46,18 +51,13 @@ impl From<ATTHandles> for ATTCCCDHandles {
struct ATTManagerState {
sender: Option<mpsc::Sender<Vec<u8>>>,
listeners: HashMap<u16, Vec<mpsc::UnboundedSender<Vec<u8>>>>,
responses: mpsc::UnboundedReceiver<Vec<u8>>,
response_tx: mpsc::UnboundedSender<Vec<u8>>,
}
impl ATTManagerState {
fn new() -> Self {
let (tx, rx) = mpsc::unbounded_channel();
ATTManagerState {
sender: None,
listeners: HashMap::new(),
responses: rx,
response_tx: tx,
listeners: HashMap::new()
}
}
}
@@ -65,13 +65,18 @@ impl ATTManagerState {
#[derive(Clone)]
pub struct ATTManager {
state: Arc<Mutex<ATTManagerState>>,
response_rx: Arc<Mutex<mpsc::UnboundedReceiver<Vec<u8>>>>,
response_tx: mpsc::UnboundedSender<Vec<u8>>,
tasks: Arc<Mutex<JoinSet<()>>>,
}
impl ATTManager {
pub fn new() -> Self {
let (tx, rx) = mpsc::unbounded_channel();
ATTManager {
state: Arc::new(Mutex::new(ATTManagerState::new())),
response_rx: Arc::new(Mutex::new(rx)),
response_tx: tx,
tasks: Arc::new(Mutex::new(JoinSet::new())),
}
}
@@ -184,11 +189,18 @@ impl ATTManager {
}
async fn read_response(&self) -> Result<Vec<u8>> {
let mut state = self.state.lock().await;
match tokio::time::timeout(Duration::from_millis(2000), state.responses.recv()).await {
debug!("Waiting for response...");
let mut rx = self.response_rx.lock().await;
match tokio::time::timeout(Duration::from_millis(RESPONSE_TIMEOUT), rx.recv()).await {
Ok(Some(resp)) => Ok(resp),
Ok(None) => Err(Error::from(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Response channel closed"))),
Err(_) => Err(Error::from(std::io::Error::new(std::io::ErrorKind::TimedOut, "Response timeout"))),
Ok(None) => Err(Error::from(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"Response channel closed"
))),
Err(_) => Err(Error::from(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Response timeout"
))),
}
}
}
@@ -217,10 +229,11 @@ async fn recv_thread(manager: ATTManager, sp: Arc<SeqPacket>) {
let _ = listener.send(value.clone());
}
}
} else if data[0] == OPCODE_WRITE_RESPONSE {
let _ = manager.response_tx.send(vec![]);
} else {
// Response
let state = manager.state.lock().await;
let _ = state.response_tx.send(data[1..].to_vec());
let _ = manager.response_tx.send(data[1..].to_vec());
}
}
Err(e) => {
+22 -1
View File
@@ -1,6 +1,8 @@
use std::io::Error;
use bluer::Adapter;
use log::debug;
pub(crate) async fn find_connected_airpods(adapter: &bluer::Adapter) -> bluer::Result<bluer::Device> {
pub(crate) async fn find_connected_airpods(adapter: &Adapter) -> bluer::Result<bluer::Device> {
let target_uuid = uuid::Uuid::parse_str("74ec2172-0bad-4d01-8f77-997b2be0722a").unwrap();
let addrs = adapter.device_addresses().await?;
@@ -17,4 +19,23 @@ pub(crate) async fn find_connected_airpods(adapter: &bluer::Adapter) -> bluer::R
}
}
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>> {
let addrs = adapter.device_addresses().await?;
let mut devices = Vec::new();
for addr in addrs {
let device = adapter.device(addr)?;
let device_mac = device.address().to_string();
let connected = device.is_connected().await.unwrap_or(false);
debug!("Checking device: {}, connected: {}", device_mac, connected);
if connected && managed_macs.contains(&device_mac) {
debug!("Found managed device: {}", device_mac);
devices.push(device);
}
}
if !devices.is_empty() {
return Ok(devices);
}
Err(bluer::Error::from(Error::new(std::io::ErrorKind::NotFound, "No other managed devices found")))
}
+14 -21
View File
@@ -1,4 +1,3 @@
use std::cmp::PartialEq;
use bluer::monitor::{Monitor, MonitorEvent, Pattern};
use bluer::{Address, Session};
use aes::Aes128;
@@ -14,7 +13,7 @@ use std::sync::Arc;
use tokio::sync::Mutex;
use crate::bluetooth::aacp::BatteryStatus;
use crate::ui::tray::MyTray;
use crate::bluetooth::aacp::{DeviceData, DeviceType};
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] {
@@ -43,14 +42,6 @@ fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool {
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<()> {
let session = Session::new().await?;
let adapter = session.default_adapter().await?;
@@ -107,15 +98,17 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
let mut found_mac = None;
for (airpods_mac, device_data) in &all_devices {
if device_data.type_ == DeviceType::AirPods {
if let Ok(irk_bytes) = hex::decode(&device_data.le.irk) {
if 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, device_data.le.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(DeviceInformation::AirPods(info)) = &device_data.information {
if let Ok(irk_bytes) = hex::decode(&info.le_keys.irk) {
if 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;
}
}
}
}
@@ -133,8 +126,8 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
if let Some(ref mac) = matched_airpods_mac {
if let Some(device_data) = all_devices.get(mac) {
if !device_data.le.enc_key.is_empty() {
if let Ok(enc_key_bytes) = hex::decode(&device_data.le.enc_key) {
if let Some(DeviceInformation::AirPods(info)) = &device_data.information {
if let Ok(enc_key_bytes) = hex::decode(&info.le_keys.enc_key) {
if enc_key_bytes.len() == 16 {
matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap());
}
+52
View File
@@ -0,0 +1,52 @@
use std::collections::HashMap;
use std::sync::Arc;
use crate::bluetooth::aacp::AACPManager;
use crate::bluetooth::att::ATTManager;
pub enum BluetoothManager {
AACP(Arc<AACPManager>),
ATT(Arc<ATTManager>),
}
pub struct DeviceManagers {
att: Option<Arc<ATTManager>>,
aacp: Option<Arc<AACPManager>>,
}
impl DeviceManagers {
fn new() -> Self {
Self { att: None, aacp: None }
}
fn with_aacp(aacp: AACPManager) -> Self {
Self { att: None, aacp: Some(Arc::new(aacp)) }
}
fn with_att(att: ATTManager) -> Self {
Self { att: Some(Arc::new(att)), aacp: None }
}
}
pub struct BluetoothDevices {
devices: HashMap<String, DeviceManagers>,
}
impl BluetoothDevices {
fn new() -> Self {
Self { devices: HashMap::new() }
}
fn add_aacp(&mut self, mac: String, manager: AACPManager) {
self.devices
.entry(mac)
.or_insert_with(DeviceManagers::new)
.aacp = Some(Arc::new(manager));
}
fn add_att(&mut self, mac: String, manager: ATTManager) {
self.devices
.entry(mac)
.or_insert_with(DeviceManagers::new)
.att = Some(Arc::new(manager));
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
pub(crate) mod discovery;
pub mod aacp;
pub mod att;
pub mod le;
pub mod le;
pub mod managers;