amdgpu_sysfs/gpu_handle/
fan_control.rs

1//! Types for working with the dedicated fan control interface.
2//! Only for Navi 3x (RDNA 3) and newer. Older GPUs have to use the HwMon interface.
3use crate::{
4    error::{Error, ErrorKind},
5    Result,
6};
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9use std::{collections::HashMap, fmt::Write, ops::RangeInclusive};
10
11/// Information about fan characteristics.
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct FanInfo {
15    /// Current value
16    pub current: u32,
17    /// Minimum and maximum allowed values.
18    /// This is empty if changes to the value are not supported.
19    pub allowed_range: Option<(u32, u32)>,
20}
21
22/// Custom fan curve
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct FanCurve {
26    /// Fan curve points in the (temperature, speed) format
27    /// This is a boxed slice as the number of curve points cannot be modified, only their values can be.
28    pub points: Box<[(i32, u8)]>,
29    /// Allowed value ranges.
30    /// Empty when changes to the fan curve are not supported.
31    pub allowed_ranges: Option<FanCurveRanges>,
32}
33
34/// Range of values allowed to be used within fan curve points
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct FanCurveRanges {
38    /// Temperature range allowed in curve points
39    pub temperature_range: RangeInclusive<i32>,
40    /// Fan speed range allowed in curve points
41    pub speed_range: RangeInclusive<u8>,
42}
43
44#[derive(PartialEq, Eq, Debug)]
45pub(crate) struct FanCtrlContents {
46    pub contents: String,
47    pub od_range: HashMap<String, (String, String)>,
48}
49
50impl FanCtrlContents {
51    pub(crate) fn parse(data: &str, expected_section_name: &str) -> Result<Self> {
52        let mut lines = data.lines().enumerate();
53        let (_, section_line) = lines
54            .next()
55            .ok_or_else(|| Error::unexpected_eol("Section name", 1))?;
56
57        let section_name = section_line.strip_suffix(':').ok_or_else(|| {
58            Error::basic_parse_error(format!("Section \"{section_line}\" should end with \":\""))
59        })?;
60
61        if section_name != expected_section_name {
62            return Err(Error::basic_parse_error(format!(
63                "Found section {section_name}, expected {expected_section_name}"
64            )));
65        }
66
67        let mut contents = String::new();
68        for (_, line) in &mut lines {
69            if line == "OD_RANGE:" {
70                break;
71            }
72            writeln!(contents, "{line}").unwrap();
73        }
74        contents.pop(); // Remove newline symbol
75
76        let mut od_range = HashMap::new();
77        for (i, range_line) in lines {
78            let (name, value) =
79                range_line
80                    .split_once(": ")
81                    .ok_or_else(|| ErrorKind::ParseError {
82                        msg: format!("Range line \"{range_line}\" does not have a separator"),
83                        line: i + 1,
84                    })?;
85            let (min, max) = value.split_once(' ').ok_or_else(|| ErrorKind::ParseError {
86                msg: format!(
87                    "Range line \"{range_line}\" does not have a separator between the values"
88                ),
89                line: i + 1,
90            })?;
91
92            od_range.insert(name.to_owned(), (min.to_owned(), max.to_owned()));
93        }
94
95        Ok(Self { contents, od_range })
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::FanCtrlContents;
102    use pretty_assertions::assert_eq;
103
104    #[test]
105    fn parse_od_acoustic_limit() {
106        let data = "\
107OD_ACOUSTIC_LIMIT:
1082450
109OD_RANGE:
110ACOUSTIC_LIMIT: 500 3100";
111        let contents = FanCtrlContents::parse(data, "OD_ACOUSTIC_LIMIT").unwrap();
112        let expected_contents = FanCtrlContents {
113            contents: "2450".to_owned(),
114            od_range: [(
115                "ACOUSTIC_LIMIT".to_owned(),
116                ("500".to_owned(), "3100".to_owned()),
117            )]
118            .into_iter()
119            .collect(),
120        };
121        assert_eq!(expected_contents, contents);
122    }
123
124    #[test]
125    fn parse_fan_curve() {
126        let data = "\
127OD_FAN_CURVE:
1280: 0C 0%
1291: 0C 0%
1302: 0C 0%
1313: 0C 0%
1324: 0C 0%
133OD_RANGE:
134FAN_CURVE(hotspot temp): 25C 100C
135FAN_CURVE(fan speed): 20% 100%";
136        let contents = FanCtrlContents::parse(data, "OD_FAN_CURVE").unwrap();
137        let expected_contents = FanCtrlContents {
138            contents: "\
1390: 0C 0%
1401: 0C 0%
1412: 0C 0%
1423: 0C 0%
1434: 0C 0%"
144                .to_owned(),
145            od_range: [
146                (
147                    "FAN_CURVE(hotspot temp)".to_owned(),
148                    ("25C".to_owned(), "100C".to_owned()),
149                ),
150                (
151                    "FAN_CURVE(fan speed)".to_owned(),
152                    ("20%".to_owned(), "100%".to_owned()),
153                ),
154            ]
155            .into_iter()
156            .collect(),
157        };
158        assert_eq!(expected_contents, contents);
159    }
160}