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>
183 lines
7.2 KiB
Swift
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) ?? "????"
|
|
}
|
|
}
|