1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
//! Types for working with the dedicated fan control interface.
//! Only for Navi 3x (RDNA 3) and newer. Older GPUs have to use the HwMon interface.
use crate::{
    error::{Error, ErrorKind},
    Result,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Write};

/// Information about fan characteristics.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FanInfo {
    /// Current value
    pub current: u32,
    /// Minimum and maximum allowed values.
    /// This is empty if changes to the value are not supported.
    pub allowed_range: Option<(u32, u32)>,
}

/// Custom fan curve
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FanCurve {
    /// Fan curve points in the (temperature, speed) format
    pub points: Vec<(u32, u8)>,
    /// Allowed value ranges.
    /// Empty when changes to the fan curve are not supported.
    pub allowed_ranges: Option<FanCurveRanges>,
}

/// Range of values allowed to be used within fan curve points
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FanCurveRanges {
    /// Temperature range allowed in curve points
    pub temperature_range: (u32, u32),
    /// Fan speed range allowed in curve points
    pub speed_range: (u8, u8),
}

#[derive(PartialEq, Eq, Debug)]
pub(crate) struct FanCtrlContents {
    pub contents: String,
    pub od_range: HashMap<String, (String, String)>,
}

impl FanCtrlContents {
    pub(crate) fn parse(data: &str, expected_section_name: &str) -> Result<Self> {
        let mut lines = data.lines().enumerate();
        let (_, section_line) = lines
            .next()
            .ok_or_else(|| Error::unexpected_eol("Section name", 1))?;

        let section_name = section_line.strip_suffix(':').ok_or_else(|| {
            Error::basic_parse_error(format!("Section \"{section_line}\" should end with \":\""))
        })?;

        if section_name != expected_section_name {
            return Err(Error::basic_parse_error(format!(
                "Found section {section_name}, expected {expected_section_name}"
            )));
        }

        let mut contents = String::new();
        for (_, line) in &mut lines {
            if line == "OD_RANGE:" {
                break;
            }
            writeln!(contents, "{line}").unwrap();
        }
        contents.pop(); // Remove newline symbol

        let mut od_range = HashMap::new();
        for (i, range_line) in lines {
            let (name, value) =
                range_line
                    .split_once(": ")
                    .ok_or_else(|| ErrorKind::ParseError {
                        msg: format!("Range line \"{range_line}\" does not have a separator"),
                        line: i + 1,
                    })?;
            let (min, max) = value.split_once(' ').ok_or_else(|| ErrorKind::ParseError {
                msg: format!(
                    "Range line \"{range_line}\" does not have a separator between the values"
                ),
                line: i + 1,
            })?;

            od_range.insert(name.to_owned(), (min.to_owned(), max.to_owned()));
        }

        Ok(Self { contents, od_range })
    }
}

#[cfg(test)]
mod tests {
    use super::FanCtrlContents;
    use pretty_assertions::assert_eq;

    #[test]
    fn parse_od_acoustic_limit() {
        let data = "\
OD_ACOUSTIC_LIMIT:
2450
OD_RANGE:
ACOUSTIC_LIMIT: 500 3100";
        let contents = FanCtrlContents::parse(data, "OD_ACOUSTIC_LIMIT").unwrap();
        let expected_contents = FanCtrlContents {
            contents: "2450".to_owned(),
            od_range: [(
                "ACOUSTIC_LIMIT".to_owned(),
                ("500".to_owned(), "3100".to_owned()),
            )]
            .into_iter()
            .collect(),
        };
        assert_eq!(expected_contents, contents);
    }

    #[test]
    fn parse_fan_curve() {
        let data = "\
OD_FAN_CURVE:
0: 0C 0%
1: 0C 0%
2: 0C 0%
3: 0C 0%
4: 0C 0%
OD_RANGE:
FAN_CURVE(hotspot temp): 25C 100C
FAN_CURVE(fan speed): 20% 100%";
        let contents = FanCtrlContents::parse(data, "OD_FAN_CURVE").unwrap();
        let expected_contents = FanCtrlContents {
            contents: "\
0: 0C 0%
1: 0C 0%
2: 0C 0%
3: 0C 0%
4: 0C 0%"
                .to_owned(),
            od_range: [
                (
                    "FAN_CURVE(hotspot temp)".to_owned(),
                    ("25C".to_owned(), "100C".to_owned()),
                ),
                (
                    "FAN_CURVE(fan speed)".to_owned(),
                    ("20%".to_owned(), "100%".to_owned()),
                ),
            ]
            .into_iter()
            .collect(),
        };
        assert_eq!(expected_contents, contents);
    }
}