| // 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. |
| |
| use bt_common::packet_encoding::Decodable; |
| use bt_gatt::client::ServiceCharacteristic; |
| use bt_gatt::types::Handle; |
| use bt_gatt::GattTypes; |
| |
| use crate::error::{Error, ServiceError}; |
| use crate::types::{BatteryLevel, BATTERY_LEVEL_UUID, READ_CHARACTERISTIC_BUFFER_SIZE}; |
| |
| // TODO(aniramakri): Implement this. |
| pub struct BatteryMonitorEventStream {} |
| |
| /// Implements the Battery Service client role. |
| pub struct BatteryMonitorClient<T: GattTypes> { |
| /// Represents the underlying GATT LE connection. Kept alive to maintain the |
| /// connection to the peer. |
| _client: T::Client, |
| /// GATT client interface for interacting with the peer's battery service. |
| gatt_client: T::PeerService, |
| /// GATT Handles associated with the peer's one or more Battery Level |
| /// characteristics. The first `Handle` in this list is expected to be |
| /// the "primary" one. |
| battery_level_handles: Vec<Handle>, |
| // TODO(b/335259516): Save Handles for additional characteristics that are discovered. |
| /// The current battery level reported by the peer's battery server. |
| battery_level: BatteryLevel, |
| } |
| |
| impl<T: GattTypes> BatteryMonitorClient<T> { |
| pub(crate) async fn create( |
| _client: T::Client, |
| gatt_client: T::PeerService, |
| ) -> Result<Self, Error> |
| where |
| <T as bt_gatt::GattTypes>::NotificationStream: std::marker::Send, |
| { |
| // All battery services must contain at least one battery level characteristic. |
| let battery_level_characteristics = |
| ServiceCharacteristic::<T>::find(&gatt_client, BATTERY_LEVEL_UUID).await?; |
| |
| if battery_level_characteristics.is_empty() { |
| return Err(Error::Service(ServiceError::MissingCharacteristic)); |
| } |
| // It is valid to have multiple Battery Level Characteristics. If multiple |
| // exist, the primary (main) characteristic has a Description field of |
| // "main". For now, we assume the first such characteristic is the |
| // primary. See BAS 1.1 Section 3.1.2.1. TODO(b/335246946): Check for |
| // Characteristic Presentation Format descriptor if |
| // multiple characteristics are present. Use this to infer the "primary". |
| let battery_level_handles: Vec<Handle> = |
| battery_level_characteristics.iter().map(|c| *c.handle()).collect(); |
| // Get the current battery level of the primary characteristic. |
| let (battery_level, _decoded_bytes) = { |
| let mut buf = vec![0; READ_CHARACTERISTIC_BUFFER_SIZE]; |
| let read_bytes = |
| battery_level_characteristics.first().unwrap().read(&mut buf[..]).await?; |
| BatteryLevel::decode(&buf[0..read_bytes])? |
| }; |
| |
| // TODO(aniramakri): Subscribe to notifications on the battery level |
| // characteristic and save as a stream of events. |
| |
| Ok(Self { _client, gatt_client, battery_level_handles, battery_level }) |
| } |
| |
| /// Returns a stream of battery events. |
| /// The returned Stream _must_ be polled in order to receive the relevant |
| /// notification and indications on the battery service. |
| /// This method should only be called once. |
| /// Returns Some<T> if the battery stream is available, None otherwise. |
| pub fn take_event_stream(&mut self) -> Option<BatteryMonitorEventStream> { |
| todo!("Implement battery event stream") |
| } |
| |
| #[cfg(test)] |
| fn battery_level(&self) -> BatteryLevel { |
| self.battery_level |
| } |
| } |
| |
| #[cfg(test)] |
| pub(crate) mod tests { |
| use super::*; |
| |
| use bt_common::packet_encoding::Error as PacketError; |
| use bt_common::Uuid; |
| use bt_gatt::test_utils::{FakeClient, FakePeerService, FakeTypes}; |
| use bt_gatt::types::{ |
| AttributePermissions, Characteristic, CharacteristicProperties, CharacteristicProperty, |
| }; |
| use futures::{pin_mut, FutureExt}; |
| use std::task::Poll; |
| |
| pub(crate) const BATTERY_LEVEL_HANDLE: Handle = Handle(0x1); |
| pub(crate) fn fake_battery_service(battery_level: u8) -> FakePeerService { |
| let mut peer_service = FakePeerService::new(); |
| peer_service.add_characteristic( |
| Characteristic { |
| handle: BATTERY_LEVEL_HANDLE, |
| uuid: BATTERY_LEVEL_UUID, |
| properties: CharacteristicProperties(vec![ |
| CharacteristicProperty::Read, |
| CharacteristicProperty::Notify, |
| ]), |
| permissions: AttributePermissions::default(), |
| descriptors: vec![], |
| }, |
| vec![battery_level], |
| ); |
| peer_service |
| } |
| |
| /// Builds a `BatteryMonitorClient` that is connected to a fake GATT service |
| /// with a Battery Service. |
| fn setup_client(battery_level: u8) -> (BatteryMonitorClient<FakeTypes>, FakePeerService) { |
| // Constructs a FakePeerService with a battery level characteristic. |
| let fake_peer_service = fake_battery_service(battery_level); |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); |
| let create_result = |
| BatteryMonitorClient::<FakeTypes>::create(FakeClient::new(), fake_peer_service.clone()); |
| pin_mut!(create_result); |
| let polled = create_result.poll_unpin(&mut noop_cx); |
| let Poll::Ready(Ok(client)) = polled else { |
| panic!("Expected BatteryMonitorClient to be successfully created"); |
| }; |
| |
| (client, fake_peer_service) |
| } |
| |
| #[test] |
| fn create_client_and_read_battery_level_success() { |
| let battery_level = 20; |
| let (monitor, _fake_peer_service) = setup_client(battery_level); |
| assert_eq!(monitor.battery_level(), BatteryLevel(battery_level)); |
| } |
| |
| #[test] |
| fn empty_battery_service_is_error() { |
| let fake_peer_service = FakePeerService::new(); |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); |
| let create_result = |
| BatteryMonitorClient::<FakeTypes>::create(FakeClient::new(), fake_peer_service); |
| pin_mut!(create_result); |
| let polled = create_result.poll_unpin(&mut noop_cx); |
| let Poll::Ready(Err(Error::Service(ServiceError::MissingCharacteristic))) = polled else { |
| panic!("Expected BatteryMonitorClient failure"); |
| }; |
| } |
| |
| #[test] |
| fn service_missing_battery_level_characteristic_is_error() { |
| let mut fake_peer_service = FakePeerService::new(); |
| // Battery Level characteristic is invalidly formatted. |
| fake_peer_service.add_characteristic( |
| Characteristic { |
| handle: BATTERY_LEVEL_HANDLE, |
| uuid: Uuid::from_u16(0x1234), // Random UUID, not Battery Level |
| properties: CharacteristicProperties(vec![ |
| CharacteristicProperty::Read, |
| CharacteristicProperty::Notify, |
| ]), |
| permissions: AttributePermissions::default(), |
| descriptors: vec![], |
| }, |
| vec![], |
| ); |
| |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); |
| let create_result = |
| BatteryMonitorClient::<FakeTypes>::create(FakeClient::new(), fake_peer_service); |
| pin_mut!(create_result); |
| let polled = create_result.poll_unpin(&mut noop_cx); |
| let Poll::Ready(Err(Error::Service(ServiceError::MissingCharacteristic))) = polled else { |
| panic!("Expected BatteryMonitorClient failure"); |
| }; |
| } |
| |
| #[test] |
| fn invalid_battery_level_value_is_error() { |
| // Battery Level characteristic has an empty battery level value. |
| let mut fake_peer_service = FakePeerService::new(); |
| fake_peer_service.add_characteristic( |
| Characteristic { |
| handle: BATTERY_LEVEL_HANDLE, |
| uuid: BATTERY_LEVEL_UUID, |
| properties: CharacteristicProperties(vec![ |
| CharacteristicProperty::Read, |
| CharacteristicProperty::Notify, |
| ]), |
| permissions: AttributePermissions::default(), |
| descriptors: vec![], |
| }, |
| vec![], |
| ); |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); |
| let create_result = |
| BatteryMonitorClient::<FakeTypes>::create(FakeClient::new(), fake_peer_service); |
| pin_mut!(create_result); |
| let polled = create_result.poll_unpin(&mut noop_cx); |
| let Poll::Ready(Err(Error::Packet(PacketError::UnexpectedDataLength))) = polled else { |
| panic!("Expected BatteryMonitorClient to be successfully created"); |
| }; |
| } |
| } |