5.3 KiB
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:
oder{"action": "display_on"}{"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:
{"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:
400bei ungültigemstate,404bei unbekanntem Screen
2. Erweiterung mqttnotifier
Neue Methode:
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:
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:
{"display_state": "on"}
Backend schreibt/aktualisiert screen_status per UPSERT.
5. Neuer Store-Methode
func (s *ScreenStore) UpsertDisplayState(ctx context.Context, screenID, displayState string) error
Agent-Änderungen
1. Neues Package displaycontroller
Pfad: player/agent/internal/displaycontroller/controller.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
actionauslesen (display_onoderdisplay_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
# 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:
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
XAUTHORITYim Systemd-Unit, oder reichtDISPLAY=:0? - Funktioniert
xsetaus dem Agent-Prozess heraus, oder muss es im X-Session-Kontext laufen?