Die M-Serie-Konvention gilt auch auf dem M5: Tp* sind die CPU-Kerne, Tg* die GPU-Cluster. Gezeigt wird der wärmste Punkt, nicht der Durchschnitt — gedrosselt wird nach dem heißesten Kern. Gemessen: CPU 76 °C, GPU 70 °C. Die Fühlerliste wird zur Laufzeit gesucht statt fest verdrahtet: welche es gibt, hängt am Modell. Auf diesem Mac sind es 285 Temperaturfühler. Zwei Leistungsprobleme, gefunden bevor sie jemandem auffallen konnten: Die Suche geht alle 3486 Keys durch und braucht knapp eine Sekunde. Auf dem Hauptthread wäre das eine spürbare Startverzögerung — sie läuft jetzt im Hintergrund, bis dahin zeigt die Oberfläche "—" statt einer erfundenen Zahl. Fünfzig Fühler zu lesen kostete 34 ms je Abtastung, bei 1 Hz also 3,4 % CPU. Zwei Maßnahmen: Typ und Größe je Key werden zwischengespeichert (sie stehen in der Firmware fest, sie bei jedem Lesen zu erfragen verdoppelte die IOKit-Aufrufe), und Temperaturen werden höchstens alle zwei Sekunden neu gelesen — sie bewegen sich in einer Sekunde ohnehin kaum. Ergebnis: 6 ms Grundlast, 18 ms alle zwei Sekunden, im Mittel gut 1 %. Dabei noch eine Eigenheit gefunden: die Byte-Reihenfolge im SMC ist nicht durchgängig. Messwerte wie B0AV kommen little-endian aus dem Akku-Baustein, die Metadaten des SMC selbst dagegen big-endian. #KEY little-endian gelesen ergibt 2 651 652 096 statt 3486 — die Fühlersuche brach damit still ab und alle Temperaturen blieben leer. Lüfterdrehzahlen stehen jetzt im Sensor-Popover, mit Balken zwischen Minimum und Maximum der Firmware: "2321 U/min" allein sagt niemandem, ob das viel ist. Die Steuerung selbst kommt mit Phase 6 — Schreibzugriff auf den SMC verlangt root — und der Popover sagt das auch. 145 Tests grün.
200 lines
8.1 KiB
Swift
200 lines
8.1 KiB
Swift
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(total) {
|
|
guard let name = key(atIndex: index) else { continue }
|
|
if prefixes.contains(where: name.hasPrefix) { found.append(name) }
|
|
}
|
|
return found
|
|
}
|
|
|
|
/// Die Anzahl der Keys — **big-endian**.
|
|
///
|
|
/// Die Byte-Reihenfolge ist im SMC nicht durchgängig, und das ist keine
|
|
/// Nachlässigkeit dieser Klasse, sondern der Hardware: Messwerte wie `B0AV`
|
|
/// kommen little-endian aus dem Akku-Baustein, die Metadaten des SMC selbst
|
|
/// dagegen big-endian. `#KEY` little-endian gelesen ergibt 2 651 652 096
|
|
/// statt 3486 — die Suche brach damit still ab und alle Temperaturen
|
|
/// blieben leer.
|
|
private func keyCount() -> 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<Param>.stride
|
|
|
|
let result = IOConnectCallStructMethod(connection, Self.handleYPCEvent,
|
|
&input, MemoryLayout<Param>.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) ?? "????"
|
|
}
|
|
}
|