Skip to main content

starnix_modules_thermal/
lib.rs

1// Copyright 2025 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#![recursion_limit = "256"]
6
7mod cooling;
8mod family;
9mod powercap;
10mod thermal_zone;
11
12use crate::thermal_zone::{SensorProps, ThermalZone};
13use anyhow::{Error, anyhow, bail};
14use cooling::cooling_device_init;
15use family::ThermalFamily;
16use fidl_fuchsia_hardware_temperature as ftemperature;
17use fidl_fuchsia_hardware_trippoint as ftrippoint;
18use fidl_fuchsia_thermal as fthermal;
19use powercap::powercap_device_init;
20use starnix_core::device::kobject::Device;
21use starnix_core::fs::sysfs::build_device_directory;
22use starnix_core::task::{ArgNameAndValue, CurrentTask, Kernel};
23use starnix_core::vfs::FsNodeOps;
24use starnix_core::vfs::pseudo::simple_directory::SimpleDirectoryMutator;
25use starnix_core::vfs::pseudo::simple_file::{BytesFile, BytesFileOps};
26use starnix_logging::{log_error, log_warn};
27
28use starnix_sync::{LockDepMutex, ThermalTripPointStateLock};
29use starnix_uapi::errors::{Errno, errno, error};
30use starnix_uapi::file_mode::mode;
31use std::borrow::Cow;
32use std::collections::HashMap;
33use std::sync::Arc;
34use thermal_netlink::{celsius_to_millicelsius, millicelsius_to_celsius};
35use zx::MonotonicInstant;
36
37fn build_thermal_zone_directory(
38    device: &Device,
39    proxy: ftemperature::DeviceSynchronousProxy,
40    trippoint_proxy: ftrippoint::TripPointSynchronousProxy,
41    sensor_manager: fthermal::SensorManagerSynchronousProxy,
42    device_type: String,
43    dir: &SimpleDirectoryMutator,
44) {
45    build_device_directory(device, dir);
46    let trip_point = Arc::new(ThermalZoneTripPoint::new(device_type.clone(), trippoint_proxy));
47    dir.entry(
48        "emul_temp",
49        EmulTempFile::new_node(device_type.clone(), sensor_manager),
50        mode!(IFREG, 0o200),
51    );
52    dir.entry("temp", TemperatureFile::new_node(proxy), mode!(IFREG, 0o664));
53    dir.entry(
54        "trip_point_0_temp",
55        TripPointTempFile::new_node(trip_point.clone()),
56        mode!(IFREG, 0o664),
57    );
58    dir.entry("trip_point_0_hyst", TripPointHystFile::new_node(trip_point), mode!(IFREG, 0o664));
59    dir.entry(
60        "type",
61        BytesFile::new_node(format!("{}\n", device_type).into_bytes()),
62        mode!(IFREG, 0o444),
63    );
64    dir.entry("policy", BytesFile::new_node(b"step_wise\n".to_vec()), mode!(IFREG, 0o444));
65    dir.entry(
66        "available_policies",
67        BytesFile::new_node(b"step_wise\n".to_vec()),
68        mode!(IFREG, 0o444),
69    );
70}
71
72struct TemperatureFile {
73    proxy: ftemperature::DeviceSynchronousProxy,
74}
75
76impl TemperatureFile {
77    pub fn new_node(proxy: ftemperature::DeviceSynchronousProxy) -> impl FsNodeOps {
78        BytesFile::new_node(Self { proxy })
79    }
80}
81
82impl BytesFileOps for TemperatureFile {
83    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
84        let (zx_status, temp) =
85            self.proxy.get_temperature_celsius(MonotonicInstant::INFINITE).map_err(|e| {
86                log_error!("get_temperature_celsius failed: {}", e);
87                errno!(ENOENT)
88            })?;
89        let _ = zx::Status::ok(zx_status).map_err(|e| {
90            log_error!("get_temperature_celsius driver returned error: {}", e);
91            errno!(ENOENT)
92        })?;
93
94        let out = format!("{}\n", celsius_to_millicelsius(temp) as i32);
95        Ok(out.as_bytes().to_owned().into())
96    }
97}
98
99fn parse_i32_sysfs_input(data: &[u8], attr_name: &str) -> Result<i32, Errno> {
100    let num_str = str::from_utf8(data).map_err(|e| {
101        log_warn!("Failed to convert {} input to utf-8: {:?}", attr_name, e);
102        errno!(EINVAL)
103    })?;
104    num_str.trim().parse().map_err(|e| {
105        log_warn!("Failed to parse {} input as i32: {:?}", attr_name, e);
106        errno!(EINVAL)
107    })
108}
109
110struct EmulTempFile {
111    device_type: String,
112    proxy: fthermal::SensorManagerSynchronousProxy,
113}
114
115impl EmulTempFile {
116    pub fn new_node(
117        device_type: String,
118        proxy: fthermal::SensorManagerSynchronousProxy,
119    ) -> impl FsNodeOps {
120        BytesFile::new_node(Self { device_type, proxy })
121    }
122}
123
124impl BytesFileOps for EmulTempFile {
125    fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
126        let temp_milli_c = parse_i32_sysfs_input(&data, "emul_temp")?;
127
128        if temp_milli_c == 0 {
129            match self
130                .proxy
131                .clear_temperature_override(&self.device_type, zx::MonotonicInstant::INFINITE)
132            {
133                Ok(Ok(_)) => Ok(()),
134                Ok(Err(error)) => {
135                    log_warn!(
136                        "Failed to clear temperature override for sensor {}: {:?}",
137                        self.device_type,
138                        error
139                    );
140                    error!(EINVAL)
141                }
142                Err(error) => {
143                    log_warn!(
144                        "Failed to call clear_temperature_override for sensor {}: {:?}",
145                        self.device_type,
146                        error
147                    );
148                    error!(EIO)
149                }
150            }
151        } else {
152            match self.proxy.set_temperature_override(
153                &self.device_type,
154                millicelsius_to_celsius(temp_milli_c as f32).into(),
155                zx::MonotonicInstant::INFINITE,
156            ) {
157                Ok(Ok(_)) => Ok(()),
158                Ok(Err(error)) => {
159                    log_warn!(
160                        "Failed to set temperature override for sensor {}: {:?}",
161                        self.device_type,
162                        error
163                    );
164                    error!(EINVAL)
165                }
166                Err(error) => {
167                    log_warn!(
168                        "Failed to call set_temperature_override for sensor {}: {:?}",
169                        self.device_type,
170                        error
171                    );
172                    error!(EIO)
173                }
174            }
175        }
176    }
177}
178
179const TRIP_POINT_FIDL_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(5);
180
181fn trip_point_fidl_deadline() -> MonotonicInstant {
182    MonotonicInstant::after(TRIP_POINT_FIDL_TIMEOUT)
183}
184
185fn map_trip_point_fidl_error(err: &fidl::Error) -> Errno {
186    match err {
187        fidl::Error::ClientCall(zx::Status::TIMED_OUT)
188        | fidl::Error::ClientRead(fidl::TransportError::Status(zx::Status::TIMED_OUT))
189        | fidl::Error::ClientWrite(fidl::TransportError::Status(zx::Status::TIMED_OUT)) => {
190            errno!(ETIMEDOUT)
191        }
192        _ => errno!(EIO),
193    }
194}
195
196/// Selects a programmable trip point descriptor of `target_type` from `descriptors`.
197///
198/// Selects the last matching descriptor of `target_type` (the programmable warning slot rather
199/// than a lower-index hardware fault threshold, regardless of whether the warning slot is
200/// currently cleared or already armed).
201fn select_descriptor_index(
202    descriptors: &[ftrippoint::TripPointDescriptor],
203    target_type: ftrippoint::TripPointType,
204) -> Option<u32> {
205    descriptors.iter().rfind(|d| d.type_ == target_type).map(|d| d.index)
206}
207
208#[derive(Default)]
209struct TripPointState {
210    temp_mc: i32,
211    hyst_mc: i32,
212    cached_indices: Option<(u32, Option<u32>)>,
213}
214
215struct ThermalZoneTripPoint {
216    device_type: String,
217    proxy: ftrippoint::TripPointSynchronousProxy,
218    state: LockDepMutex<TripPointState, ThermalTripPointStateLock>,
219}
220
221impl ThermalZoneTripPoint {
222    fn new(device_type: String, proxy: ftrippoint::TripPointSynchronousProxy) -> Self {
223        Self { device_type, proxy, state: LockDepMutex::new(TripPointState::default()) }
224    }
225
226    fn temp_mc(&self) -> i32 {
227        self.state.lock().temp_mc
228    }
229
230    fn set_temp_mc(&self, temp_mc: i32) -> Result<(), Errno> {
231        let mut state = self.state.lock();
232        let hyst_mc = state.hyst_mc;
233        self.apply_trip_points(&mut state, (temp_mc != 0).then_some(temp_mc), hyst_mc)?;
234        state.temp_mc = temp_mc;
235        Ok(())
236    }
237
238    fn hyst_mc(&self) -> i32 {
239        self.state.lock().hyst_mc
240    }
241
242    fn set_hyst_mc(&self, hyst_mc: i32) -> Result<(), Errno> {
243        if hyst_mc < 0 {
244            return error!(EINVAL);
245        }
246
247        let mut state = self.state.lock();
248        let temp_mc = state.temp_mc;
249        if temp_mc != 0 {
250            self.apply_trip_points(&mut state, Some(temp_mc), hyst_mc)?;
251        } else {
252            // Resolving descriptor indices when `temp_mc == 0` ensures writes to
253            // `trip_point_0_hyst` fail fast (`EIO`) on sensors that do not support
254            // `fuchsia.hardware.trippoint` and caches the indices for subsequent
255            // `trip_point_0_temp` writes.
256            let _ = self.resolve_descriptor_indices(&mut state)?;
257        }
258        state.hyst_mc = hyst_mc;
259        Ok(())
260    }
261
262    fn resolve_descriptor_indices(
263        &self,
264        state: &mut TripPointState,
265    ) -> Result<(u32, Option<u32>), Errno> {
266        if let Some(indices) = state.cached_indices {
267            return Ok(indices);
268        }
269
270        let descriptors = match self.proxy.get_trip_point_descriptors(trip_point_fidl_deadline()) {
271            Ok(Ok(descriptors)) => descriptors,
272            Ok(Err(status)) => {
273                log_warn!(
274                    "get_trip_point_descriptors returned status {} for sensor {}",
275                    zx::Status::err_from_raw(status),
276                    self.device_type
277                );
278                return error!(EIO);
279            }
280            Err(err) => {
281                log_warn!(
282                    "Failed to call get_trip_point_descriptors for sensor {}: {:?}",
283                    self.device_type,
284                    err
285                );
286                return Err(map_trip_point_fidl_error(&err));
287            }
288        };
289
290        let Some(high_index) =
291            select_descriptor_index(&descriptors, ftrippoint::TripPointType::OneshotTempAbove)
292        else {
293            log_warn!(
294                "No ONESHOT_TEMP_ABOVE trip point descriptor found for sensor {}",
295                self.device_type
296            );
297            return error!(EIO);
298        };
299        let low_index =
300            select_descriptor_index(&descriptors, ftrippoint::TripPointType::OneshotTempBelow);
301
302        let indices = (high_index, low_index);
303        state.cached_indices = Some(indices);
304        Ok(indices)
305    }
306
307    fn apply_trip_points(
308        &self,
309        state: &mut TripPointState,
310        temp_mc: Option<i32>,
311        hyst_mc: i32,
312    ) -> Result<(), Errno> {
313        let (high_index, low_index) = self.resolve_descriptor_indices(state)?;
314
315        let mut updates = Vec::with_capacity(2);
316        if let Some(temp_mc) = temp_mc {
317            let high_temp_c = millicelsius_to_celsius(temp_mc as f32);
318            updates.push(ftrippoint::TripPointDescriptor {
319                index: high_index,
320                type_: ftrippoint::TripPointType::OneshotTempAbove,
321                configuration: ftrippoint::TripPointValue::OneshotTempAboveTripPoint(
322                    ftrippoint::OneshotTempAboveTripPoint {
323                        critical_temperature_celsius: high_temp_c,
324                    },
325                ),
326            });
327            // Programming the static `[temp - hyst, temp]` window (`OneshotTempAbove` at `temp`
328            // and `OneshotTempBelow` at `temp - hyst`) together is intentional: Starnix acts as a
329            // stateless sysfs configuration bridge (without running an internal
330            // `WaitForAnyTripPoint` state machine loop) so that hardware/firmware trip-point
331            // controllers receive both the upper trip threshold and lower hysteresis clear
332            // threshold when userspace configures `trip_point_0_{temp,hyst}`.
333            if let Some(low_index) = low_index {
334                let low_temp_mc = temp_mc.saturating_sub(hyst_mc);
335                let low_temp_c = millicelsius_to_celsius(low_temp_mc as f32);
336                updates.push(ftrippoint::TripPointDescriptor {
337                    index: low_index,
338                    type_: ftrippoint::TripPointType::OneshotTempBelow,
339                    configuration: ftrippoint::TripPointValue::OneshotTempBelowTripPoint(
340                        ftrippoint::OneshotTempBelowTripPoint {
341                            critical_temperature_celsius: low_temp_c,
342                        },
343                    ),
344                });
345            }
346        } else {
347            updates.push(ftrippoint::TripPointDescriptor {
348                index: high_index,
349                type_: ftrippoint::TripPointType::OneshotTempAbove,
350                configuration: ftrippoint::TripPointValue::ClearedTripPoint(
351                    ftrippoint::ClearedTripPoint,
352                ),
353            });
354            if let Some(low_index) = low_index {
355                updates.push(ftrippoint::TripPointDescriptor {
356                    index: low_index,
357                    type_: ftrippoint::TripPointType::OneshotTempBelow,
358                    configuration: ftrippoint::TripPointValue::ClearedTripPoint(
359                        ftrippoint::ClearedTripPoint,
360                    ),
361                });
362            }
363        }
364
365        match self.proxy.set_trip_points(&updates, trip_point_fidl_deadline()) {
366            Ok(Ok(())) => Ok(()),
367            Ok(Err(raw_status)) => {
368                let status = zx::Status::err_from_raw(raw_status);
369                log_warn!(
370                    "set_trip_points returned status {} for sensor {}",
371                    status,
372                    self.device_type
373                );
374                match status {
375                    zx::Status::INVALID_ARGS | zx::Status::OUT_OF_RANGE => error!(EINVAL),
376                    _ => error!(EIO),
377                }
378            }
379            Err(err) => {
380                log_warn!(
381                    "Failed to call set_trip_points for sensor {}: {:?}",
382                    self.device_type,
383                    err
384                );
385                Err(map_trip_point_fidl_error(&err))
386            }
387        }
388    }
389}
390
391struct TripPointTempFile {
392    trip_point: Arc<ThermalZoneTripPoint>,
393}
394
395impl TripPointTempFile {
396    pub fn new_node(trip_point: Arc<ThermalZoneTripPoint>) -> impl FsNodeOps {
397        BytesFile::new_node(Self { trip_point })
398    }
399}
400
401impl BytesFileOps for TripPointTempFile {
402    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
403        let temp_mc = self.trip_point.temp_mc();
404        Ok(format!("{}\n", temp_mc).into_bytes().into())
405    }
406
407    fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
408        let temp_mc = parse_i32_sysfs_input(&data, "trip_point_0_temp")?;
409        self.trip_point.set_temp_mc(temp_mc)
410    }
411}
412
413struct TripPointHystFile {
414    trip_point: Arc<ThermalZoneTripPoint>,
415}
416
417impl TripPointHystFile {
418    pub fn new_node(trip_point: Arc<ThermalZoneTripPoint>) -> impl FsNodeOps {
419        BytesFile::new_node(Self { trip_point })
420    }
421}
422
423impl BytesFileOps for TripPointHystFile {
424    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
425        let hyst_mc = self.trip_point.hyst_mc();
426        Ok(format!("{}\n", hyst_mc).into_bytes().into())
427    }
428
429    fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
430        let hyst_mc = parse_i32_sysfs_input(&data, "trip_point_0_hyst")?;
431        self.trip_point.set_hyst_mc(hyst_mc)
432    }
433}
434
435fn thermal_device_init(kernel: &Kernel) -> Result<(), Error> {
436    let sensor_manager =
437        fuchsia_component::client::connect_to_protocol_sync::<fthermal::SensorManagerMarker>()
438            .map_err(|error| anyhow!("Failed to connect to SensorManager: {:?}", error))?;
439
440    let sensors = sensor_manager.list_sensors(zx::MonotonicInstant::INFINITE)?;
441
442    let registry = &kernel.device_registry;
443    let thermal_class = registry.objects.thermal_class();
444    let mut sensor_proxies = HashMap::new();
445
446    for (thermal_zone_id, sensor_info) in sensors.into_iter().enumerate() {
447        let Some(sensor_name) = sensor_info.name else {
448            log_warn!("No sensor name for thermal zone {}, skipping.", thermal_zone_id);
449            continue;
450        };
451        let sensor_name_clone = sensor_name.clone();
452
453        let thermal_zone_id = thermal_zone_id as u32;
454        let thermal_zone = format!("thermal_zone{}", thermal_zone_id);
455
456        // Create a synchronous proxy for file requests.
457        // Reads and writes are expected to block until data or an error is returned.
458        let (sensor_sync, sensor_server_sync) = fidl::endpoints::create_sync_proxy();
459
460        match sensor_manager.connect(
461            fthermal::SensorManagerConnectRequest {
462                name: Some(sensor_name.clone()),
463                server_end: Some(fthermal::SensorServer_::Temperature(sensor_server_sync)),
464                ..Default::default()
465            },
466            zx::MonotonicInstant::INFINITE,
467        ) {
468            Ok(Ok(())) => {}
469            Ok(Err(connect_err)) => {
470                log_error!("Failed to connect to sensor {} (sync): {:?}", sensor_name, connect_err);
471                continue;
472            }
473            Err(fidl_err) => {
474                log_error!(
475                    "Failed to call connect for sensor {} (sync): {:?}",
476                    sensor_name,
477                    fidl_err
478                );
479                continue;
480            }
481        }
482
483        let (trippoint_sync, trippoint_server_sync) = fidl::endpoints::create_sync_proxy();
484
485        match sensor_manager.connect(
486            fthermal::SensorManagerConnectRequest {
487                name: Some(sensor_name.clone()),
488                server_end: Some(fthermal::SensorServer_::Trippoint(trippoint_server_sync)),
489                ..Default::default()
490            },
491            zx::MonotonicInstant::INFINITE,
492        ) {
493            Ok(Ok(())) => {}
494            Ok(Err(connect_err)) => {
495                log_warn!(
496                    "Failed to connect to sensor {} trip point server (sync): {:?}",
497                    sensor_name,
498                    connect_err
499                );
500            }
501            Err(fidl_err) => {
502                log_warn!(
503                    "Failed to call connect for sensor {} trip point server (sync): {:?}",
504                    sensor_name,
505                    fidl_err
506                );
507            }
508        }
509
510        registry.add_numberless_device(
511            thermal_zone.as_str().into(),
512            /* parent = */ None,
513            thermal_class.clone(),
514            move |device, dir| {
515                match fuchsia_component::client::connect_to_protocol_sync::<fthermal::SensorManagerMarker>() {
516                    Ok(sensor_manager) => build_thermal_zone_directory(device, sensor_sync, trippoint_sync, sensor_manager, sensor_name_clone, dir),
517                    Err(error) => log_warn!("Failed to connect to SensorManager when building thermal zone for sensor {}: {:?}", sensor_name_clone, error),
518                }
519            },
520        );
521
522        // Create an asynchronous proxy for netlink calls.
523        // The thermal netlink server periodically gets temperature data from the thermal sensors in
524        // the background.
525        let (sensor, sensor_server) = fidl::endpoints::create_proxy();
526
527        match sensor_manager.connect(
528            fthermal::SensorManagerConnectRequest {
529                name: Some(sensor_name.clone()),
530                server_end: Some(fthermal::SensorServer_::Temperature(sensor_server)),
531                ..Default::default()
532            },
533            zx::MonotonicInstant::INFINITE,
534        ) {
535            Ok(Ok(())) => {}
536            Ok(Err(connect_err)) => {
537                log_error!(
538                    "Failed to connect to sensor {} (async): {:?}",
539                    sensor_name,
540                    connect_err
541                );
542                continue;
543            }
544            Err(fidl_err) => {
545                log_error!(
546                    "Failed to call connect for sensor {} (async): {:?}",
547                    sensor_name,
548                    fidl_err
549                );
550                continue;
551            }
552        }
553
554        sensor_proxies.insert(
555            SensorProps { name: sensor_name },
556            ThermalZone { id: thermal_zone_id, proxy: sensor },
557        );
558    }
559
560    let (thermal_family, thermal_family_worker) = ThermalFamily::new(sensor_proxies);
561    kernel.generic_netlink().add_family(Arc::new(thermal_family));
562    kernel
563        .kthreads
564        .spawn_future(move || async move { thermal_family_worker.await }, "thermal_netlink_worker");
565
566    Ok(())
567}
568
569fn parse_args(args: &[&str]) -> Result<Vec<String>, Error> {
570    let mut cooling_devices = Vec::new();
571    for &arg in args {
572        match ArgNameAndValue::from(arg) {
573            ArgNameAndValue { name: "cooling", value: Some(cooling) } if !cooling.is_empty() => {
574                cooling_devices = cooling.split(',').map(String::from).collect();
575            }
576            _ => bail!("invalid thermal argument: {arg}"),
577        }
578    }
579    Ok(cooling_devices)
580}
581
582/// Validates `thermal` module configuration `args`.
583pub fn validate_args(args: &[&str]) -> Result<(), Error> {
584    parse_args(args).map(|_| ())
585}
586
587/// Initializes thermal zones, powercap zones, and optional cooling devices from module
588/// configuration `args`.
589pub fn module_init(kernel: &Kernel, args: &[&str]) -> Result<(), Error> {
590    let cooling_devices = parse_args(args)?;
591    powercap_device_init(kernel);
592    thermal_device_init(kernel)?;
593    cooling_device_init(kernel, cooling_devices)
594}
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599    use fuchsia_async as fasync;
600    use futures::TryStreamExt;
601    use std::sync::Mutex;
602    use std::sync::atomic::{AtomicUsize, Ordering};
603
604    #[fuchsia::test]
605    fn test_select_descriptor_index() {
606        let descriptors = vec![
607            ftrippoint::TripPointDescriptor {
608                index: 0,
609                type_: ftrippoint::TripPointType::OneshotTempAbove,
610                configuration: ftrippoint::TripPointValue::OneshotTempAboveTripPoint(
611                    ftrippoint::OneshotTempAboveTripPoint { critical_temperature_celsius: 95.0 },
612                ),
613            },
614            ftrippoint::TripPointDescriptor {
615                index: 1,
616                type_: ftrippoint::TripPointType::OneshotTempAbove,
617                configuration: ftrippoint::TripPointValue::ClearedTripPoint(
618                    ftrippoint::ClearedTripPoint,
619                ),
620            },
621            ftrippoint::TripPointDescriptor {
622                index: 2,
623                type_: ftrippoint::TripPointType::OneshotTempAbove,
624                configuration: ftrippoint::TripPointValue::ClearedTripPoint(
625                    ftrippoint::ClearedTripPoint,
626                ),
627            },
628            ftrippoint::TripPointDescriptor {
629                index: 3,
630                type_: ftrippoint::TripPointType::OneshotTempBelow,
631                configuration: ftrippoint::TripPointValue::OneshotTempBelowTripPoint(
632                    ftrippoint::OneshotTempBelowTripPoint { critical_temperature_celsius: 80.0 },
633                ),
634            },
635            ftrippoint::TripPointDescriptor {
636                index: 4,
637                type_: ftrippoint::TripPointType::OneshotTempBelow,
638                configuration: ftrippoint::TripPointValue::OneshotTempBelowTripPoint(
639                    ftrippoint::OneshotTempBelowTripPoint { critical_temperature_celsius: 70.0 },
640                ),
641            },
642        ];
643
644        // 1. Selects the last matching descriptor of `target_type` (`rfind`).
645        assert_eq!(
646            select_descriptor_index(&descriptors, ftrippoint::TripPointType::OneshotTempAbove),
647            Some(2)
648        );
649        assert_eq!(
650            select_descriptor_index(&descriptors[..2], ftrippoint::TripPointType::OneshotTempAbove),
651            Some(1)
652        );
653        assert_eq!(
654            select_descriptor_index(&descriptors, ftrippoint::TripPointType::OneshotTempBelow),
655            Some(4)
656        );
657
658        // 2. Startup layout where a lower-index hardware fault descriptor (`index 0`) and the
659        // programmable warning high/low descriptors (`index 1`, `index 2`) all start as
660        // `ClearedTripPoint`, or where `index 1` is already armed while `index 0` is cleared:
661        // `OneshotTempAbove` always selects `Some(1)` and `OneshotTempBelow` selects `Some(2)`.
662        let all_cleared_startup = vec![
663            ftrippoint::TripPointDescriptor {
664                index: 0,
665                type_: ftrippoint::TripPointType::OneshotTempAbove,
666                configuration: ftrippoint::TripPointValue::ClearedTripPoint(
667                    ftrippoint::ClearedTripPoint,
668                ),
669            },
670            ftrippoint::TripPointDescriptor {
671                index: 1,
672                type_: ftrippoint::TripPointType::OneshotTempAbove,
673                configuration: ftrippoint::TripPointValue::ClearedTripPoint(
674                    ftrippoint::ClearedTripPoint,
675                ),
676            },
677            ftrippoint::TripPointDescriptor {
678                index: 2,
679                type_: ftrippoint::TripPointType::OneshotTempBelow,
680                configuration: ftrippoint::TripPointValue::ClearedTripPoint(
681                    ftrippoint::ClearedTripPoint,
682                ),
683            },
684        ];
685        assert_eq!(
686            select_descriptor_index(
687                &all_cleared_startup,
688                ftrippoint::TripPointType::OneshotTempAbove
689            ),
690            Some(1)
691        );
692        assert_eq!(
693            select_descriptor_index(
694                &all_cleared_startup,
695                ftrippoint::TripPointType::OneshotTempBelow
696            ),
697            Some(2)
698        );
699
700        let warning_already_armed = vec![
701            ftrippoint::TripPointDescriptor {
702                index: 0,
703                type_: ftrippoint::TripPointType::OneshotTempAbove,
704                configuration: ftrippoint::TripPointValue::ClearedTripPoint(
705                    ftrippoint::ClearedTripPoint,
706                ),
707            },
708            ftrippoint::TripPointDescriptor {
709                index: 1,
710                type_: ftrippoint::TripPointType::OneshotTempAbove,
711                configuration: ftrippoint::TripPointValue::OneshotTempAboveTripPoint(
712                    ftrippoint::OneshotTempAboveTripPoint { critical_temperature_celsius: 85.0 },
713                ),
714            },
715        ];
716        assert_eq!(
717            select_descriptor_index(
718                &warning_already_armed,
719                ftrippoint::TripPointType::OneshotTempAbove
720            ),
721            Some(1)
722        );
723
724        // 3. Returns `None` when no descriptor matches `target_type`.
725        assert_eq!(
726            select_descriptor_index(&descriptors[..3], ftrippoint::TripPointType::OneshotTempBelow),
727            None
728        );
729        assert_eq!(select_descriptor_index(&[], ftrippoint::TripPointType::OneshotTempAbove), None);
730    }
731
732    #[fuchsia::test]
733    async fn test_thermal_zone_trip_point_caching_and_errors() {
734        let scope = fasync::Scope::new();
735        let (client_end, mut stream) =
736            fidl::endpoints::create_request_stream::<ftrippoint::TripPointMarker>();
737        let get_descriptors_calls = Arc::new(AtomicUsize::new(0));
738        let get_descriptors_calls_clone = get_descriptors_calls.clone();
739
740        // Single-descriptor (`OneshotTempAbove`-only) sensor where `SetTripPoints` rejects
741        // temperatures > 120.0°C with `ZX_ERR_INVALID_ARGS`, temperatures <= -273.15°C with
742        // `ZX_ERR_OUT_OF_RANGE`, and temperatures == 100.0°C with `ZX_ERR_INTERNAL`.
743        scope.spawn(async move {
744            while let Ok(Some(req)) = stream.try_next().await {
745                match req {
746                    ftrippoint::TripPointRequest::GetTripPointDescriptors { responder } => {
747                        get_descriptors_calls_clone.fetch_add(1, Ordering::SeqCst);
748                        let _ = responder.send(Ok(&[ftrippoint::TripPointDescriptor {
749                            index: 1,
750                            type_: ftrippoint::TripPointType::OneshotTempAbove,
751                            configuration: ftrippoint::TripPointValue::ClearedTripPoint(
752                                ftrippoint::ClearedTripPoint,
753                            ),
754                        }]));
755                    }
756                    ftrippoint::TripPointRequest::SetTripPoints { descriptors, responder } => {
757                        assert_eq!(descriptors.len(), 1);
758                        assert_eq!(descriptors[0].index, 1);
759                        assert_eq!(
760                            descriptors[0].type_,
761                            ftrippoint::TripPointType::OneshotTempAbove
762                        );
763                        let status = match &descriptors[0].configuration {
764                            ftrippoint::TripPointValue::ClearedTripPoint(_) => Ok(()),
765                            ftrippoint::TripPointValue::OneshotTempAboveTripPoint(tp) => {
766                                if tp.critical_temperature_celsius > 120.0 {
767                                    Err(zx::Status::INVALID_ARGS.into_raw())
768                                } else if tp.critical_temperature_celsius <= -273.15 {
769                                    Err(zx::Status::OUT_OF_RANGE.into_raw())
770                                } else if (tp.critical_temperature_celsius - 100.0).abs() < 0.01 {
771                                    Err(zx::Status::INTERNAL.into_raw())
772                                } else {
773                                    Ok(())
774                                }
775                            }
776                            _ => Err(zx::Status::INVALID_ARGS.into_raw()),
777                        };
778                        let _ = responder.send(status);
779                    }
780                    _ => unreachable!(),
781                }
782            }
783        });
784
785        let get_descriptors_calls_check = get_descriptors_calls.clone();
786        fasync::unblock(move || {
787            let proxy = ftrippoint::TripPointSynchronousProxy::new(client_end.into_channel());
788            let trip_point = ThermalZoneTripPoint::new("test-sensor".to_string(), proxy);
789
790            // 1. Negative `hyst_mc` is rejected before any FIDL call.
791            assert_eq!(trip_point.set_hyst_mc(-1), error!(EINVAL));
792            assert_eq!(get_descriptors_calls_check.load(Ordering::SeqCst), 0);
793
794            // 2. Setting `hyst_mc` while `temp_mc == 0` resolves and caches descriptor indices
795            // (`Some((1, None))` for single-descriptor sensor) without arming a trip point.
796            assert_eq!(trip_point.set_hyst_mc(2_000), Ok(()));
797            assert_eq!(trip_point.hyst_mc(), 2_000);
798            assert_eq!(trip_point.state.lock().cached_indices, Some((1, None)));
799            assert_eq!(get_descriptors_calls_check.load(Ordering::SeqCst), 1);
800
801            // 3. Subsequent `set_temp_mc` and `set_hyst_mc` calls reuse cached indices without
802            // calling `GetTripPointDescriptors` again, including when clearing (`0`).
803            assert_eq!(trip_point.set_temp_mc(75_000), Ok(()));
804            assert_eq!(trip_point.temp_mc(), 75_000);
805            assert_eq!(trip_point.set_hyst_mc(3_000), Ok(()));
806            assert_eq!(trip_point.hyst_mc(), 3_000);
807            assert_eq!(trip_point.set_temp_mc(0), Ok(()));
808            assert_eq!(trip_point.temp_mc(), 0);
809            assert_eq!(get_descriptors_calls_check.load(Ordering::SeqCst), 1);
810
811            // 4. `SetTripPoints` error mapping: `INVALID_ARGS` and `OUT_OF_RANGE` map to `EINVAL`,
812            // while other driver statuses (`INTERNAL`) map to `EIO`, preserving previous `temp_mc`.
813            assert_eq!(trip_point.set_temp_mc(80_000), Ok(()));
814            assert_eq!(trip_point.set_temp_mc(150_000), error!(EINVAL));
815            assert_eq!(trip_point.temp_mc(), 80_000);
816            assert_eq!(trip_point.set_temp_mc(-300_000), error!(EINVAL));
817            assert_eq!(trip_point.temp_mc(), 80_000);
818            assert_eq!(trip_point.set_temp_mc(100_000), error!(EIO));
819            assert_eq!(trip_point.temp_mc(), 80_000);
820        })
821        .await;
822    }
823
824    #[fuchsia::test]
825    async fn test_thermal_zone_trip_point_dual_descriptors_and_hyst_rollback() {
826        assert_eq!(parse_i32_sysfs_input(b" 75000\n", "trip_point_0_temp"), Ok(75_000));
827        assert_eq!(parse_i32_sysfs_input(b"-5000\n", "emul_temp"), Ok(-5_000));
828        assert_eq!(parse_i32_sysfs_input(b"invalid\n", "trip_point_0_temp"), error!(EINVAL));
829        assert_eq!(parse_i32_sysfs_input(&[0xff, 0xfe], "trip_point_0_hyst"), error!(EINVAL));
830
831        let scope = fasync::Scope::new();
832        let (client_end, mut stream) =
833            fidl::endpoints::create_request_stream::<ftrippoint::TripPointMarker>();
834        let recorded_batches = Arc::new(Mutex::new(Vec::new()));
835        let recorded_batches_clone = recorded_batches.clone();
836
837        // Dual-descriptor (`OneshotTempAbove` at index 0 + `OneshotTempBelow` at index 1) sensor
838        // that rejects any trip point <= -273.15°C with `ZX_ERR_OUT_OF_RANGE`.
839        scope.spawn(async move {
840            while let Ok(Some(req)) = stream.try_next().await {
841                match req {
842                    ftrippoint::TripPointRequest::GetTripPointDescriptors { responder } => {
843                        let _ = responder.send(Ok(&[
844                            ftrippoint::TripPointDescriptor {
845                                index: 0,
846                                type_: ftrippoint::TripPointType::OneshotTempAbove,
847                                configuration: ftrippoint::TripPointValue::ClearedTripPoint(
848                                    ftrippoint::ClearedTripPoint,
849                                ),
850                            },
851                            ftrippoint::TripPointDescriptor {
852                                index: 1,
853                                type_: ftrippoint::TripPointType::OneshotTempBelow,
854                                configuration: ftrippoint::TripPointValue::ClearedTripPoint(
855                                    ftrippoint::ClearedTripPoint,
856                                ),
857                            },
858                        ]));
859                    }
860                    ftrippoint::TripPointRequest::SetTripPoints { descriptors, responder } => {
861                        let out_of_range = descriptors.iter().any(|d| match &d.configuration {
862                            ftrippoint::TripPointValue::OneshotTempAboveTripPoint(tp) => {
863                                tp.critical_temperature_celsius <= -273.15
864                            }
865                            ftrippoint::TripPointValue::OneshotTempBelowTripPoint(tp) => {
866                                tp.critical_temperature_celsius <= -273.15
867                            }
868                            _ => false,
869                        });
870                        if out_of_range {
871                            let _ = responder.send(Err(zx::Status::OUT_OF_RANGE.into_raw()));
872                        } else {
873                            recorded_batches_clone.lock().unwrap().push(descriptors);
874                            let _ = responder.send(Ok(()));
875                        }
876                    }
877                    _ => unreachable!(),
878                }
879            }
880        });
881
882        fasync::unblock(move || {
883            let proxy = ftrippoint::TripPointSynchronousProxy::new(client_end.into_channel());
884            let trip_point = ThermalZoneTripPoint::new("dual-sensor".to_string(), proxy);
885
886            // 1. Configure `hyst_mc = 3_000` then `temp_mc = 75_000`: caches `Some((0, Some(1)))`
887            // and programs `OneshotTempAbove(75.0)` and `OneshotTempBelow(72.0)`.
888            assert_eq!(trip_point.set_hyst_mc(3_000), Ok(()));
889            assert_eq!(trip_point.state.lock().cached_indices, Some((0, Some(1))));
890            assert_eq!(trip_point.set_temp_mc(75_000), Ok(()));
891
892            // 2. Updating `hyst_mc` to `5_000` while `temp_mc != 0` reprograms the window to
893            // `[70.0°C, 75.0°C]`.
894            assert_eq!(trip_point.set_hyst_mc(5_000), Ok(()));
895            assert_eq!(trip_point.hyst_mc(), 5_000);
896
897            // 3. If `set_hyst_mc` triggers a `SetTripPoints` error while `temp_mc != 0` (e.g.
898            // `75_000 - 400_000 = -325_000` m°C <= -273.15°C -> `OUT_OF_RANGE`), `set_hyst_mc`
899            // returns `EINVAL` and preserves the previous `hyst_mc` (`5_000`).
900            assert_eq!(trip_point.set_hyst_mc(400_000), error!(EINVAL));
901            assert_eq!(trip_point.hyst_mc(), 5_000);
902
903            // 4. Clearing `temp_mc` (`0`) clears both `OneshotTempAbove` and `OneshotTempBelow`.
904            assert_eq!(trip_point.set_temp_mc(0), Ok(()));
905            assert_eq!(trip_point.temp_mc(), 0);
906
907            let batches = recorded_batches.lock().unwrap();
908            assert_eq!(
909                *batches,
910                vec![
911                    vec![
912                        ftrippoint::TripPointDescriptor {
913                            index: 0,
914                            type_: ftrippoint::TripPointType::OneshotTempAbove,
915                            configuration: ftrippoint::TripPointValue::OneshotTempAboveTripPoint(
916                                ftrippoint::OneshotTempAboveTripPoint {
917                                    critical_temperature_celsius: 75.0,
918                                },
919                            ),
920                        },
921                        ftrippoint::TripPointDescriptor {
922                            index: 1,
923                            type_: ftrippoint::TripPointType::OneshotTempBelow,
924                            configuration: ftrippoint::TripPointValue::OneshotTempBelowTripPoint(
925                                ftrippoint::OneshotTempBelowTripPoint {
926                                    critical_temperature_celsius: 72.0,
927                                },
928                            ),
929                        },
930                    ],
931                    vec![
932                        ftrippoint::TripPointDescriptor {
933                            index: 0,
934                            type_: ftrippoint::TripPointType::OneshotTempAbove,
935                            configuration: ftrippoint::TripPointValue::OneshotTempAboveTripPoint(
936                                ftrippoint::OneshotTempAboveTripPoint {
937                                    critical_temperature_celsius: 75.0,
938                                },
939                            ),
940                        },
941                        ftrippoint::TripPointDescriptor {
942                            index: 1,
943                            type_: ftrippoint::TripPointType::OneshotTempBelow,
944                            configuration: ftrippoint::TripPointValue::OneshotTempBelowTripPoint(
945                                ftrippoint::OneshotTempBelowTripPoint {
946                                    critical_temperature_celsius: 70.0,
947                                },
948                            ),
949                        },
950                    ],
951                    vec![
952                        ftrippoint::TripPointDescriptor {
953                            index: 0,
954                            type_: ftrippoint::TripPointType::OneshotTempAbove,
955                            configuration: ftrippoint::TripPointValue::ClearedTripPoint(
956                                ftrippoint::ClearedTripPoint,
957                            ),
958                        },
959                        ftrippoint::TripPointDescriptor {
960                            index: 1,
961                            type_: ftrippoint::TripPointType::OneshotTempBelow,
962                            configuration: ftrippoint::TripPointValue::ClearedTripPoint(
963                                ftrippoint::ClearedTripPoint,
964                            ),
965                        },
966                    ],
967                ]
968            );
969        })
970        .await;
971    }
972}