[fp-rs] Flutter UI now only displays the closest discovered device.

This commit is contained in:
Lucas Silva Shepard
2023-08-13 16:52:28 -07:00
parent 0505a23b3f
commit f8fb8d4b52
6 changed files with 228 additions and 29 deletions
@@ -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<DecibelMilliwatts>,
tx_power: Option<DecibelMilliwatts>,
service_data_16bit_uuid: Option<Vec<ServiceData<u16>>>,
}
/// 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<DecibelMilliwatts>,
tx_power: Option<DecibelMilliwatts>,
) -> 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<DecibelMilliwatts> {
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<DecibelMilliwatts> {
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<U: Copy> {
uuid: U,
data: Vec<u8>,
+1 -1
View File
@@ -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! {
@@ -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))
}
}
@@ -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<BleAdvertisement> for FpPairingAdvertisement {
type Error = anyhow::Error;
fn try_from(adv: BleAdvertisement) -> Result<Self, Self::Error> {
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),
)
}
+99 -25
View File
@@ -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<Option<StreamSink<String>>> = RwLock::new(None);
// Saves the currently displayed device's address, to be used for pairing.
static CURR_ADDRESS: RwLock<Option<BleAddress>> = RwLock::new(None);
// Saves the currently displayed device's advertisement, to be used for pairing.
static CURR_DEVICE_ADV: RwLock<Option<FpPairingAdvertisement>> = 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<u16>,
latest_advertisement_map: &mut HashMap<BleAddress, FpPairingAdvertisement>,
) -> Option<FpPairingAdvertisement> {
// 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<String>) -> 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();
+2 -1
View File
@@ -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. */