docs: design spec für Noctalia-Plugin loki/headsetcontrol
This commit is contained in:
commit
85b048feef
1 file changed
+160
@@ -0,0 +1,160 @@
|
||||
# Design: Noctalia-Plugin `loki/headsetcontrol`
|
||||
|
||||
Datum: 2026-08-31 · Status: genehmigt (abschnittsweise reviewed am 31.08.2026)
|
||||
|
||||
## Ziel
|
||||
|
||||
Ein Noctalia-v5-Plugin, das [HeadsetControl](https://github.com/Sapd/HeadsetControl) (CLI) ansteuert,
|
||||
um Gaming-Headsets aus der Shell heraus zu überwachen und zu steuern. Referenzgerät ist das
|
||||
**Audeze Maxwell** des Autors; das Plugin soll aber geräteübergreifend funktionieren, indem es die
|
||||
vom Gerät gemeldeten Capabilities auswertet.
|
||||
|
||||
## Umgebung / Fakten (verifiziert)
|
||||
|
||||
- Noctalia v5.0.0 läuft, Plugin-System in Beta (Plugin APIs 3–28).
|
||||
- HeadsetControl installiert unter `/usr/bin/headsetcontrol`, Ausgabe `-o json` (api_version 1.4).
|
||||
- Detektiertes Gerät: `Audeze Maxwell` (`0x3329:0x4b19`), Capabilities:
|
||||
`sidetone`, `battery`, `inactive time`, `chatmix`, `voice prompts`,
|
||||
`equalizer preset` (10 Presets), `volume limiter`, `microphone noise filter`.
|
||||
- Batterie aktuell `BATTERY_UNAVAILABLE` (Headset aus). Chatmix ist read-only (nur Anzeige).
|
||||
- Ein kombinierter Aufruf `headsetcontrol -b -m -o json` liefert Batterie + Chatmix in einer JSON-Antwort.
|
||||
|
||||
## Architektur
|
||||
|
||||
**Ansatz A (gewählt): Service-zentrale Architektur.**
|
||||
|
||||
- Ein `[[service]]` pollt den Headset-Status im konfigurierbaren Intervall und publiziert ihn über
|
||||
den Noctalia-State-Channel (`noctalia.state`).
|
||||
- `[[widget]]` (Bar), `[[panel]]` (Steuerung) und `[[shortcut]]` (Control-Center-Tile) konsumieren
|
||||
den State und senden Steuerbefehle.
|
||||
- Das Panel sendet Befehle direkt per `noctalia.runAsync(argv, cb)` (argv-Array, `plugin_api = 24`,
|
||||
kein Shell-Parsing bei dynamischen Werten).
|
||||
- Capabilities werden einmalig beim Start per `--capabilities -o json` erkannt und über State geteilt.
|
||||
- Battery-Warnung lebt im Service (läuft immer), dedupliziert mit Hysterese.
|
||||
|
||||
### Komponenten
|
||||
|
||||
| Komponente | Datei | Aufgabe |
|
||||
|---|---|---|
|
||||
| Manifest | `plugin.toml` | Identität `loki/headsetcontrol`, Entries, Settings-Schema |
|
||||
| CLI-Wrapper | `lib/hsc.luau` | JSON-Parsing, Command-Builder (argv-Arrays), geteilt zwischen Entries |
|
||||
| Service | `service.luau` | Capabilities lesen, Status pollen, Battery-Warnung, State publizieren |
|
||||
| Widget | `widget.luau` | Bar-Widget: Batterie + Farben, Scroll = Sidetone, Klick = Panel öffnen |
|
||||
| Panel | `panel.luau` | Steuerungs-UI (capabilities-gesteuert) |
|
||||
| Shortcut | `shortcut.luau` | Control-Center-Tile: Sidetone an/aus |
|
||||
| Translations | `translations/en.json`, `translations/de.json` | UI-Texte + Setting-Labels |
|
||||
| Docs | `README.md`, `thumbnail.webp`, `LICENSE`, `catalog.toml` | Publishing-fertig |
|
||||
|
||||
## Manifest
|
||||
|
||||
```
|
||||
id = "loki/headsetcontrol"
|
||||
name = "Headset Control"
|
||||
version = "1.0.0"
|
||||
plugin_api = 24
|
||||
author = "loki"
|
||||
license = "MIT"
|
||||
dependencies = ["headsetcontrol"]
|
||||
```
|
||||
|
||||
Entries: `[[service]]`, `[[widget]]`, `[[panel]]`, `[[shortcut]]`.
|
||||
|
||||
### Settings (Plugin-Level, `[[setting]]`)
|
||||
|
||||
| Key | Typ | Default | Beschreibung |
|
||||
|---|---|---|---|
|
||||
| `poll_interval` | int (10–600) | 60 | Poll-Intervall in Sekunden |
|
||||
| `battery_low` | int (5–50) | 20 | Warnschwelle Batterie in % |
|
||||
| `sidetone_default` | int (0–128) | 64 | Startwert Sidetone |
|
||||
| `device` | string | `""` | Leer = Auto-Detect; sonst `VID:PID` |
|
||||
| `mock` | bool, advanced | `false` | Simuliert Audeze Maxwell (Entwicklung ohne Headset) |
|
||||
|
||||
## Datenfluss / State-Channel
|
||||
|
||||
Service publiziert (`noctalia.state`):
|
||||
|
||||
- `caps` — Liste unterstützter Features (`{"sidetone", "battery", ...}`) + `equalizer_presets_count`
|
||||
- `battery` — `{ level, status }` (`status` ∈ `BATTERY_CHARGING/FULL/NORMAL/DISCHARGING/UNAVAILABLE`)
|
||||
- `chatmix` — int (negativ = Game lauter, positiv = Chat lauter, 0 = Mitte)
|
||||
- `device` — `{ name, vendor, product, id_vendor, id_product }`
|
||||
- `sidetone` — letzter gesetzter Wert (Sidetone ist nicht auslesbar → lokaler Merker)
|
||||
- `lastUpdate` — ms-Timestamp
|
||||
- `error` — letzter Fehlerstring (oder `nil`)
|
||||
|
||||
Widget/Panel/Shortcut watch die Keys und re-rendern.
|
||||
|
||||
## Entries im Detail
|
||||
|
||||
### Service (`service.luau`)
|
||||
|
||||
- Top-Level: Capabilities einmalig lesen (respektiert `device`), in `state.caps`/`state.device` publizieren.
|
||||
- `update()` (Intervall `poll_interval`): `headsetcontrol [-d V:P] -b -m -o json` → State publizieren.
|
||||
- **Battery-Warnung:** Wenn `level <= battery_low` und nicht schon gewarnt → `noctalia.notifyError`
|
||||
(dedupliziert; Re-Arm erst wenn Status charging/full oder `level > battery_low + 5`).
|
||||
- `onConfigChanged()`: Intervall/Schwelle/Device live übernehmen.
|
||||
- `onIpc("refresh", ...)`: manueller Re-Poll (für `noctalia msg plugin loki/headsetcontrol:service all refresh`).
|
||||
- Fehlerzustand (Binary fehlt, kein Gerät, Command-Fehler) wird als `state.error` publiziert statt zu spammen.
|
||||
|
||||
### Widget (`widget.luau`) — imperative API
|
||||
|
||||
- Watcht `battery`/`chatmix`/`caps`/`error`.
|
||||
- Anzeige: Glyph (Battery, Variante für Charging) + `%`-Text.
|
||||
- Farben: `>= battery_low` normal; `< battery_low` `error`; `BATTERY_UNAVAILABLE`/kein Gerät gedimmt mit `–`.
|
||||
- **Scroll (vertikal):** Sidetone ±4 (0–128), Command senden, neuer Wert kurz als Text anzeigen.
|
||||
Startwert = `sidetone_default` bzw. letzter State-Wert.
|
||||
- **Klick:** `noctalia.togglePanel("loki/headsetcontrol:panel")`.
|
||||
- Tooltip: Gerätename + Status + Chatmix.
|
||||
- `[widget.actions]`: keine festen Bindings (Skript-Callbacks behalten Vorrang).
|
||||
|
||||
### Panel (`panel.luau`) — deklarative `ui.*`-UI
|
||||
|
||||
- Manifest: `width = 400`, `height = 480`, `placement = "floating"`, `dismiss_on_outside_click = true`.
|
||||
- Rendert in `onOpen(context)` und bei State-Updates/Commands.
|
||||
- Kopf: Gerätename, Batterie-Progressbar + %, Chatmix-Balance-Anzeige.
|
||||
- Controls (nur sichtbar bei unterstützter Capability):
|
||||
- Sidetone: Slider 0–128 + Off-Button.
|
||||
- EQ-Presets: `ui.select` mit `Preset 0..N-1` (N aus `equalizer_presets_count`).
|
||||
- Inactive Time: `ui.select` (Nie, 1, 2, 5, 10, 15, 20, 30, 45, 60, 90 min).
|
||||
- Noise Filter: 3 Buttons (Off/Low/High).
|
||||
- Voice Prompts: `ui.toggle`.
|
||||
- Volume Limiter: `ui.toggle`.
|
||||
- Befehle: `noctalia.runAsync({"headsetcontrol", "-d", dev, <flag>, <wert>}, cb)`.
|
||||
- `exitCode ~= 0` → `noctalia.notifyError` (stderr-Kurzfassung), optimistischen Wert zurücksetzen.
|
||||
- Erfolg → State `sidetone` aktualisieren; Service-Poll bestätigt den realen Zustand.
|
||||
|
||||
### Shortcut (`shortcut.luau`)
|
||||
|
||||
- Tile „Sidetone": Toggle an = letzter Wert/`sidetone_default`, aus = 0.
|
||||
- `setLabel` zeigt Batterie, `setActive` spiegelt Toggle-Zustand.
|
||||
- `onConfigChanged()` nicht nötig (Shortcut ist Widget-artig, wird bei Settings-Änderung neu gebaut).
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
- `headsetcontrol` fehlt (`noctalia.commandExists` false) → Widget: Warn-Glyph + Tooltip,
|
||||
Service loggt einmalig, keine Spam-Notifications.
|
||||
- Kein Gerät / `BATTERY_UNAVAILABLE` → normaler Zustand: gedimmte Anzeige, keine Errors.
|
||||
- Command-Fehler → `notifyError` + UI-Rollback des optimistischen Werts.
|
||||
- Fehlende State-Keys (Service noch nicht gestartet) → Defaults, keine Crashes.
|
||||
|
||||
## Testing
|
||||
|
||||
1. `mock = true`: fake Maxwell-Daten, komplette UI ohne Headset testbar.
|
||||
2. IPC: `noctalia msg plugin loki/headsetcontrol:service all refresh`.
|
||||
3. Hot-Reload der Luau-Dateien während der Entwicklung.
|
||||
4. Live-Verifikation realer Commands gegen das Maxwell im Terminal
|
||||
(`-s`, `-p`, `-i`, `--noise-filter`, `-v`, `--volume-limiter`).
|
||||
5. Dev-Install unter `~/.local/share/noctalia/plugins/headsetcontrol/`
|
||||
(höchste Source-Priorität, übersteuert alle Quellen).
|
||||
|
||||
## Publishing (optional, später)
|
||||
|
||||
- Repo-Struktur: Plugin im Unterordner `headsetcontrol/`, `catalog.toml` am Repo-Root.
|
||||
- PR gegen `noctalia-dev/community-plugins` (CI validiert Manifest + Katalog).
|
||||
- Dev-Install bleibt über das `path`-Source-Override möglich.
|
||||
|
||||
## Nicht im Scope (YAGNI)
|
||||
|
||||
- Launcher-Provider, Desktop-Widget.
|
||||
- Chatmix schreiben (HeadsetControl unterstützt das nicht).
|
||||
- EQ-Preset-Namen (API liefert keine; nur `Preset N`).
|
||||
- I18n über `en`/`de` hinaus.
|
||||
Reference in new issue
Block a user