blob: d328c634d8daaf91ade3a4604b91d615d9a83b0c [file] [log] [blame]
// Copyright 2022 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 async_utils::hanging_get::server::{HangingGet, Publisher, Subscriber};
use fidl_fuchsia_bluetooth_power::{Identifier, Information, WatcherWatchResponder};
use fuchsia_bluetooth::types::PeerId;
use fuchsia_sync::Mutex;
use std::collections::HashMap;
use tracing::warn;
use crate::error::Error;
type NotifyFn<Responder> = Box<dyn Fn(&Vec<Information>, Responder) -> bool + Send + Sync>;
type PeripheralHangingGet<Responder> = HangingGet<Vec<Information>, Responder, NotifyFn<Responder>>;
type PeripheralPublisher<Responder> = Publisher<Vec<Information>, Responder, NotifyFn<Responder>>;
pub type PeripheralSubscriber<Responder> =
Subscriber<Vec<Information>, Responder, NotifyFn<Responder>>;
/// The current state of discovered/connected Bluetooth peripherals.
pub struct PeripheralState {
/// Current state of the peripherals. Used to record new updates.
inner: Mutex<PeripheralStateInner>,
/// Hanging-get server that assigns subscriptions to FIDL clients that want to watch peripheral
/// state.
broker: Mutex<PeripheralHangingGet<WatcherWatchResponder>>,
/// Hanging-get publisher used to send updates to all hanging-get listeners about a change in
/// peripheral state.
publisher: PeripheralPublisher<WatcherWatchResponder>,
}
impl PeripheralState {
pub fn new() -> Self {
let notify_fn: NotifyFn<WatcherWatchResponder> =
Box::new(|peripherals: &Vec<Information>, responder: WatcherWatchResponder| {
if let Err(e) = responder.send(&peripherals) {
warn!("Unable to respond to Peripheral Watcher hanging get: {:?}", e);
}
true
});
let watch_peripherals_broker = HangingGet::new(Vec::new(), notify_fn);
let publisher = watch_peripherals_broker.new_publisher();
let inner = PeripheralStateInner { peripherals: HashMap::new() };
Self { inner: Mutex::new(inner), broker: Mutex::new(watch_peripherals_broker), publisher }
}
fn notify_peripheral_watchers(&self, info: Vec<Information>) {
self.publisher.update(move |state| {
if state.as_ref() == Some(&info) {
false
} else {
*state = Some(info);
true
}
});
}
pub fn new_subscriber(&self) -> PeripheralSubscriber<WatcherWatchResponder> {
self.broker.lock().new_subscriber()
}
pub fn record_power_update(&self, id: PeerId, battery: BatteryInfo) {
let info = {
let mut inner = self.inner.lock();
inner.record_power_update(id, battery);
inner.peripherals()
};
self.notify_peripheral_watchers(info);
}
#[cfg(test)]
pub fn contains_entry(&self, id: &PeerId) -> bool {
self.inner.lock().peripherals.contains_key(id)
}
}
struct PeripheralStateInner {
peripherals: HashMap<PeerId, PeripheralData>,
}
impl PeripheralStateInner {
fn peripherals(&self) -> Vec<Information> {
self.peripherals.values().map(Into::into).collect()
}
fn record_power_update(&mut self, id: PeerId, battery: BatteryInfo) {
let entry = self.peripherals.entry(id).or_insert(PeripheralData::new(id));
entry.battery = Some(battery);
}
}
/// A snapshot of properties associated with a Bluetooth peripheral.
#[derive(Debug)]
struct PeripheralData {
id: PeerId,
/// Information about battery health & status.
battery: Option<BatteryInfo>,
}
impl PeripheralData {
fn new(id: PeerId) -> Self {
Self { id, battery: None }
}
}
impl From<&PeripheralData> for Information {
fn from(src: &PeripheralData) -> Information {
Information {
identifier: Some(Identifier::PeerId(src.id.into())),
battery_info: src.battery.as_ref().map(Into::into),
..Default::default()
}
}
}
/// Battery information about a peripheral.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BatteryInfo {
level_percent: f32,
level_status: Option<fidl_fuchsia_power_battery::LevelStatus>,
}
impl TryFrom<fidl_fuchsia_power_battery::BatteryInfo> for BatteryInfo {
type Error = Error;
fn try_from(src: fidl_fuchsia_power_battery::BatteryInfo) -> Result<BatteryInfo, Self::Error> {
// The `level_percent` must be specified per the `fidl_fuchsia_bluetooth_power` docs.
let level_percent = src.level_percent.ok_or(Error::battery("missing level percent"))?;
Ok(BatteryInfo { level_percent, level_status: src.level_status })
}
}
impl From<&BatteryInfo> for fidl_fuchsia_power_battery::BatteryInfo {
fn from(src: &BatteryInfo) -> fidl_fuchsia_power_battery::BatteryInfo {
fidl_fuchsia_power_battery::BatteryInfo {
level_percent: Some(src.level_percent),
level_status: src.level_status,
..Default::default()
}
}
}
/// Returns the Bluetooth PeerId from the `identifier`, or Error otherwise.
pub fn peer_id_from_identifier(identifier: &Identifier) -> Result<PeerId, Error> {
match identifier {
Identifier::PeerId(id) => Ok((*id).into()),
id => Err(id.into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use fidl::client::QueryResponseFut;
use fidl_fuchsia_bluetooth_power::{LocalDevice, WatcherMarker};
use futures::StreamExt;
use std::sync::Arc;
#[test]
fn invalid_identifier() {
let invalid = Identifier::unknown_variant_for_testing();
assert_matches!(peer_id_from_identifier(&invalid), Err(Error::Identifier { .. }));
let unsupported = Identifier::LocalDevice(LocalDevice);
assert_matches!(peer_id_from_identifier(&unsupported), Err(Error::Identifier { .. }));
}
#[test]
fn valid_identifier() {
let id = Identifier::PeerId(PeerId(123).into());
assert_matches!(peer_id_from_identifier(&id), Ok(_));
}
#[test]
fn invalid_battery_info() {
let empty = fidl_fuchsia_power_battery::BatteryInfo::default();
let local = BatteryInfo::try_from(empty);
assert_matches!(local, Err(Error::BatteryInfo { .. }));
let missing_percent = fidl_fuchsia_power_battery::BatteryInfo {
level_status: Some(fidl_fuchsia_power_battery::LevelStatus::Low),
..Default::default()
};
let local = BatteryInfo::try_from(missing_percent);
assert_matches!(local, Err(Error::BatteryInfo { .. }));
}
#[test]
fn battery_info() {
// Extra fields are Ok - ignored.
let valid = fidl_fuchsia_power_battery::BatteryInfo {
level_percent: Some(1.0f32),
level_status: Some(fidl_fuchsia_power_battery::LevelStatus::Low),
charge_source: Some(fidl_fuchsia_power_battery::ChargeSource::Usb),
..Default::default()
};
let local = BatteryInfo::try_from(valid).expect("valid conversion");
let expected = BatteryInfo {
level_percent: 1.0f32,
level_status: Some(fidl_fuchsia_power_battery::LevelStatus::Low),
};
assert_eq!(local, expected);
}
type WatchRequest = QueryResponseFut<Vec<Information>>;
async fn make_watch_request() -> (WatchRequest, WatcherWatchResponder) {
let (c, mut s) = fidl::endpoints::create_proxy_and_stream::<WatcherMarker>().unwrap();
let watch_fut = c.watch(&[]).check().expect("can make Watch request");
let (_ids, responder) = s
.select_next_some()
.await
.expect("fidl request")
.into_watch()
.expect("Watcher::watch request");
(watch_fut, responder)
}
#[fuchsia::test]
async fn subscriber_notified_of_peripheral_update() {
let shared_state = Arc::new(PeripheralState::new());
let shared_state1 = shared_state.clone();
// New subscriber.
let subscriber = shared_state.new_subscriber();
let (watch_request, responder) = make_watch_request().await;
subscriber.register(responder).expect("can register a subscriber");
// The first update in a hanging-get should always resolve immediately with current state.
let info = watch_request.await.expect("FIDL response");
assert_eq!(info, vec![]);
// Client subscribes again.
let (watch_request1, responder1) = make_watch_request().await;
subscriber.register(responder1).expect("can register a subscriber");
// Receive a power update.
let id = PeerId(111);
let battery_info = BatteryInfo {
level_percent: 10.0,
level_status: Some(fidl_fuchsia_power_battery::LevelStatus::Low),
};
shared_state1.record_power_update(id, battery_info.clone());
// Expect subscriber to get the update.
let info2 = watch_request1.await.expect("FIDL response");
let expected_info = vec![Information {
identifier: Some(Identifier::PeerId(id.into())),
battery_info: Some(fidl_fuchsia_power_battery::BatteryInfo {
level_percent: Some(10.0),
level_status: Some(fidl_fuchsia_power_battery::LevelStatus::Low),
..Default::default()
}),
..Default::default()
}];
assert_eq!(info2, expected_info);
}
}