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 thermal_zone;
10
11use crate::thermal_zone::{SensorProps, ThermalZone};
12use anyhow::{Error, anyhow};
13use argh::FromArgs;
14use cooling::cooling_device_init;
15use family::ThermalFamily;
16use fidl_fuchsia_hardware_temperature as ftemperature;
17use fidl_fuchsia_thermal as fthermal;
18use starnix_core::device::kobject::Device;
19use starnix_core::fs::sysfs::build_device_directory;
20use starnix_core::task::{CurrentTask, Kernel};
21use starnix_core::vfs::FsNodeOps;
22use starnix_core::vfs::pseudo::simple_directory::SimpleDirectoryMutator;
23use starnix_core::vfs::pseudo::simple_file::{BytesFile, BytesFileOps};
24use starnix_logging::{log_error, log_warn};
25
26use starnix_uapi::errors::{Errno, errno, error};
27use starnix_uapi::file_mode::mode;
28use std::borrow::Cow;
29use std::collections::HashMap;
30use std::sync::Arc;
31use thermal_netlink::{celsius_to_millicelsius, millicelsius_to_celsius};
32use zx::MonotonicInstant;
33
34fn build_thermal_zone_directory(
35    device: &Device,
36    proxy: ftemperature::DeviceSynchronousProxy,
37    sensor_manager: fthermal::SensorManagerSynchronousProxy,
38    device_type: String,
39    dir: &SimpleDirectoryMutator,
40) {
41    build_device_directory(device, dir);
42    dir.entry(
43        "emul_temp",
44        EmulTempFile::new_node(device_type.clone(), sensor_manager),
45        mode!(IFREG, 0o200),
46    );
47    dir.entry("temp", TemperatureFile::new_node(proxy), mode!(IFREG, 0o664));
48    dir.entry(
49        "type",
50        BytesFile::new_node(format!("{}\n", device_type).into_bytes()),
51        mode!(IFREG, 0o444),
52    );
53    dir.entry("policy", BytesFile::new_node(b"step_wise\n".to_vec()), mode!(IFREG, 0o444));
54    dir.entry(
55        "available_policies",
56        BytesFile::new_node(b"step_wise\n".to_vec()),
57        mode!(IFREG, 0o444),
58    );
59}
60
61struct TemperatureFile {
62    proxy: ftemperature::DeviceSynchronousProxy,
63}
64
65impl TemperatureFile {
66    pub fn new_node(proxy: ftemperature::DeviceSynchronousProxy) -> impl FsNodeOps {
67        BytesFile::new_node(Self { proxy })
68    }
69}
70
71impl BytesFileOps for TemperatureFile {
72    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
73        let (zx_status, temp) =
74            self.proxy.get_temperature_celsius(MonotonicInstant::INFINITE).map_err(|e| {
75                log_error!("get_temperature_celsius failed: {}", e);
76                errno!(ENOENT)
77            })?;
78        let _ = zx::Status::ok(zx_status).map_err(|e| {
79            log_error!("get_temperature_celsius driver returned error: {}", e);
80            errno!(ENOENT)
81        })?;
82
83        let out = format!("{}\n", celsius_to_millicelsius(temp) as i32);
84        Ok(out.as_bytes().to_owned().into())
85    }
86}
87
88struct EmulTempFile {
89    device_type: String,
90    proxy: fthermal::SensorManagerSynchronousProxy,
91}
92
93impl EmulTempFile {
94    pub fn new_node(
95        device_type: String,
96        proxy: fthermal::SensorManagerSynchronousProxy,
97    ) -> impl FsNodeOps {
98        BytesFile::new_node(Self { device_type, proxy })
99    }
100}
101
102impl BytesFileOps for EmulTempFile {
103    fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
104        let num_str = str::from_utf8(&data).map_err(|e| {
105            log_warn!("Failed to convert input temp to utf-8: {:?}", e);
106            errno!(EINVAL)
107        })?;
108
109        let temp_milli_c: i32 = num_str.trim().parse().map_err(|e| {
110            log_warn!("Failed to parse input temp as i32: {:?}", e);
111            errno!(EINVAL)
112        })?;
113
114        if temp_milli_c == 0 {
115            match self
116                .proxy
117                .clear_temperature_override(&self.device_type, zx::MonotonicInstant::INFINITE)
118            {
119                Ok(Ok(_)) => Ok(()),
120                Ok(Err(error)) => {
121                    log_warn!(
122                        "Failed to clear temperature override for sensor {}: {:?}",
123                        self.device_type,
124                        error
125                    );
126                    error!(EINVAL)
127                }
128                Err(error) => {
129                    log_warn!(
130                        "Failed to call clear_temperature_override for sensor {}: {:?}",
131                        self.device_type,
132                        error
133                    );
134                    error!(EIO)
135                }
136            }
137        } else {
138            match self.proxy.set_temperature_override(
139                &self.device_type,
140                millicelsius_to_celsius(temp_milli_c as f32).into(),
141                zx::MonotonicInstant::INFINITE,
142            ) {
143                Ok(Ok(_)) => Ok(()),
144                Ok(Err(error)) => {
145                    log_warn!(
146                        "Failed to set temperature override for sensor {}: {:?}",
147                        self.device_type,
148                        error
149                    );
150                    error!(EINVAL)
151                }
152                Err(error) => {
153                    log_warn!(
154                        "Failed to call set_temperature_override for sensor {}: {:?}",
155                        self.device_type,
156                        error
157                    );
158                    error!(EIO)
159                }
160            }
161        }
162    }
163}
164
165fn thermal_device_init(kernel: &Kernel) -> Result<(), Error> {
166    let sensor_manager =
167        fuchsia_component::client::connect_to_protocol_sync::<fthermal::SensorManagerMarker>()
168            .map_err(|error| anyhow!("Failed to connect to SensorManager: {:?}", error))?;
169
170    let sensors = sensor_manager.list_sensors(zx::MonotonicInstant::INFINITE)?;
171
172    let registry = &kernel.device_registry;
173    let thermal_class = registry.objects.thermal_class();
174    let mut sensor_proxies = HashMap::new();
175
176    for (thermal_zone_id, sensor_info) in sensors.into_iter().enumerate() {
177        let Some(sensor_name) = sensor_info.name else {
178            log_warn!("No sensor name for thermal zone {}, skipping.", thermal_zone_id);
179            continue;
180        };
181        let sensor_name_clone = sensor_name.clone();
182
183        let thermal_zone_id = thermal_zone_id as u32;
184        let thermal_zone = format!("thermal_zone{}", thermal_zone_id);
185
186        // Create a synchronous proxy for file requests.
187        // Reads and writes are expected to block until data or an error is returned.
188        let (sensor_sync, sensor_server_sync) = fidl::endpoints::create_sync_proxy();
189
190        if let Err(error) = sensor_manager.connect(
191            fthermal::SensorManagerConnectRequest {
192                name: Some(sensor_name.clone()),
193                server_end: Some(fthermal::SensorServer_::Temperature(sensor_server_sync)),
194                ..Default::default()
195            },
196            zx::MonotonicInstant::INFINITE,
197        ) {
198            log_error!("Failed to connect to sensor {} (sync): {:?}", sensor_name, error);
199            continue;
200        }
201
202        registry.add_numberless_device(
203            thermal_zone.as_str().into(),
204            /* parent = */ None,
205            thermal_class.clone(),
206            move |device, dir| {
207                match fuchsia_component::client::connect_to_protocol_sync::<fthermal::SensorManagerMarker>() {
208                    Ok(sensor_manager) => build_thermal_zone_directory(device, sensor_sync, sensor_manager, sensor_name_clone, dir),
209                    Err(error) => log_warn!("Failed to connect to SensorManager when building thermal zone for sensor {}: {:?}", sensor_name_clone, error),
210                }
211            },
212        );
213
214        // Create an asynchronous proxy for netlink calls.
215        // The thermal netlink server periodically gets temperature data from the thermal sensors in
216        // the background.
217        let (sensor, sensor_server) = fidl::endpoints::create_proxy();
218
219        if let Err(error) = sensor_manager.connect(
220            fthermal::SensorManagerConnectRequest {
221                name: Some(sensor_name.clone()),
222                server_end: Some(fthermal::SensorServer_::Temperature(sensor_server)),
223                ..Default::default()
224            },
225            zx::MonotonicInstant::INFINITE,
226        ) {
227            log_error!("Failed to connect to sensor {} (async): {:?}", sensor_name, error);
228            continue;
229        }
230
231        sensor_proxies.insert(
232            SensorProps { name: sensor_name },
233            ThermalZone { id: thermal_zone_id, proxy: sensor },
234        );
235    }
236
237    let (thermal_family, thermal_family_worker) = ThermalFamily::new(sensor_proxies);
238    kernel.generic_netlink().add_family(Arc::new(thermal_family));
239    kernel
240        .kthreads
241        .spawn_future(move || async move { thermal_family_worker.await }, "thermal_netlink_worker");
242
243    Ok(())
244}
245
246/// Configuration options for the `thermal` module.
247#[derive(FromArgs, Debug, Default, PartialEq, Eq)]
248struct ThermalArgs {
249    /// comma-separated list of cooling devices to register (e.g. "cpu,fcc=8")
250    #[argh(option)]
251    cooling: Option<String>,
252}
253
254fn parse_args(args: &[&str]) -> Result<Vec<String>, Error> {
255    let parsed = ThermalArgs::from_args(&["thermal"], args).map_err(|e| anyhow!("{}", e.output))?;
256    match parsed.cooling {
257        Some(cooling) => {
258            if cooling.is_empty() {
259                return Err(anyhow!("cooling option must not be empty"));
260            }
261            Ok(cooling.split(',').map(String::from).collect())
262        }
263        None => Ok(Vec::new()),
264    }
265}
266
267/// Validates `thermal` module configuration `args`.
268pub fn validate_args(args: &[&str]) -> Result<(), Error> {
269    parse_args(args).map(|_| ())
270}
271
272/// Initializes thermal zones and optional cooling devices from module configuration `args`.
273pub fn module_init(kernel: &Kernel, args: &[&str]) -> Result<(), Error> {
274    let cooling_devices = parse_args(args)?;
275    thermal_device_init(kernel)?;
276    cooling_device_init(kernel, cooling_devices)
277}