use std::ffi::c_void;
use std::sync::OnceLock;
const MAX_CPU_TEMP_KEYS: usize = 256;
const MAX_GPU_TEMP_KEYS: usize = 512;
const MAX_FANS: u32 = 8;
const MAX_PLAUSIBLE_FAN_RPM: f64 = 20_000.0;
const MAX_PLAUSIBLE_POWER_WATTS: f64 = 2_000.0;
fn is_plausible_power_watts(value: f64) -> bool {
value.is_finite() && (0.0..=MAX_PLAUSIBLE_POWER_WATTS).contains(&value)
}
fn is_plausible_fan_rpm(value: f64) -> bool {
value.is_finite() && (0.0..=MAX_PLAUSIBLE_FAN_RPM).contains(&value)
}
struct DiscoveredTempKeys {
cpu_keys: Vec<String>,
gpu_keys: Vec<String>,
}
static DISCOVERED_KEYS: OnceLock<DiscoveredTempKeys> = OnceLock::new();
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
fn mach_task_self() -> u32;
fn IOServiceMatching(name: *const i8) -> *mut c_void;
fn IOServiceGetMatchingService(master_port: u32, matching: *mut c_void) -> u32;
fn IOServiceOpen(device: u32, owning_task: u32, conn_type: u32, conn: *mut u32) -> i32;
fn IOServiceClose(conn: u32) -> i32;
fn IOObjectRelease(object: u32) -> i32;
fn IOConnectCallStructMethod(
conn: u32,
selector: u32,
input: *const c_void,
input_size: usize,
output: *mut c_void,
output_size: *mut usize,
) -> i32;
}
const SMC_TYPE_UI8: u32 = u32::from_be_bytes(*b"ui8 ");
const SMC_TYPE_UI16: u32 = u32::from_be_bytes(*b"ui16");
const SMC_TYPE_UI32: u32 = u32::from_be_bytes(*b"ui32");
const SMC_TYPE_FLT: u32 = u32::from_be_bytes(*b"flt ");
const SMC_TYPE_SP78: u32 = u32::from_be_bytes(*b"sp78");
const SMC_TYPE_FP1F: u32 = u32::from_be_bytes(*b"fp1f");
const SMC_TYPE_FP2E: u32 = u32::from_be_bytes(*b"fp2e");
const SMC_TYPE_FP4C: u32 = u32::from_be_bytes(*b"fp4c");
const SMC_TYPE_FP5B: u32 = u32::from_be_bytes(*b"fp5b");
const SMC_TYPE_FP6A: u32 = u32::from_be_bytes(*b"fp6a");
const SMC_TYPE_FP79: u32 = u32::from_be_bytes(*b"fp79");
const SMC_TYPE_FP88: u32 = u32::from_be_bytes(*b"fp88");
const SMC_TYPE_FPA6: u32 = u32::from_be_bytes(*b"fpa6");
const SMC_TYPE_FPC4: u32 = u32::from_be_bytes(*b"fpc4");
const SMC_TYPE_FPE2: u32 = u32::from_be_bytes(*b"fpe2");
const SMC_CMD_READ_KEY: u8 = 5;
const SMC_CMD_READ_KEY_INFO: u8 = 9;
const SMC_CMD_READ_INDEX: u8 = 8;
const KERNEL_INDEX_SMC: u32 = 2;
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
struct KeyInfo {
data_size: u32,
data_type: u32,
data_attributes: u8,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
struct KeyDataVer {
major: u8,
minor: u8,
build: u8,
reserved: u8,
release: u16,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
struct PLimitData {
version: u16,
length: u16,
cpu_p_limit: u32,
gpu_p_limit: u32,
mem_p_limit: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct KeyData {
key: u32,
vers: KeyDataVer,
p_limit_data: PLimitData,
key_info: KeyInfo,
result: u8,
status: u8,
data8: u8,
data32: u32,
bytes: [u8; 32],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TempKeyCategory {
Cpu,
Gpu,
}
fn classify_temperature_key(key: &str, data_type: u32) -> Option<TempKeyCategory> {
if !is_temperature_key_candidate(key) {
return None;
}
let bytes = key.as_bytes();
match data_type {
SMC_TYPE_FLT => match bytes[1] {
b'p' | b'e' => Some(TempKeyCategory::Cpu),
b'g' => Some(TempKeyCategory::Gpu),
_ => None,
},
SMC_TYPE_SP78 => match bytes[1] {
b'C' => Some(TempKeyCategory::Cpu),
b'G' => Some(TempKeyCategory::Gpu),
_ => None,
},
_ => None,
}
}
fn is_temperature_key_candidate(key: &str) -> bool {
let bytes = key.as_bytes();
bytes.len() >= 2 && bytes[0] == b'T' && matches!(bytes[1], b'p' | b'e' | b'g' | b'C' | b'G')
}
const TEMP_KEY_RANGE_START: u32 = u32::from_be_bytes([b'T', 0, 0, 0]);
const TEMP_KEY_RANGE_END: u32 = u32::from_be_bytes([b'U', 0, 0, 0]);
#[derive(Debug, Clone, Default)]
pub struct TempKeyScan {
pub cpu_keys: Vec<String>,
pub gpu_keys: Vec<String>,
#[allow(dead_code)]
pub scanned_keys: u32,
#[allow(dead_code)]
pub total_keys: u32,
#[allow(dead_code)]
pub used_sorted_range: bool,
}
fn fourcc_to_string(key: u32) -> String {
String::from_utf8_lossy(&key.to_be_bytes()).to_string()
}
fn str_to_fourcc(s: &str) -> u32 {
let bytes = s.as_bytes();
if bytes.len() != 4 {
return 0;
}
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
#[allow(clippy::upper_case_acronyms)]
pub struct SMC {
conn: u32,
}
impl SMC {
pub fn new() -> Result<Self, &'static str> {
unsafe {
let matching = IOServiceMatching(c"AppleSMC".as_ptr());
if matching.is_null() {
return Err("Failed to create IOService matching dictionary");
}
let device = IOServiceGetMatchingService(0, matching);
if device == 0 {
return Err("SMC device not found");
}
let mut conn: u32 = 0;
let result = IOServiceOpen(device, mach_task_self(), 0, &mut conn);
IOObjectRelease(device);
if result != 0 {
return Err("Failed to open SMC connection");
}
Ok(Self { conn })
}
}
fn read(&self, input: &KeyData) -> Result<KeyData, &'static str> {
unsafe {
let mut output: KeyData = KeyData::default();
let mut output_size = std::mem::size_of::<KeyData>();
let result = IOConnectCallStructMethod(
self.conn,
KERNEL_INDEX_SMC,
input as *const KeyData as *const c_void,
std::mem::size_of::<KeyData>(),
&mut output as *mut KeyData as *mut c_void,
&mut output_size,
);
if result != 0 {
return Err("SMC read failed");
}
Ok(output)
}
}
fn read_key_info(&self, key: &str) -> Result<KeyInfo, &'static str> {
let key_code = str_to_fourcc(key);
let input = KeyData {
key: key_code,
data8: SMC_CMD_READ_KEY_INFO,
..Default::default()
};
let output = self.read(&input)?;
Ok(output.key_info)
}
pub fn get_key_count(&self) -> Result<u32, &'static str> {
let key_code = str_to_fourcc("#KEY");
let input = KeyData {
key: key_code,
data8: SMC_CMD_READ_KEY_INFO,
..Default::default()
};
let info_output = self.read(&input)?;
let input = KeyData {
key: key_code,
key_info: KeyInfo {
data_size: info_output.key_info.data_size,
..Default::default()
},
data8: SMC_CMD_READ_KEY,
..Default::default()
};
let output = self.read(&input)?;
let count = u32::from_be_bytes([
output.bytes[0],
output.bytes[1],
output.bytes[2],
output.bytes[3],
]);
Ok(count)
}
fn get_key_code_from_index(&self, index: u32) -> Result<u32, &'static str> {
let input = KeyData {
data8: SMC_CMD_READ_INDEX,
data32: index,
..Default::default()
};
Ok(self.read(&input)?.key)
}
#[allow(dead_code)]
pub fn get_key_from_index(&self, index: u32) -> Result<String, &'static str> {
Ok(fourcc_to_string(self.get_key_code_from_index(index)?))
}
pub fn discover_temperature_keys(&self) -> (Vec<String>, Vec<String>) {
let scan = self.scan_temperature_keys();
(scan.cpu_keys, scan.gpu_keys)
}
pub fn scan_temperature_keys(&self) -> TempKeyScan {
let total_keys = match self.get_key_count() {
Ok(count) => count,
Err(_) => return TempKeyScan::default(),
};
if let Some(scan) = self.scan_sorted_temperature_range(total_keys)
&& !(scan.cpu_keys.is_empty() && scan.gpu_keys.is_empty())
{
return scan;
}
self.scan_all_keys(total_keys)
}
fn scan_sorted_temperature_range(&self, total_keys: u32) -> Option<TempKeyScan> {
if total_keys == 0 {
return None;
}
let mut scanned_keys = 0u32;
let mut probes: Vec<(u32, u32)> = Vec::new();
let (mut lo, mut hi) = (0u32, total_keys);
while lo < hi {
let mid = lo + (hi - lo) / 2;
let code = self.get_key_code_from_index(mid).ok()?;
scanned_keys += 1;
probes.push((mid, code));
if code < TEMP_KEY_RANGE_START {
lo = mid + 1;
} else {
hi = mid;
}
}
if lo > 0 {
let code = self.get_key_code_from_index(lo - 1).ok()?;
scanned_keys += 1;
probes.push((lo - 1, code));
if code >= TEMP_KEY_RANGE_START {
return None;
}
}
probes.sort_unstable_by_key(|(index, _)| *index);
if probes.windows(2).any(|w| w[0].1 > w[1].1) {
return None;
}
let mut cpu_keys = Vec::new();
let mut gpu_keys = Vec::new();
for index in lo..total_keys {
let code = match self.get_key_code_from_index(index) {
Ok(code) => code,
Err(_) => {
scanned_keys += 1;
continue;
}
};
scanned_keys += 1;
if code >= TEMP_KEY_RANGE_END {
break;
}
self.classify_key_at(code, &mut cpu_keys, &mut gpu_keys);
if cpu_keys.len() >= MAX_CPU_TEMP_KEYS && gpu_keys.len() >= MAX_GPU_TEMP_KEYS {
break;
}
}
Some(TempKeyScan {
cpu_keys,
gpu_keys,
scanned_keys,
total_keys,
used_sorted_range: true,
})
}
fn scan_all_keys(&self, total_keys: u32) -> TempKeyScan {
let mut cpu_keys = Vec::new();
let mut gpu_keys = Vec::new();
let mut scanned_keys = 0u32;
for index in 0..total_keys {
if cpu_keys.len() >= MAX_CPU_TEMP_KEYS && gpu_keys.len() >= MAX_GPU_TEMP_KEYS {
break;
}
let code = match self.get_key_code_from_index(index) {
Ok(code) => code,
Err(_) => {
scanned_keys += 1;
continue;
}
};
scanned_keys += 1;
self.classify_key_at(code, &mut cpu_keys, &mut gpu_keys);
}
TempKeyScan {
cpu_keys,
gpu_keys,
scanned_keys,
total_keys,
used_sorted_range: false,
}
}
fn classify_key_at(&self, code: u32, cpu_keys: &mut Vec<String>, gpu_keys: &mut Vec<String>) {
let key = fourcc_to_string(code);
if !is_temperature_key_candidate(&key) {
return;
}
let Ok(key_info) = self.read_key_info(&key) else {
return;
};
match classify_temperature_key(&key, key_info.data_type) {
Some(TempKeyCategory::Cpu) if cpu_keys.len() < MAX_CPU_TEMP_KEYS => cpu_keys.push(key),
Some(TempKeyCategory::Gpu) if gpu_keys.len() < MAX_GPU_TEMP_KEYS => gpu_keys.push(key),
_ => {}
}
}
pub fn read_value(&mut self, key: &str) -> Result<f64, &'static str> {
let key_info = self.read_key_info(key)?;
let key_code = str_to_fourcc(key);
let input = KeyData {
key: key_code,
key_info: KeyInfo {
data_size: key_info.data_size,
..Default::default()
},
data8: SMC_CMD_READ_KEY,
..Default::default()
};
let output = self.read(&input)?;
let value = self.convert_value(&output.bytes, key_info.data_type, key_info.data_size);
Ok(value)
}
fn convert_value(&self, bytes: &[u8; 32], data_type: u32, data_size: u32) -> f64 {
let size = data_size as usize;
if size == 0 || size > 32 {
return 0.0;
}
match data_type {
SMC_TYPE_UI8 => bytes[0] as f64,
SMC_TYPE_UI16 => u16::from_be_bytes([bytes[0], bytes[1]]) as f64,
SMC_TYPE_UI32 => u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64,
SMC_TYPE_FLT => f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64,
SMC_TYPE_SP78 => {
let raw = i16::from_be_bytes([bytes[0], bytes[1]]);
raw as f64 / 256.0
}
SMC_TYPE_FP1F => {
let raw = u16::from_be_bytes([bytes[0], bytes[1]]);
raw as f64 / 32768.0
}
SMC_TYPE_FP2E => {
let raw = u16::from_be_bytes([bytes[0], bytes[1]]);
raw as f64 / 16384.0
}
SMC_TYPE_FP4C => {
let raw = u16::from_be_bytes([bytes[0], bytes[1]]);
raw as f64 / 4096.0
}
SMC_TYPE_FP5B => {
let raw = u16::from_be_bytes([bytes[0], bytes[1]]);
raw as f64 / 2048.0
}
SMC_TYPE_FP6A => {
let raw = u16::from_be_bytes([bytes[0], bytes[1]]);
raw as f64 / 1024.0
}
SMC_TYPE_FP79 => {
let raw = u16::from_be_bytes([bytes[0], bytes[1]]);
raw as f64 / 512.0
}
SMC_TYPE_FP88 => {
let raw = u16::from_be_bytes([bytes[0], bytes[1]]);
raw as f64 / 256.0
}
SMC_TYPE_FPA6 => {
let raw = u16::from_be_bytes([bytes[0], bytes[1]]);
raw as f64 / 64.0
}
SMC_TYPE_FPC4 => {
let raw = u16::from_be_bytes([bytes[0], bytes[1]]);
raw as f64 / 16.0
}
SMC_TYPE_FPE2 => {
let raw = u16::from_be_bytes([bytes[0], bytes[1]]);
raw as f64 / 4.0
}
_ => {
if size == 1 {
bytes[0] as f64
} else if size == 2 {
u16::from_be_bytes([bytes[0], bytes[1]]) as f64
} else if size >= 4 {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f64
} else {
0.0
}
}
}
}
pub fn get_cpu_temperature(&mut self) -> Option<f64> {
let mut temps: Vec<f64> = Vec::new();
let static_keys = [
"Tp01", "Tp02", "Tp05", "Tp06", "Tp09", "Tp0A", "TC0P", "TC0D",
];
for key in static_keys {
if let Ok(value) = self.read_value(key)
&& (10.0..=120.0).contains(&value)
{
temps.push(value);
}
}
if temps.is_empty() {
let discovered = DISCOVERED_KEYS.get_or_init(|| {
let (cpu_keys, gpu_keys) = self.discover_temperature_keys();
DiscoveredTempKeys { cpu_keys, gpu_keys }
});
for key in &discovered.cpu_keys {
if let Ok(value) = self.read_value(key)
&& (10.0..=120.0).contains(&value)
{
temps.push(value);
}
}
}
if temps.is_empty() {
return None;
}
Some(temps.iter().sum::<f64>() / temps.len() as f64)
}
pub fn get_gpu_temperature(&mut self) -> Option<f64> {
let mut temps: Vec<f64> = Vec::new();
let static_keys = ["Tg0f", "Tg0j", "TG0P", "TG0D"];
for key in static_keys {
if let Ok(value) = self.read_value(key)
&& (10.0..=120.0).contains(&value)
{
temps.push(value);
}
}
if temps.is_empty() {
let discovered = DISCOVERED_KEYS.get_or_init(|| {
let (cpu_keys, gpu_keys) = self.discover_temperature_keys();
DiscoveredTempKeys { cpu_keys, gpu_keys }
});
for key in &discovered.gpu_keys {
if let Ok(value) = self.read_value(key)
&& (10.0..=120.0).contains(&value)
{
temps.push(value);
}
}
}
if temps.is_empty() {
return None;
}
Some(temps.iter().sum::<f64>() / temps.len() as f64)
}
pub fn get_system_power(&mut self) -> Option<f64> {
let value = self.read_value("PSTR").ok()?;
is_plausible_power_watts(value).then_some(value)
}
pub fn get_cpu_package_power(&mut self) -> Option<f64> {
const CPU_POWER_KEYS: [&str; 3] = ["PCPT", "PCPC", "PC0C"];
for key in CPU_POWER_KEYS {
if let Ok(value) = self.read_value(key)
&& is_plausible_power_watts(value)
&& value > 0.0
{
return Some(value);
}
}
None
}
pub fn get_fan_readings(&mut self) -> Vec<FanReading> {
let mut fans = Vec::new();
let fan_count = match self.read_value("FNum") {
Ok(v) if v.is_finite() && v >= 0.0 => v as u32,
_ => 2, };
for index in 0..fan_count.min(MAX_FANS) {
let actual_rpm = match self.read_value(&format!("F{index}Ac")) {
Ok(v) if is_plausible_fan_rpm(v) => v as u32,
_ => continue,
};
let max_rpm = match self.read_value(&format!("F{index}Mx")) {
Ok(v) if is_plausible_fan_rpm(v) => v as u32,
_ => 0,
};
fans.push(FanReading {
index,
actual_rpm,
max_rpm,
});
}
fans
}
pub fn get_fan_speeds(&mut self) -> Vec<(String, u32)> {
self.get_fan_readings()
.into_iter()
.map(|fan| (fan.name(), fan.actual_rpm))
.collect()
}
}
#[derive(Default)]
pub enum SmcConnection {
#[default]
Unopened,
Open(SMC),
Unavailable,
}
impl SmcConnection {
pub fn get(&mut self) -> Option<&mut SMC> {
if matches!(self, SmcConnection::Unopened) {
*self = match SMC::new() {
Ok(smc) => SmcConnection::Open(smc),
Err(_) => SmcConnection::Unavailable,
};
}
match self {
SmcConnection::Open(smc) => Some(smc),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FanReading {
pub index: u32,
pub actual_rpm: u32,
pub max_rpm: u32,
}
impl FanReading {
pub fn name(&self) -> String {
format!("Fan {}", self.index)
}
}
impl Drop for SMC {
fn drop(&mut self) {
if self.conn != 0 {
unsafe {
IOServiceClose(self.conn);
}
}
}
}
unsafe impl Send for SMC {}
#[derive(Debug, Default, Clone)]
pub struct SMCMetrics {
pub cpu_temperature: Option<f64>,
pub gpu_temperature: Option<f64>,
pub system_power: Option<f64>,
pub fan_speeds: Vec<(String, u32)>,
}
impl SMCMetrics {
pub fn collect() -> Self {
let mut metrics = Self::default();
if let Ok(mut smc) = SMC::new() {
metrics.cpu_temperature = smc.get_cpu_temperature();
metrics.gpu_temperature = smc.get_gpu_temperature();
metrics.system_power = smc.get_system_power();
metrics.fan_speeds = smc.get_fan_speeds();
}
metrics
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn name_prefilter_accepts_everything_the_classifier_can() {
let names = [
"Tp01", "Tp0X", "Te05", "Tg0f", "Tg7L", "TC0P", "TC0D", "TG0P", "TG0D", "TB0T", "TW0P",
"Ts0P", "PSTR", "F0Ac", "#KEY", "Tp", "T", "",
];
for name in names {
for data_type in [SMC_TYPE_FLT, SMC_TYPE_SP78, SMC_TYPE_UI8, SMC_TYPE_UI32] {
if classify_temperature_key(name, data_type).is_some() {
assert!(
is_temperature_key_candidate(name),
"{name} classifies but the pre-filter would have skipped it"
);
}
}
}
}
#[test]
fn name_prefilter_rejects_non_temperature_names() {
for name in [
"PSTR", "F0Ac", "#KEY", "TB0T", "TW0P", "Ts0P", "VP0R", "T", "",
] {
assert!(
!is_temperature_key_candidate(name),
"{name} should not cost a key-info round trip"
);
}
for name in ["Tp01", "Te05", "Tg0f", "TC0P", "TG0D"] {
assert!(is_temperature_key_candidate(name), "{name} must be probed");
}
}
#[test]
fn every_candidate_name_sorts_inside_the_scanned_span() {
for second in *b"pegCG" {
for low in [0x00u8, b'0', b'z', 0xFF] {
let code = u32::from_be_bytes([b'T', second, low, low]);
assert!(
(TEMP_KEY_RANGE_START..TEMP_KEY_RANGE_END).contains(&code),
"{code:#010x} falls outside the scanned span"
);
}
}
assert!(str_to_fourcc("Sxxx") < TEMP_KEY_RANGE_START);
assert!(str_to_fourcc("U000") >= TEMP_KEY_RANGE_END);
}
#[test]
fn gpu_key_cap_clears_real_sensor_counts() {
const {
assert!(
MAX_GPU_TEMP_KEYS > 84,
"an M5 Max exposes 84 Tg* sensors; the cap must not truncate them"
);
assert!(
MAX_CPU_TEMP_KEYS > 23,
"an M5 Max exposes 23 Tp*/Te* sensors"
);
}
}
#[test]
fn fourcc_round_trips_through_its_name() {
for name in ["Tp01", "Tg0f", "TC0P", "#KEY", "PSTR"] {
assert_eq!(fourcc_to_string(str_to_fourcc(name)), name);
}
}
#[test]
#[cfg(target_os = "macos")]
fn discovery_stops_before_the_end_of_the_key_table() {
let Ok(smc) = SMC::new() else {
return;
};
let scan = smc.scan_temperature_keys();
if scan.total_keys == 0 {
return;
}
assert!(
scan.scanned_keys < scan.total_keys,
"scanned {} of {} keys; discovery walked the whole table",
scan.scanned_keys,
scan.total_keys
);
}
#[test]
#[cfg(target_os = "macos")]
fn discovered_keys_are_classifiable_and_capped() {
let Ok(smc) = SMC::new() else {
return;
};
let scan = smc.scan_temperature_keys();
assert!(scan.cpu_keys.len() <= MAX_CPU_TEMP_KEYS);
assert!(scan.gpu_keys.len() <= MAX_GPU_TEMP_KEYS);
for key in scan.cpu_keys.iter().chain(scan.gpu_keys.iter()) {
assert!(
is_temperature_key_candidate(key),
"{key} is not a temperature sensor name"
);
}
}
#[test]
#[cfg(target_os = "macos")]
fn the_span_search_finds_what_the_full_scan_finds() {
let Ok(smc) = SMC::new() else {
return;
};
let total_keys = match smc.get_key_count() {
Ok(count) if count > 0 => count,
_ => return,
};
let Some(fast) = smc.scan_sorted_temperature_range(total_keys) else {
return;
};
let full = smc.scan_all_keys(total_keys);
assert!(fast.used_sorted_range, "the fast path must report itself");
assert!(!full.used_sorted_range, "the fallback must report itself");
assert_eq!(fast.cpu_keys, full.cpu_keys, "CPU keys disagree");
assert_eq!(fast.gpu_keys, full.gpu_keys, "GPU keys disagree");
assert!(
fast.scanned_keys < full.scanned_keys,
"the span search read {} keys against the full scan's {}",
fast.scanned_keys,
full.scanned_keys
);
}
#[test]
fn test_fourcc_conversion() {
assert_eq!(str_to_fourcc("TC0P"), u32::from_be_bytes(*b"TC0P"));
assert_eq!(str_to_fourcc("PSTR"), u32::from_be_bytes(*b"PSTR"));
}
#[test]
fn test_invalid_fourcc() {
assert_eq!(str_to_fourcc("ABC"), 0); assert_eq!(str_to_fourcc("ABCDE"), 0); }
#[test]
fn test_smc_type_flt_constant() {
assert_eq!(SMC_TYPE_FLT, 1718383648);
assert_eq!(SMC_TYPE_FLT, u32::from_be_bytes(*b"flt "));
}
#[test]
fn test_hash_key_fourcc() {
assert_eq!(str_to_fourcc("#KEY"), u32::from_be_bytes(*b"#KEY"));
}
#[test]
fn classifies_apple_silicon_float_sensors() {
assert_eq!(
classify_temperature_key("Tp01", SMC_TYPE_FLT),
Some(TempKeyCategory::Cpu)
);
assert_eq!(
classify_temperature_key("Te05", SMC_TYPE_FLT),
Some(TempKeyCategory::Cpu)
);
assert_eq!(
classify_temperature_key("Tg0f", SMC_TYPE_FLT),
Some(TempKeyCategory::Gpu)
);
}
#[test]
fn classifies_intel_fixed_point_sensors() {
assert_eq!(
classify_temperature_key("TC0P", SMC_TYPE_SP78),
Some(TempKeyCategory::Cpu)
);
assert_eq!(
classify_temperature_key("TC0D", SMC_TYPE_SP78),
Some(TempKeyCategory::Cpu)
);
assert_eq!(
classify_temperature_key("TG0P", SMC_TYPE_SP78),
Some(TempKeyCategory::Gpu)
);
assert_eq!(
classify_temperature_key("TG0D", SMC_TYPE_SP78),
Some(TempKeyCategory::Gpu)
);
}
#[test]
fn temperature_families_do_not_cross_data_types() {
assert_eq!(classify_temperature_key("TC0P", SMC_TYPE_FLT), None);
assert_eq!(classify_temperature_key("TG0P", SMC_TYPE_FLT), None);
assert_eq!(classify_temperature_key("Tp01", SMC_TYPE_SP78), None);
assert_eq!(classify_temperature_key("Tg0f", SMC_TYPE_SP78), None);
}
#[test]
fn rejects_non_temperature_keys() {
assert_eq!(classify_temperature_key("Fp01", SMC_TYPE_FLT), None);
assert_eq!(classify_temperature_key("TA0P", SMC_TYPE_SP78), None);
assert_eq!(classify_temperature_key("PSTR", SMC_TYPE_FLT), None);
assert_eq!(classify_temperature_key("F0Ac", SMC_TYPE_FLT), None);
assert_eq!(classify_temperature_key("Tp01", SMC_TYPE_UI32), None);
assert_eq!(classify_temperature_key("TC0P", SMC_TYPE_UI8), None);
assert_eq!(classify_temperature_key("", SMC_TYPE_FLT), None);
assert_eq!(classify_temperature_key("T", SMC_TYPE_FLT), None);
}
#[test]
fn test_sp78_fixed_point_decoding() {
let smc = SMC { conn: 0 }; let mut bytes = [0u8; 32];
bytes[0..2].copy_from_slice(&0x3480_i16.to_be_bytes());
let value = smc.convert_value(&bytes, SMC_TYPE_SP78, 2);
assert!(
(value - 52.5).abs() < 0.01,
"expected ~52.5, got {value} for the Intel sp78 temperature encoding"
);
}
#[test]
fn fan_reading_names_are_indexed() {
let fan = FanReading {
index: 1,
actual_rpm: 2100,
max_rpm: 5500,
};
assert_eq!(fan.name(), "Fan 1");
}
#[test]
fn test_flt_little_endian_decoding() {
let smc = SMC { conn: 0 }; let mut bytes = [0u8; 32];
bytes[0..4].copy_from_slice(&0x424ccccd_u32.to_le_bytes());
let value = smc.convert_value(&bytes, SMC_TYPE_FLT, 4);
assert!(
(value - 51.2).abs() < 0.01,
"expected ~51.2, got {value} — float endianness may have regressed"
);
}
#[test]
fn plausible_power_watts_rejects_non_finite_negative_and_out_of_range_values() {
assert!(is_plausible_power_watts(0.0));
assert!(is_plausible_power_watts(127.99)); assert!(is_plausible_power_watts(MAX_PLAUSIBLE_POWER_WATTS));
assert!(!is_plausible_power_watts(-0.01));
assert!(!is_plausible_power_watts(MAX_PLAUSIBLE_POWER_WATTS + 0.01));
assert!(!is_plausible_power_watts(f64::NAN));
assert!(!is_plausible_power_watts(f64::INFINITY));
assert!(!is_plausible_power_watts(f64::NEG_INFINITY));
}
#[test]
fn plausible_fan_rpm_rejects_non_finite_negative_and_out_of_range_values() {
assert!(is_plausible_fan_rpm(0.0));
assert!(is_plausible_fan_rpm(5500.0));
assert!(is_plausible_fan_rpm(MAX_PLAUSIBLE_FAN_RPM));
assert!(!is_plausible_fan_rpm(-1.0));
assert!(!is_plausible_fan_rpm(MAX_PLAUSIBLE_FAN_RPM + 0.01));
assert!(!is_plausible_fan_rpm(f64::NAN));
assert!(!is_plausible_fan_rpm(f64::INFINITY));
}
}