From fd142f814a072e4e8517dc79c0592764055ffbdf Mon Sep 17 00:00:00 2001 From: Gregory Mallios Date: Sat, 23 Dec 2023 15:26:55 +0200 Subject: [PATCH] feat: Scaffold Windows BLE backend support --- soundcore-lib/Cargo.toml | 20 +- soundcore-lib/src/ble.rs | 73 ++++++++ soundcore-lib/src/ble/ble.rs | 1 + soundcore-lib/src/ble/windows.rs | 3 + soundcore-lib/src/ble/windows/connection.rs | 0 soundcore-lib/src/ble/windows/descriptor.rs | 27 +++ soundcore-lib/src/ble/windows/scanner.rs | 135 +++++++++++++ soundcore-lib/src/btaddr.rs | 198 ++++++++++++++++++++ 8 files changed, 454 insertions(+), 3 deletions(-) create mode 100644 soundcore-lib/src/ble.rs create mode 100644 soundcore-lib/src/ble/ble.rs create mode 100644 soundcore-lib/src/ble/windows.rs create mode 100644 soundcore-lib/src/ble/windows/connection.rs create mode 100644 soundcore-lib/src/ble/windows/descriptor.rs create mode 100644 soundcore-lib/src/ble/windows/scanner.rs create mode 100644 soundcore-lib/src/btaddr.rs diff --git a/soundcore-lib/Cargo.toml b/soundcore-lib/Cargo.toml index 29f7dab..d8e9b4a 100644 --- a/soundcore-lib/Cargo.toml +++ b/soundcore-lib/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "soundcore-lib" -version = "0.1.0" +version = "0.2.0" edition = "2021" [dependencies] serde = { version = "1", features = ["derive", "rc"] } -tokio = { version = "1", features = ["time", "macros", "rt-multi-thread"] } +tokio = { version = "1", features = ["time", "macros", "rt-multi-thread", "sync"] } strum = { version = "0.25", features = ["derive"] } nom = "7" enumflags2 = { version = "0.7.7", features = ["serde"] } @@ -16,6 +16,20 @@ log = "0.4.20" typeshare = "1.0.1" phf = { version = "0.11", default-features = false, features = ["macros"] } derive_more = { version = "0.99", features = ["from"] } +uuid = { version = "1.6.1", features = ["v4", "serde"] } [dev-dependencies] -test_data = { path = "../test_data" } \ No newline at end of file +test_data = { path = "../test_data" } + + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.52", features = [ + "Storage_Streams", + "Foundation", + "implement", + "Foundation_Collections", + "Devices_Enumeration", + "Devices_Bluetooth", + "Devices_Bluetooth_GenericAttributeProfile", + "Devices_Bluetooth_Advertisement" +] } diff --git a/soundcore-lib/src/ble.rs b/soundcore-lib/src/ble.rs new file mode 100644 index 0000000..3a5b34e --- /dev/null +++ b/soundcore-lib/src/ble.rs @@ -0,0 +1,73 @@ +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +use crate::error::SoundcoreLibResult; + +mod ble; +mod windows; + +/// The general flow should be: +/// BLEDeviceScanner -> BLEDeviceDescriptor -> BLEConnectionFactory -> BLEConnection -> SoundcoreDevice +#[async_trait] +pub trait BLEConnection { + async fn read_channel(&self) -> SoundcoreLibResult>>; + async fn write(&self, bytes: &[u8], write_type: WriteType) -> SoundcoreLibResult<()>; +} + +#[async_trait] +pub trait BLEConnectionFactory { + type Connection: BLEConnection + Send + Sync; + async fn connect( + &self, + mac_addr: &str, + uuid_set: BLEConnectionUuidSet, + ) -> SoundcoreLibResult; +} + +#[async_trait] +pub trait BLEDeviceScanner { + // type Descriptor: DeviceDescriptor + Clone + Send + Sync; + + async fn scan(&self) -> SoundcoreLibResult>; +} + +pub trait DeviceDescriptor { + fn mac_addr(&self) -> &str; + fn name(&self) -> &str; +} + +pub struct BLEDeviceDescriptor { + pub mac_addr: String, + pub name: String, +} + +impl BLEDeviceDescriptor { + pub fn new(mac_addr: impl Into, name: impl Into) -> Self { + Self { + mac_addr: mac_addr.into(), + name: name.into(), + } + } +} + +impl DeviceDescriptor for BLEDeviceDescriptor { + fn mac_addr(&self) -> &str { + &self.mac_addr + } + + fn name(&self) -> &str { + &self.name + } +} + +pub enum WriteType { + WithResponse, + WithoutResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BLEConnectionUuidSet { + pub service_uuid: uuid::Uuid, + pub read_uuid: uuid::Uuid, + pub write_uuid: uuid::Uuid, +} diff --git a/soundcore-lib/src/ble/ble.rs b/soundcore-lib/src/ble/ble.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/soundcore-lib/src/ble/ble.rs @@ -0,0 +1 @@ + diff --git a/soundcore-lib/src/ble/windows.rs b/soundcore-lib/src/ble/windows.rs new file mode 100644 index 0000000..a00aa34 --- /dev/null +++ b/soundcore-lib/src/ble/windows.rs @@ -0,0 +1,3 @@ +mod connection; +mod descriptor; +mod scanner; diff --git a/soundcore-lib/src/ble/windows/connection.rs b/soundcore-lib/src/ble/windows/connection.rs new file mode 100644 index 0000000..e69de29 diff --git a/soundcore-lib/src/ble/windows/descriptor.rs b/soundcore-lib/src/ble/windows/descriptor.rs new file mode 100644 index 0000000..032c225 --- /dev/null +++ b/soundcore-lib/src/ble/windows/descriptor.rs @@ -0,0 +1,27 @@ +use crate::ble::{BLEDeviceDescriptor, DeviceDescriptor}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WindowsBLEDescriptor { + name: String, + mac_addr: String, +} + +impl WindowsBLEDescriptor { + pub fn new(name: impl Into, mac_addr: impl Into) -> Self { + Self { + name: name.into(), + mac_addr: mac_addr.into(), + } + } +} + +impl DeviceDescriptor for WindowsBLEDescriptor { + fn mac_addr(&self) -> &str { + &self.mac_addr + } + + fn name(&self) -> &str { + &self.name + } +} diff --git a/soundcore-lib/src/ble/windows/scanner.rs b/soundcore-lib/src/ble/windows/scanner.rs new file mode 100644 index 0000000..68aed7a --- /dev/null +++ b/soundcore-lib/src/ble/windows/scanner.rs @@ -0,0 +1,135 @@ +use collections::HashMap; +use std::{collections, sync}; +use sync::{Arc, Mutex}; + +use async_trait::async_trait; +use log::trace; +use tokio::task::spawn_blocking; +use windows::Devices::Bluetooth::Advertisement::{ + BluetoothLEAdvertisementReceivedEventArgs, BluetoothLEAdvertisementWatcher, +}; +use windows::Devices::Bluetooth::BluetoothDevice; +use windows::Devices::Enumeration::DeviceInformation; +use windows::Foundation::TypedEventHandler; +use windows::Storage::Streams::DataReader; + +use crate::ble::{BLEDeviceDescriptor, BLEDeviceScanner}; +use crate::btaddr::BluetoothAdrr; +use crate::error::{SoundcoreLibError, SoundcoreLibResult}; + +const WATCH_DURATION: u64 = 10; + +pub struct WindowsBLEDeviceScanner {} + +impl WindowsBLEDeviceScanner { + pub fn new() -> Self { + Self {} + } +} + +#[async_trait] +impl BLEDeviceScanner for WindowsBLEDeviceScanner { + // type Descriptor = WindowsBLEDescriptor; + + async fn scan(&self) -> SoundcoreLibResult> { + spawn_blocking(move || { + let addr_swap_map = + Arc::new(Mutex::new(HashMap::::new())); + + let device_watcher = BluetoothLEAdvertisementWatcher::new()?; + let handler = TypedEventHandler::new( + move |_sender: &Option, + args: &Option| + -> Result<(), windows::core::Error> { + event_handler(addr_swap_map.clone(), _sender, args) + }, + ); + + // Register the event handler + device_watcher.Received(&handler)?; + + // Scan for devices + device_watcher.Start()?; + std::thread::sleep(std::time::Duration::from_secs(WATCH_DURATION)); + device_watcher.Stop()?; + + let scan_result = + DeviceInformation::FindAllAsyncAqsFilter(&BluetoothDevice::GetDeviceSelector()?)? + .get()?; + + Ok(scan_result + .into_iter() + .map(|info| BluetoothDevice::FromIdAsync(&info.Id()?)?.get()) + .filter_map(|device| device.ok()) + .map(|device| { + let mut addr = BluetoothAdrr::from(device.BluetoothAddress()?); + match addr_swap_map.lock().unwrap().get(&addr) { + Some(new_addr) => { + trace!("Swapping MAC address {:?} with {:?}", addr, new_addr); + addr = new_addr.clone(); + } + None => {} + } + Ok(BLEDeviceDescriptor::new( + device.Name()?.to_string(), + addr.to_string(), + )) as SoundcoreLibResult + }) + .filter_map(|descriptor_result| descriptor_result.ok()) + .collect::>()) + }) + .await + .map_err(|_e| SoundcoreLibError::Unknown)? + } +} + +/// This is a hack to replace the address with the one that is in the BLE advertisment +/// frames and not the one return by the device information. +/// This HashMap has the original address as the key and the new address as the value. +fn event_handler( + swap_map: Arc>>, + _sender: &Option, + args: &Option, +) -> Result<(), windows::core::Error> { + if let Some(args) = args { + let addr = BluetoothAdrr::from( + BluetoothDevice::FromBluetoothAddressAsync(args.BluetoothAddress()?)? + .get()? + .BluetoothAddress()?, + ); + let mut swap_map = swap_map.lock().unwrap(); + if addr.is_soundcore_mac() { + trace!( + "Found candidate device {:?} for swapping MACs, checking advertisement data sections...", + addr + ); + let data_sections = args.Advertisement()?.DataSections()?.into_iter(); + + for section in data_sections { + let data_buf = section.Data()?; + let data_reader = DataReader::FromBuffer(&data_buf)?; + let mut data = vec![0_u8; data_buf.Length()? as usize]; + data_reader.ReadBytes(&mut data)?; + trace!("Found advertisement data section: {:?}", data); + + match BluetoothAdrr::SOUNDCORE_MAC_PREFIXES + .iter() + .any(|prefix| data.starts_with(prefix)) + { + true => { + let addr_to_swap = BluetoothAdrr::from_bytes(&data[0..6]).unwrap(); + if addr_to_swap != addr { + trace!("Found advertisement data section with MAC address, swapping {:?} with {:?}", addr_to_swap, addr); + swap_map.insert(addr_to_swap, addr.clone()); + } + } + false => { + trace!("Found advertisement data section that does not contain a MAC address, skipping..."); + } + } + } + } + drop(swap_map); + } + Ok(()) +} diff --git a/soundcore-lib/src/btaddr.rs b/soundcore-lib/src/btaddr.rs new file mode 100644 index 0000000..d9e08fa --- /dev/null +++ b/soundcore-lib/src/btaddr.rs @@ -0,0 +1,198 @@ +use std::fmt::Display; + +use crate::error::{SoundcoreLibError, SoundcoreLibResult}; + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct BluetoothAdrr { + pub address: [u8; 6], +} + +impl BluetoothAdrr { + pub const SOUNDCORE_MAC_PREFIXES: [[u8; 3]; 2] = [[0xAC, 0x12, 0x2F], [0xE8, 0xEE, 0xCC]]; + + pub fn from_str(address: &str) -> SoundcoreLibResult { + match address.contains(':') { + true => Self::from_colon_str(address), + false => Self::from_dash_str(address), + } + } + + pub fn from_bytes(bytes: &[u8]) -> SoundcoreLibResult { + if bytes.len() != 6 { + return Err(SoundcoreLibError::InvalidMACAddress { + addr: format!("{:?}", bytes), + }); + } + Ok(Self { + address: bytes.try_into().unwrap(), + }) + } + + fn from_colon_str(address: &str) -> SoundcoreLibResult { + let addr = address + .split(':') + .map(|x| u8::from_str_radix(x, 16)) + .collect::, _>>() + .map_err(|_| SoundcoreLibError::InvalidMACAddress { + addr: address.into(), + })?; + Ok(Self { + address: addr + .try_into() + .map_err(|_| SoundcoreLibError::InvalidMACAddress { + addr: address.into(), + })?, + }) + } + + fn from_dash_str(address: &str) -> SoundcoreLibResult { + let addr = address + .split('-') + .map(|x| u8::from_str_radix(x, 16)) + .collect::, _>>() + .map_err(|_| SoundcoreLibError::InvalidMACAddress { + addr: address.into(), + })?; + Ok(Self { + address: addr + .try_into() + .map_err(|_| SoundcoreLibError::InvalidMACAddress { + addr: address.into(), + })?, + }) + } + + pub fn is_soundcore_mac(&self) -> bool { + Self::SOUNDCORE_MAC_PREFIXES + .iter() + .any(|prefix| self.address.starts_with(prefix)) + } +} + +impl Into for BluetoothAdrr { + fn into(self) -> String { + format!( + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + self.address[0], + self.address[1], + self.address[2], + self.address[3], + self.address[4], + self.address[5] + ) + } +} + +/// Windows Bluetooth Address +impl From for BluetoothAdrr { + fn from(value: u64) -> Self { + let bytes = value.to_be_bytes(); + BluetoothAdrr { + address: [bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]], + } + } +} + +impl From for u64 { + fn from(value: BluetoothAdrr) -> Self { + let mut bytes = [0u8; 8]; + bytes[2..].copy_from_slice(&value.address); + u64::from_be_bytes(bytes) + } +} + +impl Display for BluetoothAdrr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + self.address[0], + self.address[1], + self.address[2], + self.address[3], + self.address[4], + self.address[5], + ) + } +} + +#[cfg(test)] +mod mac_tests { + use super::*; + + #[test] + fn from_string_with_colon_separator() { + let address_str = String::from("11:22:33:44:55:66"); + let address = BluetoothAdrr::from_str(&address_str).unwrap(); + + assert_eq!(address.address, [0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + } + + #[test] + fn from_string_with_dash_separator() { + let address_str = String::from("11-22-33-44-55-66"); + let address = BluetoothAdrr::from_str(&address_str).unwrap(); + + assert_eq!(address.address, [0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + } + + #[test] + fn into_string() { + let address = BluetoothAdrr { + address: [0x11, 0x22, 0x33, 0x44, 0x55, 0x66], + }; + + let address_str: String = address.into(); + + assert_eq!(address_str, "11:22:33:44:55:66"); + } + + #[test] + fn display_formatting() { + let address = BluetoothAdrr { + address: [0x11, 0x22, 0x33, 0x44, 0x55, 0x66], + }; + + let formatted_address = format!("{}", address); + + assert_eq!(formatted_address, "11:22:33:44:55:66"); + } + + #[test] + fn check_soundcore_mac() { + let address = BluetoothAdrr { + address: [0xAC, 0x12, 0x2F, 0x44, 0x55, 0x66], + }; + + assert!(address.is_soundcore_mac()); + + let non_soundcore = BluetoothAdrr { + address: [0x11, 0x22, 0x33, 0x44, 0x55, 0x66], + }; + + assert!(!non_soundcore.is_soundcore_mac()); + } + + #[test] + fn test_from_windows_u64() { + let address_value: u64 = 0xB123456789AB; + let address = BluetoothAdrr::from(address_value); + assert_eq!(address.address, [0xB1, 0x23, 0x45, 0x67, 0x89, 0xAB]) + } + + #[test] + fn test_from_windows_u64_to_string() { + let address_value: u64 = 0xB123456789AB; + let address = BluetoothAdrr::from(address_value); + assert_eq!(address.to_string(), "B1:23:45:67:89:AB") + } + + #[test] + fn test_into_windows_u64() { + let address = BluetoothAdrr { + address: [0x33, 0x44, 0x55, 0x66, 0x77, 0x00], + }; + let address_value: u64 = address.into(); + assert_eq!(address_value, 0x334455667700); + } +}