mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-14 22:56:12 -04:00
[fp-rs] Flutter UI now only displays the closest discovered device.
This commit is contained in:
@@ -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>,
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user