diff --git a/fastpair/rust/Cargo.toml b/fastpair/rust/Cargo.toml index 20a09107..02b48ebc 100644 --- a/fastpair/rust/Cargo.toml +++ b/fastpair/rust/Cargo.toml @@ -20,15 +20,18 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0" futures = { version = "0.3", features = ["executor"] } tracing = "0.1.37" cfg-if = "1.0.0" async-trait = "0.1" +thiserror = "1.0.43" [target.'cfg(windows)'.dependencies] windows = { version = "0.48", features = [ "Devices_Bluetooth", + "Devices_Enumeration", "Devices_Bluetooth_Advertisement", "Foundation", + "Foundation_Collections", + "Storage_Streams", ] } diff --git a/fastpair/rust/src/bluetooth/common.rs b/fastpair/rust/src/bluetooth/api/adapter.rs similarity index 55% rename from fastpair/rust/src/bluetooth/common.rs rename to fastpair/rust/src/bluetooth/api/adapter.rs index 33ba2c4c..39ab9310 100644 --- a/fastpair/rust/src/bluetooth/common.rs +++ b/fastpair/rust/src/bluetooth/api/adapter.rs @@ -14,29 +14,26 @@ use async_trait::async_trait; +use crate::bluetooth::common::{ + BleAdvertisement, BleDataTypeId, BluetoothError, +}; + /// Concrete types implementing this trait are Bluetooth Central devices. /// They provide methods for retrieving nearby connections and device info. #[async_trait] -pub trait Adapter: Sized { - type Device: Device; - +pub trait BleAdapter: Sized { /// Retrieve the system-default Bluetooth adapter. - async fn default() -> Result; + async fn default() -> Result; - /// Begin scanning for nearby devices. - fn start_scan_devices(&mut self) -> Result<(), anyhow::Error>; + /// Begin scanning for nearby advertisements. + fn start_scan(&mut self) -> Result<(), BluetoothError>; - /// Stop scanning for nearby devices. - fn stop_scan_devices(&mut self) -> Result<(), anyhow::Error>; + /// Stop scanning for nearby advertisements. + fn stop_scan(&mut self) -> Result<(), BluetoothError>; /// Poll next discovered device. - async fn next_device(&mut self) -> Result; -} - -/// Concrete types implementing this trait represent Bluetooth Peripheral devices. -/// They provide methods for retrieving device info and running device actions, -/// such as pairing. -pub trait Device { - /// Retrieve the name advertised by this device. - fn name(&self) -> Result; + async fn next_advertisement( + &mut self, + data_selector: Option<&Vec>, + ) -> Result; } diff --git a/fastpair/rust/src/bluetooth/api/device.rs b/fastpair/rust/src/bluetooth/api/device.rs new file mode 100644 index 00000000..97cba1d8 --- /dev/null +++ b/fastpair/rust/src/bluetooth/api/device.rs @@ -0,0 +1,56 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; + +use crate::bluetooth::common::{ + BleAddress, BluetoothError, ClassicAddress, PairingResult, +}; + +/// Concrete types implementing this trait represent BLE Peripheral devices. +/// They provide methods for retrieving device info and running device actions, +/// such as pairing. +#[async_trait] +pub trait BleDevice: Sized { + /// Create a new `BleDevice` instance from a `BleAddress`, typically + /// enabled through locally cached data retrieved from a Bluetooth adapter's + /// scanning functionality. + async fn new(addr: BleAddress) -> Result; + + /// Retrieve the name advertised by this device. + fn name(&self) -> Result; + + /// Retrieve this device's Bluetooth address information. + fn address(&self) -> BleAddress; +} + +/// Concrete types implementing this trait represent BT Classic Peripheral +/// devices. They provide methods for retrieving device info and running device +/// actions, such as pairing. +#[async_trait] +pub trait ClassicDevice: Sized { + /// Create a new `ClassicDevice` instance from a `ClassicAddress`, typically + /// enabled through locally cached data retrieved from a Bluetooth adapter's + /// scanning functionality. + async fn new(addr: ClassicAddress) -> Result; + + /// Retrieve the name advertised by this device. + fn name(&self) -> Result; + + /// Retrieve this device's Bluetooth address information. + fn address(&self) -> ClassicAddress; + + /// Attempt pairing with the peripheral device. + async fn pair(&self) -> Result; +} diff --git a/fastpair/rust/src/bluetooth/api/mod.rs b/fastpair/rust/src/bluetooth/api/mod.rs new file mode 100644 index 00000000..0e410749 --- /dev/null +++ b/fastpair/rust/src/bluetooth/api/mod.rs @@ -0,0 +1,5 @@ +mod adapter; +mod device; + +pub use adapter::*; +pub use device::*; diff --git a/fastpair/rust/src/bluetooth/common/address.rs b/fastpair/rust/src/bluetooth/common/address.rs new file mode 100644 index 00000000..485ff731 --- /dev/null +++ b/fastpair/rust/src/bluetooth/common/address.rs @@ -0,0 +1,95 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::bluetooth::common::BluetoothError; + +/// BLE Addresses can either be the peripheral's public MAC address, or various +/// types of random addresses. +#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)] +pub enum BleAddressKind { + Public, + Random, +} + +/// Struct representing a 48-bit BLE Address and its type. +#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)] +pub struct BleAddress { + val: [u8; 6], + kind: BleAddressKind, +} + +/// Struct representing a 48-bit BT Classic address. +#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)] +pub struct ClassicAddress([u8; 6]); + +impl BleAddress { + /// `BleAddress` constructor. + pub fn new(addr: u64, kind: BleAddressKind) -> Self { + let addr = u64_to_6lsb(addr); + + BleAddress { val: addr, kind } + } + + /// Retrieve the type of BLE Address (public or random). + pub fn get_kind(&self) -> BleAddressKind { + self.kind + } +} + +/// Function for converting the six LSB of a u64 into a 6-byte array. +#[inline] +fn u64_to_6lsb(num: u64) -> [u8; 6] { + num.to_le_bytes()[..6] + .try_into() + .expect("Sanity check, slice length matches array length") +} + +impl From for ClassicAddress { + fn from(addr: u64) -> Self { + let addr = u64_to_6lsb(addr); + + ClassicAddress(addr) + } +} + +impl TryFrom for ClassicAddress { + type Error = BluetoothError; + + fn try_from(addr: BleAddress) -> Result { + match addr.kind { + BleAddressKind::Public => Ok(ClassicAddress(addr.val)), + BleAddressKind::Random => Err(BluetoothError::BadTypeConversion(String::from( + "can't convert BLE Random address to Bluetooth Classic address." + ))), + } + } +} + +impl From for u64 { + fn from(addr: BleAddress) -> Self { + let mut bytes = [0u8; 8]; + bytes[..6].copy_from_slice(&addr.val); + + u64::from_le_bytes(bytes) + } +} + +impl From for u64 { + fn from(addr: ClassicAddress) -> Self { + let mut bytes = [0u8; 8]; + bytes[..6].copy_from_slice(&addr.0); + + u64::from_le_bytes(bytes) + } +} diff --git a/fastpair/rust/src/bluetooth/common/advertisement.rs b/fastpair/rust/src/bluetooth/common/advertisement.rs new file mode 100644 index 00000000..8260936e --- /dev/null +++ b/fastpair/rust/src/bluetooth/common/advertisement.rs @@ -0,0 +1,89 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{BleAddress, BluetoothError}; + +/// Holds data related to an incoming BLE Advertisement. This includes +/// information about the advertisement (e.g. address of sender) as well as +/// data sections extracted from the advertisement. Platform-specific methods +/// should be written to load in data sections from incoming advertisements. +pub struct BleAdvertisement { + address: BleAddress, + service_data_16bit_uuid: Option>>, +} + +impl BleAdvertisement { + /// Construct a new `BleAdvertisement` instance. + pub(crate) fn new(address: BleAddress) -> Self { + BleAdvertisement { + address, + service_data_16bit_uuid: None, + } + } + + /// Retrieve the `BleAddress` that emitted this advertisement. + pub fn address(&self) -> BleAddress { + self.address + } + + /// Setter for `ServiceData` field with 16bit UUID. + pub(crate) fn set_service_data_16bit_uuid( + &mut self, + data_sections: Vec>, + ) { + self.service_data_16bit_uuid = Some(data_sections); + } + + /// Getter for `ServiceData` field with 16bit UUID. + pub fn service_data_16bit_uuid( + &self, + ) -> Result<&Vec>, BluetoothError> { + match &self.service_data_16bit_uuid { + Some(service_data) => Ok(&service_data), + None => Err(BluetoothError::FailedPrecondition(String::from( + "No service data has been loaded into this advertisement.", + ))), + } + } +} + +/// Enum denoting the assigned number of Bluetooth common data types. Used for +/// fetching specific data sections from a Bluetooth advertisement. +/// Bluetooth Assigned Numbers, Section 2.3 +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub enum BleDataTypeId { + ServiceData16BitUuid = 0x16, +} + +/// Struct representing the Bluetooth Service Data common data type. `U` should +/// be one of the valid uuid sizes, specified in: +/// Bluetooth Supplement to the Core Specification, Part A, Section 1.11. +pub struct ServiceData { + uuid: U, + data: Vec, +} + +impl ServiceData { + pub fn new(uuid: U, data: Vec) -> Self { + ServiceData { uuid, data } + } + + pub fn uuid(&self) -> U { + self.uuid + } + + pub fn data(&self) -> &Vec { + &self.data + } +} diff --git a/fastpair/rust/src/bluetooth/common/error.rs b/fastpair/rust/src/bluetooth/common/error.rs new file mode 100644 index 00000000..8b20b36e --- /dev/null +++ b/fastpair/rust/src/bluetooth/common/error.rs @@ -0,0 +1,60 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use thiserror::Error; + +/// Library error type. +#[non_exhaustive] +#[derive(Error, Debug)] +pub enum BluetoothError { + /// Reported when the user attempts a bad type conversion, e.g. converting + /// a BLE random address to a BT Classic address. + #[error("bad type conversion: {0}")] + BadTypeConversion(String), + /// 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 + #[error("failed precondition: {0}")] + FailedPrecondition(String), + /// Reported when the user calls an operation that is supported by their + /// Operating System, but is not supported by their device. + /// E.g. a Windows machine with an old BT Classic adapter that + /// doesn't support BLE). + #[error("bluetooth operation not supported by system: {0}")] + NotSupported(String), + /// Wrapper around OS-level errors, e.g. `windows::core::Error` for Windows. + /// These typically mean something is very wrong with the system (e.g. OOM). + #[error("bluetooth system-level error: {0}")] + System(String), + /// Reported when a bug occurs inside the library. Whenever a seemingly + /// impossible error condition arises where you could call `expect()`, + /// return this error instead. + #[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/common/mod.rs b/fastpair/rust/src/bluetooth/common/mod.rs new file mode 100644 index 00000000..1ddd9b30 --- /dev/null +++ b/fastpair/rust/src/bluetooth/common/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Module for shared functionality between all Bluetooth platforms. +mod address; +mod advertisement; +mod error; + +pub use address::*; +pub use advertisement::*; +pub use error::*; diff --git a/fastpair/rust/src/bluetooth/mod.rs b/fastpair/rust/src/bluetooth/mod.rs index 2faa2576..0acdb250 100644 --- a/fastpair/rust/src/bluetooth/mod.rs +++ b/fastpair/rust/src/bluetooth/mod.rs @@ -16,20 +16,41 @@ // instead of using anyhow. // b/290070686 +pub mod api; pub mod common; -pub use common::{Adapter, Device}; +pub use api::{BleAdapter, BleDevice, ClassicDevice}; +pub use common::{ + BleAddress, BleAdvertisement, BleDataTypeId, BluetoothError, ClassicAddress, +}; cfg_if::cfg_if! { if #[cfg(windows)] { - mod windows_ble; - use windows_ble::BleAdapter; + mod windows; + use self::windows as platform; } else { mod unsupported; - use unsupported::BleAdapter; + use unsupported as platform; } } -pub async fn default_adapter() -> Result { - BleAdapter::default().await +pub struct Platform; + +impl Platform { + pub async fn default_adapter( + ) -> Result { + platform::BleAdapter::default().await + } + + pub async fn new_ble_device( + addr: BleAddress, + ) -> Result { + platform::BleDevice::new(addr).await + } + + pub async fn new_classic_device( + addr: ClassicAddress, + ) -> Result { + platform::ClassicDevice::new(addr).await + } } diff --git a/fastpair/rust/src/bluetooth/unsupported/adapter.rs b/fastpair/rust/src/bluetooth/unsupported/adapter.rs index f7395ea5..de883e3c 100644 --- a/fastpair/rust/src/bluetooth/unsupported/adapter.rs +++ b/fastpair/rust/src/bluetooth/unsupported/adapter.rs @@ -15,30 +15,33 @@ use async_trait::async_trait; use super::BleDevice; -use crate::bluetooth::common::Adapter; +use crate::bluetooth::{ + api, common::BluetoothError, BleAdvertisement, BleDataTypeId, +}; /// Concrete type implementing `Adapter`, used for unsupported devices. /// Every method should panic. pub struct BleAdapter; #[async_trait] -impl Adapter for BleAdapter { - type Device = BleDevice; - - async fn default() -> Result { +impl api::BleAdapter for BleAdapter { + async fn default() -> Result { panic!("Unsupported target platform."); } - fn start_scan_devices(&mut self) -> Result<(), anyhow::Error> { + fn start_scan(&mut self) -> Result<(), BluetoothError> { panic!("Unsupported target platform."); } - fn stop_scan_devices(&mut self) -> Result<(), anyhow::Error> { + fn stop_scan(&mut self) -> Result<(), BluetoothError> { panic!("Unsupported target platform."); } - async fn next_device(&mut self) -> Result { - panic!("Unsupported target platform."); + async fn next_advertisement( + &mut self, + datatype_selector: Option<&Vec>, + ) -> Result { + panic!("Unsupported target platform"); } } diff --git a/fastpair/rust/src/bluetooth/unsupported/device.rs b/fastpair/rust/src/bluetooth/unsupported/device.rs index 6c8bc520..0ac68775 100644 --- a/fastpair/rust/src/bluetooth/unsupported/device.rs +++ b/fastpair/rust/src/bluetooth/unsupported/device.rs @@ -12,15 +12,52 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::bluetooth::common::Device; +use async_trait::async_trait; -/// Concrete type implementing `Device`, used for unsupported devices. +use crate::bluetooth::{ + api, + common::{BleAddress, BluetoothError, ClassicAddress, PairingResult}, +}; + +/// Concrete type implementing `api::BleDevice` for unsupported platforms. /// Every method should panic. pub struct BleDevice; -impl Device for BleDevice { - fn name(&self) -> Result { - panic!("Unsupported target platform.") +#[async_trait] +impl api::BleDevice for BleDevice { + async fn new(addr: BleAddress) -> Result { + panic!("Unsupported target platform."); + } + + fn name(&self) -> Result { + panic!("Unsupported target platform."); + } + + fn address(&self) -> BleAddress { + panic!("Unsupported target platform."); + } +} + +/// Concrete type implementing `api::ClassicDevice` for unsupported platforms. +/// Every method should panic. +pub struct ClassicDevice; + +#[async_trait] +impl api::ClassicDevice for ClassicDevice { + async fn new(addr: ClassicAddress) -> Result { + panic!("Unsupported target platform."); + } + + fn name(&self) -> Result { + panic!("Unsupported target platform."); + } + + fn address(&self) -> ClassicAddress { + panic!("Unsupported target platform."); + } + + async fn pair(&self) -> Result { + panic!("Unsupported target platform."); } } diff --git a/fastpair/rust/src/bluetooth/windows_ble/adapter.rs b/fastpair/rust/src/bluetooth/windows/adapter.rs similarity index 66% rename from fastpair/rust/src/bluetooth/windows_ble/adapter.rs rename to fastpair/rust/src/bluetooth/windows/adapter.rs index 6d7af121..cf4519f6 100644 --- a/fastpair/rust/src/bluetooth/windows_ble/adapter.rs +++ b/fastpair/rust/src/bluetooth/windows/adapter.rs @@ -12,12 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::pin::Pin; use std::sync::Arc; use async_trait::async_trait; -use futures::{stream::Stream, StreamExt}; -use tracing::{error, warn}; +use futures::{channel::mpsc::Receiver, StreamExt}; +use tracing::{error, info, warn}; use windows::{ Devices::Bluetooth::{ Advertisement::{ @@ -54,44 +53,49 @@ use windows::{ Foundation::TypedEventHandler, }; -use super::BleDevice; -use crate::bluetooth::common::Adapter; +use crate::bluetooth::{ + api, + common::{BleAdvertisement, BleDataTypeId, BluetoothError}, +}; -/// Concrete type implementing `Adapter`, used for Windows BLE. +/// Struct holding the necessary fields for listening to and handling incoming +/// BLE advertisements. +struct AdvListener { + /// Holds callback for sending received advertisement events to `receiver`. + watcher: BluetoothLEAdvertisementWatcher, + /// Can be polled to consume incoming advertisement events. + receiver: Receiver, +} + +/// Concrete type implementing `api::BleAdapter`, used for Windows BLE. pub struct BleAdapter { inner: BluetoothAdapter, - // NOTE: Using Boxed dyn here is silly because only one concrete type ever - // used. Change this to `impl Stream` once impl trait return types - // stabilized for existential types. - // b/289224233. - device_stream: Option + Send + Sync>>>, + listener: Option, } #[async_trait] -impl Adapter for BleAdapter { - type Device = BleDevice; - - async fn default() -> Result { +impl api::BleAdapter for BleAdapter { + async fn default() -> Result { let inner = BluetoothAdapter::GetDefaultAsync()?.await?; if !inner.IsLowEnergySupported()? { - return Err(anyhow::anyhow!( - "This device's Bluetooth Adapter doesn't support Bluetooth LE Transport type." - )); + return Err(BluetoothError::NotSupported(String::from( + "LE transport type", + ))); } if !inner.IsCentralRoleSupported()? { - return Err(anyhow::anyhow!( - "This device's Bluetooth Adapter doesn't support Bluetooth LE central role." - )); + return Err(BluetoothError::NotSupported(String::from( + "central role", + ))); } Ok(BleAdapter { inner, - device_stream: None, + listener: None, }) } - fn start_scan_devices(&mut self) -> Result<(), anyhow::Error> { + fn start_scan(&mut self) -> Result<(), BluetoothError> { let watcher = BluetoothLEAdvertisementWatcher::new()?; match watcher.SetScanningMode(BluetoothLEScanningMode::Active) { Ok(_) => (), @@ -149,7 +153,7 @@ impl Adapter for BleAdapter { >| { // Drop `sender`, closing the channel. let _sender = sender.take(); - println!("Watcher stopped receiving BLE advertisements."); + info!("Watcher stopped receiving BLE advertisements."); Ok(()) }, ); @@ -158,59 +162,58 @@ impl Adapter for BleAdapter { watcher.Stopped(&stopped_handler)?; watcher.Start()?; - // `receiver` is a `futures::channel::mpsc::Receiver`, which implements - // `futures::stream::Stream`. This is essentially an async Iterator. - // We apply a FilterMap to map from advertisement packet to a future - // returning `BleDevice` and filter out undesired connections. We need a - // pinned box to satisfy trait bounds for `Stream`. - self.device_stream = - Some(Box::pin(receiver.filter_map(move |event_args| { - // Move `watcher` into `FilterMap` closure. This ensures `watcher` - // is only dropped when the stream is closed. - let _watcher = &watcher; - - // Move `event_args` into async block. - async move { - match event_args.AdvertisementType().ok()? { - BluetoothLEAdvertisementType::NonConnectableUndirected => { - None - } - _ => { - let addr = event_args.BluetoothAddress().ok()?; - let kind = event_args.BluetoothAddressType().ok()?; - - match BleDevice::from_addr(addr, kind).await { - Ok(device) => Some(device), - Err(err) => { - warn!("Error creating device: {:?}", err); - None - } - } - } - } - } - }))); + self.listener = Some(AdvListener { watcher, receiver }); Ok(()) } - fn stop_scan_devices(&mut self) -> Result<(), anyhow::Error> { - if let Some(_) = &self.device_stream { - self.device_stream.take(); + fn stop_scan(&mut self) -> Result<(), BluetoothError> { + if let Some(listener) = self.listener.take() { + listener.watcher.Stop()?; Ok(()) } else { - Err(anyhow::anyhow!("Device scanning hasn't started.")) + Err(BluetoothError::FailedPrecondition(String::from( + "device scanning hasn't started, please call `start_scan()`", + ))) } } - async fn next_device(&mut self) -> Result { - if let Some(stream) = &mut self.device_stream { - stream - .next() - .await - .ok_or(anyhow::anyhow!("Device returned from stream is None.")) + async fn next_advertisement( + &mut self, + datatype_selector: Option<&Vec>, + ) -> Result { + if let Some(listener) = &mut self.listener { + let stream = &mut listener.receiver; + // We don't want the end-user to receive empty devices, so this is a + // loop to catch and skip trivial errors from advertisements that + // can't be turned into devices. + loop { + let event_args = + stream.next().await.ok_or(BluetoothError::Internal( + String::from("Event returned from stream is None."), + ))?; + + match event_args.AdvertisementType()? { + BluetoothLEAdvertisementType::NonConnectableUndirected => { + () + } + _ => { + let mut advertisement = + BleAdvertisement::try_from(&event_args)?; + + if let Some(datatype_selector) = datatype_selector { + advertisement + .load_data(&event_args, datatype_selector)?; + } + + break Ok(advertisement); + } + } + } } else { - Err(anyhow::anyhow!("Device scanning hasn't started.")) + Err(BluetoothError::FailedPrecondition(String::from( + "device scanning hasn't started, please call `start_scan()`", + ))) } } } diff --git a/fastpair/rust/src/bluetooth/windows/address.rs b/fastpair/rust/src/bluetooth/windows/address.rs new file mode 100644 index 00000000..d08ae4f3 --- /dev/null +++ b/fastpair/rust/src/bluetooth/windows/address.rs @@ -0,0 +1,52 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Whether the Bluetooth advertisement is Public, Random or Unspecified. +//https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothaddresstype?view=winrt-22621 +use windows::Devices::Bluetooth::BluetoothAddressType; + +use crate::bluetooth::common::{BleAddressKind, BluetoothError}; + +// Convenience for converting from Windows API to crate API. +impl TryFrom for BleAddressKind { + type Error = BluetoothError; + + fn try_from(kind: BluetoothAddressType) -> Result { + match kind { + BluetoothAddressType::Public => Ok(BleAddressKind::Public), + BluetoothAddressType::Random => Ok(BleAddressKind::Random), + BluetoothAddressType::Unspecified => { + Err(BluetoothError::BadTypeConversion(String::from( + "Attempting to construct `BleAddressKind` with device \ + advertising Unspecified address type.", + ))) + } + _ => Err(BluetoothError::BadTypeConversion(format!( + "Attempting to construct `BleAddressKind` with device \ + advertising invalid address type {}.", + kind.0, + ))), + } + } +} + +// Convenience for converting from crate API to Windows API. +impl From for BluetoothAddressType { + fn from(kind: BleAddressKind) -> Self { + match kind { + BleAddressKind::Public => BluetoothAddressType::Public, + BleAddressKind::Random => BluetoothAddressType::Random, + } + } +} diff --git a/fastpair/rust/src/bluetooth/windows/advertisement.rs b/fastpair/rust/src/bluetooth/windows/advertisement.rs new file mode 100644 index 00000000..526e2d5a --- /dev/null +++ b/fastpair/rust/src/bluetooth/windows/advertisement.rs @@ -0,0 +1,108 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use windows::{ + // Struct that receives Bluetooth Low Energy (LE) advertisements. + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementwatcher?view=winrt-22621 + Devices::Bluetooth::Advertisement::{ + BluetoothLEAdvertisementDataSection, + BluetoothLEAdvertisementReceivedEventArgs, + }, + + // Struct representing an immutable view into a vector. + // https://learn.microsoft.com/en-us/uwp/api/windows.foundation.collections.ivectorview-1?view=winrt-22621 + Foundation::Collections::IVectorView, + + // Struct for reading data from a Windows stream, like an IVectorView. + // https://learn.microsoft.com/en-us/uwp/api/windows.storage.streams.datareader?view=winrt-22621 + Storage::Streams::DataReader, +}; + +use crate::bluetooth::common::{ + BleAddress, BleAddressKind, BleAdvertisement, BleDataTypeId, + BluetoothError, ServiceData, +}; + +impl TryFrom<&BluetoothLEAdvertisementReceivedEventArgs> for BleAdvertisement { + type Error = BluetoothError; + + fn try_from( + adv: &BluetoothLEAdvertisementReceivedEventArgs, + ) -> Result { + let addr = adv.BluetoothAddress()?; + let kind = BleAddressKind::try_from(adv.BluetoothAddressType()?)?; + + let addr = BleAddress::new(addr, kind); + + Ok(BleAdvertisement::new(addr)) + } +} + +impl BleAdvertisement { + /// Load data of selected data types into self by parsing the raw Windows + /// advertisement. + /// See: Supplement to the Bluetooth Core Specification Part A, Section 1. + pub(crate) fn load_data( + &mut self, + adv: &BluetoothLEAdvertisementReceivedEventArgs, + datatype_ids: &[BleDataTypeId], + ) -> Result<(), BluetoothError> { + let adv = adv.Advertisement()?; + + for datatype_id in datatype_ids { + // Note `raw_data_sections` is `!Send` and `!Sync`. This means + // processing must occur in a synchronous environment. The compiler + // will complain if parsing is done in an async function. + let raw_data_sections = + adv.GetSectionsByType((*datatype_id) as u8)?; + match datatype_id { + BleDataTypeId::ServiceData16BitUuid => { + let service_data = + parse_service_data_16bit_uuid(raw_data_sections)?; + self.set_service_data_16bit_uuid(service_data) + } + }; + } + + Ok(()) + } +} + +/// Parse the advertisement's service data. +/// Further Reading: +/// * `BleMedium::AdvertisementReceivedHandler` under +/// github.com/google/nearby/internal/platform/implementation/windows_ble/ble_medium.cc. +/// * Bluetooth Supplement to the Core Specification, Part A, Section 1.11. +/// * go/fast_pair_windows_data_parse. +#[inline] +fn parse_service_data_16bit_uuid( + raw_data_sections: IVectorView, +) -> Result>, BluetoothError> { + let mut data_vec = Vec::new(); + + for raw_data in raw_data_sections { + let data_reader = DataReader::FromBuffer(&raw_data.Data()?)?; + let uuid = data_reader.ReadUInt16()?; + + let unconsumed_buffer_len = + data_reader.UnconsumedBufferLength()? as usize; + + let mut data = vec![0u8; unconsumed_buffer_len]; + data_reader.ReadBytes(&mut data)?; + + data_vec.push(ServiceData::new(uuid, data)); + } + + Ok(data_vec) +} diff --git a/fastpair/rust/src/bluetooth/windows/device.rs b/fastpair/rust/src/bluetooth/windows/device.rs new file mode 100644 index 00000000..cf0e293f --- /dev/null +++ b/fastpair/rust/src/bluetooth/windows/device.rs @@ -0,0 +1,163 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; +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::{api, common::{BleAddress, ClassicAddress, 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, +} + +#[async_trait] +impl api::BleDevice for BleDevice { + async fn new(addr: BleAddress) -> Result { + let kind = BluetoothAddressType::from(addr.get_kind()); + let raw_addr = u64::from(addr); + + let inner = BluetoothLEDevice::FromBluetoothAddressWithBluetoothAddressTypeAsync( + raw_addr, kind, + )? + .await?; + + Ok(BleDevice { inner, addr }) + } + + fn name(&self) -> Result { + Ok(self.inner.Name()?.to_string()) + } + + fn address(&self) -> BleAddress { + self.addr + } +} + +#[async_trait] +impl api::ClassicDevice for ClassicDevice { + async fn new(addr: ClassicAddress) -> Result { + let raw_addr = u64::from(addr); + + let inner = BluetoothDevice::FromBluetoothAddressAsync( + raw_addr, + )? + .await?; + + Ok(ClassicDevice { inner, addr }) + } + + fn name(&self) -> Result { + Ok(self.inner.Name()?.to_string_lossy()) + } + + fn address(&self) -> ClassicAddress { + 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 { + use super::*; + + // TODO b/288592509 unit tests +} diff --git a/fastpair/rust/src/bluetooth/windows/error.rs b/fastpair/rust/src/bluetooth/windows/error.rs new file mode 100644 index 00000000..59e12a31 --- /dev/null +++ b/fastpair/rust/src/bluetooth/windows/error.rs @@ -0,0 +1,57 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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/bluetooth/windows_ble/mod.rs b/fastpair/rust/src/bluetooth/windows/mod.rs similarity index 88% rename from fastpair/rust/src/bluetooth/windows_ble/mod.rs rename to fastpair/rust/src/bluetooth/windows/mod.rs index 7d54d4ef..8003fe00 100644 --- a/fastpair/rust/src/bluetooth/windows_ble/mod.rs +++ b/fastpair/rust/src/bluetooth/windows/mod.rs @@ -14,7 +14,12 @@ /// Bluetooth LE module for Windows devices. mod adapter; +mod address; +mod advertisement; mod device; +mod error; pub use adapter::*; +pub use address::*; +pub use advertisement::*; pub use device::*; diff --git a/fastpair/rust/src/bluetooth/windows_ble/device.rs b/fastpair/rust/src/bluetooth/windows_ble/device.rs deleted file mode 100644 index 08c3d123..00000000 --- a/fastpair/rust/src/bluetooth/windows_ble/device.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// 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 crate::bluetooth::common::Device; - -/// Concrete type implementing `Device`, used for Windows BLE. -pub struct BleDevice { - inner: BluetoothLEDevice, -} - -impl BleDevice { - /// Create a `BleDevice` instance from the raw bluetooth address information. - pub(super) async fn from_addr( - addr: u64, - kind: BluetoothAddressType, - ) -> Result { - let inner = - BluetoothLEDevice::FromBluetoothAddressWithBluetoothAddressTypeAsync(addr, kind)? - .await?; - - Ok(BleDevice { inner }) - } -} - -#[async_trait] -impl Device for BleDevice { - fn name(&self) -> Result { - Ok(self.inner.Name()?.to_string_lossy()) - } -} - -mod tests { - use super::*; - - // TODO b/288592509 unit tests -} diff --git a/fastpair/rust/src/main.rs b/fastpair/rust/src/main.rs index 5905e135..19d77c00 100644 --- a/fastpair/rust/src/main.rs +++ b/fastpair/rust/src/main.rs @@ -12,22 +12,106 @@ // See the License for the specific language governing permissions and // limitations under the License. -use futures::executor; +use std::{ + collections::HashSet, + error::Error, + io::{self, Write}, + sync::Arc, + thread, +}; + +use futures::{ + executor::{self, block_on}, + lock::Mutex, +}; mod bluetooth; -use bluetooth::{Adapter, Device}; +use crate::bluetooth::{ + BleAdapter, BleDataTypeId, BleDevice, ClassicAddress, ClassicDevice, + Platform, +}; -fn main() -> Result<(), anyhow::Error> { +async fn get_user_input( + device_vec: Arc>>, +) -> Result<(), Box> { + let mut buffer = String::new(); + loop { + io::stdout().flush()?; + buffer.clear(); + io::stdin().read_line(&mut buffer)?; + + let val = match buffer.trim().parse::() { + Ok(val) => val, + Err(_) => { + println!("Please enter a valid digit."); + continue; + } + }; + + let index_to_device = device_vec.lock().await; + match index_to_device.get(val) { + Some(device) => { + let addr = device.address(); + let classic_addr = ClassicAddress::try_from(addr)?; + + let classic_device = + Platform::new_classic_device(classic_addr).await?; + + match classic_device.pair().await { + Ok(_) => { + println!("Pairing success!"); + } + Err(err) => println!("Error {}", err), + } + break Ok(()); + } + None => println!("Please enter a valid digit."), + } + } +} + +fn main() -> Result<(), Box> { let run = async { - let mut adapter = bluetooth::default_adapter().await?; - adapter.start_scan_devices()?; + let mut adapter = Platform::default_adapter().await?; + adapter.start_scan()?; - while let Ok(device) = adapter.next_device().await { - println!("found {}", device.name()?) + let mut addr_set = HashSet::new(); + let device_vec = Arc::new(Mutex::new(Vec::new())); + + { + // Process user input in a separate thread. + let device_vec = device_vec.clone(); + thread::spawn(|| block_on(get_user_input(device_vec)).unwrap()); } - unreachable!("Done scanning"); + let mut counter: u32 = 0; + let datatype_selector = vec![BleDataTypeId::ServiceData16BitUuid]; + // Retrieve incoming device advertisements. + while let Ok(advertisement) = + adapter.next_advertisement(Some(&datatype_selector)).await + { + for service_data in advertisement.service_data_16bit_uuid()? { + let uuid = service_data.uuid(); + + // This is a Fast Pair device. + if uuid == 0x2cfe { + let addr = advertisement.address(); + let ble_device = Platform::new_ble_device(addr).await?; + let name = ble_device.name()?; + + if addr_set.insert(addr) { + // New FP device discovered. + println!("{}: {}", counter, name); + device_vec.lock().await.push(ble_device); + counter += 1; + } + break; + } + } + } + println!("Done scanning"); + Ok(()) }; executor::block_on(run)