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::{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#[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 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
72pub 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 pub fn new() -> Self {
96 Self::default()
97 }
98
99 #[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 pub fn update_proxy(&self, proxy: Arc<fodpm::OdpmDeviceSynchronousProxy>) {
126 *self.proxy.lock() = Some(proxy);
127 }
128
129 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 pub fn is_initialized(&self) -> bool {
138 self.rails.get().is_some()
139 }
140
141 fn rails(&self) -> &BTreeMap<u32, Arc<OdpmRail>> {
145 self.rails.wait()
146 }
147
148 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 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
416pub 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
430pub 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 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 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 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 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 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 = ¤t_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}