Files
onyx/Packages/OnyxKit/Sources/OnyxHelperProtocol/HelperProtocol.swift
Scarriffle 386eb6ae70 Ladelimit ist eine Anzeige, Akku-Kachel sagt etwas, Menüleiste bekommt Kürzel
Der Regler war eine Pseudobedienung. Der SMC lehnt Schreibzugriffe auf CHLT
ab — der Helfer hat es mit korrekter Größe versucht und „geschrieben (false),
zurückgelesen 85" gemeldet. Das Sicherheitsnetz hat also die Wahrheit gesagt;
die Oberfläche hat sie nur nicht gezeigt.

Weiter zu raten hieße, im Lade-Subsystem Keys durchzuprobieren, und das ist
die eine Operation in diesem Projekt, die Hardware dauerhaft beschädigen
kann. Also: das Limit wird gelesen und angezeigt, mit einem Weg in die
Systemeinstellungen, wo es tatsächlich geht. Der Schreibpfad ist aus dem
privilegierten Helfer entfernt — ein Root-Dienst trägt nur, was benutzt wird.

Die Akku-Kachel zeigte „40 %" und einen Verlauf über die letzten Sekunden.
Der Ladestand ändert sich in Minuten, nicht in Sekunden; der Graph war eine
waagerechte Linie. Jetzt: verbleibende Zeit bzw. Zeit bis voll, Leistung in
Watt mit Richtung, Ladelimit, Zustand, Zyklen — und der Energiesparmodus.
Der erklärt, warum die Maschine langsamer wirkt, und ohne diesen Hinweis
sucht man den Grund woanders.

In der Menüleiste standen fünf nackte Prozentzahlen nebeneinander. Man sah
Zahlen und wusste nicht, welche wozu gehört. Zwei neue Darstellungen: Kürzel
und Wert („CPU 34 %") sowie Symbol und Wert. Beim Akku stehen sie zuoberst in
der Auswahl.

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

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.
///
/// Nur lesen. Der SMC lehnt Schreibzugriffe auf `CHLT` ab nachgemessen,
/// siehe docs/spikes/A-smc.md. macOS setzt den Wert offenbar auf einem
/// anderen Weg als über den SMC-Userclient.
///
/// `Int` statt `Int?`: Optionals sind über `@objc` nicht darstellbar.
func chargeLimit(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\""
}