import Foundation import IOKit import OSLog private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "SMC") /// Liest Werte aus dem System Management Controller. **Ausschließlich lesend.** /// /// Es gibt hier bewusst keinen Schreibpfad. Lüfterdrehzahl und Ladegrenze /// verlangen root und gehören in den privilegierten Helfer aus Phase 6 — nicht /// in einen Typ, der im normalen App-Prozess läuft und von jedem Widget /// erreichbar ist. /// /// Die Struktur und die Erkenntnisse stammen aus `docs/spikes/A-smc.md`, gemessen /// auf Mac17,9. Wichtigster Punkt von dort: **alle Mehrbyte-Werte sind /// little-endian.** Der verbreitete Beispielcode nimmt big-endian an — er /// stammt aus der Intel-Ära und liefert auf Apple Silicon Unsinn. public final class SMCReader: @unchecked Sendable { // MARK: - Aufbau des AppleSMC-UserClients private struct Version { var major: UInt8 = 0; var minor: UInt8 = 0 var build: UInt8 = 0; var reserved: UInt8 = 0 var release: UInt16 = 0 } private struct PLimitData { var version: UInt16 = 0; var length: UInt16 = 0 var cpuPLimit: UInt32 = 0; var gpuPLimit: UInt32 = 0 var memPLimit: UInt32 = 0 } private struct KeyInfo { var dataSize: UInt32 = 0; var dataType: UInt32 = 0 var dataAttributes: UInt8 = 0 } private struct Param { var key: UInt32 = 0 var vers = Version() var pLimitData = PLimitData() var keyInfo = KeyInfo() var padding: UInt16 = 0 var result: UInt8 = 0 var status: UInt8 = 0 var data8: UInt8 = 0 var data32: UInt32 = 0 var bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) } private static let handleYPCEvent: UInt32 = 2 private static let readKey: UInt8 = 5 private static let getKeyFromIndex: UInt8 = 8 private static let getKeyInfo: UInt8 = 9 private var connection: io_connect_t = 0 private let lock = NSLock() /// Typ und Größe je Key. Beides steht in der Firmware fest und ändert sich /// zur Laufzeit nicht — es bei jedem Lesen erneut zu erfragen verdoppelt /// die Zahl der IOKit-Aufrufe. Bei fünfzig Temperaturfühlern im Sekundentakt /// ist das der Unterschied zwischen spürbar und unmerklich. private var keyInfoCache: [String: KeyInfo] = [:] public init?() { let service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSMC")) guard service != 0 else { log.error("AppleSMC nicht gefunden — Sensoren bleiben leer") return nil } defer { IOObjectRelease(service) } guard IOServiceOpen(service, mach_task_self_, 0, &connection) == kIOReturnSuccess else { log.error("AppleSMC nicht zu öffnen") return nil } } deinit { if connection != 0 { IOServiceClose(connection) } } // MARK: - Lesen /// Ein `flt`-Wert (Lüfterdrehzahl, Temperatur, Leistung). public func float(_ key: String) -> Double? { guard let value = read(key), value.type == "flt ", value.bytes.count >= 4 else { return nil } // Little-endian, siehe Klassenkommentar. let bits = UInt32(value.bytes[3]) << 24 | UInt32(value.bytes[2]) << 16 | UInt32(value.bytes[1]) << 8 | UInt32(value.bytes[0]) return Double(Float(bitPattern: bits)) } /// Ein vorzeichenloser Ganzzahlwert beliebiger Breite. public func integer(_ key: String) -> UInt64? { guard let value = read(key) else { return nil } var result: UInt64 = 0 for (index, byte) in value.bytes.prefix(8).enumerated() { result |= UInt64(byte) << (8 * index) // little-endian } return result } public func exists(_ key: String) -> Bool { read(key) != nil } /// Alle Keys, deren Name mit einem der Präfixe beginnt. /// /// Wird einmal beim Start aufgerufen. Fest verdrahtete Listen wären hier /// falsch: welche Fühler es gibt, hängt am Modell — auf dem M5 sind es /// 285 Temperaturfühler, auf einem anderen Mac ganz andere. public func keys(matching prefixes: [String]) -> [String] { guard let total = keyCount() else { return [] } var found: [String] = [] for index in 0.. UInt32? { guard let value = read("#KEY"), value.bytes.count >= 4 else { return nil } let count = UInt32(value.bytes[0]) << 24 | UInt32(value.bytes[1]) << 16 | UInt32(value.bytes[2]) << 8 | UInt32(value.bytes[3]) return (0..<100_000).contains(count) ? count : nil } private func key(atIndex index: UInt32) -> String? { lock.lock() defer { lock.unlock() } var command = Param() command.data8 = Self.getKeyFromIndex command.data32 = index guard let output = call(command) else { return nil } return Self.fourCCString(output.key) } private struct Value { let type: String; let bytes: [UInt8] } private func read(_ key: String) -> Value? { lock.lock() defer { lock.unlock() } let described: KeyInfo if let cached = keyInfoCache[key] { described = cached } else { var info = Param() info.key = Self.fourCC(key) info.data8 = Self.getKeyInfo guard let result = call(info) else { return nil } described = result.keyInfo keyInfoCache[key] = described } var command = Param() command.key = Self.fourCC(key) command.keyInfo = described command.data8 = Self.readKey guard let output = call(command) else { return nil } let size = Int(min(described.dataSize, 32)) let all = withUnsafeBytes(of: output.bytes) { Array($0) } return Value(type: Self.fourCCString(described.dataType), bytes: Array(all.prefix(size))) } private func call(_ input: Param) -> Param? { var input = input var output = Param() var size = MemoryLayout.stride let result = IOConnectCallStructMethod(connection, Self.handleYPCEvent, &input, MemoryLayout.stride, &output, &size) // Ein nicht vorhandener Key meldet sich über `result` und ist kein // Fehler: welche Keys es gibt, unterscheidet sich je nach Modell. guard result == kIOReturnSuccess, output.result == 0 else { return nil } return output } private static func fourCC(_ string: String) -> UInt32 { var value: UInt32 = 0 for character in string.utf8.prefix(4) { value = value << 8 | UInt32(character) } return value } private static func fourCCString(_ value: UInt32) -> String { let bytes = [UInt8((value >> 24) & 0xFF), UInt8((value >> 16) & 0xFF), UInt8((value >> 8) & 0xFF), UInt8(value & 0xFF)] return String(bytes: bytes, encoding: .ascii) ?? "????" } }