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