Files
onyx/OnyxHelper/SMCAccess.swift
Scarriffle 386eb6ae70 Ladelimit ist eine Anzeige, Akku-Kachel sagt etwas, Menüleiste bekommt Kürzel
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>
2026-08-11 12:37:04 +02:00

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) ?? "????"
}
}