diff --git a/fastpair/rust/demo/rust/Cargo.toml b/fastpair/rust/demo/rust/Cargo.toml index 20d9a17c..23463338 100644 --- a/fastpair/rust/demo/rust/Cargo.toml +++ b/fastpair/rust/demo/rust/Cargo.toml @@ -13,4 +13,6 @@ anyhow = "1.0" bluetooth = { version = "0.1", path = "../../bluetooth" } flutter_rust_bridge = "1" futures = { version = "0.3", features = ["executor"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" tracing = "0.1.37" diff --git a/fastpair/rust/demo/rust/src/advertisement.rs b/fastpair/rust/demo/rust/src/advertisement.rs index c94dd4c4..46aae17b 100644 --- a/fastpair/rust/demo/rust/src/advertisement.rs +++ b/fastpair/rust/demo/rust/src/advertisement.rs @@ -12,7 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use bluetooth::{BleAddress, BleAdvertisement}; +use bluetooth::{BleAddress, BleAdvertisement, ServiceData}; + +use crate::decoder::FpDecoder; + +/// Represents a FP device model ID. +pub(crate) type ModelId = String; /// Holds information required to make decisions about an incoming Fast Pair /// advertisement. @@ -21,9 +26,59 @@ pub(crate) struct FpPairingAdvertisement { inner: BleAdvertisement, /// Estimated distance in meters of device from BLE adapter. distance: f64, + model_id: ModelId, } impl FpPairingAdvertisement { + /// Create a new Fast Pair advertisement instance. + pub(crate) fn new( + adv: BleAdvertisement, + service_data: &ServiceData, + ) -> 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); + + // Extract model ID from service data. We don't need to store service + // data in the `FpPairingAdvertisement` since it's easily accessible from + // `FpPairingAdvertisement.inner`, but it's convenient to save the parsed + // model ID. + let mut model_id = + FpDecoder::get_model_id_from_service_data(service_data).or_else(|err| { + // Some FP advertisements can be GATT non-discoverable + // advertisements containing service data that isn't + // the device model ID. In this case, simply ignore + // advertisements with errors extracting the model ID. + // See: developers.google.com/nearby/fast-pair/specifications/service/provider + Err(anyhow::anyhow!("error extracting model ID: {}", err)) + })?; + + if model_id.len() != 3 { + // In this demo of Fast Pair Rust, only model ID's + // of length 3 bytes are supported. Therefore, if a + // larger model ID makes it this far, log an error. + // TODO b/294453912 + return Err(anyhow::anyhow!("Error: model ID of unsupported length")); + } + + // Pad with 0 at the beginning to successfully call `from_be_bytes`. + // Assumes `model_id.len() == 3` before the call to `insert`, otherwise + // this will panic. + model_id.insert(0, 0); + let model_id = format!("{}", u32::from_be_bytes(model_id.try_into().unwrap())); + + Ok(FpPairingAdvertisement { + inner: adv, + distance, + model_id, + }) + } + /// Retrieve estimated distance the BLE advertisement travelled between /// the sending device and this receiver. pub(crate) fn distance(&self) -> f64 { @@ -34,25 +89,11 @@ impl FpPairingAdvertisement { 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, - }) + /// Retrieve the Model ID advertised by this device, parsed from the + /// 16-bit UUID service data. + pub(crate) fn model_id(&self) -> &ModelId { + &self.model_id } } diff --git a/fastpair/rust/demo/rust/src/api.rs b/fastpair/rust/demo/rust/src/api.rs index d4abaffa..6e94476d 100644 --- a/fastpair/rust/demo/rust/src/api.rs +++ b/fastpair/rust/demo/rust/src/api.rs @@ -2,8 +2,7 @@ use std::{collections::HashMap, sync::RwLock}; use bluetooth::{ api::{BleAdapter, BleDevice, ClassicDevice}, - BleAddress, BleAdvertisement, BleDataTypeId, ClassicAddress, PairingResult, Platform, - ServiceData, + BleAdvertisement, BleDataTypeId, ClassicAddress, PairingResult, Platform, ServiceData, }; use flutter_rust_bridge::StreamSink; use futures::executor; @@ -42,7 +41,7 @@ async fn update_best_device(best_adv: FpPairingAdvertisement) { fn new_best_fp_advertisement( advertisement: BleAdvertisement, service_data: &ServiceData, - latest_advertisement_map: &mut HashMap, + latest_advertisement_map: &mut HashMap, ) -> Option { // Analyze service data sections. let uuid = service_data.uuid(); @@ -52,7 +51,7 @@ fn new_best_fp_advertisement( return None; } - let fp_adv = match FpPairingAdvertisement::try_from(advertisement) { + let fp_adv = match FpPairingAdvertisement::new(advertisement, service_data) { Ok(fp_adv) => fp_adv, Err(err) => { // If error during construction (e.g. non-discoverable @@ -64,13 +63,13 @@ fn new_best_fp_advertisement( } }; - latest_advertisement_map.insert(fp_adv.address(), fp_adv.clone()); + latest_advertisement_map.insert(fp_adv.model_id().to_owned(), 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() { + } else if best_adv.model_id() == fp_adv.model_id() { // New advertised distance by the previous best device has // increased, so select new closest device. let next_best_adv_ref = diff --git a/fastpair/rust/demo/rust/src/decoder.rs b/fastpair/rust/demo/rust/src/decoder.rs new file mode 100644 index 00000000..9c162937 --- /dev/null +++ b/fastpair/rust/demo/rust/src/decoder.rs @@ -0,0 +1,50 @@ +// 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::ServiceData; + +/// Unit struct providing parsing operations for Fast Pair advertisements. +pub(crate) struct FpDecoder; + +impl FpDecoder { + /// Retrieve the Fast Pair device model ID from a service data payload. + /// https://developers.google.com/nearby/fast-pair/specifications/service/provider. + /// * Length < 3: invalid payload + /// * Length == 3: entire payload is the model ID + /// * Length > 3: first byte specifies the length of the model ID, in bytes. + /// Currently unavailable in Fast Pair devices and not supported. + pub(crate) fn get_model_id_from_service_data( + service_data: &ServiceData, + ) -> Result, anyhow::Error> { + static MIN_MODEL_ID_LENGTH: usize = 3; + let data = service_data.data(); + + if data.len() < MIN_MODEL_ID_LENGTH { + // If service data too small, invalid payload. + Err(anyhow::anyhow!(format!( + "Invalid model ID for Fast Pair advertisement of length {}.", + data.len() + ))) + } else if data.len() == MIN_MODEL_ID_LENGTH { + // Else if service data length is exactly 3, all bytes are the ID. + Ok(data.clone()) + } else { + // Else, this Fast Pair advertisement is currently unsupported. + // b/294453912 + Err(anyhow::anyhow!( + "This Fast Pair device is currently unsupported." + )) + } + } +} diff --git a/fastpair/rust/demo/rust/src/lib.rs b/fastpair/rust/demo/rust/src/lib.rs index 8e78a507..c4831ae7 100644 --- a/fastpair/rust/demo/rust/src/lib.rs +++ b/fastpair/rust/demo/rust/src/lib.rs @@ -1,3 +1,4 @@ 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. */ +mod bridge_generated; +mod decoder; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */