blob: cbe5a64e4179dd1dd071da55d1f3f1e8aaee42f3 [file] [log] [blame]
// 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");
};
}
}