SMC Interface Engine (AppleSMC IOKit Client) #2

Closed
opened 2026-09-08 10:18:21 +01:00 by gronod · 2 comments
Owner

Purpose

Provide a robust low-level interface to the Apple System Management Controller (AppleSMC) on Intel Macs via IOKit. The AppleSMC driver governs critical Intel hardware telemetry including core temperatures, fan RPMs, voltage/current levels, and power consumption.

Priority

Critical (Prerequisite for all temperature, fan, voltage, and thermal monitoring)

Dependencies

  • Depends on #1 (Core Application Architecture & Objective-C/SwiftUI Bridging Foundation)

Scope

  • Connect to the AppleSMC service in the IOKit registry via an Objective-C wrapper.
  • Implement SMC key discovery, key info retrieval (kSMCGetKeyInfo), and data reading (kSMCReadKey).
  • Decode common SMC numeric encodings:
    • sp78 (Signed 8.8 fixed-point integer for temperatures)
    • fpe2 (Unsigned 14.2 fixed-point integer for fan speeds)
    • ui8, ui16, ui32 (Unsigned integers for counters and state flags)
    • flt (32-bit floating point numbers)
  • Safe session lifecycle: opening client connections, caching discovered keys, handling sleep/wake transitions, and closing connections cleanly.

Implementation Suggestions

  • Implement in an Objective-C class (MMAppleSMCClient) interfacing with IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSMC")) and IOServiceOpen().
  • Use IOConnectCallStructMethod targeting SMC selector indices to communicate with the SMC driver.
  • Cache key attributes (data size, data type) in an in-memory hash map to avoid repeated key-info lookup overhead during sampling intervals.
  • Include defensive checks against null descriptors or missing SMC keys to ensure stability across diverse Intel Mac models (MacBook, iMac, Mac Pro, Mac mini).
### Purpose Provide a robust low-level interface to the Apple System Management Controller (AppleSMC) on Intel Macs via IOKit. The AppleSMC driver governs critical Intel hardware telemetry including core temperatures, fan RPMs, voltage/current levels, and power consumption. ### Priority **Critical** (Prerequisite for all temperature, fan, voltage, and thermal monitoring) ### Dependencies - Depends on #1 (Core Application Architecture & Objective-C/SwiftUI Bridging Foundation) ### Scope - Connect to the `AppleSMC` service in the IOKit registry via an Objective-C wrapper. - Implement SMC key discovery, key info retrieval (`kSMCGetKeyInfo`), and data reading (`kSMCReadKey`). - Decode common SMC numeric encodings: - `sp78` (Signed 8.8 fixed-point integer for temperatures) - `fpe2` (Unsigned 14.2 fixed-point integer for fan speeds) - `ui8`, `ui16`, `ui32` (Unsigned integers for counters and state flags) - `flt` (32-bit floating point numbers) - Safe session lifecycle: opening client connections, caching discovered keys, handling sleep/wake transitions, and closing connections cleanly. ### Implementation Suggestions - Implement in an Objective-C class (`MMAppleSMCClient`) interfacing with `IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSMC"))` and `IOServiceOpen()`. - Use `IOConnectCallStructMethod` targeting SMC selector indices to communicate with the SMC driver. - Cache key attributes (data size, data type) in an in-memory hash map to avoid repeated key-info lookup overhead during sampling intervals. - Include defensive checks against null descriptors or missing SMC keys to ensure stability across diverse Intel Mac models (MacBook, iMac, Mac Pro, Mac mini).
gronod added the Kind/Feature
Priority
Critical
1
labels 2026-09-08 10:18:21 +01:00
gronod added the Project/AntigravityFeature/Backend labels 2026-09-08 10:28:18 +01:00
Author
Owner

Technical Implementation Plan & Code Solution

1. Low-Level Architecture & IOKit Mach Structures

The Apple System Management Controller (AppleSMC) communicates via the AppleSMCClient user client class in IOKit. Calls are made using Mach struct method calls via IOConnectCallStructMethod.

Core C Definitions & Structures (SMCInterface.h)

#ifndef SMCInterface_h
#define SMCInterface_h

#import <IOKit/IOKitLib.h>

#define KERNEL_INDEX_SMC 2

enum {
    kSMCUserClientOpen  = 0,
    kSMCUserClientClose = 1,
    kSMCHandleYPCEvent  = 2,
    kSMCReadKey         = 5,
    kSMCWriteKey        = 6,
    kSMCGetKeyFromIndex = 8,
    kSMCGetKeyInfo      = 9
};

typedef struct {
    uint8_t   major;
    uint8_t   minor;
    uint8_t   build;
    uint8_t   reserved;
    uint16_t  release;
} SMCKeyData_vers_t;

typedef struct {
    uint16_t  version;
    uint16_t  length;
    uint32_t  cpuPLimit;
    uint32_t  gpuPLimit;
    uint32_t  memPLimit;
} SMCKeyData_pLimitData_t;

typedef struct {
    uint32_t  dataSize;
    uint32_t  dataType;
    uint8_t   dataAttributes;
} SMCKeyData_keyInfo_t;

typedef uint8_t SMCBytes_t[32];

typedef struct {
    uint32_t               key;
    SMCKeyData_vers_t      vers;
    SMCKeyData_pLimitData_t pLimitData;
    SMCKeyData_keyInfo_t   keyInfo;
    uint8_t                result;
    uint8_t                status;
    uint8_t                data8;
    uint32_t               data32;
    SMCBytes_t             bytes;
} SMCKeyData_t;

typedef struct {
    uint32_t  dataSize;
    uint32_t  dataType;
    SMCBytes_t bytes;
} SMCVal_t;

#endif

2. Client Class Implementation (MMAppleSMCClient.m)

#import "MMAppleSMCClient.h"
#import "SMCInterface.h"

@interface MMAppleSMCClient () {
    io_connect_t _connection;
    os_unfair_lock _lock;
}
@property (nonatomic, strong) NSCache<NSString *, NSValue *> *keyInfoCache;
@end

@implementation MMAppleSMCClient

+ (instancetype)sharedClient {
    static MMAppleSMCClient *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[MMAppleSMCClient alloc] init];
    });
    return instance;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _lock = OS_UNFAIR_LOCK_INIT;
        _keyInfoCache = [[NSCache alloc] init];
        [self openSMCConnection];
    }
    return self;
}

- (void)dealloc {
    [self closeSMCConnection];
}

- (BOOL)openSMCConnection {
    if (_connection != 0) return YES;

    io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSMC"));
    if (!service) return NO;

    kern_return_t result = IOServiceOpen(service, mach_task_self(), 0, &_connection);
    IOObjectRelease(service);
    return (result == kIOReturnSuccess);
}

- (void)closeSMCConnection {
    if (_connection != 0) {
        IOServiceClose(_connection);
        _connection = 0;
    }
}

- (uint32_t)fourCharCodeFromKey:(NSString *)key {
    uint32_t code = 0;
    const char *str = [key UTF8String];
    for (int i = 0; i < 4 && str[i] != '\0'; i++) {
        code = (code << 8) | (uint8_t)str[i];
    }
    return code;
}

- (BOOL)callSMCWithInput:(SMCKeyData_t *)input output:(SMCKeyData_t *)output {
    size_t inSize = sizeof(SMCKeyData_t);
    size_t outSize = sizeof(SMCKeyData_t);

    kern_return_t result = IOConnectCallStructMethod(
        self->_connection,
        KERNEL_INDEX_SMC,
        input,
        inSize,
        output,
        &outSize
    );
    return (result == kIOReturnSuccess && output->result == 0);
}

- (BOOL)readKey:(NSString *)key value:(SMCVal_t *)val {
    os_unfair_lock_lock(&_lock);

    if (_connection == 0) {
        if (![self openSMCConnection]) {
            os_unfair_lock_unlock(&_lock);
            return NO;
        }
    }

    uint32_t fourCC = [self fourCharCodeFromKey:key];
    SMCKeyData_t input;
    SMCKeyData_t output;
    memset(&input, 0, sizeof(SMCKeyData_t));
    memset(&output, 0, sizeof(SMCKeyData_t));

    // 1. Get Key Info
    input.key = fourCC;
    input.data8 = kSMCGetKeyInfo;
    if (![self callSMCWithInput:&input output:&output]) {
        os_unfair_lock_unlock(&_lock);
        return NO;
    }

    val->dataSize = output.keyInfo.dataSize;
    val->dataType = output.keyInfo.dataType;

    // 2. Read Key Data
    memset(&input, 0, sizeof(SMCKeyData_t));
    input.key = fourCC;
    input.keyInfo.dataSize = val->dataSize;
    input.data8 = kSMCReadKey;

    if (![self callSMCWithInput:&input output:&output]) {
        os_unfair_lock_unlock(&_lock);
        return NO;
    }

    memcpy(val->bytes, output.bytes, sizeof(output.bytes));
    os_unfair_lock_unlock(&_lock);
    return YES;
}

// MARK: - Numeric Parsers

- (nullable NSNumber *)readTemperatureForKey:(NSString *)key {
    SMCVal_t val;
    if (![self readKey:key value:&val]) return nil;

    // sp78: signed 8.8 fixed-point integer
    if (val.dataType == [self fourCharCodeFromKey:@"sp78"] && val.dataSize == 2) {
        int16_t raw = (val.bytes[0] << 8) | val.bytes[1];
        float tempC = (float)raw / 256.0f;
        return (tempC > 0 && tempC < 130) ? @(tempC) : nil;
    }
    // flt: 32-bit float
    if (val.dataType == [self fourCharCodeFromKey:@"flt "] && val.dataSize == 4) {
        float tempC;
        memcpy(&tempC, val.bytes, 4);
        return (tempC > 0 && tempC < 130) ? @(tempC) : nil;
    }
    return nil;
}

- (nullable NSNumber *)readFanSpeedForKey:(NSString *)key {
    SMCVal_t val;
    if (![self readKey:key value:&val]) return nil;

    // fpe2: unsigned 14.2 fixed-point
    if (val.dataType == [self fourCharCodeFromKey:@"fpe2"] && val.dataSize == 2) {
        uint16_t raw = (val.bytes[0] << 8) | val.bytes[1];
        float rpm = (float)raw / 4.0f;
        return @(rpm);
    }
    return nil;
}
@end

3. Edge Cases & Resilience

  • OS Unfair Lock: SMC calls must be synchronized across threads; os_unfair_lock provides zero-overhead thread synchronization.
  • Sleep & Wake Handling: On sleep, close _connection via IOServiceClose to prevent stale Mach port handles upon waking. Re-open dynamically on first read.
## Technical Implementation Plan & Code Solution ### 1. Low-Level Architecture & IOKit Mach Structures The Apple System Management Controller (AppleSMC) communicates via the `AppleSMCClient` user client class in IOKit. Calls are made using Mach struct method calls via `IOConnectCallStructMethod`. #### Core C Definitions & Structures (`SMCInterface.h`) ```objc #ifndef SMCInterface_h #define SMCInterface_h #import <IOKit/IOKitLib.h> #define KERNEL_INDEX_SMC 2 enum { kSMCUserClientOpen = 0, kSMCUserClientClose = 1, kSMCHandleYPCEvent = 2, kSMCReadKey = 5, kSMCWriteKey = 6, kSMCGetKeyFromIndex = 8, kSMCGetKeyInfo = 9 }; typedef struct { uint8_t major; uint8_t minor; uint8_t build; uint8_t reserved; uint16_t release; } SMCKeyData_vers_t; typedef struct { uint16_t version; uint16_t length; uint32_t cpuPLimit; uint32_t gpuPLimit; uint32_t memPLimit; } SMCKeyData_pLimitData_t; typedef struct { uint32_t dataSize; uint32_t dataType; uint8_t dataAttributes; } SMCKeyData_keyInfo_t; typedef uint8_t SMCBytes_t[32]; typedef struct { uint32_t key; SMCKeyData_vers_t vers; SMCKeyData_pLimitData_t pLimitData; SMCKeyData_keyInfo_t keyInfo; uint8_t result; uint8_t status; uint8_t data8; uint32_t data32; SMCBytes_t bytes; } SMCKeyData_t; typedef struct { uint32_t dataSize; uint32_t dataType; SMCBytes_t bytes; } SMCVal_t; #endif ``` --- ### 2. Client Class Implementation (`MMAppleSMCClient.m`) ```objc #import "MMAppleSMCClient.h" #import "SMCInterface.h" @interface MMAppleSMCClient () { io_connect_t _connection; os_unfair_lock _lock; } @property (nonatomic, strong) NSCache<NSString *, NSValue *> *keyInfoCache; @end @implementation MMAppleSMCClient + (instancetype)sharedClient { static MMAppleSMCClient *instance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[MMAppleSMCClient alloc] init]; }); return instance; } - (instancetype)init { self = [super init]; if (self) { _lock = OS_UNFAIR_LOCK_INIT; _keyInfoCache = [[NSCache alloc] init]; [self openSMCConnection]; } return self; } - (void)dealloc { [self closeSMCConnection]; } - (BOOL)openSMCConnection { if (_connection != 0) return YES; io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSMC")); if (!service) return NO; kern_return_t result = IOServiceOpen(service, mach_task_self(), 0, &_connection); IOObjectRelease(service); return (result == kIOReturnSuccess); } - (void)closeSMCConnection { if (_connection != 0) { IOServiceClose(_connection); _connection = 0; } } - (uint32_t)fourCharCodeFromKey:(NSString *)key { uint32_t code = 0; const char *str = [key UTF8String]; for (int i = 0; i < 4 && str[i] != '\0'; i++) { code = (code << 8) | (uint8_t)str[i]; } return code; } - (BOOL)callSMCWithInput:(SMCKeyData_t *)input output:(SMCKeyData_t *)output { size_t inSize = sizeof(SMCKeyData_t); size_t outSize = sizeof(SMCKeyData_t); kern_return_t result = IOConnectCallStructMethod( self->_connection, KERNEL_INDEX_SMC, input, inSize, output, &outSize ); return (result == kIOReturnSuccess && output->result == 0); } - (BOOL)readKey:(NSString *)key value:(SMCVal_t *)val { os_unfair_lock_lock(&_lock); if (_connection == 0) { if (![self openSMCConnection]) { os_unfair_lock_unlock(&_lock); return NO; } } uint32_t fourCC = [self fourCharCodeFromKey:key]; SMCKeyData_t input; SMCKeyData_t output; memset(&input, 0, sizeof(SMCKeyData_t)); memset(&output, 0, sizeof(SMCKeyData_t)); // 1. Get Key Info input.key = fourCC; input.data8 = kSMCGetKeyInfo; if (![self callSMCWithInput:&input output:&output]) { os_unfair_lock_unlock(&_lock); return NO; } val->dataSize = output.keyInfo.dataSize; val->dataType = output.keyInfo.dataType; // 2. Read Key Data memset(&input, 0, sizeof(SMCKeyData_t)); input.key = fourCC; input.keyInfo.dataSize = val->dataSize; input.data8 = kSMCReadKey; if (![self callSMCWithInput:&input output:&output]) { os_unfair_lock_unlock(&_lock); return NO; } memcpy(val->bytes, output.bytes, sizeof(output.bytes)); os_unfair_lock_unlock(&_lock); return YES; } // MARK: - Numeric Parsers - (nullable NSNumber *)readTemperatureForKey:(NSString *)key { SMCVal_t val; if (![self readKey:key value:&val]) return nil; // sp78: signed 8.8 fixed-point integer if (val.dataType == [self fourCharCodeFromKey:@"sp78"] && val.dataSize == 2) { int16_t raw = (val.bytes[0] << 8) | val.bytes[1]; float tempC = (float)raw / 256.0f; return (tempC > 0 && tempC < 130) ? @(tempC) : nil; } // flt: 32-bit float if (val.dataType == [self fourCharCodeFromKey:@"flt "] && val.dataSize == 4) { float tempC; memcpy(&tempC, val.bytes, 4); return (tempC > 0 && tempC < 130) ? @(tempC) : nil; } return nil; } - (nullable NSNumber *)readFanSpeedForKey:(NSString *)key { SMCVal_t val; if (![self readKey:key value:&val]) return nil; // fpe2: unsigned 14.2 fixed-point if (val.dataType == [self fourCharCodeFromKey:@"fpe2"] && val.dataSize == 2) { uint16_t raw = (val.bytes[0] << 8) | val.bytes[1]; float rpm = (float)raw / 4.0f; return @(rpm); } return nil; } @end ``` --- ### 3. Edge Cases & Resilience - **OS Unfair Lock**: SMC calls must be synchronized across threads; `os_unfair_lock` provides zero-overhead thread synchronization. - **Sleep & Wake Handling**: On sleep, close `_connection` via `IOServiceClose` to prevent stale Mach port handles upon waking. Re-open dynamically on first read.
gronod added this to the M1: Architecture Foundation & Hardware Abstraction milestone 2026-09-08 10:41:52 +01:00
gronod added a new dependency 2026-09-08 11:35:06 +01:00
gronod added a new dependency 2026-09-08 11:37:03 +01:00
gronod added a new dependency 2026-09-08 11:37:03 +01:00
gronod added a new dependency 2026-09-08 11:37:09 +01:00
Author
Owner

Implementation Completed (Milestone 1)

The SMC Interface Engine (AppleSMC IOKit Client) has been implemented in commit dba5af3 on branch feat/2-smc-client and merged into milestone/m1-foundation (commit a274927).

Delivered Components:

  • SMC Definitions (MMSMCDefines.h): Low-level Mach/IOKit structure definitions (SMCKeyData_t, SMCVal_t, SMCKeyInfoData) and command selectors (kSMCHandleYPCEvent, kSMCReadKey, kSMCGetKeyInfo).
  • Zero-Allocation Binary Parsers (MMSMCParser.m): High-performance decoders for fixed-point sp78 (temperature), fpe2 (fan RPM), flt (IEEE 754), ui8, ui16, ui32, and boolean flags.
  • AppleSMC IOKit Client (MMAppleSMCClient.m): Thread-safe driver interface using os_unfair_lock, connecting to AppleSMCClient via IOServiceOpen, automatic key info caching, sensor key enumeration (#KEY), and fallback handling for non-Mac/virtualized environments.
  • Bridging Exposure: Integrated into MacMonitor-Bridging-Header.h.
  • Synthetic Unit Test Suite (MMSMCTests.swift): 6 comprehensive unit tests verifying numeric decoding precision against synthetic byte buffers and driver availability probing. 100% passing.
### Implementation Completed (Milestone 1) The SMC Interface Engine (AppleSMC IOKit Client) has been implemented in commit `dba5af3` on branch `feat/2-smc-client` and merged into `milestone/m1-foundation` (commit `a274927`). **Delivered Components:** - **SMC Definitions (`MMSMCDefines.h`)**: Low-level Mach/IOKit structure definitions (`SMCKeyData_t`, `SMCVal_t`, `SMCKeyInfoData`) and command selectors (`kSMCHandleYPCEvent`, `kSMCReadKey`, `kSMCGetKeyInfo`). - **Zero-Allocation Binary Parsers (`MMSMCParser.m`)**: High-performance decoders for fixed-point `sp78` (temperature), `fpe2` (fan RPM), `flt` (IEEE 754), `ui8`, `ui16`, `ui32`, and boolean flags. - **AppleSMC IOKit Client (`MMAppleSMCClient.m`)**: Thread-safe driver interface using `os_unfair_lock`, connecting to `AppleSMCClient` via `IOServiceOpen`, automatic key info caching, sensor key enumeration (`#KEY`), and fallback handling for non-Mac/virtualized environments. - **Bridging Exposure**: Integrated into `MacMonitor-Bridging-Header.h`. - **Synthetic Unit Test Suite (`MMSMCTests.swift`)**: 6 comprehensive unit tests verifying numeric decoding precision against synthetic byte buffers and driver availability probing. 100% passing.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/MacMonitor#2