Skip to main content

fdf_power/
lib.rs

1// Copyright 2025 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
5use core::future::Future;
6use fdf_component::{Driver, DriverContext, DriverError};
7use fidl_fuchsia_hardware_power as fhw_power;
8use fidl_fuchsia_io as fio;
9use fidl_fuchsia_power_broker as fpower_broker;
10use fidl_fuchsia_power_system as fpower;
11use fuchsia_async as fasync;
12use fuchsia_component::client::{Connect, SVC_DIR};
13use fuchsia_component::directory::Directory;
14use futures::TryStreamExt;
15use log::{error, warn};
16use std::sync::{Arc, Weak};
17use zx::Status;
18
19use fidl_next as _;
20use fidl_next_fuchsia_hardware_powerdomain as fpowerdomain;
21
22/// Implement this trait if you'd like to get notifications when the system is about to go into
23/// suspend and come out of resume.
24pub trait SuspendableDriver: Driver {
25    /// Called prior the system entering suspend. The system is not guaranteed to enter suspend,
26    /// but `resume` will be called regardless before a subsequent suspension attempt occurs.
27    fn suspend(&self) -> impl Future<Output = ()> + Send;
28
29    /// Called after `suspend` to indicate the system is no longer in suspend. The system may not
30    /// have actually entered suspension in between `suspend` and `resume` invocations. NOTE: there
31    /// is no initial call to `resume`; drivers are expected to start in a non-suspended state and
32    /// `resume` will only be called after `suspend`.
33    fn resume(&self) -> impl Future<Output = ()> + Send;
34
35    /// Returns whether or not suspend is enabled. If false is returned, suspend and resume methods
36    /// will never be called.
37    fn suspend_enabled(&self) -> bool;
38}
39
40/// Wrapper trait to indicate the driver supports power operations.
41pub struct Suspendable<T: Driver> {
42    #[expect(unused)]
43    scope: Option<fasync::Scope>,
44    driver: Arc<T>,
45}
46
47async fn run_suspend_blocker<T: SuspendableDriver>(
48    driver: Weak<T>,
49    mut service: fpower::SuspendBlockerRequestStream,
50) {
51    use fpower::SuspendBlockerRequest::*;
52    while let Some(req) = service.try_next().await.unwrap() {
53        match req {
54            BeforeSuspend { responder, .. } => {
55                if let Some(driver) = driver.upgrade() {
56                    driver.suspend().await;
57                } else {
58                    return;
59                }
60                let _ = responder.send();
61            }
62            AfterResume { responder, .. } => {
63                if let Some(driver) = driver.upgrade() {
64                    driver.resume().await;
65                } else {
66                    return;
67                }
68                let _ = responder.send();
69            }
70            // Ignore unknown requests.
71            _ => {
72                warn!("Received unknown sag listener request");
73            }
74        }
75    }
76}
77
78async fn run_element_runner<T: SuspendableDriver>(
79    driver: Weak<T>,
80    mut service: fpower_broker::ElementRunnerRequestStream,
81) {
82    let mut first_activation_occurred = false;
83    while let Some(req) = service.try_next().await.unwrap_or_default() {
84        if let fpower_broker::ElementRunnerRequest::SetLevel { level, responder } = req {
85            let Some(driver) = driver.upgrade() else { return };
86            if level != fhw_power::FrameworkElementLevels::Off.into_primitive() as u8 {
87                // Hide the initial resume because drivers should start in a resumed state and it is
88                // easier for users if we guarantee that a call to resume always follows a call to
89                // suspend.
90                if first_activation_occurred {
91                    driver.resume().await;
92                }
93            } else {
94                driver.suspend().await;
95            }
96            let _ = responder.send();
97            first_activation_occurred = true;
98        }
99    }
100}
101
102impl<T: SuspendableDriver + Send + Sync> Driver for Suspendable<T> {
103    const NAME: &str = T::NAME;
104
105    async fn start(mut context: DriverContext) -> Result<Self, DriverError> {
106        let mut runner = context
107            .start_args
108            .power_element_args
109            .as_mut()
110            .and_then(|args| args.runner_server.take());
111
112        let (svc, svc_server) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
113        context
114            .incoming
115            .open(
116                SVC_DIR,
117                fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE,
118                svc_server.into_channel(),
119            )
120            .map_err(|error| {
121                error!(error:?; "Error opening svc directory");
122                Status::INTERNAL
123            })?;
124
125        let driver = Arc::new(T::start(context).await?);
126
127        let scope = if driver.suspend_enabled() {
128            let scope = fasync::Scope::new_with_name("suspend");
129            if let Some(runner) = runner.take() {
130                let weak_driver = Arc::downgrade(&driver);
131                scope.spawn(
132                    async move { run_element_runner(weak_driver, runner.into_stream()).await },
133                );
134            } else {
135                let sag =
136                    fpower::ActivityGovernorProxy::connect_at_dir_root(&svc).map_err(|error| {
137                        error!(error:?; "Error connecting to sag");
138                        Status::INTERNAL
139                    })?;
140
141                let (client, server) = fidl::endpoints::create_endpoints();
142
143                let _ = sag
144                    .register_suspend_blocker(
145                        fpower::ActivityGovernorRegisterSuspendBlockerRequest {
146                            suspend_blocker: Some(client),
147                            name: Some(Self::NAME.into()),
148                            ..Default::default()
149                        },
150                    )
151                    .await
152                    .map_err(|error| {
153                        error!(error:?; "Error connecting to sag");
154                        Status::INTERNAL
155                    })?
156                    .map_err(|error| {
157                        error!(error:?; "Error connecting to sag");
158                        Status::INTERNAL
159                    })?;
160
161                let weak_driver = Arc::downgrade(&driver);
162                scope.spawn(
163                    async move { run_suspend_blocker(weak_driver, server.into_stream()).await },
164                );
165            }
166            Some(scope)
167        } else {
168            None
169        };
170
171        Ok(Self { driver, scope })
172    }
173
174    async fn stop(&self) {
175        self.driver.stop().await;
176    }
177}
178
179/// Extension trait for [`DriverContext`] to simplify connecting to power resources.
180pub trait PowerExt {
181    /// Connects to a power domain resource with the given instance name.
182    fn connect_to_powerdomain(
183        &self,
184        instance: &str,
185    ) -> Result<fidl_next::Client<fpowerdomain::Domain>, DriverError>;
186}
187
188impl PowerExt for DriverContext {
189    fn connect_to_powerdomain(
190        &self,
191        instance: &str,
192    ) -> Result<fidl_next::Client<fpowerdomain::Domain>, DriverError> {
193        let service = self
194            .incoming
195            .service::<fdf_component::ServiceInstance<fpowerdomain::Service>>()
196            .instance(instance)
197            .connect_next()?;
198        let (client, server) = fidl_next::fuchsia::create_channel();
199        service.domain(server)?;
200        Ok(client.spawn())
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use fdf_component::testing::harness::TestHarness;
208    use fidl_fuchsia_driver_framework as fdf;
209    use std::sync::atomic::{AtomicBool, Ordering};
210
211    struct TestDriver {
212        suspend_called: Arc<AtomicBool>,
213        resume_called: Arc<AtomicBool>,
214        suspend_enabled: bool,
215        stop_called: Arc<AtomicBool>,
216    }
217
218    impl Driver for TestDriver {
219        const NAME: &str = "test_driver";
220
221        async fn start(_context: DriverContext) -> Result<Self, DriverError> {
222            Ok(Self {
223                suspend_called: Arc::new(AtomicBool::new(false)),
224                resume_called: Arc::new(AtomicBool::new(false)),
225                suspend_enabled: true,
226                stop_called: Arc::new(AtomicBool::new(false)),
227            })
228        }
229
230        async fn stop(&self) {
231            self.stop_called.store(true, Ordering::SeqCst);
232        }
233    }
234
235    impl SuspendableDriver for TestDriver {
236        async fn suspend(&self) {
237            self.suspend_called.store(true, Ordering::SeqCst);
238        }
239
240        async fn resume(&self) {
241            self.resume_called.store(true, Ordering::SeqCst);
242        }
243
244        fn suspend_enabled(&self) -> bool {
245            self.suspend_enabled
246        }
247    }
248
249    #[fuchsia::test]
250    async fn test_suspend_resume_with_runner() {
251        let (runner_client, runner_server) =
252            fidl::endpoints::create_endpoints::<fpower_broker::ElementRunnerMarker>();
253
254        let mut harness = TestHarness::<Suspendable<TestDriver>>::new().set_power_element_args(
255            fdf::PowerElementArgs { runner_server: Some(runner_server), ..Default::default() },
256        );
257
258        let driver_under_test = harness.start_driver().await.expect("Failed to start driver");
259        let (test_driver_stop_called, test_driver_resume_called, test_driver_suspend_called) = {
260            let suspendable = driver_under_test.get_driver().expect("Failed to get driver");
261            (
262                suspendable.driver.stop_called.clone(),
263                suspendable.driver.resume_called.clone(),
264                suspendable.driver.suspend_called.clone(),
265            )
266        };
267
268        let runner_proxy = runner_client.into_proxy();
269
270        // Level 1 should NOT trigger resume as it's the initial activation
271        runner_proxy.set_level(1).await.expect("Failed to set level");
272        assert!(!test_driver_resume_called.load(Ordering::SeqCst));
273
274        // Level 0 should trigger suspend
275        runner_proxy.set_level(0).await.expect("Failed to set level");
276        assert!(test_driver_suspend_called.load(Ordering::SeqCst));
277        test_driver_suspend_called.store(false, Ordering::SeqCst);
278
279        // Level 1 again should trigger resume
280        runner_proxy.set_level(1).await.expect("Failed to set level");
281        assert!(test_driver_resume_called.load(Ordering::SeqCst));
282
283        driver_under_test.stop_driver().await;
284        assert!(test_driver_stop_called.load(Ordering::SeqCst));
285    }
286}