@@ -82,8 +82,9 @@ const EC_FAN_SPEED_ENTRIES: usize = 4;
8282const EC_FAN_SPEED_STALLED_DEPRECATED : u16 = 0xFFFE ;
8383const 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