use std::ffi::c_void;
use std::sync::OnceLock;
const MAX_TEMP_KEYS: usize = 64;
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 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],
}
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);
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)
}
pub fn get_key_from_index(&self, index: u32) -> Result<String, &'static str> {
let input = KeyData {
data8: SMC_CMD_READ_INDEX,
data32: index,
..Default::default()
};
let output = self.read(&input)?;
let key = output.key;
let key_bytes = key.to_be_bytes();
let key_str = String::from_utf8_lossy(&key_bytes).to_string();
Ok(key_str)
}
pub fn discover_temperature_keys(&self) -> (Vec<String>, Vec<String>) {
let mut cpu_keys = Vec::with_capacity(MAX_TEMP_KEYS);
let mut gpu_keys = Vec::with_capacity(MAX_TEMP_KEYS);
let key_count = match self.get_key_count() {
Ok(count) => count,
Err(_) => return (cpu_keys, gpu_keys),
};
for i in 0..key_count {
if cpu_keys.len() >= MAX_TEMP_KEYS && gpu_keys.len() >= MAX_TEMP_KEYS {
break;
}
let key = match self.get_key_from_index(i) {
Ok(k) => k,
Err(_) => continue,
};
let key_info = match self.read_key_info(&key) {
Ok(info) => info,
Err(_) => continue,
};
if key_info.data_type != SMC_TYPE_FLT {
continue;
}
let key_bytes = key.as_bytes();
if key_bytes.len() < 2 {
continue;
}
if key_bytes[0] == b'T'
&& (key_bytes[1] == b'p' || key_bytes[1] == b'e')
&& cpu_keys.len() < MAX_TEMP_KEYS
{
cpu_keys.push(key);
}
else if key_bytes[0] == b'T' && key_bytes[1] == b'g' && gpu_keys.len() < MAX_TEMP_KEYS
{
gpu_keys.push(key);
}
}
(cpu_keys, gpu_keys)
}
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> {
self.read_value("PSTR").ok()
}
pub fn get_fan_speeds(&mut self) -> Vec<(String, u32)> {
let mut fans = Vec::new();
let fan_count = match self.read_value("FNum") {
Ok(v) => v as u32,
Err(_) => 2, };
for i in 0..fan_count.min(8) {
let key = format!("F{i}Ac");
if let Ok(speed) = self.read_value(&key) {
fans.push((format!("Fan {i}"), speed as u32));
}
}
fans
}
}
impl Drop for SMC {
fn drop(&mut self) {
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 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 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"
);
}
}