battery_client/
lib.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! A wrapper around the `fuchsia.power.battery.BatteryManager` capability.
6//!
7//! This library provides an API for receiving updates about the current battery information on the
8//! local Fuchsia device.
9//! A `Stream` implementation is provided for clients that rely on notification-style updates.
10//! Additional methods are also provided to get the most recently received battery information.
11//!
12//! ### Example Usage:
13//!
14//! // Create the BatteryClient. Under the hood, this will connect to the `BatteryManager`
15//! // capability.
16//! let batt_client = BatteryClient::create()?;
17//!
18//! // Listen for events from the BatteryClient stream implementation.
19//! if let Some(battery_info) = batt_client.next().await? {
20//!     if let Some(level_percent) = battery_info.level() {
21//!       // Report to peer.
22//!     }
23//! }
24//!
25
26use anyhow::format_err;
27use core::pin::Pin;
28use core::task::{Context, Poll};
29use derivative::Derivative;
30use fidl::endpoints::create_request_stream;
31use fidl_fuchsia_power_battery as fpower;
32use fuchsia_component::client::connect_to_protocol;
33use futures::stream::{FusedStream, Stream, StreamExt};
34use log::debug;
35
36/// Error type used by this library.
37mod error;
38pub use crate::error::BatteryClientError;
39
40/// The minimum battery level (in %) that will be reported.
41pub const MIN_BATTERY_LEVEL: u8 = 0;
42/// The maximum battery level (in %) that will be reported.
43pub const MAX_BATTERY_LEVEL: u8 = 100;
44
45/// The current battery level, represented as an integer level in the range
46/// [MIN_BATTERY_LEVEL, MAX_BATTERY_LEVEL].
47#[derive(Debug, PartialEq, Clone)]
48pub enum BatteryLevel {
49    Normal(u8),
50    Warning(u8),
51    Critical(u8),
52    FullCharge,
53}
54
55impl BatteryLevel {
56    pub fn level(&self) -> u8 {
57        match self {
58            Self::Normal(l) => *l,
59            Self::Warning(l) => *l,
60            Self::Critical(l) => *l,
61            Self::FullCharge => MAX_BATTERY_LEVEL,
62        }
63    }
64}
65
66#[derive(Debug, PartialEq, Clone)]
67pub enum BatteryInfo {
68    /// No battery information is available.
69    NotAvailable,
70    /// Currently on battery power with a specified level percentage.
71    Battery(BatteryLevel),
72    /// Currently on an external power source.
73    External,
74}
75
76impl BatteryInfo {
77    pub fn level(&self) -> Option<u8> {
78        if let Self::Battery(l) = &self {
79            return Some(l.level());
80        }
81        None
82    }
83}
84
85impl TryFrom<fpower::BatteryInfo> for BatteryInfo {
86    type Error = BatteryClientError;
87
88    fn try_from(src: fpower::BatteryInfo) -> Result<Self, Self::Error> {
89        if src.status != Some(fpower::BatteryStatus::Ok) {
90            return Ok(BatteryInfo::NotAvailable);
91        }
92
93        let fidl_level = src.level_status.unwrap_or(fpower::LevelStatus::Unknown);
94        if fidl_level == fpower::LevelStatus::Unknown {
95            return Ok(BatteryInfo::NotAvailable);
96        }
97
98        // Per the `fidl_fuchsia_power_battery` documentation, if `level_status` is known, then the
99        // level percentage will also be provided.
100        let level = src.level_percent.ok_or_else(|| {
101            BatteryClientError::info(format_err!("Missing battery level percentage"))
102        })?;
103        if level < MIN_BATTERY_LEVEL as f32 || level > MAX_BATTERY_LEVEL as f32 {
104            return Err(BatteryClientError::info(format_err!(
105                "Invalid battery level percentage: {:?}",
106                level
107            )));
108        }
109        // This operation is safe as we guarantee `level` is in [0, 100]. The floor of the level %
110        // is used so that we never overestimate the battery level when rounding to the nearest
111        // integer %.
112        let level_floor: u8 = level.floor() as u8;
113
114        let battery_level = match fidl_level {
115            _s if level_floor == MAX_BATTERY_LEVEL => BatteryLevel::FullCharge,
116            fpower::LevelStatus::Ok | fpower::LevelStatus::Low => BatteryLevel::Normal(level_floor),
117            fpower::LevelStatus::Warning => BatteryLevel::Warning(level_floor),
118            fpower::LevelStatus::Critical => BatteryLevel::Critical(level_floor),
119            fpower::LevelStatus::Unknown => unreachable!("LevelStatus is known"),
120        };
121
122        Ok(BatteryInfo::Battery(battery_level))
123    }
124}
125
126/// Manages the connection to the Fuchsia `BatteryManager` service.
127#[derive(Derivative)]
128#[derivative(Debug)]
129pub struct BatteryClient {
130    /// The client end of the connection to the `BatteryManager` service.
131    _svc: fpower::BatteryManagerProxy,
132    /// A stream of battery updates received from the system.
133    #[derivative(Debug = "ignore")]
134    watcher: fpower::BatteryInfoWatcherRequestStream,
135    /// The most recent battery information received from the `watcher`.
136    current_info: BatteryInfo,
137    /// A flag indicating the termination status of the `BatteryClient`.
138    terminated: bool,
139}
140
141impl BatteryClient {
142    /// Creates and returns an object to track updates from the Fuchsia Battery Service.
143    pub fn create() -> Result<Self, BatteryClientError> {
144        let battery_svc = connect_to_protocol::<fpower::BatteryManagerMarker>()
145            .map_err(BatteryClientError::manager_unavailable)?;
146        Self::register_updates(battery_svc)
147    }
148
149    /// Register for battery change updates from the Fuchsia battery `svc`.
150    pub fn register_updates(
151        battery_svc: fpower::BatteryManagerProxy,
152    ) -> Result<Self, BatteryClientError> {
153        let (watcher_client, watcher) = create_request_stream::<fpower::BatteryInfoWatcherMarker>();
154        battery_svc.watch(watcher_client)?;
155
156        Ok(Self {
157            _svc: battery_svc,
158            watcher,
159            current_info: BatteryInfo::NotAvailable,
160            terminated: false,
161        })
162    }
163
164    /// The current battery percentage (from 0 to 100), or None if it is not known.
165    pub fn battery_percent(&self) -> Option<u8> {
166        self.current_info.level()
167    }
168
169    pub fn battery_status(&self) -> &BatteryInfo {
170        &self.current_info
171    }
172
173    /// Handle a battery update request - returns the extracted battery information on success,
174    /// BatteryClientError otherwise.
175    fn handle_battery_info_request(
176        &mut self,
177        request: fpower::BatteryInfoWatcherRequest,
178    ) -> Result<BatteryInfo, BatteryClientError> {
179        let fpower::BatteryInfoWatcherRequest::OnChangeBatteryInfo { info, responder, .. } =
180            request;
181        debug!("Received battery update from system: {:?}", info);
182        responder.send()?;
183
184        // TODO(https://fxbug.dev/42171284): Invalid upstream information likely indicates a bug or a mismatch
185        // between this library and the battery manager. Close the watcher channel and attempt to
186        // re-register for updates.
187        let converted_result: Result<BatteryInfo, BatteryClientError> = info.try_into();
188        self.current_info =
189            converted_result.as_ref().map_or(BatteryInfo::NotAvailable, Clone::clone);
190        converted_result
191    }
192}
193
194impl Stream for BatteryClient {
195    type Item = Result<BatteryInfo, BatteryClientError>;
196
197    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
198        if self.terminated {
199            panic!("Cannot poll a terminated stream");
200        }
201
202        match self.watcher.poll_next_unpin(cx) {
203            Poll::Pending => Poll::Pending,
204            Poll::Ready(Some(Ok(request))) => {
205                let update = self.handle_battery_info_request(request);
206                Poll::Ready(Some(update))
207            }
208            Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(BatteryClientError::watcher(e)))),
209            Poll::Ready(None) => {
210                self.terminated = true;
211                Poll::Ready(None)
212            }
213        }
214    }
215}
216
217impl FusedStream for BatteryClient {
218    fn is_terminated(&self) -> bool {
219        self.terminated
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    use assert_matches::assert_matches;
228    use async_test_helpers::{expect_stream_item, expect_stream_pending};
229    use async_utils::PollExt;
230    use fuchsia_async as fasync;
231    use std::pin::pin;
232
233    fn setup_battery_client(
234    ) -> (fasync::TestExecutor, BatteryClient, fpower::BatteryInfoWatcherProxy) {
235        let mut exec = fasync::TestExecutor::new();
236
237        let (c, mut stream) =
238            fidl::endpoints::create_proxy_and_stream::<fpower::BatteryManagerMarker>();
239        let mut client = BatteryClient::register_updates(c).expect("can register");
240        expect_stream_pending(&mut exec, &mut client);
241
242        let upstream_battery_notifier = {
243            let fut = stream.next();
244            let mut fut = pin!(fut);
245            match exec.run_until_stalled(&mut fut).expect("fut is ready").unwrap() {
246                Ok(fpower::BatteryManagerRequest::Watch { watcher, .. }) => watcher.into_proxy(),
247                x => panic!("Expected Watch request, got: {:?}", x),
248            }
249        };
250
251        (exec, client, upstream_battery_notifier)
252    }
253
254    /// Simulates a battery update by sending the `update` via the `upstream_battery_notifier`.
255    /// Expects the update to be received by the `battery_client` and returns the resulting
256    /// converted update result.
257    fn send_update_and_poll_battery_client(
258        exec: &mut fasync::TestExecutor,
259        battery_client: &mut BatteryClient,
260        upstream_battery_notifier: &fpower::BatteryInfoWatcherProxy,
261        update: fpower::BatteryInfo,
262    ) -> Result<BatteryInfo, BatteryClientError> {
263        let update_fut = upstream_battery_notifier.on_change_battery_info(&update);
264        let mut update_fut = pin!(update_fut);
265        exec.run_until_stalled(&mut update_fut).expect_pending("waiting for fidl response");
266
267        let item = expect_stream_item(exec, battery_client);
268        // Once the BatteryClient receives the update, the FIDL method should resolve.
269        assert_matches!(exec.run_until_stalled(&mut update_fut).expect("should resolve"), Ok(_));
270
271        item
272    }
273
274    #[fuchsia::test]
275    fn battery_client_terminates_when_watcher_terminates() {
276        let (mut exec, mut client, upstream_battery_notifier) = setup_battery_client();
277
278        // Upstream watcher (owned by the Battery Manager) disconnects for some reason.
279        drop(upstream_battery_notifier);
280
281        // Expect the stream impl of the BatteryClient to finish.
282        {
283            let client_stream = client.next();
284            let mut client_stream = pin!(client_stream);
285            let res = exec
286                .run_until_stalled(&mut client_stream)
287                .expect("battery client should terminate");
288            assert_matches!(res, None);
289        }
290        assert!(client.is_terminated());
291    }
292
293    #[fuchsia::test]
294    fn battery_client_stream_impl_with_empty_updates() {
295        let (mut exec, mut client, upstream_battery_notifier) = setup_battery_client();
296        assert!(!client.is_terminated());
297
298        // An empty update from the `fuchsia.power` service should trigger a stream event.
299        let update = fpower::BatteryInfo::default();
300        let info = send_update_and_poll_battery_client(
301            &mut exec,
302            &mut client,
303            &upstream_battery_notifier,
304            update,
305        )
306        .expect("valid update");
307        assert_eq!(info, BatteryInfo::NotAvailable);
308
309        // A not-OK battery status is fine. Stream event should still be NotAvailable.
310        let update = fpower::BatteryInfo {
311            status: Some(fpower::BatteryStatus::NotPresent),
312            ..Default::default()
313        };
314        let info = send_update_and_poll_battery_client(
315            &mut exec,
316            &mut client,
317            &upstream_battery_notifier,
318            update,
319        )
320        .expect("valid update");
321        assert_eq!(info, BatteryInfo::NotAvailable);
322
323        // An unknown Battery Level is also a non-interesting update.
324        let update = fpower::BatteryInfo {
325            status: Some(fpower::BatteryStatus::Ok),
326            level_status: Some(fpower::LevelStatus::Unknown),
327            ..Default::default()
328        };
329        let info = send_update_and_poll_battery_client(
330            &mut exec,
331            &mut client,
332            &upstream_battery_notifier,
333            update,
334        )
335        .expect("valid update");
336        assert_eq!(info, BatteryInfo::NotAvailable);
337    }
338
339    #[fuchsia::test]
340    fn battery_client_stream_impl_with_invalid_updates() {
341        let (mut exec, mut client, upstream_battery_notifier) = setup_battery_client();
342
343        // Battery level known, but missing level_percent is invalid per `fuchsia.power` docs.
344        let update = fpower::BatteryInfo {
345            status: Some(fpower::BatteryStatus::Ok),
346            level_status: Some(fpower::LevelStatus::Warning),
347            ..Default::default()
348        };
349        let info = send_update_and_poll_battery_client(
350            &mut exec,
351            &mut client,
352            &upstream_battery_notifier,
353            update,
354        );
355        assert_matches!(info, Err(_));
356
357        // Level percent of more than 100 is an error.
358        let update = fpower::BatteryInfo {
359            status: Some(fpower::BatteryStatus::Ok),
360            level_status: Some(fpower::LevelStatus::Ok),
361            level_percent: Some(125.58f32),
362            ..Default::default()
363        };
364        let info = send_update_and_poll_battery_client(
365            &mut exec,
366            &mut client,
367            &upstream_battery_notifier,
368            update,
369        );
370        assert_matches!(info, Err(_));
371
372        // Level percent of less than 0 is an error.
373        let update = fpower::BatteryInfo {
374            status: Some(fpower::BatteryStatus::Ok),
375            level_status: Some(fpower::LevelStatus::Critical),
376            level_percent: Some(-10.332),
377            ..Default::default()
378        };
379        let info = send_update_and_poll_battery_client(
380            &mut exec,
381            &mut client,
382            &upstream_battery_notifier,
383            update,
384        );
385        assert_matches!(info, Err(_));
386    }
387
388    #[fuchsia::test]
389    fn battery_client_stream_impl_with_normal_updates() {
390        let (mut exec, mut client, upstream_battery_notifier) = setup_battery_client();
391        assert!(!client.is_terminated());
392        assert_eq!(client.battery_percent(), None);
393        assert_eq!(client.battery_status(), &BatteryInfo::NotAvailable);
394
395        // A typical battery info update with a level percent should trigger a battery client stream
396        // item.
397        let update = fpower::BatteryInfo {
398            status: Some(fpower::BatteryStatus::Ok),
399            level_status: Some(fpower::LevelStatus::Low),
400            level_percent: Some(88f32),
401            ..Default::default()
402        };
403        let info = send_update_and_poll_battery_client(
404            &mut exec,
405            &mut client,
406            &upstream_battery_notifier,
407            update,
408        )
409        .expect("valid update");
410        assert_eq!(info, BatteryInfo::Battery(BatteryLevel::Normal(88)));
411
412        // A client can also query the BatteryClient to figure out the current battery %.
413        assert_eq!(client.battery_percent(), Some(88));
414
415        // Invalid update should be an error on the stream. The current battery info should be
416        // reflect the error and be not available.
417        let update = fpower::BatteryInfo {
418            status: Some(fpower::BatteryStatus::Ok),
419            level_status: Some(fpower::LevelStatus::Critical),
420            level_percent: Some(-10.332),
421            ..Default::default()
422        };
423        let info = send_update_and_poll_battery_client(
424            &mut exec,
425            &mut client,
426            &upstream_battery_notifier,
427            update,
428        );
429        assert_matches!(info, Err(_));
430        assert_eq!(client.battery_status(), &BatteryInfo::NotAvailable);
431
432        let update = fpower::BatteryInfo {
433            status: Some(fpower::BatteryStatus::Ok),
434            level_status: Some(fpower::LevelStatus::Critical),
435            level_percent: Some(5.58f32),
436            ..Default::default()
437        };
438        let info = send_update_and_poll_battery_client(
439            &mut exec,
440            &mut client,
441            &upstream_battery_notifier,
442            update,
443        )
444        .expect("valid update");
445        assert_eq!(info, BatteryInfo::Battery(BatteryLevel::Critical(5)));
446
447        // Minimum battery %.
448        let update = fpower::BatteryInfo {
449            status: Some(fpower::BatteryStatus::Ok),
450            level_status: Some(fpower::LevelStatus::Critical),
451            level_percent: Some(0.0f32),
452            ..Default::default()
453        };
454        let info = send_update_and_poll_battery_client(
455            &mut exec,
456            &mut client,
457            &upstream_battery_notifier,
458            update,
459        )
460        .expect("valid update");
461        assert_eq!(info, BatteryInfo::Battery(BatteryLevel::Critical(0)));
462
463        // Maximum battery %.
464        let update = fpower::BatteryInfo {
465            status: Some(fpower::BatteryStatus::Ok),
466            level_status: Some(fpower::LevelStatus::Critical),
467            level_percent: Some(100.0f32),
468            ..Default::default()
469        };
470        let info = send_update_and_poll_battery_client(
471            &mut exec,
472            &mut client,
473            &upstream_battery_notifier,
474            update,
475        )
476        .expect("valid update");
477        assert_eq!(info, BatteryInfo::Battery(BatteryLevel::FullCharge));
478    }
479}