Skip to content

Commit ecebf5f

Browse files
blackdragoon26JohnAZoidberg
authored andcommitted
--thermal: Return data instead of only printing it
print_thermal and print_thermal_thresholds read the EC, decoded the result and formatted it all in one function, so nothing but the commandline tool could use them. Split the reading and decoding into get_thermal and get_thermal_thresholds, which return the data, and leave the print functions as thin formatters on top. Their output is unchanged. The decoded values get proper types instead of raw memmap bytes: TempSensor is public now, fan speeds decode into FanSpeed instead of comparing against magic values at print time and disabled thresholds are None instead of a "-" string. print_thermal used to panic when the memmap read failed, now it prints an error instead. Add tests for the decoding, power.rs had none.
1 parent 34c3163 commit ecebf5f

1 file changed

Lines changed: 247 additions & 42 deletions

File tree

‎framework_lib/src/power.rs‎

Lines changed: 247 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ const EC_FAN_SPEED_ENTRIES: usize = 4;
8282
const EC_FAN_SPEED_STALLED_DEPRECATED: u16 = 0xFFFE;
8383
const EC_FAN_SPEED_NOT_PRESENT: u16 = 0xFFFF;
8484

85-
#[derive(Debug, PartialEq)]
86-
enum TempSensor {
85+
/// Reading of a single temperature sensor
86+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87+
pub enum TempSensor {
8788
Ok(u8),
8889
NotPresent,
8990
Error,
@@ -250,20 +251,51 @@ pub fn print_memmap_version_info(ec: &CrosEc) {
250251
let _events_ver = ec.read_memory(EC_MEMMAP_EVENTS_VERSION, 2).unwrap();
251252
}
252253

253-
/// Format a thermal threshold in degrees Celsius, zero means disabled
254-
fn format_threshold(kelvin: u32) -> String {
254+
/// Thermal thresholds of a single temperature sensor
255+
///
256+
/// All temperatures are in degrees Celsius, `None` means the threshold is disabled.
257+
#[derive(Debug, Clone, PartialEq, Eq)]
258+
pub struct ThermalThresholds {
259+
/// Index of the sensor in the EC memory map
260+
pub index: u32,
261+
/// Name as reported by the EC
262+
pub name: String,
263+
/// Warn the OS above this temperature (limit sPPT on AMD Framework systems)
264+
pub warn: Option<i32>,
265+
/// Throttle the CPU (PROCHOT)
266+
pub high: Option<i32>,
267+
/// Hard shutdown above this temperature
268+
pub halt: Option<i32>,
269+
/// Turn the fan off below this temperature
270+
pub fan_off: Option<i32>,
271+
/// Run the fan at maximum speed above this temperature
272+
pub fan_max: Option<i32>,
273+
}
274+
275+
/// Convert a threshold from Kelvin to Celsius, zero means disabled
276+
fn threshold_celsius(kelvin: u32) -> Option<i32> {
255277
if kelvin == 0 {
256-
"-".to_string()
278+
None
257279
} else {
258-
(kelvin as i32 - 273).to_string()
280+
Some(kelvin as i32 - 273)
259281
}
260282
}
261283

262-
/// Print the thermal thresholds of all temperature sensors
263-
pub fn print_thermal_thresholds(ec: &CrosEc) -> Option<()> {
284+
/// Format a thermal threshold in degrees Celsius, `None` means disabled
285+
fn format_threshold(celsius: Option<i32>) -> String {
286+
if let Some(celsius) = celsius {
287+
celsius.to_string()
288+
} else {
289+
"-".to_string()
290+
}
291+
}
292+
293+
/// Read the thermal thresholds of all present temperature sensors
294+
///
295+
/// Stops at the first sensor that fails to respond, like ectool does.
296+
pub fn get_thermal_thresholds(ec: &CrosEc) -> Option<Vec<ThermalThresholds>> {
264297
let temps = ec.read_memory(EC_MEMMAP_TEMP_SENSOR, 0x0F)?;
265-
println!("sensor warn high halt fan_off fan_max name");
266-
let mut printed = 0;
298+
let mut thresholds = vec![];
267299
for (i, temp) in temps.iter().enumerate() {
268300
if TempSensor::from(*temp) == TempSensor::NotPresent {
269301
continue;
@@ -277,19 +309,36 @@ pub fn print_thermal_thresholds(ec: &CrosEc) -> Option<()> {
277309
.unwrap_or_else(|_| "?".to_string());
278310
// Copy out of the packed struct to allow taking references
279311
let temp_host = { cfg.temp_host };
312+
thresholds.push(ThermalThresholds {
313+
index: i as u32,
314+
name,
315+
warn: threshold_celsius(temp_host[EcTempThreshold::Warn as usize]),
316+
high: threshold_celsius(temp_host[EcTempThreshold::High as usize]),
317+
halt: threshold_celsius(temp_host[EcTempThreshold::Halt as usize]),
318+
fan_off: threshold_celsius(cfg.temp_fan_off),
319+
fan_max: threshold_celsius(cfg.temp_fan_max),
320+
});
321+
}
322+
Some(thresholds)
323+
}
324+
325+
/// Print the thermal thresholds of all temperature sensors
326+
pub fn print_thermal_thresholds(ec: &CrosEc) -> Option<()> {
327+
let thresholds = get_thermal_thresholds(ec)?;
328+
println!("sensor warn high halt fan_off fan_max name");
329+
for threshold in &thresholds {
280330
println!(
281331
" {:2} {:>3} {:>3} {:>3} {:>3} {:>3} {}",
282-
i,
283-
format_threshold(temp_host[EcTempThreshold::Warn as usize]),
284-
format_threshold(temp_host[EcTempThreshold::High as usize]),
285-
format_threshold(temp_host[EcTempThreshold::Halt as usize]),
286-
format_threshold(cfg.temp_fan_off),
287-
format_threshold(cfg.temp_fan_max),
288-
name
332+
threshold.index,
333+
format_threshold(threshold.warn),
334+
format_threshold(threshold.high),
335+
format_threshold(threshold.halt),
336+
format_threshold(threshold.fan_off),
337+
format_threshold(threshold.fan_max),
338+
threshold.name
289339
);
290-
printed += 1;
291340
}
292-
if printed > 0 {
341+
if !thresholds.is_empty() {
293342
println!("(all temps in degrees Celsius)");
294343
}
295344
Some(())
@@ -473,9 +522,77 @@ pub fn print_sensors(ec: &CrosEc) {
473522
}
474523
}
475524

476-
pub fn print_thermal(ec: &CrosEc) {
477-
let temps = ec.read_memory(EC_MEMMAP_TEMP_SENSOR, 0x0F).unwrap();
478-
let fans = ec.read_memory(EC_MEMMAP_FAN, 0x08).unwrap();
525+
/// All thermal information the EC reports at once
526+
///
527+
/// Use [`get_thermal`] to read it and [`print_thermal`] to show it like the
528+
/// commandline tool does.
529+
#[derive(Debug, Clone, PartialEq, Eq)]
530+
pub struct ThermalInfo {
531+
/// All temperature sensors that are present
532+
pub sensors: Vec<TempSensorInfo>,
533+
/// All fan slots, including those that have no fan connected
534+
pub fans: Vec<FanInfo>,
535+
/// Whether the EC is throttling the AP, `None` if it doesn't report it
536+
pub throttle: Option<ApThrottleInfo>,
537+
}
538+
539+
/// A single temperature sensor and its current reading
540+
#[derive(Debug, Clone, PartialEq, Eq)]
541+
pub struct TempSensorInfo {
542+
/// Index of the sensor in the EC memory map
543+
pub index: u8,
544+
/// Name as reported by the EC
545+
pub name: String,
546+
/// Current reading
547+
pub temp: TempSensor,
548+
}
549+
550+
/// A single fan and its current speed
551+
#[derive(Debug, Clone, PartialEq, Eq)]
552+
pub struct FanInfo {
553+
/// Index of the fan in the EC memory map
554+
pub index: usize,
555+
/// Human readable name, depends on the platform
556+
pub name: String,
557+
/// Current speed
558+
pub speed: FanSpeed,
559+
}
560+
561+
/// Current speed of a single fan
562+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
563+
pub enum FanSpeed {
564+
/// Current speed in RPM
565+
Rpm(u16),
566+
/// Fan is stalled, only reported by non-zephyr EC firmware (Framework Laptop Intel 11th-13th Gen)
567+
Stalled,
568+
/// No fan is connected to this slot
569+
NotPresent,
570+
}
571+
impl From<u16> for FanSpeed {
572+
fn from(rpm: u16) -> Self {
573+
match rpm {
574+
EC_FAN_SPEED_STALLED_DEPRECATED => FanSpeed::Stalled,
575+
EC_FAN_SPEED_NOT_PRESENT => FanSpeed::NotPresent,
576+
rpm => FanSpeed::Rpm(rpm),
577+
}
578+
}
579+
}
580+
581+
/// Whether the EC is throttling the AP
582+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
583+
pub struct ApThrottleInfo {
584+
pub soft: bool,
585+
pub hard: bool,
586+
}
587+
588+
/// Read temperatures, fan speeds and AP throttle status from the EC
589+
pub fn get_thermal(ec: &CrosEc) -> EcResult<ThermalInfo> {
590+
let temps = ec
591+
.read_memory(EC_MEMMAP_TEMP_SENSOR, 0x0F)
592+
.ok_or_else(|| EcError::DeviceError("Failed to read temperature sensors".to_string()))?;
593+
let fans = ec
594+
.read_memory(EC_MEMMAP_FAN, 0x08)
595+
.ok_or_else(|| EcError::DeviceError("Failed to read fan speeds".to_string()))?;
479596

480597
let family = smbios::get_family();
481598

@@ -489,17 +606,14 @@ pub fn print_thermal(ec: &CrosEc) {
489606
let name = ec
490607
.get_temp_sensor_name(i as u8)
491608
.unwrap_or_else(|_| format!("Temp {}", i));
492-
sensors.push((name, temp));
493-
}
494-
let width = sensors
495-
.iter()
496-
.map(|(name, _)| name.len() + 1)
497-
.max()
498-
.unwrap_or(13);
499-
for (name, temp) in sensors {
500-
println!(" {:<width$} {:>4}", format!("{name}:"), temp);
609+
sensors.push(TempSensorInfo {
610+
index: i as u8,
611+
name,
612+
temp,
613+
});
501614
}
502615

616+
let mut fan_infos = vec![];
503617
for i in 0..EC_FAN_SPEED_ENTRIES {
504618
let name = match (i, family) {
505619
(0, Some(PlatformFamily::Framework12)) => "APU Fan".to_string(),
@@ -511,22 +625,66 @@ pub fn print_thermal(ec: &CrosEc) {
511625
(2, Some(PlatformFamily::FrameworkDesktop)) => "Third Fan".to_string(),
512626
_ => format!("Fan {i}"),
513627
};
514-
let name = format!("{name}:");
628+
fan_infos.push(FanInfo {
629+
index: i,
630+
name,
631+
speed: FanSpeed::from(u16::from_le_bytes([fans[i * 2], fans[1 + i * 2]])),
632+
});
633+
}
515634

516-
let fan = u16::from_le_bytes([fans[i * 2], fans[1 + i * 2]]);
517-
if fan == EC_FAN_SPEED_STALLED_DEPRECATED {
518-
println!(" {name:<width$} {:>4} RPM (Stalled)", fan);
519-
} else if fan == EC_FAN_SPEED_NOT_PRESENT {
520-
info!(" {name:<width$} Not present");
521-
} else {
522-
println!(" {name:<width$} {:>4} RPM", fan);
635+
let throttle = ec
636+
.get_ap_throttle_status()
637+
.ok()
638+
.map(|throttle| ApThrottleInfo {
639+
soft: throttle.soft_ap_throttle == 1,
640+
hard: throttle.hard_ap_throttle == 1,
641+
});
642+
643+
Ok(ThermalInfo {
644+
sensors,
645+
fans: fan_infos,
646+
throttle,
647+
})
648+
}
649+
650+
/// Print temperatures, fan speeds and AP throttle status
651+
pub fn print_thermal(ec: &CrosEc) {
652+
let info = match get_thermal(ec) {
653+
Ok(info) => info,
654+
Err(err) => {
655+
println!("Failed to read thermal information: {:?}", err);
656+
return;
657+
}
658+
};
659+
660+
let width = info
661+
.sensors
662+
.iter()
663+
.map(|sensor| sensor.name.len() + 1)
664+
.max()
665+
.unwrap_or(13);
666+
for sensor in &info.sensors {
667+
let name = format!("{}:", sensor.name);
668+
println!(" {:<width$} {:>4}", name, sensor.temp);
669+
}
670+
671+
for fan in &info.fans {
672+
let name = format!("{}:", fan.name);
673+
match fan.speed {
674+
// Keep printing the raw value, like we always did
675+
FanSpeed::Stalled => println!(
676+
" {name:<width$} {:>4} RPM (Stalled)",
677+
EC_FAN_SPEED_STALLED_DEPRECATED
678+
),
679+
FanSpeed::NotPresent => info!(" {name:<width$} Not present"),
680+
FanSpeed::Rpm(rpm) => println!(" {name:<width$} {:>4} RPM", rpm),
523681
}
524682
}
525683

526684
println!(" AP Throttle Status");
527-
if let Ok(throttle) = ec.get_ap_throttle_status() {
528-
println!(" Soft: {:?}", throttle.soft_ap_throttle == 1);
529-
println!(" Hard: {:?}", throttle.hard_ap_throttle == 1);
685+
if let Some(throttle) = info.throttle {
686+
println!(" Soft: {:?}", throttle.soft);
687+
println!(" Hard: {:?}", throttle.hard);
530688
} else {
531689
println!(" Unknown");
532690
}
@@ -1133,3 +1291,50 @@ pub fn standalone_mode(ec: &CrosEc) -> bool {
11331291
true
11341292
}
11351293
}
1294+
1295+
#[cfg(test)]
1296+
mod tests {
1297+
use super::*;
1298+
1299+
#[test]
1300+
fn decode_fan_speed() {
1301+
assert_eq!(FanSpeed::from(0), FanSpeed::Rpm(0));
1302+
assert_eq!(FanSpeed::from(3500), FanSpeed::Rpm(3500));
1303+
// Just below the first special value is still a valid speed
1304+
assert_eq!(FanSpeed::from(0xFFFD), FanSpeed::Rpm(0xFFFD));
1305+
assert_eq!(
1306+
FanSpeed::from(EC_FAN_SPEED_STALLED_DEPRECATED),
1307+
FanSpeed::Stalled
1308+
);
1309+
assert_eq!(
1310+
FanSpeed::from(EC_FAN_SPEED_NOT_PRESENT),
1311+
FanSpeed::NotPresent
1312+
);
1313+
}
1314+
1315+
#[test]
1316+
fn decode_temp_sensor() {
1317+
assert_eq!(TempSensor::from(0xFF), TempSensor::NotPresent);
1318+
assert_eq!(TempSensor::from(0xFE), TempSensor::Error);
1319+
assert_eq!(TempSensor::from(0xFD), TempSensor::NotPowered);
1320+
assert_eq!(TempSensor::from(0xFC), TempSensor::NotCalibrated);
1321+
// Raw sensor values are offset by 73 K
1322+
assert_eq!(TempSensor::from(73), TempSensor::Ok(0));
1323+
assert_eq!(TempSensor::from(100), TempSensor::Ok(27));
1324+
}
1325+
1326+
#[test]
1327+
fn decode_threshold() {
1328+
// Zero means the threshold is disabled
1329+
assert_eq!(threshold_celsius(0), None);
1330+
assert_eq!(threshold_celsius(273), Some(0));
1331+
assert_eq!(threshold_celsius(373), Some(100));
1332+
}
1333+
1334+
#[test]
1335+
fn format_disabled_threshold() {
1336+
assert_eq!(format_threshold(None), "-");
1337+
assert_eq!(format_threshold(Some(0)), "0");
1338+
assert_eq!(format_threshold(Some(100)), "100");
1339+
}
1340+
}

0 commit comments

Comments
 (0)