1#![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#[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 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 pub fn update_proxy(&self, proxy: Arc<fodpm::DeviceSynchronousProxy>) {
78 *self.proxy.lock() = proxy;
79 }
80}
81
82pub 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 pub fn new() -> Self {
105 Self::default()
106 }
107
108 #[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 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 pub fn is_initialized(&self) -> bool {
139 self.rails.get().is_some()
140 }
141
142 fn rails(&self) -> &BTreeMap<u32, Arc<OdpmRail>> {
146 self.rails.wait()
147 }
148
149 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 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
424pub 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
438pub 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 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 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 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 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 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 = ¤t_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}