Phase 0: Repo-Gerüst und vier Verifikations-Spikes
Vier Annahmen des Plans vor jedem Implementierungscode geprüft:
A (SMC): Lüftersteuerung bestätigt — F0md/F1md (klein!) als Auto/Manuell-
Umschalter, F0Tg als Ziel, Grenzen aus F0Mn/F0Mx. SMC-Integer sind
little-endian, nicht big-endian wie im verbreiteten Intel-Beispielcode.
Ladelimit: CHWA/CH0B/CH0C/bfE0/bfF0 existieren auf Mac17,9 nicht.
Key noch nicht identifiziert, deshalb smc-diff.sh für eine
Differenzmessung ohne jeden Schreibzugriff.
B (Media): mediaremote-adapter läuft unter 26.6.1, aus dem Quellcode gebaut.
Liefert vollständige Metadaten inkl. Cover aus Google Chrome — der
Fall, den AppleScript nicht erreicht.
C (Audio): Process Tap + privates Aggregate-Device liefern echte Samples
(469 Callbacks/5 s, 0 stille Puffer). Konflikt gefunden: SoundSource
6.1.1 mit ARK.driver ist bereits installiert.
D (Group): App-Group-Container ist aus einem nicht-sandboxed Prozess erreichbar.
Damit trägt die Sandbox-Entscheidung, die Bridge-App entfällt.
Spikes sind Wegwerfcode zur Verifikation, kein Produktionscode.
This commit is contained in:
90
docs/spikes/A-smc.md
Normal file
90
docs/spikes/A-smc.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Spike A — SMC-Keys auf Mac17,9 (M5 Pro)
|
||||
|
||||
Gemessen am 10.08.2026, macOS 26.6.1 (25G76), **ohne root**, ausschließlich lesend.
|
||||
Werkzeug: `Spikes/smc-dump.swift`. 3486 Keys insgesamt.
|
||||
|
||||
## Ergebnis in einem Satz
|
||||
|
||||
**Lüftersteuerung: bestätigt und umsetzbar.** **Ladelimit: Key noch nicht identifiziert** —
|
||||
das Standardverfahren aus AlDente/batt greift auf dieser Maschine nicht, weil die dort benutzten
|
||||
Keys schlicht nicht existieren.
|
||||
|
||||
## Wichtige Querbefunde
|
||||
|
||||
**SMC-Integer sind little-endian.** Meine erste BE-Dekodierung ergab Unsinn (Akkuspannung
|
||||
31023 mV). LE ergibt konsistente Werte, zweifach gegengeprüft:
|
||||
|
||||
| Key | Roh | LE-Wert | Gegenprobe |
|
||||
|---|---|---|---|
|
||||
| `B0AV` | `AA 2F` | 12202 mV | plausible Zellspannung |
|
||||
| `CH0V` | `AA 2F 00 00` | 12202 mV | identisch zu `B0AV` |
|
||||
| `B0AC` | `85 FD` | −635 mA | entlädt; passt zu `PMVR` = 0 W (kein Netzteil) |
|
||||
| `PPBR` | — | 12,13 W | ≈ 12,2 V × 0,64 A ✓ |
|
||||
|
||||
`flt`-Keys sind ebenfalls little-endian IEEE-754. Für die Implementierung heißt das:
|
||||
**alle Mehrbyte-SMC-Werte little-endian lesen**, entgegen dem verbreiteten Beispielcode, der
|
||||
big-endian annimmt (der stammt aus der Intel-Ära).
|
||||
|
||||
## Lüfter — vollständig nutzbar
|
||||
|
||||
Zwei Lüfter vorhanden (`FNum` = 2).
|
||||
|
||||
| Key | Typ | Wert | Bedeutung |
|
||||
|---|---|---|---|
|
||||
| `F0md` / `F1md` | ui8 | 0 | **Modus: 0 = auto, 1 = manuell** |
|
||||
| `F0Tg` / `F1Tg` | flt | 2317 / 2502 | Ziel-Drehzahl |
|
||||
| `F0Ac` / `F1Ac` | flt | 2321 / 2499 | Ist-Drehzahl |
|
||||
| `F0Mn` / `F1Mn` | flt | 2317 | Minimum |
|
||||
| `F0Mx` / `F1Mx` | flt | 7826 | Maximum |
|
||||
| `F0St` / `F1St` | ui8 | 5 | Status |
|
||||
| `FEna` | hex | 07 | Freigabe-Bitmaske |
|
||||
|
||||
**Korrektur zur Planannahme:** der Modus-Key heißt `F0md` **klein geschrieben**, nicht `F0Md`.
|
||||
Mit der Großschreibung aus der Intel-Dokumentation findet man ihn nicht — genau deshalb lief
|
||||
dieser Spike vor dem Implementierungscode.
|
||||
|
||||
`FS! ` (die alte Force-Bitmaske) existiert **nicht**. Der Weg ist also:
|
||||
`F0md = 1` → `F0Tg = Wunschdrehzahl`, zurück auf Automatik mit `F0md = 0`.
|
||||
|
||||
Der Sicherheitsrahmen aus dem Plan lässt sich damit sauber umsetzen: `F0Mn`/`F0Mx` liefern
|
||||
die harten Grenzen direkt aus der Firmware, und `F0md = 0` ist ein einzelner, immer
|
||||
verfügbarer Rückfallbefehl.
|
||||
|
||||
## Ladelimit — noch offen
|
||||
|
||||
**Nicht vorhanden:** `CHWA`, `CH0B`, `CH0C`, `CH0I`, `CH0J`, `CH0K`, `BCLM`, `bfE0`, `bfF0`.
|
||||
|
||||
Damit scheiden sowohl das AlDente-Legacy-Verfahren (`CH0B`/`CH0C`) als auch das
|
||||
80-%-Flag (`CHWA`) als auch das vollständige Firmware-Backend (`bfD0`/`bfE0`/`bfF0`) aus —
|
||||
von letzterem existiert nur `bfD0` (hex, 2 Byte, `00 00`).
|
||||
|
||||
Vorhanden ist stattdessen ein umfangreicher `CH*`-Satz. Plausible Kandidaten:
|
||||
|
||||
| Key | Typ | Wert (LE) | Vermutung |
|
||||
|---|---|---|---|
|
||||
| `CHTE` | ui32 | 0 | Ladeziel — 0 könnte „kein Limit" heißen |
|
||||
| `CHTL` | ui32 | 3600 | untere Schwelle? |
|
||||
| `CHTU` | ui32 | 4100 | obere Schwelle? (mV je Zelle) |
|
||||
| `CHBV` | ui32 | 4125 | Ladeschlussspannung mV |
|
||||
| `CHIL` / `CHIO` | flag | false | Inhibit-Flags |
|
||||
| `CHSE` / `CHST` / `CHRT` | ui8 | 0 | Status/Steuerung |
|
||||
|
||||
**Das bleibt Vermutung, und Vermutungen werden hier nicht geschrieben.** Ein falscher Wert im
|
||||
Lade-Subsystem ist die eine Operation in diesem Projekt, die Hardware dauerhaft beschädigen kann.
|
||||
|
||||
### Nächster Schritt: Differenzmessung statt Raten
|
||||
|
||||
macOS hat selbst eine Ladebegrenzung (Systemeinstellungen → Batterie → Batteriezustand →
|
||||
„Optimiertes Laden" bzw. das 80-%-Limit). Wird sie umgeschaltet, muss sich der zuständige
|
||||
SMC-Key ändern. `Spikes/smc-diff.sh` nimmt zwei vollständige Dumps auf und zeigt die Differenz —
|
||||
damit ist der Key **ohne einen einzigen Schreibzugriff** identifizierbar.
|
||||
|
||||
Bis dieser Test gelaufen ist, bleibt die Akkukarte reine Anzeige.
|
||||
|
||||
## Auswirkungen auf den Plan
|
||||
|
||||
1. Phase 6 wird geteilt: **Lüftersteuerung kann gebaut werden**, das Ladelimit wartet auf die
|
||||
Differenzmessung.
|
||||
2. Alle SMC-Werte werden little-endian gelesen — betrifft `MetricsProvider` und `OnyxHelper`.
|
||||
3. `F0Mn`/`F0Mx` werden zur Laufzeit aus der Firmware gelesen statt fest verdrahtet.
|
||||
4. Lüfterdrehzahlen sind `flt`, nicht `fpe2`.
|
||||
81
docs/spikes/B-mediaremote.md
Normal file
81
docs/spikes/B-mediaremote.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Spike B — mediaremote-adapter unter macOS 26.6.1
|
||||
|
||||
Gemessen am 10.08.2026. Quelle aus dem Git geklont und **selbst kompiliert**, keine
|
||||
Fremd-Binary geladen: `Spikes/build-mediaremote-adapter.sh` ersetzt das CMake-Setup
|
||||
des Projekts durch einen direkten clang-Aufruf (cmake ist auf dieser Maschine nicht
|
||||
installiert).
|
||||
|
||||
Projekt: [ungive/mediaremote-adapter](https://github.com/ungive/mediaremote-adapter), BSD-3-Clause,
|
||||
~400 KB Objective-C plus ein Perl-Skript.
|
||||
|
||||
## Ergebnis: funktioniert, auch im schwierigsten Fall
|
||||
|
||||
```
|
||||
perl mediaremote-adapter.pl <framework> <testclient> test → Exitcode 0
|
||||
```
|
||||
|
||||
Exitcode 0 heißt: der Adapter ist berechtigt, MediaRemote zu benutzen. Apples Sperre aus
|
||||
macOS 15.4 ist damit auf 26.6.1 weiterhin über den Perl-Trampolinweg umgehbar.
|
||||
|
||||
### `get` — vollständige Metadaten
|
||||
|
||||
Gelesen wurde eine laufende Wiedergabe in **Google Chrome** (YouTube). Das ist der wichtige
|
||||
Befund: genau diese Quelle erreicht AppleScript nicht, sie ist der Grund für die
|
||||
Adapter-Ebene überhaupt.
|
||||
|
||||
Gelieferte Felder:
|
||||
|
||||
| Feld | Beispiel |
|
||||
|---|---|
|
||||
| `title` | „Hanjos große Entdeckung in Thailand! \| Hanjo 2012" |
|
||||
| `artist` | „FilowLIVE" |
|
||||
| `album` | `""` |
|
||||
| `duration` | 3051,04 s |
|
||||
| `elapsedTime` | 432,06 s |
|
||||
| `playbackRate` | 1 |
|
||||
| `playing` | `true` |
|
||||
| `bundleIdentifier` | `com.google.Chrome` |
|
||||
| `processIdentifier` | 4525 |
|
||||
| `artworkData` | JPEG, base64 |
|
||||
| `artworkMimeType` | `image/jpeg` |
|
||||
| `timestamp` | `2026-08-10T14:22:16Z` |
|
||||
| `contentItemIdentifier` | UUID |
|
||||
|
||||
Das Cover kam sofort mit — die Warnung aus der Projekt-README („artwork often takes a bit of
|
||||
time") traf hier nicht zu, ist aber weiterhin einzuplanen.
|
||||
|
||||
### `stream` — der Modus, den Onyx benutzt
|
||||
|
||||
Zeilenweises JSON auf stdout:
|
||||
|
||||
```json
|
||||
{"type":"data","diff":false,"payload":{}}
|
||||
{"type":"data","diff":false,"payload":{"title":"…","elapsedTime":432.06,…}}
|
||||
```
|
||||
|
||||
Erst ein leerer Payload, dann der vollständige. `diff` unterscheidet Voll- von
|
||||
Teilaktualisierung — der Parser in `MediaProvider` muss beide Fälle behandeln und darf bei
|
||||
einem leeren Payload **nicht** auf „nichts läuft" schließen.
|
||||
|
||||
`elapsedTime` wird nicht sekündlich nachgeliefert. Die Fortschrittsanzeige muss zwischen den
|
||||
Ereignissen lokal hochzählen, ausgehend von `elapsedTime` + `timestamp` + `playbackRate`,
|
||||
sonst steht der Scrubber still.
|
||||
|
||||
## Nicht getestet
|
||||
|
||||
Die Steuerbefehle (`send`, `seek`, `shuffle`, `repeat`, `speed`) wurden **absichtlich nicht**
|
||||
ausgeführt — sie hätten die laufende Wiedergabe des Nutzers angehalten oder verstellt. Die
|
||||
Prüfung erfolgt in Phase 4 an einer eigens gestarteten Wiedergabe.
|
||||
|
||||
## Auswirkungen auf den Plan
|
||||
|
||||
1. Die Kaskade aus dem Plan bleibt, aber die Gewichtung verschiebt sich: der Adapter deckt
|
||||
mehr ab als erwartet, AppleScript ist reiner Rückfall.
|
||||
2. Der Adapter wird **aus dem Quellcode mitgebaut**, nicht als Fremd-Binary eingebunden.
|
||||
`Spikes/build-mediaremote-adapter.sh` wandert dafür als Build-Phase ins Xcode-Projekt.
|
||||
Vorteil: kein blind eingebundenes Binärartefakt, und die Ad-hoc-Signatur passt zum
|
||||
restlichen Bundle.
|
||||
3. Die Perl-Datei und das Framework kommen nach `Onyx/Resources/Vendor/` und werden
|
||||
**nicht** gelinkt (sonst greift Apples Entitlement-Prüfung im eigenen Prozess).
|
||||
4. `disable-library-validation` in den Entitlements ist bestätigt nötig — das Framework ist
|
||||
ad-hoc signiert, der Host wird es mit Team-Zertifikat sein.
|
||||
64
docs/spikes/C-audiotap.md
Normal file
64
docs/spikes/C-audiotap.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Spike C — Core Audio Process Taps auf M5 Pro / macOS 26.6.1
|
||||
|
||||
Gemessen am 10.08.2026. Werkzeug: `Spikes/audiotap-probe.swift`, signiert mit dem
|
||||
Team-Zertifikat. Der Tap lief mit `muteBehavior = .unmuted` — der Spike hört nur zu und
|
||||
verändert die Wiedergabe nicht.
|
||||
|
||||
## Ergebnis: bestätigt
|
||||
|
||||
| Schritt | Ergebnis |
|
||||
|---|---|
|
||||
| `kAudioHardwarePropertyProcessObjectList` | 35 Audio-Prozesse, mit PID, Bundle-ID und `IsRunningOutput` |
|
||||
| `AudioHardwareCreateProcessTap` | erzeugt, Tap-Objekt 195, Format 48 kHz / 2 Kanäle / 32 bit Float |
|
||||
| `AudioHardwareCreateAggregateDevice` (privat) | erzeugt, Device 196 |
|
||||
| IOProc über 5 s | 469 Callbacks (93/s), 480 256 Frames, Spitzenpegel 0,4516, **0 stille Puffer** |
|
||||
|
||||
480 256 Frames ÷ 2 Kanäle ÷ 5 s = 48 025 Hz — deckt sich exakt mit der Abtastrate.
|
||||
93 Callbacks/s entsprechen ~512 Frames je Puffer, also rund 10,7 ms Latenz. Für einen
|
||||
Lautstärkemixer unkritisch.
|
||||
|
||||
Der bekannte Fehlerfall „Puffer laufen nach einiger Zeit auf Null" trat im 5-Sekunden-Fenster
|
||||
nicht auf. Das widerlegt ihn nicht — der Watchdog aus dem Plan bleibt.
|
||||
|
||||
**Kein TCC-Dialog erschienen.** Das ist noch kein Freibrief: `.unmuted` ist der harmlose Fall.
|
||||
Ob `.mutedWhenTapped` — der Modus, den der Mixer wirklich braucht — eine Berechtigung anfordert,
|
||||
muss in Phase 7 gegengeprüft werden.
|
||||
|
||||
## Fallstrick beim Bauen
|
||||
|
||||
Die verbreitete Kurzform für Core-Audio-Property-Arrays
|
||||
|
||||
```swift
|
||||
var out = [T](repeating: unsafeBitCast(0 as Int, to: T.self), count: count)
|
||||
```
|
||||
|
||||
**stürzt mit SIGTRAP ab**: `unsafeBitCast` verlangt identische Größen, `Int` hat 8 Byte,
|
||||
`AudioObjectID` 4. Da stdout in eine Pipe voll gepuffert ist, kam nicht einmal die erste
|
||||
Zeile Ausgabe — der Absturz sah aus wie ein stiller Fehlstart. `MetricsProvider` und
|
||||
`AudioProvider` verwenden deshalb die Variante über `UnsafeMutableRawPointer.allocate`
|
||||
mit `MemoryLayout<T>.stride`.
|
||||
|
||||
## Konflikt: SoundSource ist bereits installiert
|
||||
|
||||
Die Prozessliste zeigt `com.rogueamoeba.soundsource` und `com.rogueamoeba.arkaudiod`,
|
||||
dazu liegt `/Library/Audio/Plug-Ins/HAL/ARK.driver` im System. **SoundSource 6.1.1** ist
|
||||
Rogue Amoebas Per-App-Lautstärkemixer — funktional genau das, was Phase 7 bauen soll.
|
||||
|
||||
Zwei Folgen:
|
||||
|
||||
1. **Doppelung.** Der Mixer aus Phase 7 wäre ein Nachbau einer bereits vorhandenen,
|
||||
ausgereiften und bezahlten App.
|
||||
2. **Technisches Risiko.** ARK.driver ist ein HAL-Plugin, das Audio abfängt, bevor es das
|
||||
Ausgabegerät erreicht. Leitet SoundSource einen Prozess bereits um, sieht ein
|
||||
Onyx-Process-Tap auf denselben Prozess unter Umständen nichts mehr — oder beide Ebenen
|
||||
schalten sich gegenseitig stumm. Sauber vorhersagen lässt sich das nicht; es müsste
|
||||
gemessen werden.
|
||||
|
||||
Vor Phase 7 zu klären. Bis dahin ist der Befund neutral festgehalten, nicht entschieden.
|
||||
|
||||
## Nebenbefund
|
||||
|
||||
`Alcove.app` und `Sapphire.app` sind beide installiert — also die beiden Apps, die als Vorbild
|
||||
genannt wurden. Für die Notch-Shell in Phase 1 heißt das: mindestens eine davon greift
|
||||
denselben Bildschirmbereich ab. Für die Entwicklung sollten sie beendet sein, sonst überlagern
|
||||
sich Hover-Zonen und die Panels streiten sich um dieselbe Fläche.
|
||||
45
docs/spikes/D-appgroup.md
Normal file
45
docs/spikes/D-appgroup.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Spike D — App-Group-Container ohne Sandbox
|
||||
|
||||
Gemessen am 10.08.2026. Werkzeug: `Spikes/appgroup-probe.swift`,
|
||||
gebaut und signiert über `Spikes/build-appgroup-probe.sh`.
|
||||
|
||||
## Ergebnis: bestätigt
|
||||
|
||||
Ein **nicht sandboxed** Prozess, signiert mit `Apple Development (PP34X97WS3)` und der
|
||||
Entitlement `com.apple.security.application-groups`, erhält den Container:
|
||||
|
||||
```
|
||||
/Users/scarriffle/Library/Group Containers/PP34X97WS3.group.com.scarriffleservices.calendarr
|
||||
```
|
||||
|
||||
- `containerURL(forSecurityApplicationGroupIdentifier:)` liefert einen Pfad, nicht `nil`
|
||||
- Schreibzugriff vorhanden
|
||||
- `CFNotificationCenterAddObserver` auf dem Darwin-Center registriert ohne Fehler
|
||||
|
||||
Damit ist die Kernannahme des Plans belegt: Onyx kann nicht-sandboxed sein — und damit
|
||||
IOKit, den privilegierten Helper und den MediaRemote-Adapter nutzen — **und** trotzdem den
|
||||
Calendarr-Snapshot über `CalendarrCore` lesen. Die Bridge-App als Rückfalloption entfällt.
|
||||
|
||||
## Zustand des Containers
|
||||
|
||||
```
|
||||
.com.apple.containermanagerd.metadata.plist
|
||||
Library/
|
||||
```
|
||||
|
||||
Kein `widget-cache.json`. Die Calendarr-Mac-App ist auf dieser Maschine noch nie gelaufen,
|
||||
`SnapshotStore().read()` wird also korrekt `.neverWritten` liefern. Das ist der erste Zustand,
|
||||
den das Kalender-Widget behandeln muss, und der einzige, der sich derzeit real testen lässt.
|
||||
|
||||
Anmerkung: das Verzeichnis existierte vor diesem Spike nicht — `containermanagerd` legt es beim
|
||||
ersten Zugriff an. Ein vorhandener Container ist also **kein** Beleg dafür, dass Calendarr
|
||||
jemals geschrieben hat. Genau deshalb ist `.neverWritten` ein eigener Zustand und nicht
|
||||
„Datei fehlt".
|
||||
|
||||
## Offener Punkt
|
||||
|
||||
Der Spike signiert eine nackte Binary ohne Provisioning-Profil. Das App-Bundle bekommt von
|
||||
Xcode ein eingebettetes Profil — das ist der weniger strenge Fall, der Zugriff kann dort nicht
|
||||
schlechter sein. Beim ersten echten Build wird trotzdem gegengeprüft, dass `containerURL`
|
||||
nicht `nil` ist; das ist laut Integrationsanleitung der Fehler, der sich sonst als leere App
|
||||
ohne jede Meldung zeigt.
|
||||
Reference in New Issue
Block a user