Skip to main content

starnix_modules_odpm/
lib.rs

1// Copyright 2026 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//! Implementation of the On-Device Power Monitor (ODPM) module for Starnix.
6//!
7//! This module registers an Industrial I/O (IIO) device at `/sys/devices/platform/cpm/cpm:ODPM/iio:device0`
8//! with a symlink under `/sys/bus/iio/devices/iio:device0`, exposing power rail energy measurements
9//! from the Fuchsia ODPM FIDL service (`fuchsia.hardware.google.odpm`).
10
11#![recursion_limit = "256"]
12
13use fidl::endpoints::Proxy;
14use fidl_fuchsia_hardware_google_odpm as fodpm;
15use fuchsia_async as fasync;
16use fuchsia_component::client::Service;
17use futures::{FutureExt, TryStreamExt};
18use starnix_core::fs::sysfs::get_sysfs;
19use starnix_core::task::{CurrentTask, Kernel};
20use starnix_core::vfs::pseudo::dynamic_file::{DynamicFile, DynamicFileBuf, DynamicFileSource};
21use starnix_core::vfs::pseudo::simple_directory::SimpleDirectoryMutator;
22use starnix_core::vfs::pseudo::simple_file::BytesFile;
23use starnix_core::vfs::{FsNodeOps, FsString};
24use starnix_logging::{log_debug, log_error, log_warn};
25use starnix_sync::{LockDepMutex, OdpmProxyLock, OdpmRailLastReadingLock};
26use starnix_uapi::errors::Errno;
27use starnix_uapi::file_mode::mode;
28use std::collections::{BTreeMap, btree_map};
29use std::fmt::Write as _;
30use std::sync::{Arc, OnceLock};
31
32const NANOS_PER_MILLI: i64 = 1_000_000;
33const SERVICE_INITIAL_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
34const SYNC_PROXY_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(2);
35
36fn nanos_to_millis(nanos: i64) -> i64 {
37    nanos / NANOS_PER_MILLI
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
41struct RailReading {
42    timestamp_ms: i64,
43    duration_ms: i64,
44    energy_uj: i64,
45}
46
47/// A power rail monitored by ODPM.
48#[derive(Debug)]
49pub struct OdpmRail {
50    name: FsString,
51    channel_id: u32,
52    schematic_name: FsString,
53    last_reading: LockDepMutex<Option<RailReading>, OdpmRailLastReadingLock>,
54}
55
56impl OdpmRail {
57    /// Creates a new [`OdpmRail`].
58    pub fn new(
59        name: impl Into<FsString>,
60        channel_id: u32,
61        schematic_name: impl Into<FsString>,
62    ) -> Self {
63        Self {
64            name: name.into(),
65            channel_id,
66            schematic_name: schematic_name.into(),
67            last_reading: LockDepMutex::new(None),
68        }
69    }
70}
71
72/// An ODPM device aggregating power rails.
73pub struct OdpmDevice {
74    rails: OnceLock<BTreeMap<u32, Arc<OdpmRail>>>,
75    proxy: LockDepMutex<Option<Arc<fodpm::OdpmDeviceSynchronousProxy>>, OdpmProxyLock>,
76}
77
78impl std::fmt::Debug for OdpmDevice {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.debug_struct("OdpmDevice")
81            .field("rails", &self.rails)
82            .field("is_initialized", &self.rails.get().is_some())
83            .finish()
84    }
85}
86
87impl Default for OdpmDevice {
88    fn default() -> Self {
89        Self { rails: OnceLock::new(), proxy: LockDepMutex::new(None) }
90    }
91}
92
93impl OdpmDevice {
94    /// Creates a new, uninitialized [`OdpmDevice`].
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Creates a new [`OdpmDevice`] with the given power rails and proxy.
100    #[cfg(test)]
101    pub fn new_with_rails(
102        rails: Vec<OdpmRail>,
103        proxy: Option<Arc<fodpm::OdpmDeviceSynchronousProxy>>,
104    ) -> Self {
105        let mut map = BTreeMap::new();
106        for rail in rails {
107            match map.entry(rail.channel_id) {
108                btree_map::Entry::Vacant(entry) => {
109                    entry.insert(Arc::new(rail));
110                }
111                btree_map::Entry::Occupied(_) => {
112                    log_warn!(
113                        "ODPM rail with channel_id {} already exists; ignoring duplicate",
114                        rail.channel_id
115                    );
116                }
117            }
118        }
119        let rails_lock = OnceLock::new();
120        let _ = rails_lock.set(map);
121        Self { rails: rails_lock, proxy: LockDepMutex::new(proxy) }
122    }
123
124    /// Updates the synchronous proxy connection for this device (e.g. following a driver restart).
125    pub fn update_proxy(&self, proxy: Arc<fodpm::OdpmDeviceSynchronousProxy>) {
126        *self.proxy.lock() = Some(proxy);
127    }
128
129    /// Initializes this device with the discovered rails.
130    pub fn init(&self, rails: BTreeMap<u32, Arc<OdpmRail>>) {
131        if self.rails.set(rails).is_err() {
132            log_warn!("ODPM rails are already initialized");
133        }
134    }
135
136    /// Returns whether the device has completed initial rail discovery.
137    pub fn is_initialized(&self) -> bool {
138        self.rails.get().is_some()
139    }
140
141    /// Returns a reference to the discovered rails map, waiting for initialization if needed.
142    ///
143    /// Blocks the current thread until initial rail discovery has completed.
144    fn rails(&self) -> &BTreeMap<u32, Arc<OdpmRail>> {
145        self.rails.wait()
146    }
147
148    /// Reads energy measurements across all rails and formats them for the `energy_value` sysfs file.
149    ///
150    /// Returns a formatted string containing the maximum timestamp across all rail samples,
151    /// followed by each rail's channel ID, sample duration, schematic name, and cumulative energy.
152    /// If reading fails, cached readings or default values are used.
153    pub fn energy_value(&self) -> Result<FsString, Errno> {
154        let rails = self.rails();
155        if rails.is_empty() {
156            return Ok("t=0\n".into());
157        }
158
159        let mut max_timestamp_ms = 0i64;
160        let mut readings = Vec::with_capacity(rails.len());
161
162        let options = fodpm::Options {
163            measurement_type: Some(fodpm::MeasurementType::Cumulative),
164            ..Default::default()
165        };
166
167        let proxy = self.proxy.lock().clone();
168        let fetched_readings = match proxy {
169            Some(proxy) => match proxy
170                .get_energy_joules(&options, zx::MonotonicInstant::after(SYNC_PROXY_TIMEOUT))
171            {
172                Ok(Ok(readings)) => Some(readings),
173                Ok(Err(status)) => {
174                    log_debug!("ODPM GetEnergyJoules failed: status {}", status);
175                    None
176                }
177                Err(e) => {
178                    log_debug!("ODPM GetEnergyJoules FIDL error: {:?}", e);
179                    None
180                }
181            },
182            None => None,
183        };
184
185        for rail in rails.values() {
186            let reading = match fetched_readings
187                .as_ref()
188                .and_then(|list| list.iter().find(|r| r.channel_id == Some(rail.channel_id)))
189            {
190                Some(reading) => {
191                    let timestamp_ms = reading.timestamp.map(nanos_to_millis).unwrap_or(0);
192                    let duration_ms = reading.interval.map(nanos_to_millis).unwrap_or(timestamp_ms);
193                    let energy_uj = reading.energy_uj.unwrap_or(0);
194                    let rail_reading = RailReading { timestamp_ms, duration_ms, energy_uj };
195                    *rail.last_reading.lock() = Some(rail_reading);
196                    rail_reading
197                }
198                None => (*rail.last_reading.lock()).unwrap_or_default(),
199            };
200
201            if max_timestamp_ms < reading.timestamp_ms {
202                max_timestamp_ms = reading.timestamp_ms;
203            }
204            readings.push((
205                rail.channel_id,
206                reading.duration_ms,
207                &rail.schematic_name,
208                reading.energy_uj,
209            ));
210        }
211
212        let mut output = String::with_capacity(32 + readings.len() * 64);
213        let _ = write!(output, "t={max_timestamp_ms}\n");
214        for (channel_id, duration_ms, schematic_name, energy_uj) in readings {
215            let _ =
216                write!(output, "CH{channel_id}(T={duration_ms})[{schematic_name}], {energy_uj}\n");
217        }
218
219        Ok(output.into())
220    }
221
222    /// Formats rail configuration for the `enabled_rails` sysfs file.
223    pub fn enabled_rails(&self) -> FsString {
224        let rails = self.rails();
225        let mut output = String::with_capacity(rails.len() * 40);
226        for rail in rails.values() {
227            let _ =
228                write!(output, "CH{}[{}]:{}\n", rail.channel_id, rail.schematic_name, rail.name);
229        }
230        output.into()
231    }
232}
233
234#[derive(Clone, Debug)]
235struct EnergyValueFile {
236    device: Arc<OdpmDevice>,
237}
238
239impl EnergyValueFile {
240    fn new_node(device: Arc<OdpmDevice>) -> impl FsNodeOps {
241        DynamicFile::new_node(Self { device })
242    }
243}
244
245impl DynamicFileSource for EnergyValueFile {
246    fn generate(
247        &self,
248        _current_task: &CurrentTask,
249        sink: &mut DynamicFileBuf,
250    ) -> Result<(), Errno> {
251        let content = self.device.energy_value()?;
252        sink.write(&content);
253        Ok(())
254    }
255}
256
257#[derive(Clone, Debug)]
258struct EnabledRailsFile {
259    device: Arc<OdpmDevice>,
260}
261
262impl EnabledRailsFile {
263    fn new_node(device: Arc<OdpmDevice>) -> impl FsNodeOps {
264        DynamicFile::new_node(Self { device })
265    }
266}
267
268impl DynamicFileSource for EnabledRailsFile {
269    fn generate(
270        &self,
271        _current_task: &CurrentTask,
272        sink: &mut DynamicFileBuf,
273    ) -> Result<(), Errno> {
274        let content = self.device.enabled_rails();
275        sink.write(&content);
276        Ok(())
277    }
278}
279
280fn build_odpm_directory(dir: &SimpleDirectoryMutator, odpm_device: Arc<OdpmDevice>) {
281    dir.entry("name", BytesFile::new_node(b"cpm:ODPM\n".to_vec()), mode!(IFREG, 0o444));
282    dir.entry(
283        "enabled_rails",
284        EnabledRailsFile::new_node(odpm_device.clone()),
285        mode!(IFREG, 0o444),
286    );
287    dir.entry("energy_value", EnergyValueFile::new_node(odpm_device), mode!(IFREG, 0o444));
288}
289
290async fn watch_odpm_rails(odpm_device: Arc<OdpmDevice>) {
291    let service = match Service::open(fodpm::ServiceMarker) {
292        Ok(service) => service,
293        Err(e) => {
294            log_warn!("Failed to open ODPM service: {:?}", e);
295            odpm_device.init(BTreeMap::new());
296            return;
297        }
298    };
299    let mut watcher = match service.watch().await {
300        Ok(watcher) => watcher,
301        Err(e) => {
302            log_warn!("Failed to watch ODPM service: {:?}", e);
303            odpm_device.init(BTreeMap::new());
304            return;
305        }
306    };
307
308    let mut timeout = std::pin::pin!(
309        fasync::Timer::new(zx::MonotonicInstant::after(SERVICE_INITIAL_TIMEOUT)).fuse()
310    );
311
312    loop {
313        futures::select! {
314            entry = watcher.try_next().fuse() => {
315                match entry {
316                    Ok(Some(service_proxy)) => {
317                        let device_proxy = match service_proxy.connect_to_device() {
318                            Ok(proxy) => proxy,
319                            Err(e) => {
320                                log_warn!("Failed to connect to ODPM device: {:?}", e);
321                                continue;
322                            }
323                        };
324
325                        match device_proxy.get_rail_metadata().await {
326                            Ok(Ok(metadata_list)) => {
327                                let client_end = match device_proxy.into_client_end() {
328                                    Ok(client_end) => client_end,
329                                    Err(_) => {
330                                        log_warn!("Failed to get client end from ODPM device proxy");
331                                        continue;
332                                    }
333                                };
334                                let sync_proxy = Arc::new(fodpm::OdpmDeviceSynchronousProxy::new(
335                                    client_end.into_channel(),
336                                ));
337                                odpm_device.update_proxy(sync_proxy);
338
339                                if !odpm_device.is_initialized() {
340                                    let mut rails = BTreeMap::new();
341                                    for metadata in metadata_list {
342                                        let name = match metadata.name {
343                                            Some(name) => name,
344                                            None => {
345                                                log_warn!("ODPM rail missing name");
346                                                continue;
347                                            }
348                                        };
349                                        let channel_id = match metadata.channel_id {
350                                            Some(channel_id) => channel_id,
351                                            None => {
352                                                log_warn!("ODPM rail '{name}' missing channel_id");
353                                                continue;
354                                            }
355                                        };
356                                        let schematic_name = match metadata.schematic_name {
357                                            Some(schematic_name) => schematic_name,
358                                            None => {
359                                                log_warn!("ODPM rail '{name}' missing schematic_name");
360                                                continue;
361                                            }
362                                        };
363
364                                        match rails.entry(channel_id) {
365                                            btree_map::Entry::Vacant(entry) => {
366                                                entry.insert(Arc::new(OdpmRail::new(
367                                                    name,
368                                                    channel_id,
369                                                    schematic_name,
370                                                )));
371                                            }
372                                            btree_map::Entry::Occupied(_) => {
373                                                log_warn!(
374                                                    "Duplicate ODPM rail '{name}' (channel_id {channel_id}); ignoring"
375                                                );
376                                            }
377                                        }
378                                    }
379                                    odpm_device.init(rails);
380                                    timeout.set(futures::future::Fuse::terminated());
381                                } else {
382                                    log_debug!("Updated ODPM device proxy with new service instance");
383                                }
384                            }
385                            Ok(Err(status)) => {
386                                log_warn!("ODPM GetRailMetadata failed with status: {:?}", status);
387                            }
388                            Err(e) => {
389                                log_warn!("ODPM GetRailMetadata FIDL error: {:?}", e);
390                            }
391                        }
392                    }
393                    Ok(None) => {
394                        log_error!("ODPM service watcher stream ended");
395                        break;
396                    }
397                    Err(e) => {
398                        log_error!("ODPM service watcher error: {:?}", e);
399                        break;
400                    }
401                }
402            }
403            _ = timeout => {
404                log_warn!("ODPM service watcher timed out waiting for initial service");
405                odpm_device.init(BTreeMap::new());
406                break;
407            }
408        }
409    }
410
411    if !odpm_device.is_initialized() {
412        odpm_device.init(BTreeMap::new());
413    }
414}
415
416/// Discovers ODPM rails and registers the ODPM sysfs device.
417pub fn odpm_device_init(kernel: &Kernel) {
418    let odpm_device = Arc::new(OdpmDevice::new());
419    register_odpm_device(kernel, Arc::clone(&odpm_device));
420
421    let odpm_device_clone = Arc::clone(&odpm_device);
422    kernel.kthreads.spawn_future(
423        move || async move {
424            watch_odpm_rails(odpm_device_clone).await;
425        },
426        "odpm_device_watcher",
427    );
428}
429
430/// Registers the ODPM device within sysfs at `/sys/devices/platform/cpm/cpm:ODPM/iio:device0`
431/// and creates the symlink at `/sys/bus/iio/devices/iio:device0`.
432pub fn register_odpm_device(kernel: &Kernel, odpm_device: Arc<OdpmDevice>) {
433    let fs = get_sysfs(kernel);
434    let root = SimpleDirectoryMutator::new(fs, kernel.device_registry.objects.root.clone());
435
436    // 1. Create the canonical platform device hierarchy:
437    //    /sys/devices/platform/cpm/cpm:ODPM/iio:device0
438    root.subdir("devices", 0o755, |devices_dir| {
439        devices_dir.subdir("platform", 0o755, |platform_dir| {
440            platform_dir.subdir("cpm", 0o755, |cpm_dir| {
441                cpm_dir.subdir("cpm:ODPM", 0o755, |odpm_dir| {
442                    odpm_dir.subdir("iio:device0", 0o755, |iio_device_dir| {
443                        build_odpm_directory(iio_device_dir, odpm_device);
444                        iio_device_dir.entry(
445                            "uevent",
446                            BytesFile::new_node(
447                                b"DEVPATH=/devices/platform/cpm/cpm:ODPM/iio:device0\nSUBSYSTEM=iio\n".to_vec(),
448                            ),
449                            mode!(IFREG, 0o644),
450                        );
451                        iio_device_dir.symlink(
452                            "subsystem".into(),
453                            "../../../../../bus/iio".into(),
454                        );
455                    });
456                });
457            });
458        });
459    });
460
461    // 2. Create the IIO bus symlink:
462    //    /sys/bus/iio/devices/iio:device0 -> ../../../devices/platform/cpm/cpm:ODPM/iio:device0
463    root.subdir("bus", 0o755, |bus_dir| {
464        bus_dir.subdir("iio", 0o755, |iio_dir| {
465            iio_dir.subdir("devices", 0o755, |devices_dir| {
466                devices_dir.symlink(
467                    "iio:device0".into(),
468                    "../../../devices/platform/cpm/cpm:ODPM/iio:device0".into(),
469                );
470            });
471        });
472    });
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use futures::StreamExt;
479    use starnix_core::testing::spawn_kernel_and_run;
480
481    fn millis_to_nanos(millis: i64) -> i64 {
482        millis * NANOS_PER_MILLI
483    }
484
485    fn spawn_fake_odpm_server(
486        readings: Vec<fodpm::EnergyReading>,
487    ) -> Arc<fodpm::OdpmDeviceSynchronousProxy> {
488        let (client_end, server_end) =
489            fidl::endpoints::create_endpoints::<fodpm::OdpmDeviceMarker>();
490        let proxy = fodpm::OdpmDeviceSynchronousProxy::new(client_end.into_channel());
491        std::thread::spawn(move || {
492            let mut executor = fuchsia_async::LocalExecutorBuilder::new().build();
493            let mut stream = server_end.into_stream();
494            executor.run_singlethreaded(async move {
495                while let Some(Ok(request)) = stream.next().await {
496                    if let fodpm::OdpmDeviceRequest::GetEnergyJoules { payload: _, responder } =
497                        request
498                    {
499                        let _ = responder.send(Ok(&readings));
500                    }
501                }
502            });
503        });
504        Arc::new(proxy)
505    }
506
507    #[::fuchsia::test]
508    async fn test_odpm_energy_value_output() {
509        let proxy = spawn_fake_odpm_server(vec![
510            fodpm::EnergyReading {
511                channel_id: Some(0),
512                energy_uj: Some(51899976),
513                interval: Some(millis_to_nanos(1279315)),
514                timestamp: Some(millis_to_nanos(1279315)),
515                ..Default::default()
516            },
517            fodpm::EnergyReading {
518                channel_id: Some(1),
519                energy_uj: Some(185487825),
520                interval: Some(millis_to_nanos(1279315)),
521                timestamp: Some(millis_to_nanos(1279315)),
522                ..Default::default()
523            },
524        ]);
525
526        let odpm_device = OdpmDevice::new_with_rails(
527            vec![OdpmRail::new("amb", 0, "S1M_VDD_AMB"), OdpmRail::new("cpu2", 1, "S2M_VDD_CPU2")],
528            Some(proxy),
529        );
530
531        let energy_val = odpm_device.energy_value().expect("energy_value failed");
532        assert_eq!(
533            energy_val,
534            "t=1279315\nCH0(T=1279315)[S1M_VDD_AMB], 51899976\nCH1(T=1279315)[S2M_VDD_CPU2], 185487825\n"
535        );
536    }
537
538    #[::fuchsia::test]
539    async fn test_odpm_energy_value_preserves_reading_on_error() {
540        let (client_end, server_end) =
541            fidl::endpoints::create_endpoints::<fodpm::OdpmDeviceMarker>();
542        let proxy = Arc::new(fodpm::OdpmDeviceSynchronousProxy::new(client_end.into_channel()));
543        std::thread::spawn(move || {
544            let mut executor = fuchsia_async::LocalExecutorBuilder::new().build();
545            let mut stream = server_end.into_stream();
546            executor.run_singlethreaded(async move {
547                let mut first_call = true;
548                while let Some(Ok(request)) = stream.next().await {
549                    match request {
550                        fodpm::OdpmDeviceRequest::GetEnergyJoules { payload: _, responder } => {
551                            if first_call {
552                                first_call = false;
553                                let readings = [fodpm::EnergyReading {
554                                    channel_id: Some(0),
555                                    energy_uj: Some(100),
556                                    interval: Some(millis_to_nanos(1000)),
557                                    timestamp: Some(millis_to_nanos(1000)),
558                                    ..Default::default()
559                                }];
560                                let _ = responder.send(Ok(&readings));
561                            } else {
562                                // Second call returns an error.
563                                let _ = responder.send(Err(zx::sys::ZX_ERR_INTERNAL));
564                            }
565                        }
566                        _ => {}
567                    }
568                }
569            });
570        });
571
572        let odpm_device =
573            OdpmDevice::new_with_rails(vec![OdpmRail::new("amb", 0, "S1M_VDD_AMB")], Some(proxy));
574
575        // First read succeeds.
576        let energy_val1 = odpm_device.energy_value().expect("first read should succeed");
577        assert_eq!(energy_val1, "t=1000\nCH0(T=1000)[S1M_VDD_AMB], 100\n");
578
579        // Second read encounters error, but reuses prior reading.
580        let energy_val2 =
581            odpm_device.energy_value().expect("second read should preserve prior reading");
582        assert_eq!(energy_val2, "t=1000\nCH0(T=1000)[S1M_VDD_AMB], 100\n");
583    }
584
585    #[::fuchsia::test]
586    async fn test_odpm_enabled_rails() {
587        let odpm_device = OdpmDevice::new_with_rails(
588            vec![OdpmRail::new("cpu2", 1, "S2M_VDD_CPU2"), OdpmRail::new("amb", 0, "S1M_VDD_AMB")],
589            None,
590        );
591
592        assert_eq!(odpm_device.enabled_rails(), "CH0[S1M_VDD_AMB]:amb\nCH1[S2M_VDD_CPU2]:cpu2\n");
593    }
594
595    #[::fuchsia::test]
596    async fn test_odpm_duplicate_rail_ignored() {
597        let odpm_device = OdpmDevice::new_with_rails(
598            vec![
599                OdpmRail::new("cpu2", 1, "S2M_VDD_CPU2"),
600                OdpmRail::new("cpu2_dup", 1, "S2M_VDD_CPU2_DUP"),
601            ],
602            None,
603        );
604        assert_eq!(odpm_device.enabled_rails(), "CH1[S2M_VDD_CPU2]:cpu2\n");
605    }
606
607    #[::fuchsia::test]
608    async fn test_odpm_device_update_proxy() {
609        let proxy1 = spawn_fake_odpm_server(vec![fodpm::EnergyReading {
610            channel_id: Some(0),
611            energy_uj: Some(100),
612            interval: Some(millis_to_nanos(1000)),
613            timestamp: Some(millis_to_nanos(1000)),
614            ..Default::default()
615        }]);
616        let proxy2 = spawn_fake_odpm_server(vec![fodpm::EnergyReading {
617            channel_id: Some(0),
618            energy_uj: Some(200),
619            interval: Some(millis_to_nanos(2000)),
620            timestamp: Some(millis_to_nanos(2000)),
621            ..Default::default()
622        }]);
623
624        let odpm_device =
625            OdpmDevice::new_with_rails(vec![OdpmRail::new("amb", 0, "S1M_VDD_AMB")], Some(proxy1));
626
627        let val1 = odpm_device.energy_value().expect("read with proxy1");
628        assert_eq!(val1, "t=1000\nCH0(T=1000)[S1M_VDD_AMB], 100\n");
629
630        odpm_device.update_proxy(proxy2);
631
632        let val2 = odpm_device.energy_value().expect("read with proxy2");
633        assert_eq!(val2, "t=2000\nCH0(T=2000)[S1M_VDD_AMB], 200\n");
634    }
635
636    #[::fuchsia::test]
637    async fn test_odpm_energy_value_defaults_to_zero_on_initial_failure() {
638        let (client_end, server_end) =
639            fidl::endpoints::create_endpoints::<fodpm::OdpmDeviceMarker>();
640        let proxy = Arc::new(fodpm::OdpmDeviceSynchronousProxy::new(client_end.into_channel()));
641        std::thread::spawn(move || {
642            let mut executor = fuchsia_async::LocalExecutorBuilder::new().build();
643            let mut stream = server_end.into_stream();
644            executor.run_singlethreaded(async move {
645                while let Some(Ok(request)) = stream.next().await {
646                    match request {
647                        fodpm::OdpmDeviceRequest::GetEnergyJoules { payload: _, responder } => {
648                            let _ = responder.send(Err(zx::sys::ZX_ERR_INTERNAL));
649                        }
650                        _ => {}
651                    }
652                }
653            });
654        });
655
656        let odpm_device =
657            OdpmDevice::new_with_rails(vec![OdpmRail::new("amb", 0, "S1M_VDD_AMB")], Some(proxy));
658
659        let energy_val =
660            odpm_device.energy_value().expect("energy_value should succeed with default 0");
661        assert_eq!(energy_val, "t=0\nCH0(T=0)[S1M_VDD_AMB], 0\n");
662    }
663
664    #[::fuchsia::test]
665    async fn test_odpm_energy_value_empty_rails_skips_rpc() {
666        let (client_end, server_end) =
667            fidl::endpoints::create_endpoints::<fodpm::OdpmDeviceMarker>();
668        let proxy = Arc::new(fodpm::OdpmDeviceSynchronousProxy::new(client_end.into_channel()));
669        let server_thread = std::thread::spawn(move || {
670            let mut executor = fuchsia_async::LocalExecutorBuilder::new().build();
671            let mut stream = server_end.into_stream();
672            executor.run_singlethreaded(async move {
673                if let Some(request) = stream.next().await {
674                    panic!("Unexpected RPC when rails are empty: {:?}", request);
675                }
676            });
677        });
678
679        let odpm_device = OdpmDevice::new_with_rails(vec![], Some(proxy));
680        let energy_val = odpm_device.energy_value().expect("energy_value should succeed");
681        assert_eq!(energy_val, "t=0\n");
682
683        drop(odpm_device);
684        server_thread.join().expect("server thread should finish cleanly");
685    }
686
687    #[::fuchsia::test]
688    async fn test_register_odpm_sysfs_device() {
689        spawn_kernel_and_run(async |current_task| {
690            let odpm_device = Arc::new(OdpmDevice::new_with_rails(vec![], None));
691
692            register_odpm_device(current_task.kernel(), odpm_device);
693
694            let root = &current_task.kernel().device_registry.objects.root;
695            assert!(root.lookup("bus/iio/devices/iio:device0".into()).is_some());
696            assert!(root.lookup("devices/platform/cpm/cpm:ODPM/iio:device0/name".into()).is_some());
697            assert!(
698                root.lookup("devices/platform/cpm/cpm:ODPM/iio:device0/enabled_rails".into())
699                    .is_some()
700            );
701            assert!(
702                root.lookup("devices/platform/cpm/cpm:ODPM/iio:device0/energy_value".into())
703                    .is_some()
704            );
705            assert!(
706                root.lookup("devices/platform/cpm/cpm:ODPM/iio:device0/uevent".into()).is_some()
707            );
708            assert!(
709                root.lookup("devices/platform/cpm/cpm:ODPM/iio:device0/subsystem".into()).is_some()
710            );
711        })
712        .await;
713    }
714
715    #[::fuchsia::test]
716    async fn test_nanos_to_millis() {
717        assert_eq!(nanos_to_millis(1_000_000), 1);
718        assert_eq!(nanos_to_millis(2_500_000), 2);
719        assert_eq!(nanos_to_millis(0), 0);
720        assert_eq!(millis_to_nanos(5), 5_000_000);
721    }
722}