diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 2583f175..b29d16f9 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -47,6 +47,16 @@ jobs: submodules: recursive - name: Build FPP run: cargo build --manifest-path presence/fpp/fpp/Cargo.toml - - name: Build Fairpair + - name: Build Fast Pair run: cargo build --manifest-path fastpair/rust/Cargo.toml - + + build-rust-windows: + name: Build Rust on Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Build Fast Pair + run: cargo build --manifest-path fastpair/rust/Cargo.toml + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 39b82caf..b1c8f94a 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,9 @@ Carthage/Build .UlyssesRoot .Ulysses-Settings.plist .Ulysses-Group.plist + +# IntelliJ +.idea + +# Rust +Cargo.lock \ No newline at end of file diff --git a/fastpair/rust/.gitignore b/fastpair/rust/.gitignore new file mode 100644 index 00000000..c1bf028b --- /dev/null +++ b/fastpair/rust/.gitignore @@ -0,0 +1,2 @@ +# Build files +target diff --git a/fastpair/rust/Cargo.toml b/fastpair/rust/Cargo.toml index be7a0a1c..20a09107 100644 --- a/fastpair/rust/Cargo.toml +++ b/fastpair/rust/Cargo.toml @@ -20,3 +20,15 @@ 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" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.48", features = [ + "Devices_Bluetooth", + "Devices_Bluetooth_Advertisement", + "Foundation", +] } diff --git a/fastpair/rust/rustfmt.toml b/fastpair/rust/rustfmt.toml new file mode 100644 index 00000000..5c8d9318 --- /dev/null +++ b/fastpair/rust/rustfmt.toml @@ -0,0 +1 @@ +max_width = 80 \ No newline at end of file diff --git a/fastpair/rust/src/bluetooth/common.rs b/fastpair/rust/src/bluetooth/common.rs new file mode 100644 index 00000000..33ba2c4c --- /dev/null +++ b/fastpair/rust/src/bluetooth/common.rs @@ -0,0 +1,42 @@ +// 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; + +/// 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; + + /// Retrieve the system-default Bluetooth adapter. + async fn default() -> Result; + + /// Begin scanning for nearby devices. + fn start_scan_devices(&mut self) -> Result<(), anyhow::Error>; + + /// Stop scanning for nearby devices. + fn stop_scan_devices(&mut self) -> Result<(), anyhow::Error>; + + /// 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; +} diff --git a/fastpair/rust/src/bluetooth/mod.rs b/fastpair/rust/src/bluetooth/mod.rs new file mode 100644 index 00000000..2faa2576 --- /dev/null +++ b/fastpair/rust/src/bluetooth/mod.rs @@ -0,0 +1,35 @@ +// 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. + +// Split into separate crate once demo is finished, providing custom error types +// instead of using anyhow. +// b/290070686 + +pub mod common; + +pub use common::{Adapter, Device}; + +cfg_if::cfg_if! { + if #[cfg(windows)] { + mod windows_ble; + use windows_ble::BleAdapter; + } else { + mod unsupported; + use unsupported::BleAdapter; + } +} + +pub async fn default_adapter() -> Result { + BleAdapter::default().await +} diff --git a/fastpair/rust/src/bluetooth/unsupported/adapter.rs b/fastpair/rust/src/bluetooth/unsupported/adapter.rs new file mode 100644 index 00000000..f7395ea5 --- /dev/null +++ b/fastpair/rust/src/bluetooth/unsupported/adapter.rs @@ -0,0 +1,49 @@ +// 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 super::BleDevice; +use crate::bluetooth::common::Adapter; + +/// 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 { + panic!("Unsupported target platform."); + } + + fn start_scan_devices(&mut self) -> Result<(), anyhow::Error> { + panic!("Unsupported target platform."); + } + + fn stop_scan_devices(&mut self) -> Result<(), anyhow::Error> { + panic!("Unsupported target platform."); + } + + async fn next_device(&mut self) -> Result { + panic!("Unsupported target platform."); + } +} + +mod tests { + use super::*; + + // TODO b/288592509 unit tests +} diff --git a/fastpair/rust/src/bluetooth/unsupported/device.rs b/fastpair/rust/src/bluetooth/unsupported/device.rs new file mode 100644 index 00000000..6c8bc520 --- /dev/null +++ b/fastpair/rust/src/bluetooth/unsupported/device.rs @@ -0,0 +1,31 @@ +// 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::Device; + +/// Concrete type implementing `Device`, used for unsupported devices. +/// Every method should panic. +pub struct BleDevice; + +impl Device for BleDevice { + fn name(&self) -> Result { + panic!("Unsupported target platform.") + } +} + +mod tests { + use super::*; + + // TODO b/288592509 unit tests +} diff --git a/fastpair/rust/src/bluetooth/unsupported/mod.rs b/fastpair/rust/src/bluetooth/unsupported/mod.rs new file mode 100644 index 00000000..a1af0e2d --- /dev/null +++ b/fastpair/rust/src/bluetooth/unsupported/mod.rs @@ -0,0 +1,20 @@ +// 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. + +/// Bluetooth LE module for unsupported devices. Every method panics. +mod adapter; +mod device; + +pub use adapter::*; +pub use device::*; diff --git a/fastpair/rust/src/bluetooth/windows_ble/adapter.rs b/fastpair/rust/src/bluetooth/windows_ble/adapter.rs new file mode 100644 index 00000000..6d7af121 --- /dev/null +++ b/fastpair/rust/src/bluetooth/windows_ble/adapter.rs @@ -0,0 +1,222 @@ +// 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 std::pin::Pin; +use std::sync::Arc; + +use async_trait::async_trait; +use futures::{stream::Stream, StreamExt}; +use tracing::{error, warn}; +use windows::{ + Devices::Bluetooth::{ + Advertisement::{ + // Struct that receives Bluetooth Low Energy (LE) advertisements. + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementwatcher?view=winrt-22621 + BluetoothLEAdvertisementReceivedEventArgs, + + // Enum describing the type of advertisement (connectable, directed, etc). + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothaddresstype?view=winrt-22621 + BluetoothLEAdvertisementType, + + // Provides data for a Received event on a `BluetoothLEAdvertisementWatcher`. + // Instance is created when the Received event occurs in the watcher struct. + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementreceivedeventargs?view=winrt-22621 + BluetoothLEAdvertisementWatcher, + + // Provides data for a Stopped event on a `BluetoothLEAdvertisementWatcher`. + // Instance is created when the Stopped event occurs on a watcher struct. + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementwatcherstoppedeventargs?view=winrt-22621 + BluetoothLEAdvertisementWatcherStoppedEventArgs, + + // Defines constants that specify a Bluetooth LE scanning mode. + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothlescanningmode?view=winrt-22621 + BluetoothLEScanningMode, + }, + // Struct for obtaining global constant information about a computer's + // Bluetooth adapter. + // https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothadapter?view=winrt-22621 + BluetoothAdapter, + }, + // Wraps a closure for handling events associated with a struct + // (e.g. Received and Stopped events in BluetoothLEAdvertisementWatcher). + // https://learn.microsoft.com/en-us/uwp/api/windows.foundation.typedeventhandler-2?view=winrt-22621 + Foundation::TypedEventHandler, +}; + +use super::BleDevice; +use crate::bluetooth::common::Adapter; + +/// Concrete type implementing `Adapter`, 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>>>, +} + +#[async_trait] +impl Adapter for BleAdapter { + type Device = BleDevice; + + 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." + )); + } + if !inner.IsCentralRoleSupported()? { + return Err(anyhow::anyhow!( + "This device's Bluetooth Adapter doesn't support Bluetooth LE central role." + )); + } + + Ok(BleAdapter { + inner, + device_stream: None, + }) + } + + fn start_scan_devices(&mut self) -> Result<(), anyhow::Error> { + let watcher = BluetoothLEAdvertisementWatcher::new()?; + match watcher.SetScanningMode(BluetoothLEScanningMode::Active) { + Ok(_) => (), + Err(err) => { + warn!("Failed to turn on active scanning. Error: {}", err) + } + }; + + if self.inner.IsExtendedAdvertisingSupported()? { + watcher.SetAllowExtendedAdvertisements(true)?; + } + + // `futures::channel::mpsc` is like `std::sync::mpsc` but `impl Stream`. + let (sender, receiver) = futures::channel::mpsc::channel(16); + let sender = Arc::new(std::sync::Mutex::new(sender)); + + // `received_handler` closure holds non-owning channel reference, to + // ensure `stopped_handler` can close the channel when + // `received_handler` is done. + let weak_sender = Arc::downgrade(&sender); + let received_handler = TypedEventHandler::new( + // Move `weak_sender` into closure. + move |watcher: &Option, + event_args: &Option< + BluetoothLEAdvertisementReceivedEventArgs, + >| { + if watcher.is_some() { + if let Some(event_args) = event_args { + if let Some(sender) = weak_sender.upgrade() { + match sender + .lock() + .unwrap() + .try_send(event_args.clone()) + { + Ok(_) => (), + Err(err) => { + error!("Error while handling Received event: {:?}", err) + } + } + } + } + } + + Ok(()) + }, + ); + + // `stopped_handler` closure owns channel reference, can close channel. + let mut sender = Some(sender); + let stopped_handler = TypedEventHandler::new( + // Move `sender` into closure. + move |_watcher, + _event_args: &Option< + BluetoothLEAdvertisementWatcherStoppedEventArgs, + >| { + // Drop `sender`, closing the channel. + let _sender = sender.take(); + println!("Watcher stopped receiving BLE advertisements."); + Ok(()) + }, + ); + + watcher.Received(&received_handler)?; + 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 + } + } + } + } + } + }))); + + Ok(()) + } + + fn stop_scan_devices(&mut self) -> Result<(), anyhow::Error> { + if let Some(_) = &self.device_stream { + self.device_stream.take(); + Ok(()) + } else { + Err(anyhow::anyhow!("Device scanning hasn't started.")) + } + } + + 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.")) + } else { + Err(anyhow::anyhow!("Device scanning hasn't started.")) + } + } +} + +mod tests { + use super::*; + + // TODO b/288592509 unit tests +} diff --git a/fastpair/rust/src/bluetooth/windows_ble/device.rs b/fastpair/rust/src/bluetooth/windows_ble/device.rs new file mode 100644 index 00000000..08c3d123 --- /dev/null +++ b/fastpair/rust/src/bluetooth/windows_ble/device.rs @@ -0,0 +1,58 @@ +// 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/bluetooth/windows_ble/mod.rs b/fastpair/rust/src/bluetooth/windows_ble/mod.rs new file mode 100644 index 00000000..7d54d4ef --- /dev/null +++ b/fastpair/rust/src/bluetooth/windows_ble/mod.rs @@ -0,0 +1,20 @@ +// 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. + +/// Bluetooth LE module for Windows devices. +mod adapter; +mod device; + +pub use adapter::*; +pub use device::*; diff --git a/fastpair/rust/src/lib.rs b/fastpair/rust/src/lib.rs new file mode 100644 index 00000000..c9a294f5 --- /dev/null +++ b/fastpair/rust/src/lib.rs @@ -0,0 +1,16 @@ +// 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. + +/// Library file, exports modules for use in integration tests and external crates. +pub mod bluetooth; diff --git a/fastpair/rust/src/main.rs b/fastpair/rust/src/main.rs index 548089bc..5905e135 100644 --- a/fastpair/rust/src/main.rs +++ b/fastpair/rust/src/main.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// 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. @@ -12,9 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod message_stream; -mod types; +use futures::executor; -fn main() { - println!("Fastpair Rust!"); +mod bluetooth; + +use bluetooth::{Adapter, Device}; + +fn main() -> Result<(), anyhow::Error> { + 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()?) + } + + unreachable!("Done scanning"); + }; + + executor::block_on(run) } diff --git a/fastpair/rust/tests/integration_test.rs b/fastpair/rust/tests/integration_test.rs new file mode 100644 index 00000000..18021531 --- /dev/null +++ b/fastpair/rust/tests/integration_test.rs @@ -0,0 +1,21 @@ +// 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 fastpair::*; + +mod tests { + use super::*; + + // TODO b/288592509 write integration tests +}