| // Copyright 2024 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| //! Implements the Bluetooth Battery Service (BAS) client role. | |
| //! | |
| //! Use the toplevel `BatteryMonitor` to construct a new battery monitoring | |
| //! client. The client will scan for compatible peers that support the Battery | |
| //! Service. When a compatible peer is found, use `BatteryMonitor::connect` to | |
| //! establish a GATT connection to the peer's service. The battery level and | |
| //! other characteristics are available in the returned `BatteryMonitorClient`. | |
| //! | |
| //! For example: | |
| //! | |
| //! // Set up a GATT Central and construct the battery monitor. | |
| //! let central = ...; | |
| //! let monitor = BatteryMonitor::new(central); | |
| //! | |
| //! // Start scanning for compatible peers and find the next available one. | |
| //! let mut scan_results = monitor.start()?; | |
| //! let compatible_peer_id = scan_results.next().await?; | |
| //! | |
| //! // Connect to the peer's Battery Service. | |
| //! let connected_service = monitor.connect(compatible_peer_id).await?; | |
| //! | |
| //! // Process battery events (notifications/indications) from the peer. | |
| //! let battery_events = connected_service.take_event_stream()?; | |
| //! while let Some(battery_event) = battery_events.next().await? { | |
| //! // Do something with `battery_event` | |
| //! } | |
| use bt_common::PeerId; | |
| use bt_gatt::central::{Filter, ScanFilter}; | |
| use bt_gatt::client::PeerServiceHandle; | |
| use bt_gatt::{Central, Client, GattTypes}; | |
| use crate::error::Error; | |
| use crate::types::BATTERY_SERVICE_UUID; | |
| mod client; | |
| pub use client::{BatteryMonitorClient, BatteryMonitorEventStream}; | |
| /// Monitors the battery properties on a remote peer's Battery Service (BAS). | |
| /// | |
| /// Use `BatteryMonitor::start` to scan for compatible peers that provide the | |
| /// battery service. Once a suitable peer has been found, use | |
| /// `BatteryMonitor::connect` to initiate an outbound connection to the peer's | |
| /// Battery GATT service. Use the returned `BatteryMonitorClient` to | |
| /// interact with the GATT service (e.g. read battery level, receive battery | |
| /// level updates, etc.). | |
| pub struct BatteryMonitor<T: GattTypes> { | |
| central: T::Central, | |
| scan_stream: Option<T::ScanResultStream>, | |
| } | |
| impl<T: GattTypes> BatteryMonitor<T> { | |
| pub fn new(central: T::Central) -> Self { | |
| let scan_stream = central.scan(&Self::scan_filter()); | |
| Self { central, scan_stream: Some(scan_stream) } | |
| } | |
| fn scan_filter() -> Vec<ScanFilter> { | |
| vec![ScanFilter { | |
| filters: vec![Filter::ServiceUuid(BATTERY_SERVICE_UUID), Filter::IsConnectable], | |
| }] | |
| } | |
| /// Start scanning for compatible Battery peers. | |
| /// Returns a stream of scan results on success, Error if the scan couldn't | |
| /// complete for any reason. | |
| /// Can only be called once, returns `Error::ScanAlreadyStarted` on | |
| /// subsequent attempts. | |
| pub fn start(&mut self) -> Result<T::ScanResultStream, Error> | |
| where | |
| <T as bt_gatt::GattTypes>::ScanResultStream: std::marker::Send, | |
| { | |
| let Some(scan_stream) = self.scan_stream.take() else { | |
| return Err(Error::ScanAlreadyStarted); | |
| }; | |
| Ok(scan_stream) | |
| } | |
| /// Attempts to connect to the remote peer's Battery service. | |
| /// Returns a battery monitor which can be used to interact with the peer's | |
| /// battery service on success, Error if the connection couldn't be made | |
| /// or if the peer's Battery service is invalid. | |
| pub async fn connect(&mut self, id: PeerId) -> Result<BatteryMonitorClient<T>, Error> | |
| where | |
| <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send, | |
| { | |
| let client = self.central.connect(id).await.map_err(Error::GattLibrary)?; | |
| let peer_service_handles = | |
| client.find_service(BATTERY_SERVICE_UUID).await.map_err(Error::GattLibrary)?; | |
| for handle in peer_service_handles { | |
| if handle.uuid() != BATTERY_SERVICE_UUID || !handle.is_primary() { | |
| return Err(Error::ServiceNotFound); | |
| } | |
| let service = handle.connect().await.map_err(Error::GattLibrary)?; | |
| let monitor = BatteryMonitorClient::<T>::create(client, service).await?; | |
| // TODO(b/335246946): This short circuits after the first valid service is | |
| // found. Expand this to read all of the battery services to provide | |
| // an aggregated view of the peer. | |
| return Ok(monitor); | |
| } | |
| Err(Error::ServiceNotFound) | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use bt_common::Uuid; | |
| use bt_gatt::test_utils::{FakeCentral, FakeClient, FakeTypes}; | |
| use futures::{pin_mut, FutureExt}; | |
| use std::task::Poll; | |
| use crate::monitor::client::tests::fake_battery_service; | |
| #[test] | |
| fn battery_monitor_start_scan() { | |
| let fake_central = FakeCentral::new(); | |
| let mut monitor = BatteryMonitor::<FakeTypes>::new(fake_central); | |
| let _monitor_scan_stream = monitor.start().expect("can start scanning"); | |
| // The scan stream can only be acquired once. | |
| assert!(monitor.start().is_err()); | |
| } | |
| #[test] | |
| fn battery_monitor_connect_success() { | |
| // Instantiate a fake client with a battery service. | |
| let id = PeerId(1); | |
| let mut fake_central = FakeCentral::new(); | |
| let mut fake_client = FakeClient::new(); | |
| fake_central.add_client(id, fake_client.clone()); | |
| let fake_battery_service = fake_battery_service(50); | |
| fake_client.add_service(BATTERY_SERVICE_UUID, true, fake_battery_service); | |
| let mut monitor = BatteryMonitor::<FakeTypes>::new(fake_central); | |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); | |
| let connect_fut = monitor.connect(id); | |
| pin_mut!(connect_fut); | |
| let polled = connect_fut.poll_unpin(&mut noop_cx); | |
| let Poll::Ready(Ok(_monitor_service)) = polled else { | |
| panic!("Expected connect success"); | |
| }; | |
| } | |
| #[test] | |
| fn connect_no_services_is_error() { | |
| let id = PeerId(1); | |
| let mut fake_central = FakeCentral::new(); | |
| let fake_client = FakeClient::new(); | |
| fake_central.add_client(id, fake_client.clone()); | |
| // No battery service. | |
| let mut monitor = BatteryMonitor::<FakeTypes>::new(fake_central); | |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); | |
| let connect_fut = monitor.connect(id); | |
| pin_mut!(connect_fut); | |
| let polled = connect_fut.poll_unpin(&mut noop_cx); | |
| let Poll::Ready(Err(Error::ServiceNotFound)) = polled else { | |
| panic!("Expected connect failure"); | |
| }; | |
| } | |
| #[test] | |
| fn connect_invalid_battery_service_is_error() { | |
| let id = PeerId(1); | |
| let mut fake_central = FakeCentral::new(); | |
| let mut fake_client = FakeClient::new(); | |
| fake_central.add_client(id, fake_client.clone()); | |
| let fake_battery_service = fake_battery_service(50); | |
| let random_uuid = Uuid::from_u16(0x1234); | |
| fake_client.add_service(random_uuid, true, fake_battery_service); | |
| let mut monitor = BatteryMonitor::<FakeTypes>::new(fake_central); | |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); | |
| let connect_fut = monitor.connect(id); | |
| pin_mut!(connect_fut); | |
| let polled = connect_fut.poll_unpin(&mut noop_cx); | |
| let Poll::Ready(Err(Error::ServiceNotFound)) = polled else { | |
| panic!("Expected connect failure"); | |
| }; | |
| } | |
| } |