blob: 70ce1846932715d4e7e2462a1293024529c06231 [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.
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");
};
}
}