Vier Annahmen des Plans vor jedem Implementierungscode geprüft:
A (SMC): Lüftersteuerung bestätigt — F0md/F1md (klein!) als Auto/Manuell-
Umschalter, F0Tg als Ziel, Grenzen aus F0Mn/F0Mx. SMC-Integer sind
little-endian, nicht big-endian wie im verbreiteten Intel-Beispielcode.
Ladelimit: CHWA/CH0B/CH0C/bfE0/bfF0 existieren auf Mac17,9 nicht.
Key noch nicht identifiziert, deshalb smc-diff.sh für eine
Differenzmessung ohne jeden Schreibzugriff.
B (Media): mediaremote-adapter läuft unter 26.6.1, aus dem Quellcode gebaut.
Liefert vollständige Metadaten inkl. Cover aus Google Chrome — der
Fall, den AppleScript nicht erreicht.
C (Audio): Process Tap + privates Aggregate-Device liefern echte Samples
(469 Callbacks/5 s, 0 stille Puffer). Konflikt gefunden: SoundSource
6.1.1 mit ARK.driver ist bereits installiert.
D (Group): App-Group-Container ist aus einem nicht-sandboxed Prozess erreichbar.
Damit trägt die Sandbox-Entscheidung, die Bridge-App entfällt.
Spikes sind Wegwerfcode zur Verifikation, kein Produktionscode.
334 lines
12 KiB
Swift
334 lines
12 KiB
Swift
// 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<SMCParamStruct>.stride
|
|
|
|
let result = IOConnectCallStructMethod(connection, kSMCHandleYPCEvent,
|
|
&input, MemoryLayout<SMCParamStruct>.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..<max(fanCount, 2) {
|
|
for (suffix, note) in fanSuffixes {
|
|
if let v = try? smc.read(key: "F\(fan)\(suffix)") {
|
|
printRow(v, note: note)
|
|
}
|
|
}
|
|
}
|
|
if let force = try? smc.read(key: "FS! ") { printRow(force, note: "Force-Bits (Bitmaske manuell)") }
|
|
|
|
// --- Ladesteuerung --------------------------------------------------------
|
|
|
|
section("AKKU / LADESTEUERUNG")
|
|
let chargeKeys: [(String, String)] = [
|
|
("CHWA", "80%-Limit (neuere Firmware, flag)"),
|
|
("CHTE", "Ladeziel"),
|
|
("CH0B", "Laden aus (legacy)"),
|
|
("CH0C", "Laden aus (legacy)"),
|
|
("CH0I", "Netzteil trennen (legacy)"),
|
|
("CH0J", "Ladezustand"),
|
|
("CH0K", "Ladezustand"),
|
|
("bfD0", "Untergrenze (Firmware-Backend)"),
|
|
("bfE0", "Obergrenze (Firmware-Backend)"),
|
|
("bfF0", "Aktivierung (Firmware-Backend)"),
|
|
("ACLC", "AC-Ladelogik"),
|
|
("BCLM", "Ladelimit (Intel-Ära)"),
|
|
("BRSC", "Ladung roh"),
|
|
("B0RM", "Restkapazität mAh"),
|
|
("B0FC", "Volle Kapazität mAh"),
|
|
("B0AC", "Strom mA"),
|
|
("B0AV", "Spannung mV"),
|
|
("BC1V", "Ladeschlussspannung"),
|
|
]
|
|
var foundCharge = 0
|
|
for (key, note) in chargeKeys {
|
|
if let v = try? smc.read(key: key) { printRow(v, note: note); foundCharge += 1 }
|
|
}
|
|
if foundCharge == 0 { print(" (keiner der bekannten Lade-Keys vorhanden)") }
|
|
|
|
// --- Temperaturen ---------------------------------------------------------
|
|
|
|
section("TEMPERATUR-SENSOREN (Auswahl)")
|
|
let tempKeys = ["TC0P", "TC0D", "TC0E", "TC0F", "TG0P", "TG0D", "Tp01", "Tp05", "Tp09",
|
|
"Tp0D", "Tg05", "Tg0D", "Ts0P", "Ts1P", "TB0T", "TB1T", "TW0P", "TH0x"]
|
|
var foundTemp = 0
|
|
for key in tempKeys {
|
|
if let v = try? smc.read(key: key), v.size > 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 ---------------------------------------------------
|
|
|
|
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..<total {
|
|
guard let name = try? smc.key(atIndex: i), let v = try? smc.read(key: name) else { continue }
|
|
printRow(v)
|
|
}
|
|
} else {
|
|
if let total = try? smc.keyCount() {
|
|
print("\n\(total) Keys insgesamt. Vollständiger Dump mit --all")
|
|
}
|
|
}
|