| // Copyright 2026 The Fuchsia Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| //! Implementation of the On-Device Power Monitor (ODPM) module for Starnix. | |
| //! | |
| //! This module registers an Industrial I/O (IIO) device at `/sys/devices/platform/cpm/cpm:ODPM/iio:device0` | |
| //! with a symlink under `/sys/bus/iio/devices/iio:device0`, exposing power rail energy measurements | |
| //! from the Fuchsia ODPM FIDL service (`fuchsia.hardware.google.odpm`). | |
| #![recursion_limit = "256"] | |
| use fidl::endpoints::Proxy; | |
| use fidl_fuchsia_hardware_google_odpm as fodpm; | |
| use fuchsia_async as fasync; | |
| use fuchsia_component::client::Service; | |
| use futures::{FutureExt, TryStreamExt}; | |
| use starnix_core::fs::sysfs::get_sysfs; | |
| use starnix_core::task::{CurrentTask, Kernel}; | |
| use starnix_core::vfs::pseudo::dynamic_file::{DynamicFile, DynamicFileBuf, DynamicFileSource}; | |
| use starnix_core::vfs::pseudo::simple_directory::SimpleDirectoryMutator; | |
| use starnix_core::vfs::pseudo::simple_file::BytesFile; | |
| use starnix_core::vfs::{FsNodeOps, FsString}; | |
| use starnix_logging::{log_debug, log_error, log_warn}; | |
| use starnix_sync::{LockDepMutex, OdpmProxyLock, OdpmRailLastReadingLock}; | |
| use starnix_uapi::errors::Errno; | |
| use starnix_uapi::file_mode::mode; | |
| use std::collections::{BTreeMap, btree_map}; | |
| use std::fmt::Write as _; | |
| use std::sync::{Arc, OnceLock}; | |
| const NANOS_PER_MILLI: i64 = 1_000_000; | |
| const SERVICE_INITIAL_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5); | |
| const SYNC_PROXY_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(2); | |
| fn nanos_to_millis(nanos: i64) -> i64 { | |
| nanos / NANOS_PER_MILLI | |
| } | |
| #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] | |
| struct RailReading { | |
| timestamp_ms: i64, | |
| duration_ms: i64, | |
| energy_uj: i64, | |
| } | |
| /// A power rail monitored by ODPM. | |
| #[derive(Debug)] | |
| pub struct OdpmRail { | |
| name: FsString, | |
| channel_id: u32, | |
| schematic_name: FsString, | |
| last_reading: LockDepMutex<Option<RailReading>, OdpmRailLastReadingLock>, | |
| } | |
| impl OdpmRail { | |
| /// Creates a new [`OdpmRail`]. | |
| pub fn new( | |
| name: impl Into<FsString>, | |
| channel_id: u32, | |
| schematic_name: impl Into<FsString>, | |
| ) -> Self { | |
| Self { | |
| name: name.into(), | |
| channel_id, | |
| schematic_name: schematic_name.into(), | |
| last_reading: LockDepMutex::new(None), | |
| } | |
| } | |
| } | |
| /// An ODPM device aggregating power rails. | |
| pub struct OdpmDevice { | |
| rails: OnceLock<BTreeMap<u32, Arc<OdpmRail>>>, | |
| proxy: LockDepMutex<Option<Arc<fodpm::OdpmDeviceSynchronousProxy>>, OdpmProxyLock>, | |
| } | |
| impl std::fmt::Debug for OdpmDevice { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| f.debug_struct("OdpmDevice") | |
| .field("rails", &self.rails) | |
| .field("is_initialized", &self.rails.get().is_some()) | |
| .finish() | |
| } | |
| } | |
| impl Default for OdpmDevice { | |
| fn default() -> Self { | |
| Self { rails: OnceLock::new(), proxy: LockDepMutex::new(None) } | |
| } | |
| } | |
| impl OdpmDevice { | |
| /// Creates a new, uninitialized [`OdpmDevice`]. | |
| pub fn new() -> Self { | |
| Self::default() | |
| } | |
| /// Creates a new [`OdpmDevice`] with the given power rails and proxy. | |
| #[cfg(test)] | |
| pub fn new_with_rails( | |
| rails: Vec<OdpmRail>, | |
| proxy: Option<Arc<fodpm::OdpmDeviceSynchronousProxy>>, | |
| ) -> Self { | |
| let mut map = BTreeMap::new(); | |
| for rail in rails { | |
| match map.entry(rail.channel_id) { | |
| btree_map::Entry::Vacant(entry) => { | |
| entry.insert(Arc::new(rail)); | |
| } | |
| btree_map::Entry::Occupied(_) => { | |
| log_warn!( | |
| "ODPM rail with channel_id {} already exists; ignoring duplicate", | |
| rail.channel_id | |
| ); | |
| } | |
| } | |
| } | |
| let rails_lock = OnceLock::new(); | |
| let _ = rails_lock.set(map); | |
| Self { rails: rails_lock, proxy: LockDepMutex::new(proxy) } | |
| } | |
| /// Updates the synchronous proxy connection for this device (e.g. following a driver restart). | |
| pub fn update_proxy(&self, proxy: Arc<fodpm::OdpmDeviceSynchronousProxy>) { | |
| *self.proxy.lock() = Some(proxy); | |
| } | |
| /// Initializes this device with the discovered rails. | |
| pub fn init(&self, rails: BTreeMap<u32, Arc<OdpmRail>>) { | |
| if self.rails.set(rails).is_err() { | |
| log_warn!("ODPM rails are already initialized"); | |
| } | |
| } | |
| /// Returns whether the device has completed initial rail discovery. | |
| pub fn is_initialized(&self) -> bool { | |
| self.rails.get().is_some() | |
| } | |
| /// Returns a reference to the discovered rails map, waiting for initialization if needed. | |
| /// | |
| /// Blocks the current thread until initial rail discovery has completed. | |
| fn rails(&self) -> &BTreeMap<u32, Arc<OdpmRail>> { | |
| self.rails.wait() | |
| } | |
| /// Reads energy measurements across all rails and formats them for the `energy_value` sysfs file. | |
| /// | |
| /// Returns a formatted string containing the maximum timestamp across all rail samples, | |
| /// followed by each rail's channel ID, sample duration, schematic name, and cumulative energy. | |
| /// If reading fails, cached readings or default values are used. | |
| pub fn energy_value(&self) -> Result<FsString, Errno> { | |
| let rails = self.rails(); | |
| if rails.is_empty() { | |
| return Ok("t=0\n".into()); | |
| } | |
| let mut max_timestamp_ms = 0i64; | |
| let mut readings = Vec::with_capacity(rails.len()); | |
| let options = fodpm::Options { | |
| measurement_type: Some(fodpm::MeasurementType::Cumulative), | |
| ..Default::default() | |
| }; | |
| let proxy = self.proxy.lock().clone(); | |
| let fetched_readings = match proxy { | |
| Some(proxy) => match proxy | |
| .get_energy_joules(&options, zx::MonotonicInstant::after(SYNC_PROXY_TIMEOUT)) | |
| { | |
| Ok(Ok(readings)) => Some(readings), | |
| Ok(Err(status)) => { | |
| log_debug!("ODPM GetEnergyJoules failed: status {}", status); | |
| None | |
| } | |
| Err(e) => { | |
| log_debug!("ODPM GetEnergyJoules FIDL error: {:?}", e); | |
| None | |
| } | |
| }, | |
| None => None, | |
| }; | |
| for rail in rails.values() { | |
| let reading = match fetched_readings | |
| .as_ref() | |
| .and_then(|list| list.iter().find(|r| r.channel_id == Some(rail.channel_id))) | |
| { | |
| Some(reading) => { | |
| let timestamp_ms = reading.timestamp.map(nanos_to_millis).unwrap_or(0); | |
| let duration_ms = reading.interval.map(nanos_to_millis).unwrap_or(timestamp_ms); | |
| let energy_uj = reading.energy_uj.unwrap_or(0); | |
| let rail_reading = RailReading { timestamp_ms, duration_ms, energy_uj }; | |
| *rail.last_reading.lock() = Some(rail_reading); | |
| rail_reading | |
| } | |
| None => (*rail.last_reading.lock()).unwrap_or_default(), | |
| }; | |
| if max_timestamp_ms < reading.timestamp_ms { | |
| max_timestamp_ms = reading.timestamp_ms; | |
| } | |
| readings.push(( | |
| rail.channel_id, | |
| reading.duration_ms, | |
| &rail.schematic_name, | |
| reading.energy_uj, | |
| )); | |
| } | |
| let mut output = String::with_capacity(32 + readings.len() * 64); | |
| let _ = write!(output, "t={max_timestamp_ms}\n"); | |
| for (channel_id, duration_ms, schematic_name, energy_uj) in readings { | |
| let _ = | |
| write!(output, "CH{channel_id}(T={duration_ms})[{schematic_name}], {energy_uj}\n"); | |
| } | |
| Ok(output.into()) | |
| } | |
| /// Formats rail configuration for the `enabled_rails` sysfs file. | |
| pub fn enabled_rails(&self) -> FsString { | |
| let rails = self.rails(); | |
| let mut output = String::with_capacity(rails.len() * 40); | |
| for rail in rails.values() { | |
| let _ = | |
| write!(output, "CH{}[{}]:{}\n", rail.channel_id, rail.schematic_name, rail.name); | |
| } | |
| output.into() | |
| } | |
| } | |
| #[derive(Clone, Debug)] | |
| struct EnergyValueFile { | |
| device: Arc<OdpmDevice>, | |
| } | |
| impl EnergyValueFile { | |
| fn new_node(device: Arc<OdpmDevice>) -> impl FsNodeOps { | |
| DynamicFile::new_node(Self { device }) | |
| } | |
| } | |
| impl DynamicFileSource for EnergyValueFile { | |
| fn generate( | |
| &self, | |
| _current_task: &CurrentTask, | |
| sink: &mut DynamicFileBuf, | |
| ) -> Result<(), Errno> { | |
| let content = self.device.energy_value()?; | |
| sink.write(&content); | |
| Ok(()) | |
| } | |
| } | |
| #[derive(Clone, Debug)] | |
| struct EnabledRailsFile { | |
| device: Arc<OdpmDevice>, | |
| } | |
| impl EnabledRailsFile { | |
| fn new_node(device: Arc<OdpmDevice>) -> impl FsNodeOps { | |
| DynamicFile::new_node(Self { device }) | |
| } | |
| } | |
| impl DynamicFileSource for EnabledRailsFile { | |
| fn generate( | |
| &self, | |
| _current_task: &CurrentTask, | |
| sink: &mut DynamicFileBuf, | |
| ) -> Result<(), Errno> { | |
| let content = self.device.enabled_rails(); | |
| sink.write(&content); | |
| Ok(()) | |
| } | |
| } | |
| fn build_odpm_directory(dir: &SimpleDirectoryMutator, odpm_device: Arc<OdpmDevice>) { | |
| dir.entry("name", BytesFile::new_node(b"cpm:ODPM\n".to_vec()), mode!(IFREG, 0o444)); | |
| dir.entry( | |
| "enabled_rails", | |
| EnabledRailsFile::new_node(odpm_device.clone()), | |
| mode!(IFREG, 0o444), | |
| ); | |
| dir.entry("energy_value", EnergyValueFile::new_node(odpm_device), mode!(IFREG, 0o444)); | |
| } | |
| async fn watch_odpm_rails(odpm_device: Arc<OdpmDevice>) { | |
| let service = match Service::open(fodpm::ServiceMarker) { | |
| Ok(service) => service, | |
| Err(e) => { | |
| log_warn!("Failed to open ODPM service: {:?}", e); | |
| odpm_device.init(BTreeMap::new()); | |
| return; | |
| } | |
| }; | |
| let mut watcher = match service.watch().await { | |
| Ok(watcher) => watcher, | |
| Err(e) => { | |
| log_warn!("Failed to watch ODPM service: {:?}", e); | |
| odpm_device.init(BTreeMap::new()); | |
| return; | |
| } | |
| }; | |
| let mut timeout = std::pin::pin!( | |
| fasync::Timer::new(zx::MonotonicInstant::after(SERVICE_INITIAL_TIMEOUT)).fuse() | |
| ); | |
| loop { | |
| futures::select! { | |
| entry = watcher.try_next().fuse() => { | |
| match entry { | |
| Ok(Some(service_proxy)) => { | |
| let device_proxy = match service_proxy.connect_to_device() { | |
| Ok(proxy) => proxy, | |
| Err(e) => { | |
| log_warn!("Failed to connect to ODPM device: {:?}", e); | |
| continue; | |
| } | |
| }; | |
| match device_proxy.get_rail_metadata().await { | |
| Ok(Ok(metadata_list)) => { | |
| let client_end = match device_proxy.into_client_end() { | |
| Ok(client_end) => client_end, | |
| Err(_) => { | |
| log_warn!("Failed to get client end from ODPM device proxy"); | |
| continue; | |
| } | |
| }; | |
| let sync_proxy = Arc::new(fodpm::OdpmDeviceSynchronousProxy::new( | |
| client_end.into_channel(), | |
| )); | |
| odpm_device.update_proxy(sync_proxy); | |
| if !odpm_device.is_initialized() { | |
| let mut rails = BTreeMap::new(); | |
| for metadata in metadata_list { | |
| let name = match metadata.name { | |
| Some(name) => name, | |
| None => { | |
| log_warn!("ODPM rail missing name"); | |
| continue; | |
| } | |
| }; | |
| let channel_id = match metadata.channel_id { | |
| Some(channel_id) => channel_id, | |
| None => { | |
| log_warn!("ODPM rail '{name}' missing channel_id"); | |
| continue; | |
| } | |
| }; | |
| let schematic_name = match metadata.schematic_name { | |
| Some(schematic_name) => schematic_name, | |
| None => { | |
| log_warn!("ODPM rail '{name}' missing schematic_name"); | |
| continue; | |
| } | |
| }; | |
| match rails.entry(channel_id) { | |
| btree_map::Entry::Vacant(entry) => { | |
| entry.insert(Arc::new(OdpmRail::new( | |
| name, | |
| channel_id, | |
| schematic_name, | |
| ))); | |
| } | |
| btree_map::Entry::Occupied(_) => { | |
| log_warn!( | |
| "Duplicate ODPM rail '{name}' (channel_id {channel_id}); ignoring" | |
| ); | |
| } | |
| } | |
| } | |
| odpm_device.init(rails); | |
| timeout.set(futures::future::Fuse::terminated()); | |
| } else { | |
| log_debug!("Updated ODPM device proxy with new service instance"); | |
| } | |
| } | |
| Ok(Err(status)) => { | |
| log_warn!("ODPM GetRailMetadata failed with status: {:?}", status); | |
| } | |
| Err(e) => { | |
| log_warn!("ODPM GetRailMetadata FIDL error: {:?}", e); | |
| } | |
| } | |
| } | |
| Ok(None) => { | |
| log_error!("ODPM service watcher stream ended"); | |
| break; | |
| } | |
| Err(e) => { | |
| log_error!("ODPM service watcher error: {:?}", e); | |
| break; | |
| } | |
| } | |
| } | |
| _ = timeout => { | |
| log_warn!("ODPM service watcher timed out waiting for initial service"); | |
| odpm_device.init(BTreeMap::new()); | |
| break; | |
| } | |
| } | |
| } | |
| if !odpm_device.is_initialized() { | |
| odpm_device.init(BTreeMap::new()); | |
| } | |
| } | |
| /// Discovers ODPM rails and registers the ODPM sysfs device. | |
| pub fn odpm_device_init(kernel: &Kernel) { | |
| let odpm_device = Arc::new(OdpmDevice::new()); | |
| register_odpm_device(kernel, Arc::clone(&odpm_device)); | |
| let odpm_device_clone = Arc::clone(&odpm_device); | |
| kernel.kthreads.spawn_future( | |
| move || async move { | |
| watch_odpm_rails(odpm_device_clone).await; | |
| }, | |
| "odpm_device_watcher", | |
| ); | |
| } | |
| /// Registers the ODPM device within sysfs at `/sys/devices/platform/cpm/cpm:ODPM/iio:device0` | |
| /// and creates the symlink at `/sys/bus/iio/devices/iio:device0`. | |
| pub fn register_odpm_device(kernel: &Kernel, odpm_device: Arc<OdpmDevice>) { | |
| let fs = get_sysfs(kernel); | |
| let root = SimpleDirectoryMutator::new(fs, kernel.device_registry.objects.root.clone()); | |
| // 1. Create the canonical platform device hierarchy: | |
| // /sys/devices/platform/cpm/cpm:ODPM/iio:device0 | |
| root.subdir("devices", 0o755, |devices_dir| { | |
| devices_dir.subdir("platform", 0o755, |platform_dir| { | |
| platform_dir.subdir("cpm", 0o755, |cpm_dir| { | |
| cpm_dir.subdir("cpm:ODPM", 0o755, |odpm_dir| { | |
| odpm_dir.subdir("iio:device0", 0o755, |iio_device_dir| { | |
| build_odpm_directory(iio_device_dir, odpm_device); | |
| iio_device_dir.entry( | |
| "uevent", | |
| BytesFile::new_node( | |
| b"DEVPATH=/devices/platform/cpm/cpm:ODPM/iio:device0\nSUBSYSTEM=iio\n".to_vec(), | |
| ), | |
| mode!(IFREG, 0o644), | |
| ); | |
| iio_device_dir.symlink( | |
| "subsystem".into(), | |
| "../../../../../bus/iio".into(), | |
| ); | |
| }); | |
| }); | |
| }); | |
| }); | |
| }); | |
| // 2. Create the IIO bus symlink: | |
| // /sys/bus/iio/devices/iio:device0 -> ../../../devices/platform/cpm/cpm:ODPM/iio:device0 | |
| root.subdir("bus", 0o755, |bus_dir| { | |
| bus_dir.subdir("iio", 0o755, |iio_dir| { | |
| iio_dir.subdir("devices", 0o755, |devices_dir| { | |
| devices_dir.symlink( | |
| "iio:device0".into(), | |
| "../../../devices/platform/cpm/cpm:ODPM/iio:device0".into(), | |
| ); | |
| }); | |
| }); | |
| }); | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use futures::StreamExt; | |
| use starnix_core::testing::spawn_kernel_and_run; | |
| fn millis_to_nanos(millis: i64) -> i64 { | |
| millis * NANOS_PER_MILLI | |
| } | |
| fn spawn_fake_odpm_server( | |
| readings: Vec<fodpm::EnergyReading>, | |
| ) -> Arc<fodpm::OdpmDeviceSynchronousProxy> { | |
| let (client_end, server_end) = | |
| fidl::endpoints::create_endpoints::<fodpm::OdpmDeviceMarker>(); | |
| let proxy = fodpm::OdpmDeviceSynchronousProxy::new(client_end.into_channel()); | |
| std::thread::spawn(move || { | |
| let mut executor = fuchsia_async::LocalExecutorBuilder::new().build(); | |
| let mut stream = server_end.into_stream(); | |
| executor.run_singlethreaded(async move { | |
| while let Some(Ok(request)) = stream.next().await { | |
| if let fodpm::OdpmDeviceRequest::GetEnergyJoules { payload: _, responder } = | |
| request | |
| { | |
| let _ = responder.send(Ok(&readings)); | |
| } | |
| } | |
| }); | |
| }); | |
| Arc::new(proxy) | |
| } | |
| #[::fuchsia::test] | |
| async fn test_odpm_energy_value_output() { | |
| let proxy = spawn_fake_odpm_server(vec![ | |
| fodpm::EnergyReading { | |
| channel_id: Some(0), | |
| energy_uj: Some(51899976), | |
| interval: Some(millis_to_nanos(1279315)), | |
| timestamp: Some(millis_to_nanos(1279315)), | |
| ..Default::default() | |
| }, | |
| fodpm::EnergyReading { | |
| channel_id: Some(1), | |
| energy_uj: Some(185487825), | |
| interval: Some(millis_to_nanos(1279315)), | |
| timestamp: Some(millis_to_nanos(1279315)), | |
| ..Default::default() | |
| }, | |
| ]); | |
| let odpm_device = OdpmDevice::new_with_rails( | |
| vec![OdpmRail::new("amb", 0, "S1M_VDD_AMB"), OdpmRail::new("cpu2", 1, "S2M_VDD_CPU2")], | |
| Some(proxy), | |
| ); | |
| let energy_val = odpm_device.energy_value().expect("energy_value failed"); | |
| assert_eq!( | |
| energy_val, | |
| "t=1279315\nCH0(T=1279315)[S1M_VDD_AMB], 51899976\nCH1(T=1279315)[S2M_VDD_CPU2], 185487825\n" | |
| ); | |
| } | |
| #[::fuchsia::test] | |
| async fn test_odpm_energy_value_preserves_reading_on_error() { | |
| let (client_end, server_end) = | |
| fidl::endpoints::create_endpoints::<fodpm::OdpmDeviceMarker>(); | |
| let proxy = Arc::new(fodpm::OdpmDeviceSynchronousProxy::new(client_end.into_channel())); | |
| std::thread::spawn(move || { | |
| let mut executor = fuchsia_async::LocalExecutorBuilder::new().build(); | |
| let mut stream = server_end.into_stream(); | |
| executor.run_singlethreaded(async move { | |
| let mut first_call = true; | |
| while let Some(Ok(request)) = stream.next().await { | |
| match request { | |
| fodpm::OdpmDeviceRequest::GetEnergyJoules { payload: _, responder } => { | |
| if first_call { | |
| first_call = false; | |
| let readings = [fodpm::EnergyReading { | |
| channel_id: Some(0), | |
| energy_uj: Some(100), | |
| interval: Some(millis_to_nanos(1000)), | |
| timestamp: Some(millis_to_nanos(1000)), | |
| ..Default::default() | |
| }]; | |
| let _ = responder.send(Ok(&readings)); | |
| } else { | |
| // Second call returns an error. | |
| let _ = responder.send(Err(zx::sys::ZX_ERR_INTERNAL)); | |
| } | |
| } | |
| _ => {} | |
| } | |
| } | |
| }); | |
| }); | |
| let odpm_device = | |
| OdpmDevice::new_with_rails(vec![OdpmRail::new("amb", 0, "S1M_VDD_AMB")], Some(proxy)); | |
| // First read succeeds. | |
| let energy_val1 = odpm_device.energy_value().expect("first read should succeed"); | |
| assert_eq!(energy_val1, "t=1000\nCH0(T=1000)[S1M_VDD_AMB], 100\n"); | |
| // Second read encounters error, but reuses prior reading. | |
| let energy_val2 = | |
| odpm_device.energy_value().expect("second read should preserve prior reading"); | |
| assert_eq!(energy_val2, "t=1000\nCH0(T=1000)[S1M_VDD_AMB], 100\n"); | |
| } | |
| #[::fuchsia::test] | |
| async fn test_odpm_enabled_rails() { | |
| let odpm_device = OdpmDevice::new_with_rails( | |
| vec![OdpmRail::new("cpu2", 1, "S2M_VDD_CPU2"), OdpmRail::new("amb", 0, "S1M_VDD_AMB")], | |
| None, | |
| ); | |
| assert_eq!(odpm_device.enabled_rails(), "CH0[S1M_VDD_AMB]:amb\nCH1[S2M_VDD_CPU2]:cpu2\n"); | |
| } | |
| #[::fuchsia::test] | |
| async fn test_odpm_duplicate_rail_ignored() { | |
| let odpm_device = OdpmDevice::new_with_rails( | |
| vec![ | |
| OdpmRail::new("cpu2", 1, "S2M_VDD_CPU2"), | |
| OdpmRail::new("cpu2_dup", 1, "S2M_VDD_CPU2_DUP"), | |
| ], | |
| None, | |
| ); | |
| assert_eq!(odpm_device.enabled_rails(), "CH1[S2M_VDD_CPU2]:cpu2\n"); | |
| } | |
| #[::fuchsia::test] | |
| async fn test_odpm_device_update_proxy() { | |
| let proxy1 = spawn_fake_odpm_server(vec![fodpm::EnergyReading { | |
| channel_id: Some(0), | |
| energy_uj: Some(100), | |
| interval: Some(millis_to_nanos(1000)), | |
| timestamp: Some(millis_to_nanos(1000)), | |
| ..Default::default() | |
| }]); | |
| let proxy2 = spawn_fake_odpm_server(vec![fodpm::EnergyReading { | |
| channel_id: Some(0), | |
| energy_uj: Some(200), | |
| interval: Some(millis_to_nanos(2000)), | |
| timestamp: Some(millis_to_nanos(2000)), | |
| ..Default::default() | |
| }]); | |
| let odpm_device = | |
| OdpmDevice::new_with_rails(vec![OdpmRail::new("amb", 0, "S1M_VDD_AMB")], Some(proxy1)); | |
| let val1 = odpm_device.energy_value().expect("read with proxy1"); | |
| assert_eq!(val1, "t=1000\nCH0(T=1000)[S1M_VDD_AMB], 100\n"); | |
| odpm_device.update_proxy(proxy2); | |
| let val2 = odpm_device.energy_value().expect("read with proxy2"); | |
| assert_eq!(val2, "t=2000\nCH0(T=2000)[S1M_VDD_AMB], 200\n"); | |
| } | |
| #[::fuchsia::test] | |
| async fn test_odpm_energy_value_defaults_to_zero_on_initial_failure() { | |
| let (client_end, server_end) = | |
| fidl::endpoints::create_endpoints::<fodpm::OdpmDeviceMarker>(); | |
| let proxy = Arc::new(fodpm::OdpmDeviceSynchronousProxy::new(client_end.into_channel())); | |
| std::thread::spawn(move || { | |
| let mut executor = fuchsia_async::LocalExecutorBuilder::new().build(); | |
| let mut stream = server_end.into_stream(); | |
| executor.run_singlethreaded(async move { | |
| while let Some(Ok(request)) = stream.next().await { | |
| match request { | |
| fodpm::OdpmDeviceRequest::GetEnergyJoules { payload: _, responder } => { | |
| let _ = responder.send(Err(zx::sys::ZX_ERR_INTERNAL)); | |
| } | |
| _ => {} | |
| } | |
| } | |
| }); | |
| }); | |
| let odpm_device = | |
| OdpmDevice::new_with_rails(vec![OdpmRail::new("amb", 0, "S1M_VDD_AMB")], Some(proxy)); | |
| let energy_val = | |
| odpm_device.energy_value().expect("energy_value should succeed with default 0"); | |
| assert_eq!(energy_val, "t=0\nCH0(T=0)[S1M_VDD_AMB], 0\n"); | |
| } | |
| #[::fuchsia::test] | |
| async fn test_odpm_energy_value_empty_rails_skips_rpc() { | |
| let (client_end, server_end) = | |
| fidl::endpoints::create_endpoints::<fodpm::OdpmDeviceMarker>(); | |
| let proxy = Arc::new(fodpm::OdpmDeviceSynchronousProxy::new(client_end.into_channel())); | |
| let server_thread = std::thread::spawn(move || { | |
| let mut executor = fuchsia_async::LocalExecutorBuilder::new().build(); | |
| let mut stream = server_end.into_stream(); | |
| executor.run_singlethreaded(async move { | |
| if let Some(request) = stream.next().await { | |
| panic!("Unexpected RPC when rails are empty: {:?}", request); | |
| } | |
| }); | |
| }); | |
| let odpm_device = OdpmDevice::new_with_rails(vec![], Some(proxy)); | |
| let energy_val = odpm_device.energy_value().expect("energy_value should succeed"); | |
| assert_eq!(energy_val, "t=0\n"); | |
| drop(odpm_device); | |
| server_thread.join().expect("server thread should finish cleanly"); | |
| } | |
| #[::fuchsia::test] | |
| async fn test_register_odpm_sysfs_device() { | |
| spawn_kernel_and_run(async |current_task| { | |
| let odpm_device = Arc::new(OdpmDevice::new_with_rails(vec![], None)); | |
| register_odpm_device(current_task.kernel(), odpm_device); | |
| let root = ¤t_task.kernel().device_registry.objects.root; | |
| assert!(root.lookup("bus/iio/devices/iio:device0".into()).is_some()); | |
| assert!(root.lookup("devices/platform/cpm/cpm:ODPM/iio:device0/name".into()).is_some()); | |
| assert!( | |
| root.lookup("devices/platform/cpm/cpm:ODPM/iio:device0/enabled_rails".into()) | |
| .is_some() | |
| ); | |
| assert!( | |
| root.lookup("devices/platform/cpm/cpm:ODPM/iio:device0/energy_value".into()) | |
| .is_some() | |
| ); | |
| assert!( | |
| root.lookup("devices/platform/cpm/cpm:ODPM/iio:device0/uevent".into()).is_some() | |
| ); | |
| assert!( | |
| root.lookup("devices/platform/cpm/cpm:ODPM/iio:device0/subsystem".into()).is_some() | |
| ); | |
| }) | |
| .await; | |
| } | |
| #[::fuchsia::test] | |
| async fn test_nanos_to_millis() { | |
| assert_eq!(nanos_to_millis(1_000_000), 1); | |
| assert_eq!(nanos_to_millis(2_500_000), 2); | |
| assert_eq!(nanos_to_millis(0), 0); | |
| assert_eq!(millis_to_nanos(5), 5_000_000); | |
| } | |
| } |