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>
93 lines
3.8 KiB
Swift
93 lines
3.8 KiB
Swift
import Foundation
|
|
|
|
/// Die Schnittstelle zwischen App und privilegiertem Helfer.
|
|
///
|
|
/// **Die App sagt dem Helfer nicht, wie warm es ist.** Der Helfer misst selbst.
|
|
/// Andernfalls hinge das Sicherheitsnetz an der Ehrlichkeit eines Prozesses,
|
|
/// der abstürzen, hängen oder — bei einer manipulierten Kopie — schlicht lügen
|
|
/// kann. Der Herzschlag sagt deshalb nur „ich lebe noch", nichts weiter.
|
|
@objc public protocol OnyxHelperProtocol {
|
|
|
|
/// Version des Helfers. Läuft nach einem App-Update noch die alte, muss sie
|
|
/// ersetzt werden — sonst passen die Aufrufe nicht mehr zusammen.
|
|
func version(reply: @escaping (String) -> Void)
|
|
|
|
/// Aktueller Zustand aller Lüfter, als JSON-kodierter ``FanStatus``.
|
|
///
|
|
/// JSON statt eines eigenen Typs: `NSSecureCoding` über XPC verlangt
|
|
/// registrierte Klassen auf beiden Seiten, und dafür ist die Datenmenge
|
|
/// hier zu klein.
|
|
func fanStatus(reply: @escaping (Data?) -> Void)
|
|
|
|
/// Setzt eine Wunschdrehzahl. Der Helfer prüft sie gegen sein
|
|
/// Sicherheitsnetz und kann sie begrenzen oder ablehnen.
|
|
func setFan(index: Int, targetRPM: Double, reply: @escaping (Data?) -> Void)
|
|
|
|
/// Zurück zur Automatik.
|
|
func setFanAutomatic(index: Int, reply: @escaping (Data?) -> Void)
|
|
|
|
/// „Ich lebe noch." Bleibt er aus, fallen alle Lüfter auf Automatik zurück.
|
|
func heartbeat(reply: @escaping (Bool) -> Void)
|
|
|
|
/// Das aktuelle Ladelimit in Prozent, `-1` wenn keines gilt.
|
|
///
|
|
/// `Int` statt `Int?`: Optionals sind über `@objc` nicht darstellbar.
|
|
func chargeLimit(reply: @escaping (Int) -> Void)
|
|
|
|
/// Setzt das Ladelimit. Der Helfer begrenzt auf den gemessenen Bereich und
|
|
/// liest nach dem Schreiben zurück — gemeldet wird, was tatsächlich steht.
|
|
func setChargeLimit(percent: Int, reply: @escaping (Int) -> Void)
|
|
}
|
|
|
|
/// Was der Helfer über einen Lüfter berichtet.
|
|
public struct FanStatus: Codable, Equatable, Sendable {
|
|
public let index: Int
|
|
public let currentRPM: Double
|
|
public let targetRPM: Double
|
|
public let limits: FanLimits
|
|
public let isManual: Bool
|
|
/// Warum gerade die Automatik läuft — `nil` im manuellen Betrieb.
|
|
public let automaticReason: String?
|
|
|
|
public init(index: Int, currentRPM: Double, targetRPM: Double, limits: FanLimits,
|
|
isManual: Bool, automaticReason: String?) {
|
|
self.index = index
|
|
self.currentRPM = currentRPM
|
|
self.targetRPM = targetRPM
|
|
self.limits = limits
|
|
self.isManual = isManual
|
|
self.automaticReason = automaticReason
|
|
}
|
|
}
|
|
|
|
public struct FanStatusReport: Codable, Equatable, Sendable {
|
|
public let fans: [FanStatus]
|
|
public let hottestCelsius: Double?
|
|
public let helperVersion: String
|
|
|
|
public init(fans: [FanStatus], hottestCelsius: Double?, helperVersion: String) {
|
|
self.fans = fans
|
|
self.hottestCelsius = hottestCelsius
|
|
self.helperVersion = helperVersion
|
|
}
|
|
}
|
|
|
|
public enum OnyxHelper {
|
|
/// Muss zum Namen der Plist unter `Contents/Library/LaunchDaemons` passen.
|
|
public static let machServiceName = "com.scarriffleservices.onyx.helper"
|
|
public static let version = "1"
|
|
|
|
/// Nur ein Prozess mit exakt dieser Signatur darf mit dem Helfer sprechen.
|
|
///
|
|
/// Ohne diese Prüfung könnte **jedes** Programm auf dem Rechner die Lüfter
|
|
/// eines root-Dienstes steuern — der Mach-Dienst ist systemweit sichtbar.
|
|
public static let clientRequirement =
|
|
"anchor apple generic and identifier \"com.scarriffleservices.onyx\" "
|
|
+ "and certificate leaf[subject.OU] = \"PP34X97WS3\""
|
|
|
|
/// Und die App spricht nur mit einem Helfer, der ebenso signiert ist.
|
|
public static let helperRequirement =
|
|
"anchor apple generic and identifier \"com.scarriffleservices.onyx.helper\" "
|
|
+ "and certificate leaf[subject.OU] = \"PP34X97WS3\""
|
|
}
|