From f8fb8d4b52815d15a11bb01dd8c2618967b77397 Mon Sep 17 00:00:00 2001 From: Lucas Silva Shepard Date: Wed, 2 Aug 2023 12:12:29 -0700 Subject: [PATCH] [fp-rs] Flutter UI now only displays the closest discovered device. --- .../bluetooth/src/common/advertisement.rs | 30 ++++- fastpair/rust/bluetooth/src/lib.rs | 2 +- .../bluetooth/src/windows/advertisement.rs | 9 +- fastpair/rust/demo/rust/src/advertisement.rs | 89 +++++++++++++ fastpair/rust/demo/rust/src/api.rs | 124 ++++++++++++++---- fastpair/rust/demo/rust/src/lib.rs | 3 +- 6 files changed, 228 insertions(+), 29 deletions(-) create mode 100644 fastpair/rust/demo/rust/src/advertisement.rs diff --git a/fastpair/rust/bluetooth/src/common/advertisement.rs b/fastpair/rust/bluetooth/src/common/advertisement.rs index 8260936e..9fb7f999 100644 --- a/fastpair/rust/bluetooth/src/common/advertisement.rs +++ b/fastpair/rust/bluetooth/src/common/advertisement.rs @@ -18,16 +18,30 @@ use super::{BleAddress, BluetoothError}; /// 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. +#[derive(Clone)] pub struct BleAdvertisement { address: BleAddress, + rssi: Option, + tx_power: Option, service_data_16bit_uuid: Option>>, } +/// Decibel-milliwatt or dBm is a dimensionless absolute unit expressing the +/// power of a signal relative to one milliwatt (mW). The unit is in log10, i.e. +/// 1 mW is 0 dBm and a 10 dBm increase represents a ten-fold increase in power. +type DecibelMilliwatts = i16; + impl BleAdvertisement { /// Construct a new `BleAdvertisement` instance. - pub(crate) fn new(address: BleAddress) -> Self { + pub(crate) fn new( + address: BleAddress, + rssi: Option, + tx_power: Option, + ) -> Self { BleAdvertisement { address, + rssi, + tx_power, service_data_16bit_uuid: None, } } @@ -37,6 +51,19 @@ impl BleAdvertisement { self.address } + /// Retrieve the Received Signal Strength Indicator (RSSI) value for this + /// advertisement, expressed in dBm. The RSSI might be the raw value or the + /// filtered RSSI, depending on the configured signal strength filter. + pub fn rssi(&self) -> Option { + self.rssi + } + + /// Retrieve the transmit power advertised by this device, if any. + /// For BLE communication, values will range from -127 dBm to 20 dBm. + pub fn tx_power(&self) -> Option { + self.tx_power + } + /// Setter for `ServiceData` field with 16bit UUID. pub(crate) fn set_service_data_16bit_uuid( &mut self, @@ -69,6 +96,7 @@ pub enum BleDataTypeId { /// 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. +#[derive(Clone)] pub struct ServiceData { uuid: U, data: Vec, diff --git a/fastpair/rust/bluetooth/src/lib.rs b/fastpair/rust/bluetooth/src/lib.rs index bc50cc11..ebe8384a 100644 --- a/fastpair/rust/bluetooth/src/lib.rs +++ b/fastpair/rust/bluetooth/src/lib.rs @@ -18,7 +18,7 @@ mod common; use api::{BleAdapter, BleDevice, ClassicDevice}; pub use common::{ BleAddress, BleAdvertisement, BleDataTypeId, BluetoothError, - ClassicAddress, PairingResult, + ClassicAddress, PairingResult, ServiceData, }; cfg_if::cfg_if! { diff --git a/fastpair/rust/bluetooth/src/windows/advertisement.rs b/fastpair/rust/bluetooth/src/windows/advertisement.rs index dbbdfa2e..bf80b5a3 100644 --- a/fastpair/rust/bluetooth/src/windows/advertisement.rs +++ b/fastpair/rust/bluetooth/src/windows/advertisement.rs @@ -44,8 +44,15 @@ impl TryFrom<&BluetoothLEAdvertisementReceivedEventArgs> for BleAdvertisement { let kind = BleAddressKind::try_from(adv.BluetoothAddressType()?)?; let addr = BleAddress::new(addr, kind); + // `rssi` and tx_power` aren't always advertised, so convert to None if + // can't extract value. + let rssi = adv.RawSignalStrengthInDBm().ok(); + let tx_power = match adv.TransmitPowerLevelInDBm() { + Ok(val_ref) => val_ref.GetInt16().ok(), + Err(_) => None, + }; - Ok(BleAdvertisement::new(addr)) + Ok(BleAdvertisement::new(addr, rssi, tx_power)) } } diff --git a/fastpair/rust/demo/rust/src/advertisement.rs b/fastpair/rust/demo/rust/src/advertisement.rs new file mode 100644 index 00000000..c94dd4c4 --- /dev/null +++ b/fastpair/rust/demo/rust/src/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 bluetooth::{BleAddress, BleAdvertisement}; + +/// Holds information required to make decisions about an incoming Fast Pair +/// advertisement. +#[derive(Clone)] +pub(crate) struct FpPairingAdvertisement { + inner: BleAdvertisement, + /// Estimated distance in meters of device from BLE adapter. + distance: f64, +} + +impl FpPairingAdvertisement { + /// Retrieve estimated distance the BLE advertisement travelled between + /// the sending device and this receiver. + pub(crate) fn distance(&self) -> f64 { + self.distance + } + + /// Retrieve the BLE Address of the advertising device. + pub(crate) fn address(&self) -> BleAddress { + self.inner.address() + } +} + +impl TryFrom for FpPairingAdvertisement { + type Error = anyhow::Error; + + fn try_from(adv: BleAdvertisement) -> Result { + let rssi = adv.rssi().ok_or(anyhow::anyhow!( + "Windows advertisements should contain RSSI information." + ))?; + let tx_power = adv.tx_power().ok_or(anyhow::anyhow!( + "Fast Pair advertisements should advertise their transmit power." + ))?; + + let distance = distance_from_rssi_and_tx_power(rssi, tx_power); + + Ok(FpPairingAdvertisement { + inner: adv, + distance, + }) + } +} + +/// Convert RSSI and transmit power to distance using log-distance path loss +/// model, with reference path loss of 1m at 41dB in free space. +/// See: https://en.wikipedia.org/wiki/Log-distance_path_loss_model. +#[inline] +pub(crate) fn distance_from_rssi_and_tx_power(rssi: i16, tx_power: i16) -> f64 { + // Source: Android Nearby implementation, `RangingUtils.java`. + // + // PL = total path loss in db + // txPower = TxPower in dbm + // rssi = Received signal strength in dbm + // PL_0 = Path loss at reference distance d_0 {@link RSSI_DROP_OFF_AT_1_M} dbm + // d = length of path + // d_0 = reference distance (1 m) + // gamma = path loss exponent (2 in free space) + // + // Log-distance path loss (LDPL) formula: + // + // PL = txPower - rssi = PL_0 + 10 * gamma * log_10(d / d_0) + // txPower - rssi = RSSI_DROP_OFF_AT_1_M + 10 * gamma * log_10(d / d_0) + // txPower - rssi - RSSI_DROP_OFF_AT_1_M = 10 * 2 * log_10(distanceInMeters / 1) + // txPower - rssi - RSSI_DROP_OFF_AT_1_M = 20 * log_10(distanceInMeters / 1) + // (txPower - rssi - RSSI_DROP_OFF_AT_1_M) / 20 = log_10(distanceInMeters) + // 10 ^ ((txPower - rssi - RSSI_DROP_OFF_AT_1_M) / 20) = distanceInMeters + + const RSSI_DROPOFF_AT_1_M: i16 = 41; + const PATH_LOSS_EXPONENT: i16 = 2; + + f64::from(10.0).powf( + (f64::from(tx_power - rssi - RSSI_DROPOFF_AT_1_M)) / f64::from(10 * PATH_LOSS_EXPONENT), + ) +} diff --git a/fastpair/rust/demo/rust/src/api.rs b/fastpair/rust/demo/rust/src/api.rs index e826e235..d4abaffa 100644 --- a/fastpair/rust/demo/rust/src/api.rs +++ b/fastpair/rust/demo/rust/src/api.rs @@ -1,18 +1,100 @@ -use std::sync::RwLock; +use std::{collections::HashMap, sync::RwLock}; use bluetooth::{ api::{BleAdapter, BleDevice, ClassicDevice}, - BleAddress, BleDataTypeId, ClassicAddress, PairingResult, Platform, + BleAddress, BleAdvertisement, BleDataTypeId, ClassicAddress, PairingResult, Platform, + ServiceData, }; use flutter_rust_bridge::StreamSink; use futures::executor; -use tracing::info; +use tracing::{info, warn}; + +use crate::advertisement::FpPairingAdvertisement; // Sends a device name to Flutter via `StreamSink` FFI layer. static NAME_STREAM: RwLock>> = RwLock::new(None); -// Saves the currently displayed device's address, to be used for pairing. -static CURR_ADDRESS: RwLock> = RwLock::new(None); +// Saves the currently displayed device's advertisement, to be used for pairing. +static CURR_DEVICE_ADV: RwLock> = RwLock::new(None); + +/// Updates the device name as displayed by Flutter. +#[inline] +async fn update_best_device(best_adv: FpPairingAdvertisement) { + let addr = best_adv.address(); + let ble_device = Platform::new_ble_device(addr).await.unwrap(); + let name = ble_device.name().unwrap(); + + match NAME_STREAM.read().unwrap().as_ref() { + Some(stream) => { + stream.add(name); + } + None => info!("Stream is None"), + } + let mut curr_adv = CURR_DEVICE_ADV.write().unwrap(); + *curr_adv = Some(best_adv); +} + +/// Determines whether the device advertised by the provided service data is the +/// closest Fast Pair device. +/// If this device has been seen previously but has now moved further away, +/// decide which other seen device is now closer. +#[inline] +fn new_best_fp_advertisement( + advertisement: BleAdvertisement, + service_data: &ServiceData, + latest_advertisement_map: &mut HashMap, +) -> Option { + // Analyze service data sections. + let uuid = service_data.uuid(); + + // This is not a Fast Pair device. + if uuid != 0x2cfe { + return None; + } + + let fp_adv = match FpPairingAdvertisement::try_from(advertisement) { + Ok(fp_adv) => fp_adv, + Err(err) => { + // If error during construction (e.g. non-discoverable + // Fast Pair device not advertising tx_power or + // with service data that isn't model ID) ignore + // this advertisement section. + warn!("Error creating FP Advertisement: {}", err); + return None; + } + }; + + latest_advertisement_map.insert(fp_adv.address(), fp_adv.clone()); + + if let Some(best_adv) = CURR_DEVICE_ADV.read().unwrap().as_ref() { + if best_adv.distance() >= fp_adv.distance() { + // New advertised distance is closer. + Some(fp_adv) + } else if best_adv.address() == fp_adv.address() { + // New advertised distance by the previous best device has + // increased, so select new closest device. + let next_best_adv_ref = + latest_advertisement_map + .values() + .into_iter() + .min_by(|adv1, adv2| { + // We should never get NaN, so it's okay to unwrap. + adv1.distance().partial_cmp(&adv2.distance()).unwrap() + }); + + if let Some(next_best_adv) = next_best_adv_ref { + Some(next_best_adv.to_owned()) + } else { + None + } + } else { + None + } + } else { + // First discovered device, must be closest. + Some(fp_adv) + } +} /// Sets up initial constructs and infinitely polls for advertisements. pub fn init() { @@ -22,31 +104,23 @@ pub fn init() { let mut adapter = Platform::default_adapter().await.unwrap(); adapter.start_scan().unwrap(); + let mut latest_advertisement_map = HashMap::new(); let datatype_selector = vec![BleDataTypeId::ServiceData16BitUuid]; + loop { + // Retrieve the next received advertisement. let advertisement = adapter .next_advertisement(Some(&datatype_selector)) .await .unwrap(); for service_data in advertisement.service_data_16bit_uuid().unwrap() { - 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.unwrap(); - let name = ble_device.name().unwrap(); - info!("device: {}", name); - if name.contains("LE_WH-1000XM3") { - match NAME_STREAM.read().unwrap().as_ref() { - Some(stream) => { - stream.add(name); - let mut curr_addr = CURR_ADDRESS.write().unwrap(); - *curr_addr = Some(addr); - } - None => info!("Stream is None"), - } - } + if let Some(best_adv) = new_best_fp_advertisement( + advertisement.clone(), + service_data, + &mut latest_advertisement_map, + ) { + update_best_device(best_adv).await; } } } @@ -64,10 +138,10 @@ pub fn event_stream(s: StreamSink) -> Result<(), anyhow::Error> { /// Attempt classic pairing with device of address `CURR_ADDRESS`. pub fn pair() -> String { - let result = match CURR_ADDRESS.read().unwrap().as_ref() { - Some(addr) => { + let result = match CURR_DEVICE_ADV.read().unwrap().as_ref() { + Some(adv) => { let run = async { - let classic_addr = ClassicAddress::try_from(*addr).unwrap(); + let classic_addr = ClassicAddress::try_from(adv.address()).unwrap(); let classic_device = Platform::new_classic_device(classic_addr).await.unwrap(); diff --git a/fastpair/rust/demo/rust/src/lib.rs b/fastpair/rust/demo/rust/src/lib.rs index 97e8ba25..8e78a507 100644 --- a/fastpair/rust/demo/rust/src/lib.rs +++ b/fastpair/rust/demo/rust/src/lib.rs @@ -1,2 +1,3 @@ -mod bridge_generated; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */ +mod advertisement; mod api; +mod bridge_generated; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */