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>
69 lines
2.6 KiB
Swift
69 lines
2.6 KiB
Swift
import Foundation
|
|
|
|
/// Das Ladelimit — Kodierung und harte Grenzen.
|
|
///
|
|
/// Der SMC-Key heißt `CHLT`, drei Bytes. Gemessen am 11.08.2026 auf Mac17,9,
|
|
/// rein lesend, gegen macOS' eigene Ladebegrenzung:
|
|
///
|
|
/// | Einstellung | `CHLT` |
|
|
/// |---|---|
|
|
/// | 80 % | `50 01 00` |
|
|
/// | 85 % | `55 01 00` |
|
|
/// | 95 % | `5F 01 00` |
|
|
///
|
|
/// Erstes Byte ist die Prozentzahl, das zweite offenbar „Limit aktiv", das
|
|
/// dritte unbenutzt. Mehr ist nicht belegt, und mehr wird hier auch nicht
|
|
/// angenommen.
|
|
///
|
|
/// **Warum das strenger gehandhabt wird als die Lüfter:** dort liefert die
|
|
/// Firmware mit `F0Mn`/`F0Mx` ihre eigenen Grenzen mit, an denen sich das
|
|
/// Sicherheitsnetz festhalten kann. Hier gibt es nichts dergleichen — es gibt
|
|
/// nur die Werte, die wir gemessen haben. Ein falscher Wert im Lade-Subsystem
|
|
/// ist die eine Operation in diesem Projekt, die den Akku dauerhaft
|
|
/// beschädigen kann.
|
|
public enum ChargeSafety {
|
|
|
|
/// Der SMC-Key.
|
|
public static let key = "CHLT"
|
|
|
|
/// Untergrenze. macOS selbst bietet nichts darunter an; darunter ist
|
|
/// ungemessenes Gebiet.
|
|
public static let minimumPercent = 80
|
|
public static let maximumPercent = 100
|
|
|
|
/// „Kein Limit" heißt hundert Prozent.
|
|
///
|
|
/// Und nicht: das Aktiv-Byte auf null setzen. Diese Kodierung hat macOS nie
|
|
/// geschrieben, sie ist also ungemessen — hundert Prozent erreicht dasselbe
|
|
/// innerhalb dessen, was belegt ist.
|
|
public static let noLimit = 100
|
|
|
|
public static func clamp(_ percent: Int) -> Int {
|
|
// Anheben statt ablehnen: ein Regler, der bei einer Fehleingabe gar
|
|
// nichts tut, wirkt kaputt.
|
|
min(max(percent, minimumPercent), maximumPercent)
|
|
}
|
|
|
|
/// Die drei Bytes für einen Prozentwert.
|
|
///
|
|
/// Nur das erste Byte trägt den Wunsch. Die beiden anderen bekommen exakt
|
|
/// das, was macOS dort schreibt — ungemessene Bytes im Ladepfad werden
|
|
/// nicht angefasst.
|
|
public static func encode(_ percent: Int) -> [UInt8] {
|
|
[UInt8(clamp(percent)), 0x01, 0x00]
|
|
}
|
|
|
|
/// Der Prozentwert aus den gelesenen Bytes. `nil`, wenn kein Limit gilt
|
|
/// oder die Daten nicht plausibel sind.
|
|
public static func decode(_ bytes: [UInt8]) -> Int? {
|
|
guard bytes.count >= 2 else { return nil }
|
|
// Zweites Byte null: kein Limit, egal was im ersten steht.
|
|
guard bytes[1] != 0 else { return nil }
|
|
let percent = Int(bytes[0])
|
|
// Ein Wert außerhalb des Möglichen wird nicht angezeigt. „Unbekannt"
|
|
// ist eine ehrlichere Auskunft als eine Zahl, die es nicht geben kann.
|
|
guard (1...100).contains(percent) else { return nil }
|
|
return percent
|
|
}
|
|
}
|