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}