1use 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
36mod error;
38pub use crate::error::BatteryClientError;
39
40pub const MIN_BATTERY_LEVEL: u8 = 0;
42pub const MAX_BATTERY_LEVEL: u8 = 100;
44
45#[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 NotAvailable,
70 Battery(BatteryLevel),
72 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 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 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#[derive(Derivative)]
128#[derivative(Debug)]
129pub struct BatteryClient {
130 _svc: fpower::BatteryManagerProxy,
132 #[derivative(Debug = "ignore")]
134 watcher: fpower::BatteryInfoWatcherRequestStream,
135 current_info: BatteryInfo,
137 terminated: bool,
139}
140
141impl BatteryClient {
142 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 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 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 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 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 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 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 drop(upstream_battery_notifier);
280
281 {
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 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 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 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 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 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 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 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 assert_eq!(client.battery_percent(), Some(88));
414
415 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 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 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}