// Spike A — SMC-Key-Dump für Onyx // // Zweck: herausfinden, welche Lüfter- und Ladesteuerungs-Keys auf diesem Mac // (Mac17,9 / M5 Pro) tatsächlich existieren, welchen Typ und welche Größe sie // haben und welche Werte gerade anliegen. // // AUSSCHLIESSLICH LESEND. Dieser Spike enthält bewusst keinen Schreibpfad — // kSMCWriteKey wird nirgends verwendet. // // Bauen: xcrun swiftc -O Spikes/smc-dump.swift -o /tmp/smc-dump // Nutzen: /tmp/smc-dump → Zusammenfassung der interessanten Keys // /tmp/smc-dump --all → vollständiger Dump aller Keys import Foundation import IOKit // MARK: - Layout des AppleSMC-UserClients struct SMCVersion { var major: UInt8 = 0 var minor: UInt8 = 0 var build: UInt8 = 0 var reserved: UInt8 = 0 var release: UInt16 = 0 } struct SMCPLimitData { var version: UInt16 = 0 var length: UInt16 = 0 var cpuPLimit: UInt32 = 0 var gpuPLimit: UInt32 = 0 var memPLimit: UInt32 = 0 } struct SMCKeyInfoData { var dataSize: UInt32 = 0 var dataType: UInt32 = 0 var dataAttributes: UInt8 = 0 } struct SMCParamStruct { var key: UInt32 = 0 var vers = SMCVersion() var pLimitData = SMCPLimitData() var keyInfo = SMCKeyInfoData() 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 let kSMCHandleYPCEvent: UInt32 = 2 private let kSMCReadKey: UInt8 = 5 private let kSMCGetKeyFromIndex: UInt8 = 8 private let kSMCGetKeyInfo: UInt8 = 9 // MARK: - FourCC-Hilfen func fourCC(_ s: String) -> UInt32 { var result: UInt32 = 0 for ch in s.utf8.prefix(4) { result = result << 8 | UInt32(ch) } return result } func fourCCString(_ v: UInt32) -> String { let bytes = [UInt8((v >> 24) & 0xFF), UInt8((v >> 16) & 0xFF), UInt8((v >> 8) & 0xFF), UInt8(v & 0xFF)] return String(bytes: bytes, encoding: .ascii) ?? "????" } // MARK: - Verbindung struct SMCError: Error, CustomStringConvertible { let description: String } final class SMC { private var connection: io_connect_t = 0 init() throws { let service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSMC")) guard service != 0 else { throw SMCError(description: "AppleSMC-Service nicht gefunden") } defer { IOObjectRelease(service) } let result = IOServiceOpen(service, mach_task_self_, 0, &connection) guard result == kIOReturnSuccess else { throw SMCError(description: "IOServiceOpen fehlgeschlagen: 0x\(String(result, radix: 16))") } } deinit { if connection != 0 { IOServiceClose(connection) } } private func call(_ input: SMCParamStruct) throws -> SMCParamStruct { var input = input var output = SMCParamStruct() var outputSize = MemoryLayout.stride let result = IOConnectCallStructMethod(connection, kSMCHandleYPCEvent, &input, MemoryLayout.stride, &output, &outputSize) guard result == kIOReturnSuccess else { throw SMCError(description: "IOConnectCallStructMethod: 0x\(String(result, radix: 16))") } // result == 132 heißt "Key existiert nicht" — das ist ein erwarteter Zustand. guard output.result == 0 else { throw SMCError(description: "SMC-Status \(output.result)") } return output } func keyCount() throws -> UInt32 { let value = try read(key: "#KEY") return value.uint32 } func key(atIndex index: UInt32) throws -> String { var input = SMCParamStruct() input.data8 = kSMCGetKeyFromIndex input.data32 = index return fourCCString(try call(input).key) } struct Value { let key: String let type: String let size: UInt32 let bytes: [UInt8] var uint32: UInt32 { var v: UInt32 = 0 for b in bytes.prefix(4) { v = v << 8 | UInt32(b) } return v } /// Best-effort-Interpretation anhand des SMC-Typs. var decoded: String { guard !bytes.isEmpty else { return "—" } switch type { case "ui8 ", "ui16", "ui32", "ui64": var v: UInt64 = 0 for b in bytes.prefix(Int(size)) { v = v << 8 | UInt64(b) } return String(v) case "si8 ": return String(Int8(bitPattern: bytes[0])) case "si16": let v = Int16(bitPattern: UInt16(bytes[0]) << 8 | UInt16(bytes[1])) return String(v) case "flag": return bytes[0] == 1 ? "true" : "false" case "flt ": guard bytes.count >= 4 else { return "—" } let bits = UInt32(bytes[3]) << 24 | UInt32(bytes[2]) << 16 | UInt32(bytes[1]) << 8 | UInt32(bytes[0]) return String(format: "%.2f", Float(bitPattern: bits)) case "fpe2": guard bytes.count >= 2 else { return "—" } return String((UInt16(bytes[0]) << 8 | UInt16(bytes[1])) >> 2) case "sp78": guard bytes.count >= 2 else { return "—" } let raw = Int16(bitPattern: UInt16(bytes[0]) << 8 | UInt16(bytes[1])) return String(format: "%.2f", Float(raw) / 256.0) case "ch8*": return String(bytes: bytes.prefix(Int(size)), encoding: .ascii)? .trimmingCharacters(in: CharacterSet(charactersIn: "\0")) ?? "—" case "{fds": // Fan-Descriptor-Struktur: die ersten 12 Byte sind der Fan-Name. return String(bytes: bytes.prefix(12), encoding: .ascii)? .trimmingCharacters(in: CharacterSet(charactersIn: "\0 ")) ?? "—" default: return "—" } } var hex: String { bytes.prefix(Int(size)).map { String(format: "%02X", $0) }.joined(separator: " ") } } func read(key: String) throws -> Value { var info = SMCParamStruct() info.key = fourCC(key) info.data8 = kSMCGetKeyInfo let keyInfo = try call(info).keyInfo var readCmd = SMCParamStruct() readCmd.key = fourCC(key) readCmd.keyInfo = keyInfo readCmd.data8 = kSMCReadKey let output = try call(readCmd) let all = withUnsafeBytes(of: output.bytes) { Array($0) } return Value(key: key, type: fourCCString(keyInfo.dataType), size: min(keyInfo.dataSize, 32), bytes: Array(all.prefix(Int(min(keyInfo.dataSize, 32))))) } } // MARK: - Ausgabe func printRow(_ v: SMC.Value, note: String = "") { let key = v.key.padding(toLength: 6, withPad: " ", startingAt: 0) let type = v.type.padding(toLength: 6, withPad: " ", startingAt: 0) let size = String(v.size).padding(toLength: 3, withPad: " ", startingAt: 0) let dec = v.decoded.padding(toLength: 14, withPad: " ", startingAt: 0) print(" \(key) \(type) \(size) \(dec) \(v.hex)\(note.isEmpty ? "" : " ← \(note)")") } func section(_ title: String) { print("\n\(title)") print(String(repeating: "─", count: 72)) print(" KEY TYPE SZ WERT ROH") } let smc: SMC do { smc = try SMC() } catch { print("FEHLER: \(error)") exit(1) } print("SMC-Dump — \(ProcessInfo.processInfo.hostName)") var model = [CChar](repeating: 0, count: 64) var modelSize = 64 sysctlbyname("hw.model", &model, &modelSize, nil, 0) print("Modell: \(String(cString: model)) macOS \(ProcessInfo.processInfo.operatingSystemVersionString)") print("Als root: \(getuid() == 0 ? "ja" : "nein")") // --- Lüfter --------------------------------------------------------------- section("LÜFTER") var fanCount = 0 if let count = try? smc.read(key: "FNum") { printRow(count, note: "Anzahl Lüfter") fanCount = Int(count.uint32) } else { print(" FNum nicht lesbar — keine Lüfterzählung verfügbar") } // Pro Lüfter: Ist, Min, Max, Ziel, Modus (0 = auto, 1 = manuell), Beschreibung let fanSuffixes: [(String, String)] = [ ("Ac", "Ist-Drehzahl"), ("Mn", "Minimum"), ("Mx", "Maximum"), ("Tg", "Ziel-Drehzahl"), ("Md", "Modus 0=auto 1=manuell"), ("ID", "Beschreibung"), ] for fan in 0.. 0 { printRow(v); foundTemp += 1 } } if foundTemp == 0 { print(" Keine klassischen T-Keys — auf Apple Silicon liegen die Temperaturen") print(" überwiegend hinter IOHIDEventSystemClient, nicht hinter SMC-Keys.") } // --- Leistung ------------------------------------------------------------- section("LEISTUNG") for (key, note) in [("PSTR", "Systemleistung W"), ("PDTR", "DC-Eingang W"), ("PMVR", "Netzteil W"), ("PPBR", "Akku-Leistung W")] { if let v = try? smc.read(key: key) { printRow(v, note: note) } } // --- Vollständiger Dump --------------------------------------------------- // --- Wächter: Änderungen an den Ladekandidaten protokollieren ------------- // // Findet den Ladelimit-Key durch Beobachtung, nicht durch Raten: es wird nur // gelesen, während macOS selbst die Ladegrenze umstellt. if CommandLine.arguments.contains("--watch") { let interval = 2.0 let deadline = Date().addingTimeInterval(45 * 60) // Alle Keys, die für die Ladesteuerung in Frage kommen, plus Kontext. var watched: [String] = [] if let total = try? smc.keyCount() { for i in 0.. [String: String] { var out: [String: String] = [:] for key in watched { if let v = try? smc.read(key: key) { out[key] = v.hex } } return out } let stamp = { () -> String in let f = DateFormatter(); f.dateFormat = "HH:mm:ss"; return f.string(from: Date()) } // --- Selbstkalibrierung ------------------------------------------------ // // Eine feste Ausschlussliste ist zwecklos: Spannungen, Ströme, Temperaturen // und Zähler ändern sich im Sekundentakt und begraben das eine Ereignis, // auf das es ankommt. Stattdessen erst zusehen, was von allein zappelt. let calibrationSeconds = 60.0 print("KALIBRIERUNG — \(watched.count) Kandidaten, \(Int(calibrationSeconds)) s") print("Bitte in dieser Zeit NICHTS umstellen. Nur lesend.") print(String(repeating: "─", count: 72)) var baseline = snapshotNow() var noisy = Set() let calibrationEnd = Date().addingTimeInterval(calibrationSeconds) while Date() < calibrationEnd { Thread.sleep(forTimeInterval: interval) let current = snapshotNow() for (key, value) in current where baseline[key] != value { noisy.insert(key) baseline[key] = value } } let stable = watched.filter { !noisy.contains($0) } print("[\(stamp())] \(noisy.count) Keys zappeln von allein und werden ignoriert.") print("[\(stamp())] \(stable.count) Keys sind stabil und werden beobachtet:") print(" \(stable.sorted().joined(separator: " "))") print(String(repeating: "─", count: 72)) print("JETZT: Netzteil anstecken, dann die Ladegrenze umschalten.") fflush(stdout) watched = stable var previous = snapshotNow() var lastPower: String? = nil while Date() < deadline { Thread.sleep(forTimeInterval: interval) let current = snapshotNow() // Netzteilzustand getrennt mitschreiben, damit die Ereignisse // zeitlich einzuordnen sind. if let ac = try? smc.read(key: "AC-W"), ac.hex != lastPower { print("[\(stamp())] ⚡︎ NETZTEIL: AC-W \(lastPower ?? "—") → \(ac.hex)") lastPower = ac.hex fflush(stdout) } for (key, value) in current where previous[key] != value { let old = previous[key] ?? "—" let v = try? smc.read(key: key) print("[\(stamp())] ★ \(key) (\(v?.type ?? "?")): \(old) → \(value) = \(v?.decoded ?? "—")") fflush(stdout) } previous = current } print("[\(stamp())] Wächter beendet.") exit(0) } if CommandLine.arguments.contains("--all") { section("ALLE KEYS") guard let total = try? smc.keyCount() else { print(" Key-Anzahl nicht lesbar") exit(0) } print(" (\(total) Keys insgesamt)\n") for i in 0..