diff --git a/fastpair/rust/Cargo.toml b/fastpair/rust/Cargo.toml index 02a7b908..a8bc4428 100644 --- a/fastpair/rust/Cargo.toml +++ b/fastpair/rust/Cargo.toml @@ -29,6 +29,7 @@ thiserror = "1.0.43" [target.'cfg(windows)'.dependencies] windows = { version = "0.48", features = [ "Devices_Bluetooth", + "Devices_Enumeration", "Devices_Bluetooth_Advertisement", "Foundation", ] } diff --git a/fastpair/rust/src/bluetooth/common/address.rs b/fastpair/rust/src/bluetooth/common/address.rs index 87c9139d..dc54e067 100644 --- a/fastpair/rust/src/bluetooth/common/address.rs +++ b/fastpair/rust/src/bluetooth/common/address.rs @@ -93,7 +93,7 @@ impl From for u64 { impl From for u64 { fn from(addr: ClassicAddress) -> Self { let mut bytes = [0u8; 8]; - bytes.copy_from_slice(&addr.0); + bytes[..6].copy_from_slice(&addr.0); u64::from_le_bytes(bytes) } diff --git a/fastpair/rust/src/bluetooth/common/device.rs b/fastpair/rust/src/bluetooth/common/device.rs index 33ef11c2..a4c5756c 100644 --- a/fastpair/rust/src/bluetooth/common/device.rs +++ b/fastpair/rust/src/bluetooth/common/device.rs @@ -12,12 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. +use async_trait::async_trait; + +use super::{Address, BluetoothError, PairingResult}; + /// Concrete types implementing this trait represent Bluetooth Peripheral devices. /// They provide methods for retrieving device info and running device actions, /// such as pairing. -use super::BluetoothError; - -pub trait Device { +#[async_trait] +pub trait Device: Sized { /// Retrieve the name advertised by this device. fn name(&self) -> Result; + + /// Retrieve this device's Bluetooth address information. + fn address(&self) -> Address; + + /// Attempt pairing with the peripheral device. + async fn pair(&self) -> Result; } diff --git a/fastpair/rust/src/bluetooth/common/error.rs b/fastpair/rust/src/bluetooth/common/error.rs index 3e7ac640..5c9e49e9 100644 --- a/fastpair/rust/src/bluetooth/common/error.rs +++ b/fastpair/rust/src/bluetooth/common/error.rs @@ -18,10 +18,12 @@ use thiserror::Error; #[non_exhaustive] #[derive(Error, Debug)] pub enum BluetoothError { + /// Reported when Bluetooth device pairing fails. + #[error("pairing error: {0}")] + PairingFailed(String), /// Indicates that the operation was rejected because the system is not in /// a state required for the operation's execution. /// E.g. The user calls `stop_scan()` or polls the advertisement stream - /// before calling `start_scan()`. #[error("failed precondition: {0}")] FailedPrecondition(String), /// Reported when the user calls an operation that is supported by their @@ -40,3 +42,15 @@ pub enum BluetoothError { #[error("internal error: {0}")] Internal(String), } + +/// Abstraction around platform-specific pairing status enums. +/// `PairingResult::Failure` should eventually be converted to +/// `BluetoothError::PairingFailed`. +#[non_exhaustive] +#[derive(Debug)] +pub enum PairingResult { + Success, + AlreadyPaired, + AlreadyInProgress, + Failure(String), +} diff --git a/fastpair/rust/src/bluetooth/mod.rs b/fastpair/rust/src/bluetooth/mod.rs index 765086f2..fb374de6 100644 --- a/fastpair/rust/src/bluetooth/mod.rs +++ b/fastpair/rust/src/bluetooth/mod.rs @@ -18,18 +18,24 @@ pub mod common; -pub use common::{Adapter, BluetoothError, Device}; +pub use common::{Adapter, Address, BluetoothError, ClassicAddress, Device}; cfg_if::cfg_if! { if #[cfg(windows)] { mod windows; - use self::windows::BleAdapter; + use self::windows::{ClassicDevice, BleAdapter}; } else { mod unsupported; - use unsupported::BleAdapter; + use unsupported::{ClassicDevice, BleAdapter}; } } pub async fn default_adapter() -> Result { BleAdapter::default().await } + +pub async fn new_classic_device( + addr: ClassicAddress, +) -> Result { + ClassicDevice::new(addr).await +} diff --git a/fastpair/rust/src/bluetooth/windows/device.rs b/fastpair/rust/src/bluetooth/windows/device.rs index 6d1a4a29..7e9f9cf0 100644 --- a/fastpair/rust/src/bluetooth/windows/device.rs +++ b/fastpair/rust/src/bluetooth/windows/device.rs @@ -13,42 +13,162 @@ // limitations under the License. use async_trait::async_trait; -use windows::Devices::Bluetooth::{ - // Enum describing the type of address (public, random, unspecified). - // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothaddresstype?view=winrt-22621 - BluetoothAddressType, - - // Struct for interacting with and pairing to a discovered BLE device. - // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice?view=winrt-22621 - BluetoothLEDevice, +use tracing::{info, warn}; +use windows::{ + Devices::{ + Bluetooth::{ + // Tuple struct describing the type of address (public, random, unspecified). + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothaddresstype?view=winrt-22621 + BluetoothAddressType, + + // Struct for interacting with a discovered BT Classic device. + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothdevice?view=winrt-22621 + BluetoothDevice, + + // Struct for interacting with a discovered BLE device. + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice?view=winrt-22621 + BluetoothLEDevice, + }, + Enumeration::{ + // Struct for custom pairing with a device. + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.enumeration.deviceinformationcustompairing?view=winrt-22621 + DeviceInformationCustomPairing, + + // Tuple struct to indicate the kinds of pairing supported by the application. + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.enumeration.devicepairingkinds?view=winrt-22621 + DevicePairingKinds, + + // Struct for retrieving data about a PairingRequested event. + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.enumeration.devicepairingrequestedeventargs?view=winrt-22621 + DevicePairingRequestedEventArgs, + }, + }, + // Wraps a closure for handling events associated with a struct + // (e.g. PairingRequested event in `DeviceInformationCustomPairing`). + // https://learn.microsoft.com/en-us/uwp/api/windows.foundation.typedeventhandler-2?view=winrt-22621 + Foundation::TypedEventHandler, }; -use crate::bluetooth::common::{BleAddress, BluetoothError, Device}; +use crate::bluetooth::common::{Address, BleAddress, ClassicAddress, Device, BluetoothError, PairingResult}; /// Concrete type implementing `Device`, used for Windows BLE. pub struct BleDevice { inner: BluetoothLEDevice, + addr: BleAddress, +} + +/// Concrete type implementing `Device`, used for Windows Bluetooth Classic. +pub struct ClassicDevice { + inner: BluetoothDevice, + addr: ClassicAddress, } impl BleDevice { + /// `BleDevice` constructor. pub async fn new(addr: BleAddress) -> Result { let kind = BluetoothAddressType::from(addr.get_kind()); - let addr = u64::from(addr); + let raw_addr = u64::from(addr); let inner = BluetoothLEDevice::FromBluetoothAddressWithBluetoothAddressTypeAsync( - addr, kind, + raw_addr, kind, )? .await?; - Ok(BleDevice { inner }) + Ok(BleDevice { inner, addr }) } } #[async_trait] impl Device for BleDevice { + fn name(&self) -> Result { + Ok(self.inner.Name()?.to_string()) + } + + fn address(&self) -> Address { + Address::Ble(self.addr) + } + + async fn pair(&self) -> Result { + // BLE Audio isn't supported on Windows natively, so devices can pair + // but don't playback. Might possibly work with UWP. Since the Classic + // and BLE APIs are very similar, it might be possible to copy-paste + // `ClassicDevice::pair` directly. + unimplemented!("BLE Pairing is currently unsupported.") + } +} + + +impl ClassicDevice { + /// `ClassicDevice` constructor. + pub async fn new(addr: ClassicAddress) -> Result { + let raw_addr = u64::from(addr); + + let inner = BluetoothDevice::FromBluetoothAddressAsync( + raw_addr, + )? + .await?; + + Ok(ClassicDevice { inner, addr }) + } +} + +#[async_trait] +impl Device for ClassicDevice { fn name(&self) -> Result { Ok(self.inner.Name()?.to_string_lossy()) } + + fn address(&self) -> Address { + Address::Classic(self.addr) + } + + async fn pair(&self) -> Result { + let pair_info = self.inner.DeviceInformation()?.Pairing()?; + if pair_info.IsPaired()? { + info!("Device already paired"); + Ok(PairingResult::AlreadyPaired) + } else if !pair_info.CanPair()? { + info!("Device can't pair"); + Err(BluetoothError::PairingFailed(String::from("device can't pair"))) + } else { + let custom = pair_info.Custom()?; + custom.PairingRequested(&TypedEventHandler::new( + |_custom: &Option, + event_args: &Option, + | { + if let Some(event_args) = event_args { + match event_args.PairingKind()? { + DevicePairingKinds::ConfirmOnly => { + event_args.Accept() + } + _ => { + warn!("Unsupported pairing kind {:?}", event_args.PairingKind()); + Ok(()) + } + } + } else { + warn!("Empty pairing event arguments"); + Ok(()) + } + + }, + ))?; + let res = custom + .PairAsync( + DevicePairingKinds::ConfirmOnly + | DevicePairingKinds::ProvidePin + | DevicePairingKinds::ConfirmPinMatch + | DevicePairingKinds::DisplayPin, + )? + .await?; + let status = PairingResult::from(res.Status()?); + + match status { + PairingResult::Failure(msg) => Err(BluetoothError::PairingFailed(msg)), + _ => Ok(status), + } + } + } } mod tests { diff --git a/fastpair/rust/src/bluetooth/windows/error.rs b/fastpair/rust/src/bluetooth/windows/error.rs index 04c6bce3..59e12a31 100644 --- a/fastpair/rust/src/bluetooth/windows/error.rs +++ b/fastpair/rust/src/bluetooth/windows/error.rs @@ -12,10 +12,46 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::bluetooth::common::BluetoothError; +use windows::Devices::Enumeration::DevicePairingResultStatus; + +use crate::bluetooth::common::{BluetoothError, PairingResult}; impl From for BluetoothError { fn from(err: windows::core::Error) -> Self { BluetoothError::System(err.to_string()) } } + +// https://learn.microsoft.com/en-us/uwp/api/windows.devices.enumeration.devicepairingresultstatus?view=winrt-22621 +impl From for PairingResult { + fn from(status: DevicePairingResultStatus) -> Self { + match status { + DevicePairingResultStatus::Paired => PairingResult::Success, + DevicePairingResultStatus::AlreadyPaired => { + PairingResult::AlreadyPaired + } + DevicePairingResultStatus::OperationAlreadyInProgress => { + PairingResult::AlreadyInProgress + } + DevicePairingResultStatus::NotReadyToPair => PairingResult::Failure( + String::from("the device object is not in a state where it can be paired"), + ), + DevicePairingResultStatus::NotPaired => PairingResult::Failure(String::from("the device object is not currently paired.")), + DevicePairingResultStatus::ConnectionRejected => PairingResult::Failure(String::from("the device object rejected the connection.")), + DevicePairingResultStatus::TooManyConnections => PairingResult::Failure(String::from("the device object indicated it cannot accept any more incoming connections.")), + DevicePairingResultStatus::HardwareFailure => PairingResult::Failure(String::from("the device object indicated there was a hardware failure.")), + DevicePairingResultStatus::AuthenticationTimeout => PairingResult::Failure(String::from("the authentication process timed out before it could complete.")), + DevicePairingResultStatus::AuthenticationNotAllowed => PairingResult::Failure(String::from("the authentication protocol is not supported, so the device is not paired.")), + DevicePairingResultStatus::AuthenticationFailure => PairingResult::Failure(String::from("authentication failed, so the device is not paired. Either the device object or the application rejected the authentication.")), + DevicePairingResultStatus::NoSupportedProfiles => PairingResult::Failure(String::from("there are no network profiles for this device object to use.")), + DevicePairingResultStatus::ProtectionLevelCouldNotBeMet => PairingResult::Failure(String::from("the minimum level of protection is not supported by the device object or the application.")), + DevicePairingResultStatus::AccessDenied => PairingResult::Failure(String::from("your application does not have the appropriate permissions level to pair the device object.")), + DevicePairingResultStatus::InvalidCeremonyData => PairingResult::Failure(String::from("the ceremony data was incorrect.")), + DevicePairingResultStatus::PairingCanceled => PairingResult::Failure(String::from("the pairing action was canceled before completion.")), + DevicePairingResultStatus::RequiredHandlerNotRegistered => PairingResult::Failure(String::from("either the event handler wasn't registered or a required DevicePairingKinds was not supported.",)), + DevicePairingResultStatus::RejectedByHandler => PairingResult::Failure(String::from("the application handler rejected the pairing.")), + DevicePairingResultStatus::RemoteDeviceHasAssociation => PairingResult::Failure(String::from("the remote device already has an association.")), + DevicePairingResultStatus::Failed | _ => PairingResult::Failure(String::from("an unknown failure occurred.")), + } + } +} diff --git a/fastpair/rust/src/main.rs b/fastpair/rust/src/main.rs index c0326ba6..7b796f5a 100644 --- a/fastpair/rust/src/main.rs +++ b/fastpair/rust/src/main.rs @@ -18,18 +18,48 @@ use futures::executor; mod bluetooth; -use bluetooth::{Adapter, Device}; +use bluetooth::{Adapter, Address, ClassicAddress, Device}; fn main() -> Result<(), Box> { let run = async { let mut adapter = bluetooth::default_adapter().await?; adapter.start_scan_devices()?; - while let Ok(device) = adapter.next_device().await { - println!("found {}", device.name()?) - } + while let Ok(ble_device) = adapter.next_device().await { + let name = ble_device.name()?; - unreachable!("Done scanning"); + if name.contains("LE_WF-1000XM3") { + println!("FOUND {} ", name); + + let addr: Address = ble_device.address(); + + // Dynamic dispatch is necessary here because `BleDevice` and + // `ClassicDevice` share the `Device` trait (and thus must have + // the same return type for `address()` method). This can be + // changed if `Device` trait should exclusively define + // cross-platform behavior. + let classic_addr = match addr { + Address::Ble(ble) => ClassicAddress::try_from(ble), + Address::Classic(_) => unreachable!( + "Address should come from BLE Device, therefore \ + shouldn't be Classic." + ), + }?; + + let classic_device = + bluetooth::new_classic_device(classic_addr).await?; + + match classic_device.pair().await { + Ok(_) => { + println!("Pairing success!"); + } + Err(err) => println!("Error {}", err), + } + break; + } + } + println!("Done scanning"); + Ok(()) }; executor::block_on(run)