Der Regler war eine Pseudobedienung. Der SMC lehnt Schreibzugriffe auf CHLT ab — der Helfer hat es mit korrekter Größe versucht und „geschrieben (false), zurückgelesen 85" gemeldet. Das Sicherheitsnetz hat also die Wahrheit gesagt; die Oberfläche hat sie nur nicht gezeigt. Weiter zu raten hieße, im Lade-Subsystem Keys durchzuprobieren, und das ist die eine Operation in diesem Projekt, die Hardware dauerhaft beschädigen kann. Also: das Limit wird gelesen und angezeigt, mit einem Weg in die Systemeinstellungen, wo es tatsächlich geht. Der Schreibpfad ist aus dem privilegierten Helfer entfernt — ein Root-Dienst trägt nur, was benutzt wird. Die Akku-Kachel zeigte „40 %" und einen Verlauf über die letzten Sekunden. Der Ladestand ändert sich in Minuten, nicht in Sekunden; der Graph war eine waagerechte Linie. Jetzt: verbleibende Zeit bzw. Zeit bis voll, Leistung in Watt mit Richtung, Ladelimit, Zustand, Zyklen — und der Energiesparmodus. Der erklärt, warum die Maschine langsamer wirkt, und ohne diesen Hinweis sucht man den Grund woanders. In der Menüleiste standen fünf nackte Prozentzahlen nebeneinander. Man sah Zahlen und wusste nicht, welche wozu gehört. Zwei neue Darstellungen: Kürzel und Wert („CPU 34 %") sowie Symbol und Wert. Beim Akku stehen sie zuoberst in der Auswahl. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
177 lines
7.0 KiB
Swift
177 lines
7.0 KiB
Swift
import Foundation
|
|
import IOKit
|
|
|
|
/// SMC-Zugriff **mit** Schreibpfad — nur im privilegierten Helfer.
|
|
///
|
|
/// Bewusst eine eigene Kopie und keine geteilte Bibliothek mit der App: der
|
|
/// Schreibpfad soll gar nicht erst in einem Prozess vorhanden sein, der ohne
|
|
/// Sonderrechte läuft und in dem jedes Widget Code ausführt. Was nicht da ist,
|
|
/// kann auch nicht versehentlich aufgerufen werden.
|
|
///
|
|
/// Byte-Reihenfolge und Struktur stammen aus `docs/spikes/A-smc.md`.
|
|
final class SMCAccess {
|
|
|
|
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 writeKey: UInt8 = 6
|
|
private static let getKeyFromIndex: UInt8 = 8
|
|
private static let getKeyInfo: UInt8 = 9
|
|
|
|
private var connection: io_connect_t = 0
|
|
private var keyInfoCache: [String: KeyInfo] = [:]
|
|
|
|
init?() {
|
|
let service = IOServiceGetMatchingService(kIOMainPortDefault,
|
|
IOServiceMatching("AppleSMC"))
|
|
guard service != 0 else { return nil }
|
|
defer { IOObjectRelease(service) }
|
|
guard IOServiceOpen(service, mach_task_self_, 0, &connection) == kIOReturnSuccess
|
|
else { return nil }
|
|
}
|
|
|
|
deinit { if connection != 0 { IOServiceClose(connection) } }
|
|
|
|
// MARK: - Lesen
|
|
|
|
func float(_ key: String) -> Double? {
|
|
guard let value = read(key), value.bytes.count >= 4 else { return nil }
|
|
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))
|
|
}
|
|
|
|
func uint8(_ key: String) -> UInt8? { read(key)?.bytes.first }
|
|
|
|
func keys(withPrefix prefix: String) -> [String] {
|
|
guard let value = read("#KEY"), value.bytes.count >= 4 else { return [] }
|
|
// Big-endian, anders als die Messwerte — siehe Spike A.
|
|
let total = UInt32(value.bytes[0]) << 24 | UInt32(value.bytes[1]) << 16
|
|
| UInt32(value.bytes[2]) << 8 | UInt32(value.bytes[3])
|
|
guard total > 0, total < 100_000 else { return [] }
|
|
|
|
var found: [String] = []
|
|
for index in 0..<total {
|
|
var command = Param()
|
|
command.data8 = Self.getKeyFromIndex
|
|
command.data32 = index
|
|
guard let output = call(command) else { continue }
|
|
let name = Self.fourCCString(output.key)
|
|
if name.hasPrefix(prefix) { found.append(name) }
|
|
}
|
|
return found
|
|
}
|
|
|
|
// MARK: - Schreiben
|
|
|
|
/// Schreibt einen `flt`-Wert (Zieldrehzahl).
|
|
@discardableResult
|
|
func writeFloat(_ key: String, _ value: Double) -> Bool {
|
|
let bits = Float(value).bitPattern
|
|
return write(key, bytes: [UInt8(bits & 0xFF), UInt8((bits >> 8) & 0xFF),
|
|
UInt8((bits >> 16) & 0xFF), UInt8((bits >> 24) & 0xFF)])
|
|
}
|
|
|
|
/// Schreibt ein einzelnes Byte (Betriebsart).
|
|
@discardableResult
|
|
func writeUInt8(_ key: String, _ value: UInt8) -> Bool {
|
|
write(key, bytes: [value])
|
|
}
|
|
|
|
private func write(_ key: String, bytes: [UInt8]) -> Bool {
|
|
guard let info = keyInfo(for: key) else { return false }
|
|
// Nur schreiben, wenn die Größe zur Firmware passt. Ein zu kurzer oder
|
|
// zu langer Schreibvorgang landet sonst in benachbarten Feldern.
|
|
guard Int(info.dataSize) == bytes.count else { return false }
|
|
|
|
var command = Param()
|
|
command.key = Self.fourCC(key)
|
|
command.keyInfo = info
|
|
command.data8 = Self.writeKey
|
|
withUnsafeMutableBytes(of: &command.bytes) { raw in
|
|
for (index, byte) in bytes.enumerated() { raw[index] = byte }
|
|
}
|
|
return call(command) != nil
|
|
}
|
|
|
|
// MARK: - Innereien
|
|
|
|
private struct Value { let bytes: [UInt8] }
|
|
|
|
private func keyInfo(for key: String) -> KeyInfo? {
|
|
if let cached = keyInfoCache[key] { return cached }
|
|
var command = Param()
|
|
command.key = Self.fourCC(key)
|
|
command.data8 = Self.getKeyInfo
|
|
guard let output = call(command) else { return nil }
|
|
keyInfoCache[key] = output.keyInfo
|
|
return output.keyInfo
|
|
}
|
|
|
|
/// Liest die Rohbytes eines Keys — für `CHLT`, dessen Typ `hex_` ist und
|
|
/// das sich damit keiner der Zahlenfassungen zuordnen lässt.
|
|
func readBytes(_ key: String) -> [UInt8]? {
|
|
read(key)?.bytes
|
|
}
|
|
|
|
private func read(_ key: String) -> Value? {
|
|
guard let info = keyInfo(for: key) else { return nil }
|
|
var command = Param()
|
|
command.key = Self.fourCC(key)
|
|
command.keyInfo = info
|
|
command.data8 = Self.readKey
|
|
guard let output = call(command) else { return nil }
|
|
let all = withUnsafeBytes(of: output.bytes) { Array($0) }
|
|
return Value(bytes: Array(all.prefix(Int(min(info.dataSize, 32)))))
|
|
}
|
|
|
|
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)
|
|
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) ?? "????"
|
|
}
|
|
}
|