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>
118 lines
4.4 KiB
Swift
118 lines
4.4 KiB
Swift
import Testing
|
|
@testable import OnyxHelperProtocol
|
|
|
|
// Gemessen am 11.08.2026 auf Mac17,9, rein lesend, gegen macOS' eigene
|
|
// Ladebegrenzung:
|
|
//
|
|
// Limit 80 % → CHLT = 50 01 00
|
|
// Limit 95 % → CHLT = 5F 01 00
|
|
// Limit 85 % → CHLT = 55 01 00
|
|
//
|
|
// Erstes Byte ist die Prozentzahl, zweites offenbar „Limit aktiv", drittes
|
|
// unbenutzt. Was hier steht, ist gemessen — nicht abgeleitet.
|
|
|
|
@Suite("Ladelimit — Kodierung")
|
|
struct ChargeEncodingTests {
|
|
|
|
@Test("Die drei gemessenen Werte werden richtig kodiert")
|
|
func encodesMeasuredValues() {
|
|
#expect(ChargeSafety.encode(80) == [0x50, 0x01, 0x00])
|
|
#expect(ChargeSafety.encode(95) == [0x5F, 0x01, 0x00])
|
|
#expect(ChargeSafety.encode(85) == [0x55, 0x01, 0x00])
|
|
}
|
|
|
|
@Test("Und wieder gelesen ergeben sie dasselbe")
|
|
func decodesMeasuredValues() {
|
|
#expect(ChargeSafety.decode([0x50, 0x01, 0x00]) == 80)
|
|
#expect(ChargeSafety.decode([0x5F, 0x01, 0x00]) == 95)
|
|
#expect(ChargeSafety.decode([0x55, 0x01, 0x00]) == 85)
|
|
}
|
|
|
|
@Test("Ein abgeschaltetes Limit liest sich als kein Limit")
|
|
func disabledReadsAsNoLimit() {
|
|
// Zweites Byte auf null: macOS schreibt das zwar nicht selbst, aber
|
|
// fremde Werkzeuge könnten es. Dann gilt kein Limit, egal was im
|
|
// ersten Byte steht.
|
|
#expect(ChargeSafety.decode([0x50, 0x00, 0x00]) == nil)
|
|
}
|
|
|
|
@Test("Zu kurze Daten ergeben kein Ergebnis statt eines falschen")
|
|
func shortDataIsRefused() {
|
|
#expect(ChargeSafety.decode([0x50]) == nil)
|
|
#expect(ChargeSafety.decode([]) == nil)
|
|
}
|
|
|
|
@Test("Ein unsinniger Wert im Speicher wird nicht als Limit ausgegeben")
|
|
func nonsenseIsRefused() {
|
|
// 200 % ist kein Ladestand. Lieber „unbekannt" melden, als eine Zahl
|
|
// anzuzeigen, die es nicht geben kann.
|
|
#expect(ChargeSafety.decode([200, 0x01, 0x00]) == nil)
|
|
}
|
|
}
|
|
|
|
@Suite("Ladelimit — Sicherheitsnetz")
|
|
struct ChargeSafetyTests {
|
|
|
|
@Test("Der gemessene Bereich wird eingehalten")
|
|
func staysInMeasuredRange() {
|
|
// macOS selbst lässt nur 80 bis 100 zu. Darunter ist ungemessenes
|
|
// Gebiet, und im Lade-Subsystem wird nicht geraten: das ist die eine
|
|
// Stelle in diesem Projekt, an der ein falscher Wert die Hardware
|
|
// dauerhaft beschädigen kann.
|
|
#expect(ChargeSafety.minimumPercent == 80)
|
|
#expect(ChargeSafety.maximumPercent == 100)
|
|
}
|
|
|
|
@Test("Zu kleine Werte werden angehoben, nicht abgelehnt")
|
|
func lowValuesAreClamped() {
|
|
// Anheben statt ablehnen: ein Regler, der bei einer Fehleingabe gar
|
|
// nichts tut, wirkt kaputt.
|
|
#expect(ChargeSafety.clamp(50) == 80)
|
|
#expect(ChargeSafety.clamp(0) == 80)
|
|
#expect(ChargeSafety.clamp(-10) == 80)
|
|
}
|
|
|
|
@Test("Zu große Werte werden gedeckelt")
|
|
func highValuesAreClamped() {
|
|
#expect(ChargeSafety.clamp(120) == 100)
|
|
#expect(ChargeSafety.clamp(255) == 100)
|
|
}
|
|
|
|
@Test("Werte im Bereich bleiben unverändert")
|
|
func validValuesPass() {
|
|
for percent in 80...100 {
|
|
#expect(ChargeSafety.clamp(percent) == percent)
|
|
}
|
|
}
|
|
|
|
@Test("Kein Limit heißt hundert Prozent")
|
|
func noLimitIsHundred() {
|
|
// Und nicht: das Aktiv-Byte auf null setzen. Diese Kodierung hat macOS
|
|
// nie geschrieben, also ist sie ungemessen — hundert Prozent erreicht
|
|
// dasselbe innerhalb dessen, was wir belegt haben.
|
|
#expect(ChargeSafety.encode(ChargeSafety.noLimit) == [0x64, 0x01, 0x00])
|
|
#expect(ChargeSafety.noLimit == 100)
|
|
}
|
|
|
|
@Test("Geschriebene Bytes behalten immer die Form, die macOS benutzt")
|
|
func writtenShapeIsAlwaysTheSame() {
|
|
// Nur das erste Byte wird verändert. Alles andere ist ungemessen, und
|
|
// ungemessene Bytes im Ladepfad werden nicht angefasst.
|
|
for percent in [80, 85, 90, 95, 100] {
|
|
let bytes = ChargeSafety.encode(percent)
|
|
#expect(bytes.count == 3)
|
|
#expect(bytes[1] == 0x01)
|
|
#expect(bytes[2] == 0x00)
|
|
}
|
|
}
|
|
|
|
@Test("Was geschrieben wurde, muss auch zurückgelesen werden können")
|
|
func writeIsVerifiable() {
|
|
// Der Helfer prüft nach dem Schreiben nach. Bleibt der Wert aus, hat
|
|
// der SMC ihn abgelehnt — und das muss auffallen, statt als Erfolg
|
|
// gemeldet zu werden.
|
|
let bytes = ChargeSafety.encode(85)
|
|
#expect(ChargeSafety.decode(bytes) == 85)
|
|
}
|
|
}
|