amdgpu_sysfs/gpu_handle/
fan_control.rs1use 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#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct FanInfo {
15 pub current: u32,
17 pub allowed_range: Option<(u32, u32)>,
20}
21
22#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct FanCurve {
26 pub points: Box<[(i32, u8)]>,
29 pub allowed_ranges: Option<FanCurveRanges>,
32}
33
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct FanCurveRanges {
38 pub temperature_range: RangeInclusive<i32>,
40 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(); 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}