Files
onyx/OnyxHelper/SMCAccess.swift
Scarriffle b2f69f34ae Ladelimit: Key gemessen, Schreibpfad gebaut
Der Key heißt CHLT, drei Bytes. Identifiziert per Differenzmessung gegen
macOS' eigene Ladebegrenzung, ohne einen einzigen Schreibzugriff:

  80 %  →  50 01 00
  95 %  →  5F 01 00
  85 %  →  55 01 00

Erstes Byte ist die Prozentzahl, zweites offenbar „Limit aktiv", drittes
unbenutzt. Drei Messpunkte, weil einer Zufall sein kann: BACC hatte beim
ersten Vergleich ebenfalls gepasst — ein Zähler, in dem gerade 50 stand.

ChargerConfiguration aus ioreg sah zunächst wie eine zweite Quelle aus
(niedriges Byte 0x50 = 80), blieb aber unverändert, als CHLT längst auf
95 stand. Also Zufall, und gut, dass darauf nichts gebaut wurde.

Geschrieben wird nur das erste Byte, und nur zwischen 80 und 100 — dem
Bereich, den macOS selbst anbietet und in dem gemessen wurde. Darunter ist
ungemessenes Gebiet. Bei den Lüftern liefert die Firmware mit F0Mn/F0Mx
eigene Grenzen mit, an denen sich ein Sicherheitsnetz festhalten kann; hier
gibt es das nicht, hier gibt es nur die Messung. Ein falscher Wert im
Lade-Subsystem ist die eine Operation in diesem Projekt, die den Akku
dauerhaft beschädigen kann.

Nach jedem Schreiben wird zurückgelesen und gemeldet, was tatsächlich
steht — nicht, was gewünscht war. „Kein Limit" heißt 100 Prozent und nicht
das Aktiv-Byte auf null: diese Kodierung hat macOS nie geschrieben, sie
wäre also ungemessen.

Bedienung im Lüfter-Bereich der Einstellungen und in der Lüfterkachel,
also auch im Menüleisten-Popover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:01:50 +02:00

183 lines
7.2 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])
}
/// Schreibt beliebige Bytes für `CHLT`, das drei davon hat.
@discardableResult
func writeBytes(_ key: String, _ bytes: [UInt8]) -> Bool {
write(key, bytes: bytes)
}
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) ?? "????"
}
}