thermal_config/
lib.rs

1// Copyright 2021 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
5use anyhow::{ensure, Context as _, Error};
6use serde_derive::Deserialize;
7use std::collections::{HashMap, HashSet};
8use std::fs::File;
9use std::io::Read as _;
10use std::path::Path;
11
12/// This library is used to parse a thermal configuration JSON file into a data structure which also
13/// implements some convenience methods for accessing and consuming the data.
14///
15/// The intended usage is that `ThermalConfig::read()` is called with a thermal configuration JSON
16/// file path. If successful, the function returns a ThermalConfig instance containing the parsed
17/// config data.
18///
19/// The parser expects a JSON5 file of the following format:
20///     {
21///         clients: {
22///             audio: [
23///                 {
24///                     state: 1,
25///                     trip_points: [
26///                         {
27///                             sensor_name: 'CPU thermal',
28///                             activate_at: 75,
29///                             deactivate_below: 71,
30///                         },
31///                     ],
32///                 },
33///                 {
34///                     state: 2,
35///                     trip_points: [
36///                         {
37///                             sensor_name: 'CPU thermal',
38///                             activate_at: 86,
39///                             deactivate_below: 82,
40///                         },
41///                     ],
42///                 },
43///             ],
44///         },
45///     }
46
47/// Represents the top level of a thermal configuration structure.
48#[derive(Deserialize, Debug)]
49pub struct ThermalConfig {
50    /// Maps the name of a client (e.g., "audio") to its corresponding configuration.
51    clients: HashMap<String, ClientConfig>,
52}
53
54/// Defines the configuration for a single client (made up of a vector of `StateConfig` instances).
55#[derive(Deserialize, Debug)]
56pub struct ClientConfig(Vec<StateConfig>);
57
58/// Defines the configuration for a single thermal state. Together with other `StateConfig`
59/// instances, this makes up a client's `ClientConfig`.
60#[derive(Deserialize, Debug)]
61pub struct StateConfig {
62    /// Thermal state number.
63    pub state: u32,
64
65    /// Vector of trip points that will activate this state.
66    pub trip_points: Vec<TripPoint>,
67}
68
69/// Defines a trip point with hysteresis for a specific temperature sensor.
70#[derive(Deserialize, Debug, Clone)]
71pub struct TripPoint {
72    /// Name of the temperature sensor.
73    pub sensor_name: String,
74
75    /// Temperature at which this trip point becomes active.
76    pub activate_at: u32,
77
78    /// Temperature below which this trip point becomes inactive.
79    pub deactivate_below: u32,
80}
81
82impl TripPoint {
83    /// Creates a new trip point.
84    ///
85    /// Note: this is only intended for use in tests. However, it isn't marked as cfg(test) so that
86    /// code outside of the library can use it in their tests as well.
87    pub fn new(sensor: &str, deactivate_below: u32, activate_at: u32) -> Self {
88        Self { sensor_name: sensor.into(), deactivate_below, activate_at }
89    }
90}
91
92impl ThermalConfig {
93    /// Creates a new, empty ThermalConfig instance.
94    ///
95    /// An empty ThermalConfig instance corresponds to the case where no client has specified a
96    /// thermal trip point configuration.
97    ///
98    /// Note: this is only intended for use in tests. However, it isn't marked as cfg(test) so that
99    /// code outside of the library can use it in their tests as well.
100    pub fn new() -> Self {
101        Self { clients: HashMap::new() }
102    }
103
104    /// Read the supplied JSON file path and parse into a ThermalConfig instance.
105    ///
106    /// Attempts to open, read, and parse the supplied JSON file into a valid ThermalConfig
107    /// instance. If a ThermalConfig instance could be created with the JSON configuration, then it
108    /// is also tested for validity. If a ThermalConfig instance could not be created, or validation
109    /// fails, then an error is returned.
110    pub fn read(json_file_path: &Path) -> Result<ThermalConfig, Error> {
111        let mut buffer = String::new();
112        File::open(&json_file_path)?.read_to_string(&mut buffer)?;
113
114        let config = serde_json5::from_str::<ThermalConfig>(&buffer)?;
115        config.validate()?;
116        Ok(config)
117    }
118
119    /// Validates the thermal configuration.
120    pub fn validate(&self) -> Result<(), Error> {
121        // Iterate and validate each underlying ClientConfig instance
122        for (client_name, client_config) in self.clients.iter() {
123            client_config
124                .validate()
125                .context(format!("Validation failed for client {}", client_name))?;
126        }
127
128        Ok(())
129    }
130
131    /// Adds a configuration entry for the specified client.
132    ///
133    /// Note: this is only intended for use in tests. However, it isn't marked as cfg(test) so that
134    /// code outside of the library can use it in their tests as well.
135    pub fn add_client_config(mut self, client: &str, config: ClientConfig) -> Self {
136        self.clients.insert(client.to_string(), config);
137        self
138    }
139
140    /// Gets the ClientConfig instance for the specified client.
141    pub fn get_client_config(&self, client: &String) -> Option<&ClientConfig> {
142        self.clients.get(client)
143    }
144
145    pub fn into_iter(self) -> impl Iterator<Item = (String, ClientConfig)> {
146        self.clients.into_iter()
147    }
148}
149
150impl ClientConfig {
151    /// Creates a new empty ClientConfig.
152    ///
153    /// Note: this is only intended for use in tests. However, it isn't marked as cfg(test) so that
154    /// code outside of the library can use it in their tests as well.
155    pub fn new() -> Self {
156        Self(vec![])
157    }
158
159    /// Adds a new thermal state (defined by the supplied trip points) to the client config.
160    ///
161    /// This will create a new thermal state using the supplied trip points, assigning it a valid
162    /// thermal state number (which is equal to the number of existing thermal states plus one).
163    ///
164    /// Note: this is only intended for use in tests. However, it isn't marked as cfg(test) so that
165    /// code outside of the library can use it in their tests as well.
166    pub fn add_thermal_state(mut self, trip_points: Vec<TripPoint>) -> Self {
167        self.0.push(StateConfig { state: self.0.len() as u32 + 1, trip_points });
168        self
169    }
170
171    /// Validates the client config.
172    ///
173    /// Performs a series of validations to check if the configuration defined by this
174    /// `ClientConfig` instance is valid. The instance is valid if:
175    ///     1) thermal state numbers are monotonically increasing starting at 1
176    ///     2) trip points are well-formed (`deactivate_below` <= `activate_at`)
177    ///     3) for a given sensor, the [`deactivate_below`..=`activate_at`] range is strictly
178    ///        increasing and non-overlapping for successive trip points
179    ///     4) the same sensor is not referenced by multiple trip points in the same thermal state
180    fn validate(&self) -> Result<(), Error> {
181        // Ensure state numbers are monotonically increasing starting at 1
182        {
183            let state_numbers: Vec<_> = self.0.iter().map(|s| s.state).collect();
184            let expected_state_numbers: Vec<_> = (1..self.0.len() as u32 + 1).collect();
185            ensure!(
186                state_numbers == expected_state_numbers,
187                "State numbers must increase monotonically starting at 1 \
188                (got invalid state numbers: {:?})",
189                state_numbers
190            );
191        }
192
193        // Ensure:
194        //  1) trip points are well-formed (`deactivate_below` <= `activate_at`)
195        //  2) for a given sensor, the [`deactivate_below`..=`activate_at`] range is strictly
196        //     increasing and non-overlapping for successive trip points
197        //  3) the same sensor is not referenced by multiple trip points in the same thermal state
198        {
199            // This map will be used to keep track of the highest encountered `activate_at` value
200            // for each sensor as we iterate through the trip points. This will let us detect if any
201            // trip points are overlapping or not increasing.
202            let mut highest_activate_value: HashMap<String, u32> = HashMap::new();
203
204            for state_config in self.0.iter() {
205                // This set will be used to determine which sensors have already had trip points
206                // added for a given thermal state. This will let us detect if a sensor is
207                // referenced by multiple trip points in the same thermal state.
208                let mut sensors_configured_for_state = HashSet::new();
209
210                for tp in state_config.trip_points.iter() {
211                    ensure!(
212                        sensors_configured_for_state.insert(&tp.sensor_name) == true,
213                        "A sensor cannot be referenced by multiple trip points in the same thermal \
214                        state (violated by sensor {} in state {})",
215                        tp.sensor_name,
216                        state_config.state
217                    );
218
219                    ensure!(
220                        tp.activate_at >= tp.deactivate_below,
221                        "activate_at must be greater or equal to deactivate_below \
222                        (invalid for state {}: activate_at={}; deactivate_below={}",
223                        state_config.state,
224                        tp.activate_at,
225                        tp.deactivate_below
226                    );
227
228                    // If we've already encountered a trip point for this sensor, make sure the new
229                    // trip point has a `deactivate_below` value that is greater than the previously
230                    // observed highest activate_at value. Otherwise, the new trip point will be
231                    // overlapping (or otherwise non-increasing) with a previously observed trip
232                    // point.
233                    if let Some(activate_value) = highest_activate_value.get_mut(&tp.sensor_name) {
234                        ensure!(
235                            tp.deactivate_below > *activate_value,
236                            "Trip point ranges must not overlap (range for state {} ({} - {}) \
237                            overlaps with previously specified range for sensor {})",
238                            state_config.state,
239                            tp.deactivate_below,
240                            tp.activate_at,
241                            tp.sensor_name
242                        );
243                        *activate_value = tp.activate_at;
244                    } else {
245                        highest_activate_value.insert(tp.sensor_name.clone(), tp.activate_at);
246                    }
247                }
248            }
249        }
250
251        Ok(())
252    }
253
254    /// Gets the thermal states that make up this client configuration.
255    pub fn into_thermal_states(self) -> Vec<StateConfig> {
256        self.0
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use crate::*;
263    use assert_matches::assert_matches;
264
265    /// Tests that valid ThermalConfig instances pass the validation.
266    #[test]
267    fn test_thermal_config_validation_success() {
268        // Basic, empty thermal config
269        let thermal_config = ThermalConfig::new();
270        assert_matches!(thermal_config.validate(), Ok(()));
271
272        // Multiple clients, each with multiple thermal states consisting of multiple trip points
273        let thermal_config = ThermalConfig::new()
274            .add_client_config(
275                "client1",
276                ClientConfig::new()
277                    .add_thermal_state(vec![
278                        TripPoint::new("sensor1", 1, 1),
279                        TripPoint::new("sensor2", 1, 1),
280                    ])
281                    .add_thermal_state(vec![
282                        TripPoint::new("sensor1", 2, 2),
283                        TripPoint::new("sensor2", 2, 2),
284                    ]),
285            )
286            .add_client_config(
287                "client2",
288                ClientConfig::new()
289                    .add_thermal_state(vec![
290                        TripPoint::new("sensor1", 1, 1),
291                        TripPoint::new("sensor2", 1, 1),
292                    ])
293                    .add_thermal_state(vec![
294                        TripPoint::new("sensor1", 2, 2),
295                        TripPoint::new("sensor2", 2, 2),
296                    ]),
297            );
298        assert_matches!(thermal_config.validate(), Ok(()));
299    }
300
301    /// Tests that invalid ClientConfig instances fail the validation.
302    #[test]
303    fn test_thermal_config_validation_failures() {
304        // Decreasing thermal state numbers
305        let thermal_config = ThermalConfig::new().add_client_config(
306            "client1",
307            ClientConfig(vec![
308                StateConfig { state: 2, trip_points: vec![] },
309                StateConfig { state: 1, trip_points: vec![] },
310            ]),
311        );
312        assert_matches!(thermal_config.validate(), Err(_));
313
314        // Repeated thermal state numbers
315        let thermal_config = ThermalConfig::new().add_client_config(
316            "client1",
317            ClientConfig(vec![
318                StateConfig { state: 1, trip_points: vec![] },
319                StateConfig { state: 1, trip_points: vec![] },
320            ]),
321        );
322        assert_matches!(thermal_config.validate(), Err(_));
323
324        // Thermal state numbers below 1
325        let thermal_config = ThermalConfig::new().add_client_config(
326            "client1",
327            ClientConfig(vec![
328                StateConfig { state: 0, trip_points: vec![] },
329                StateConfig { state: 1, trip_points: vec![] },
330            ]),
331        );
332        assert_matches!(thermal_config.validate(), Err(_));
333
334        // Trip point with deactivate_below > activate_at
335        let thermal_config = ThermalConfig::new().add_client_config(
336            "client1",
337            ClientConfig::new().add_thermal_state(vec![TripPoint::new("sensor1", 10, 8)]),
338        );
339        assert_matches!(thermal_config.validate(), Err(_));
340
341        // Repeated sensor for a given thermal state
342        let thermal_config = ThermalConfig::new().add_client_config(
343            "client1",
344            ClientConfig::new().add_thermal_state(vec![
345                TripPoint::new("sensor1", 5, 6),
346                TripPoint::new("sensor1", 4, 5),
347            ]),
348        );
349        assert_matches!(thermal_config.validate(), Err(_));
350
351        // Thermal states with overlapping trip points
352        let thermal_config = ThermalConfig::new().add_client_config(
353            "client1",
354            ClientConfig::new()
355                .add_thermal_state(vec![TripPoint::new("sensor1", 1, 2)])
356                .add_thermal_state(vec![TripPoint::new("sensor1", 2, 3)]),
357        );
358        assert_matches!(thermal_config.validate(), Err(_));
359    }
360}