docs: Design-Spec Display-Steuerung Schritt 1 (Command-Pipeline)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
052cf199ae
commit
52bc1fbd6f
1 changed files with 185 additions and 0 deletions
|
|
@ -0,0 +1,185 @@
|
|||
# Design: Display-Steuerung Schritt 1 — Command-Pipeline
|
||||
|
||||
**Datum:** 2026-03-26
|
||||
**Scope:** Command-Pipeline (MQTT + Agent + Backend-API + Ansible-DPMS-Fix)
|
||||
**Nicht in Scope:** Zeitplan (Schritt 2), UI-Schalter (Schritt 3)
|
||||
|
||||
## Ziel
|
||||
|
||||
Das Backend kann ein einzelnes Display per API-Aufruf ein- oder ausschalten. Der Befehl wird per MQTT an den Agent auf dem Pi übermittelt. Der Agent führt `xset dpms force on/off` aus und meldet den Ist-Zustand zurück.
|
||||
|
||||
## Gesamtarchitektur
|
||||
|
||||
```
|
||||
Aufrufer (manuell / später: Zeitplan oder UI)
|
||||
│
|
||||
▼
|
||||
POST /api/v1/screens/{screenSlug}/display
|
||||
│
|
||||
├─► MQTT publish (retained, QoS 1)
|
||||
│ Topic: signage/screen/{slug}/command
|
||||
│ Payload: {"action": "display_on"}
|
||||
│
|
||||
▼
|
||||
Agent (auf dem Pi)
|
||||
│
|
||||
├─► xset dpms force on/off
|
||||
│
|
||||
├─► MQTT publish (QoS 0, nicht retained)
|
||||
│ Topic: signage/screen/{slug}/display-state
|
||||
│ Payload: {"display_state": "on", "ts": "..."}
|
||||
│
|
||||
└─► nächster HTTP-Status-Report (POST /api/v1/player/status)
|
||||
+ display_state: "on"|"off"|"unknown"
|
||||
│
|
||||
▼
|
||||
Backend: speichert in screen_status-Tabelle
|
||||
```
|
||||
|
||||
## MQTT-Vertrag
|
||||
|
||||
### Command-Topic (Backend → Agent)
|
||||
|
||||
- **Topic:** `signage/screen/{screenSlug}/command`
|
||||
- **QoS:** 1
|
||||
- **Retained:** true
|
||||
- **Payload:**
|
||||
```json
|
||||
{"action": "display_on"}
|
||||
```
|
||||
oder
|
||||
```json
|
||||
{"action": "display_off"}
|
||||
```
|
||||
|
||||
Retained sorgt dafür, dass der Agent nach einem Neustart automatisch den letzten Sollzustand empfängt und wiederherstellt.
|
||||
|
||||
### Display-State-Topic (Agent → Welt)
|
||||
|
||||
- **Topic:** `signage/screen/{screenSlug}/display-state`
|
||||
- **QoS:** 0
|
||||
- **Retained:** false
|
||||
- **Payload:**
|
||||
```json
|
||||
{"display_state": "on", "ts": "2026-03-26T10:00:00Z"}
|
||||
```
|
||||
|
||||
`display_state`: `"on"` | `"off"` | `"unknown"`
|
||||
|
||||
## Backend-Änderungen
|
||||
|
||||
### 1. Neuer API-Endpunkt
|
||||
|
||||
`POST /api/v1/screens/{screenSlug}/display`
|
||||
|
||||
- Auth: `authScreen` (bestehende Middleware, Tenant-Isolation)
|
||||
- Request-Body: `{"state": "on"}` oder `{"state": "off"}`
|
||||
- Aktion: publiziert MQTT-Command (retained, QoS 1)
|
||||
- Response: `204 No Content`
|
||||
- Fehler: `400` bei ungültigem `state`, `404` bei unbekanntem Screen
|
||||
|
||||
### 2. Erweiterung `mqttnotifier`
|
||||
|
||||
Neue Methode:
|
||||
```go
|
||||
func (n *Notifier) SendDisplayCommand(screenSlug, action string) error
|
||||
```
|
||||
Publiziert auf `signage/screen/{screenSlug}/command` mit QoS 1 und `retained=true`.
|
||||
|
||||
Das bestehende `publish()`-Helper nutzt QoS 0 und `retain=false` — `SendDisplayCommand` ruft den MQTT-Client direkt auf, um QoS 1 und `retain=true` zu setzen. Gibt Fehler zurück wenn der Publish fehlschlägt; der HTTP-Handler antwortet dann mit `502`.
|
||||
|
||||
### 3. Neue DB-Tabelle `screen_status`
|
||||
|
||||
Migration:
|
||||
```sql
|
||||
create table if not exists screen_status (
|
||||
screen_id text primary key references screens(id) on delete cascade,
|
||||
display_state text, -- 'on' | 'off' | 'unknown'
|
||||
reported_at timestamptz not null default now()
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Erweiterung `POST /api/v1/player/status`
|
||||
|
||||
Request-Payload bekommt optionales Feld:
|
||||
```json
|
||||
{"display_state": "on"}
|
||||
```
|
||||
|
||||
Backend schreibt/aktualisiert `screen_status` per UPSERT.
|
||||
|
||||
### 5. Neuer Store-Methode
|
||||
|
||||
```go
|
||||
func (s *ScreenStore) UpsertDisplayState(ctx context.Context, screenID, displayState string) error
|
||||
```
|
||||
|
||||
## Agent-Änderungen
|
||||
|
||||
### 1. Neues Package `displaycontroller`
|
||||
|
||||
Pfad: `player/agent/internal/displaycontroller/controller.go`
|
||||
|
||||
```go
|
||||
type Controller struct {
|
||||
display string // DISPLAY-Env, z.B. ":0"
|
||||
currentState string // "on" | "off" | "unknown"
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func New(display string) *Controller
|
||||
func (c *Controller) TurnOn() error // xset -display :0 dpms force on
|
||||
func (c *Controller) TurnOff() error // xset -display :0 dpms force off
|
||||
func (c *Controller) State() string // aktueller Zustand (thread-safe)
|
||||
```
|
||||
|
||||
Nach `TurnOn`/`TurnOff` publiziert der Controller den neuen Zustand per MQTT auf `display-state`-Topic.
|
||||
|
||||
`DISPLAY` wird aus Env-Var gelesen (Default: `:0`).
|
||||
|
||||
### 2. Erweiterung `mqttsubscriber`
|
||||
|
||||
Drittes Topic: `signage/screen/{slug}/command`
|
||||
|
||||
Bei eingehender Nachricht:
|
||||
- JSON parsen
|
||||
- `action` auslesen (`display_on` oder `display_off`)
|
||||
- `DisplayController.TurnOn()` bzw. `TurnOff()` aufrufen
|
||||
- Unbekannte Actions: loggen, ignorieren
|
||||
|
||||
### 3. Erweiterung `statusreporter`
|
||||
|
||||
Request-Payload bekommt Feld `display_state string` — aus `DisplayController.State()` befüllt.
|
||||
|
||||
## Ansible-Änderungen
|
||||
|
||||
Datei: `ansible/roles/signage_display/templates/morz-kiosk.j2`
|
||||
|
||||
```bash
|
||||
# Vorher:
|
||||
xset -dpms
|
||||
|
||||
# Nachher:
|
||||
xset +dpms
|
||||
xset dpms 0 0 0 # Timeouts deaktivieren — Steuerung nur über Backend
|
||||
```
|
||||
|
||||
Optional (falls Agent nicht im X-Kontext läuft):
|
||||
`ansible/roles/signage_display/templates/morz-kiosk.service.j2`:
|
||||
```ini
|
||||
Environment=XAUTHORITY=/home/{{ signage_user }}/.Xauthority
|
||||
```
|
||||
Wird beim Testen geprüft.
|
||||
|
||||
## Nicht in Scope (Schritt 1)
|
||||
|
||||
- Zeitplan (`power_on_time`, `power_off_time`, `schedule_enabled`)
|
||||
- UI-Schalter in der Playlist-Verwaltung
|
||||
- Sammelschalter für mehrere Displays
|
||||
- `device_commands`-Tabelle (Audit-Log)
|
||||
- Backend-MQTT-Subscriber (Backend empfängt keine MQTT-Messages)
|
||||
|
||||
## Offen / beim Testen zu klären
|
||||
|
||||
- Braucht der Agent explizit `XAUTHORITY` im Systemd-Unit, oder reicht `DISPLAY=:0`?
|
||||
- Funktioniert `xset` aus dem Agent-Prozess heraus, oder muss es im X-Session-Kontext laufen?
|
||||
Loading…
Add table
Reference in a new issue