26 KiB
Display-Steuerung Schritt 1 — Command-Pipeline Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Das Backend kann per POST /api/v1/screens/{slug}/display ein Display ein- oder ausschalten; der Befehl wird per MQTT (retained, QoS 1) an den Agent übermittelt, der xset dpms force on/off ausführt und den Zustand per HTTP und MQTT zurückmeldet.
Architecture: Backend publiziert einen retained MQTT-Command auf signage/screen/{slug}/command. Der Agent abonniert dieses Topic, führt xset aus und meldet den Zustand im nächsten HTTP-Status-Report (und per MQTT-Broadcast) zurück. Der zuletzt gemeldete Zustand wird in einer neuen screen_status-Tabelle persistiert.
Tech Stack: Go, PostgreSQL/pgx, paho.mqtt.golang, xset (X11)
Dateiübersicht
Backend (create/modify):
- Create:
server/backend/internal/db/migrations/004_screen_status.sql - Modify:
server/backend/internal/store/store.go—ScreenStatusstruct +UpsertDisplayState - Modify:
server/backend/internal/mqttnotifier/notifier.go—SendDisplayCommand - Create:
server/backend/internal/httpapi/manage/display.go—HandleDisplayCommand - Modify:
server/backend/internal/httpapi/router.go— neue Route registrieren - Modify:
server/backend/internal/httpapi/playerstatus.go—display_stateim Request
Agent (create/modify):
- Create:
player/agent/internal/displaycontroller/controller.go - Modify:
player/agent/internal/mqttheartbeat/heartbeat.go—SendDisplayState - Modify:
player/agent/internal/app/app.go— Interface + Wiring - Modify:
player/agent/internal/mqttsubscriber/subscriber.go— Command-Topic - Modify:
player/agent/internal/statusreporter/reporter.go—display_state-Feld
Ansible:
- Modify:
ansible/roles/signage_display/templates/morz-kiosk.j2
Task 1: DB Migration — screen_status
Files:
-
Create:
server/backend/internal/db/migrations/004_screen_status.sql -
Schritt 1: Migrationsdatei anlegen
-- Migration 004: Display-Steuerung – screen_status -- Speichert den zuletzt vom Agent gemeldeten Display-Zustand pro Screen. create table if not exists screen_status ( screen_id text primary key references screens(id) on delete cascade, display_state text not null default 'unknown', reported_at timestamptz not null default now() ); -
Schritt 2: Backend neu starten, Migration prüfen
docker compose -f compose/server-stack.yml up --build backendErwartet: Log-Zeile
event=migration_applied version=4 file=004_screen_status.sql -
Schritt 3: Committen
git add server/backend/internal/db/migrations/004_screen_status.sql git commit -m "feat(db): screen_status-Tabelle für Display-Zustand"
Task 2: Store — UpsertDisplayState
Files:
-
Modify:
server/backend/internal/store/store.go -
Schritt 1:
ScreenStatus-Typ nach demScreen-Typ einfügen (nach Zeile 35)type ScreenStatus struct { ScreenID string `json:"screen_id"` DisplayState string `json:"display_state"` ReportedAt time.Time `json:"reported_at"` } -
Schritt 2:
UpsertDisplayState-Methode am Ende des ScreenStore-Blocks einfügen (nachscanScreen, vor// MediaStore)// UpsertDisplayState speichert den zuletzt gemeldeten Display-Zustand eines Screens. func (s *ScreenStore) UpsertDisplayState(ctx context.Context, screenID, displayState string) error { _, err := s.pool.Exec(ctx, `insert into screen_status (screen_id, display_state, reported_at) values ($1, $2, now()) on conflict (screen_id) do update set display_state = excluded.display_state, reported_at = excluded.reported_at`, screenID, displayState) return err } -
Schritt 3: Kompilieren
cd server/backend && go build ./...Erwartet: keine Ausgabe, Exit 0.
-
Schritt 4: Committen
git add server/backend/internal/store/store.go git commit -m "feat(store): UpsertDisplayState für screen_status"
Task 3: mqttnotifier — SendDisplayCommand
Files:
-
Modify:
server/backend/internal/mqttnotifier/notifier.go -
Schritt 1: Methode nach
RequestScreenshoteinfügen (nach Zeile 94)Das bestehende
publish()-Helper nutzt QoS 0 undretain=false.SendDisplayCommandpubliziert direkt mit QoS 1 undretain=true.// SendDisplayCommand publiziert einen Display-Befehl (display_on/display_off) // auf das Command-Topic des Screens. Retained + QoS 1, damit der Agent den // letzten Sollzustand auch nach einem Reconnect erhält. func (n *Notifier) SendDisplayCommand(screenSlug, action string) error { if n.client == nil { return nil } topic := fmt.Sprintf("signage/screen/%s/command", screenSlug) payload := []byte(fmt.Sprintf(`{"action":%q}`, action)) token := n.client.Publish(topic, 1, true, payload) if !token.WaitTimeout(5 * time.Second) { return fmt.Errorf("mqtt publish display command: timeout") } return token.Error() } -
Schritt 2: Kompilieren
cd server/backend && go build ./...Erwartet: keine Ausgabe, Exit 0.
-
Schritt 3: Committen
git add server/backend/internal/mqttnotifier/notifier.go git commit -m "feat(mqtt): SendDisplayCommand mit retained QoS 1"
Task 4: Backend-Handler — POST /api/v1/screens/{screenSlug}/display
Files:
-
Create:
server/backend/internal/httpapi/manage/display.go -
Modify:
server/backend/internal/httpapi/router.go -
Schritt 1: Handler-Datei anlegen
package manage import ( "encoding/json" "log/slog" "net/http" "git.az-it.net/az/morz-infoboard/server/backend/internal/mqttnotifier" "git.az-it.net/az/morz-infoboard/server/backend/internal/store" ) // HandleDisplayCommand nimmt {"state":"on"} oder {"state":"off"} entgegen und // schickt den entsprechenden MQTT-Befehl an den Agent. func HandleDisplayCommand(screens *store.ScreenStore, notifier *mqttnotifier.Notifier) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { screenSlug := r.PathValue("screenSlug") screen, err := screens.GetBySlug(r.Context(), screenSlug) if err != nil { http.Error(w, "screen not found", http.StatusNotFound) return } if !requireScreenAccess(w, r, screen) { return } var body struct { State string `json:"state"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } var action string switch body.State { case "on": action = "display_on" case "off": action = "display_off" default: http.Error(w, `state must be "on" or "off"`, http.StatusBadRequest) return } if err := notifier.SendDisplayCommand(screenSlug, action); err != nil { slog.Error("send display command", "err", err) http.Error(w, "failed to send command", http.StatusBadGateway) return } w.WriteHeader(http.StatusNoContent) } } -
Schritt 2: Route in
router.goregistrierenIn
router.goim Block der API-Screen-Routen (grep nachapi/v1/screens) folgende Zeile ergänzen:mux.Handle("POST /api/v1/screens/{screenSlug}/display", authOnly(http.HandlerFunc(manage.HandleDisplayCommand(d.ScreenStore, d.Notifier))))Hinweis:
d.Notifierheißt im router eventuellnotifier(lokale Variable). Prüfe den bestehenden Code-Kontext inNewRouter. -
Schritt 3: Kompilieren
cd server/backend && go build ./...Erwartet: keine Ausgabe, Exit 0.
-
Schritt 4: Committen
git add server/backend/internal/httpapi/manage/display.go \ server/backend/internal/httpapi/router.go git commit -m "feat(api): POST /api/v1/screens/{slug}/display"
Task 5: Player-Status-Handler — display_state persistieren
Files:
- Modify:
server/backend/internal/httpapi/playerstatus.go - Modify:
server/backend/internal/httpapi/router.go
Der Handler handlePlayerStatus bekommt *store.ScreenStore als neuen Parameter, um display_state in screen_status zu speichern.
-
Schritt 1:
display_state-Feld zuplayerStatusRequesthinzufügen (nachLastHeartbeatAt, Zeile 49)type playerStatusRequest struct { ScreenID string `json:"screen_id"` Timestamp string `json:"ts"` Status string `json:"status"` ServerConnectivity string `json:"server_connectivity"` ServerURL string `json:"server_url"` MQTTBroker string `json:"mqtt_broker"` HeartbeatEverySeconds int `json:"heartbeat_every_seconds"` StartedAt string `json:"started_at"` LastHeartbeatAt string `json:"last_heartbeat_at"` DisplayState string `json:"display_state,omitempty"` } -
Schritt 2:
*store.ScreenStore-Parameter + Import hinzufügenFunktionssignatur ändern von:
func handlePlayerStatus(store playerStatusStore, mqttBroker, mqttUsername, mqttPassword string) http.HandlerFunc {zu:
func handlePlayerStatus(store playerStatusStore, screenStore *storePackage.ScreenStore, mqttBroker, mqttUsername, mqttPassword string) http.HandlerFunc {Import am Anfang der Datei ergänzen:
import ( "errors" "log/slog" "net/http" "net/url" "sort" "strconv" "strings" "time" storePackage "git.az-it.net/az/morz-infoboard/server/backend/internal/store" ) -
Schritt 3:
display_statepersistieren — nach demstore.Save(...)Block (nach Zeile 131) einfügen:if request.DisplayState != "" && screenStore != nil { if err := screenStore.UpsertDisplayState(r.Context(), request.ScreenID, request.DisplayState); err != nil { slog.Error("upsert display state", "screen_id", request.ScreenID, "err", err) // Fehler ist nicht fatal — Statusmeldung wird trotzdem beantwortet } } -
Schritt 4: Route in
router.goaktualisierenIn
router.godie Zeile mithandlePlayerStatus(finden (grep nachhandlePlayerStatus) undd.ScreenStoreals zweites Argument einfügen:// Vorher: handlePlayerStatus(deps.StatusStore, cfg.MQTTBroker, cfg.MQTTUsername, cfg.MQTTPassword) // Nachher: handlePlayerStatus(deps.StatusStore, deps.ScreenStore, cfg.MQTTBroker, cfg.MQTTUsername, cfg.MQTTPassword)(Variablenname in NewRouter prüfen — evtl.
dstattdeps.) -
Schritt 5: Kompilieren
cd server/backend && go build ./...Erwartet: keine Ausgabe, Exit 0.
-
Schritt 6: Committen
git add server/backend/internal/httpapi/playerstatus.go \ server/backend/internal/httpapi/router.go git commit -m "feat(api): display_state im Player-Status-Report persistieren"
Task 6: Agent — displaycontroller Package
Files:
-
Create:
player/agent/internal/displaycontroller/controller.go -
Create:
player/agent/internal/displaycontroller/controller_test.go -
Schritt 1: Test schreiben
package displaycontroller import "testing" func TestNew_initialState(t *testing.T) { c := New(":0", "test-screen", nil) if got := c.State(); got != "unknown" { t.Fatalf("initial state = %q, want %q", got, "unknown") } } func TestExecute_unknownAction(t *testing.T) { c := New(":0", "test-screen", nil) // Unbekannte Action darf nicht paniken und Zustand darf sich nicht ändern. c.Execute("invalid_action") if got := c.State(); got != "unknown" { t.Fatalf("state after unknown action = %q, want %q", got, "unknown") } } -
Schritt 2: Test ausführen (muss fehlschlagen)
cd player/agent && go test ./internal/displaycontroller/...Erwartet: FAIL mit "no Go files"
-
Schritt 3: Controller implementieren
// Package displaycontroller steuert das physische Display per xset DPMS. package displaycontroller import ( "fmt" "log/slog" "os/exec" "sync" ) // Controller führt xset-Befehle aus und verfolgt den letzten Display-Zustand. // onStateChange wird nach jeder erfolgreichen Ausführung mit dem neuen Zustand // aufgerufen (z. B. um ihn per MQTT zu publizieren). Darf nil sein. type Controller struct { display string // X-Display, z. B. ":0" screenSlug string onStateChange func(screenSlug, state string) mu sync.Mutex currentState string } // New erstellt einen Controller. display ist der Wert der DISPLAY-Env-Var (z. B. ":0"). func New(display, screenSlug string, onStateChange func(screenSlug, state string)) *Controller { return &Controller{ display: display, screenSlug: screenSlug, onStateChange: onStateChange, currentState: "unknown", } } // Execute führt eine Display-Aktion aus. Bekannte Aktionen: "display_on", "display_off". // Unbekannte Aktionen werden geloggt und ignoriert. func (c *Controller) Execute(action string) { switch action { case "display_on": if err := c.runXset("on"); err != nil { slog.Error("display_on failed", "err", err) return } c.setState("on") case "display_off": if err := c.runXset("off"); err != nil { slog.Error("display_off failed", "err", err) return } c.setState("off") default: slog.Warn("unknown display action", "action", action) } } // State gibt den zuletzt gesetzten Display-Zustand zurück: "on", "off" oder "unknown". func (c *Controller) State() string { c.mu.Lock() defer c.mu.Unlock() return c.currentState } func (c *Controller) runXset(arg string) error { out, err := exec.Command("xset", "-display", c.display, "dpms", "force", arg).CombinedOutput() if err != nil { return fmt.Errorf("xset dpms force %s: %w (output: %s)", arg, err, out) } return nil } func (c *Controller) setState(state string) { c.mu.Lock() c.currentState = state c.mu.Unlock() if c.onStateChange != nil { go c.onStateChange(c.screenSlug, state) } } -
Schritt 4: Tests ausführen
cd player/agent && go test ./internal/displaycontroller/...Erwartet: PASS (beide Tests grün)
-
Schritt 5: Kompilieren
cd player/agent && go build ./...Erwartet: keine Ausgabe, Exit 0.
-
Schritt 6: Committen
git add player/agent/internal/displaycontroller/ git commit -m "feat(agent): displaycontroller Package (xset DPMS)"
Task 7: Agent — mqttheartbeat um SendDisplayState erweitern + Interface updaten
Files:
-
Modify:
player/agent/internal/mqttheartbeat/heartbeat.go -
Modify:
player/agent/internal/app/app.go—mqttSenderInterface -
Schritt 1:
SendDisplayStateinheartbeat.goergänzen (nachSendHeartbeat, Zeile 72)// SendDisplayState publiziert den aktuellen Display-Zustand auf dem display-state-Topic. // QoS 0, nicht retained — Informationszweck/Monitoring. func (p *Publisher) SendDisplayState(screenSlug, state string) error { type dsPayload struct { DisplayState string `json:"display_state"` Timestamp string `json:"ts"` } data, err := json.Marshal(dsPayload{ DisplayState: state, Timestamp: time.Now().UTC().Format(time.RFC3339), }) if err != nil { return err } topic := "signage/screen/" + screenSlug + "/display-state" token := p.client.Publish(topic, 0, false, data) token.WaitTimeout(3 * time.Second) return token.Error() } -
Schritt 2:
mqttSender-Interface inapp.goerweitern (Zeilen 79-82)type mqttSender interface { SendHeartbeat(status, connectivity string, ts time.Time) error SendDisplayState(screenSlug, state string) error Close() } -
Schritt 3: Kompilieren
cd player/agent && go build ./...Erwartet: keine Ausgabe, Exit 0.
-
Schritt 4: Committen
git add player/agent/internal/mqttheartbeat/heartbeat.go \ player/agent/internal/app/app.go git commit -m "feat(agent): SendDisplayState im MQTT-Publisher + Interface"
Task 8: Agent — mqttsubscriber Command-Topic
Files:
-
Modify:
player/agent/internal/mqttsubscriber/subscriber.go -
Schritt 1: Konstante + Callback-Typ ergänzen (nach
screenshotRequestTopicTemplate, Zeile 20)// commandTopicTemplate ist das Topic für eingehende Display-Befehle vom Backend. commandTopicTemplate = "signage/screen/%s/command"// DisplayCommandFunc wird aufgerufen wenn ein display-command eintrifft. type DisplayCommandFunc func(action string) -
Schritt 2:
commandC-Kanal + Callback zuSubscriberhinzufügen (nachscreenshotReqC, Zeile 36)type Subscriber struct { client mqtt.Client timer *time.Timer onChange PlaylistChangedFunc onScreenshotRequest ScreenshotRequestFunc onDisplayCommand DisplayCommandFunc resetC chan struct{} screenshotReqC chan struct{} commandC chan string stopC chan struct{} } -
Schritt 3:
CommandTopic-Hilfsfunktion ergänzen (nachScreenshotRequestTopic)// CommandTopic returns the MQTT topic for display commands for a given screenSlug. func CommandTopic(screenSlug string) string { return "signage/screen/" + screenSlug + "/command" } -
Schritt 4:
New()-Signatur + Initialisierung aktualisierenSignatur:
func New(broker, screenSlug, username, password string, onChange PlaylistChangedFunc, onScreenshotRequest ScreenshotRequestFunc, onDisplayCommand DisplayCommandFunc, ) *Subscriber {In der Struct-Initialisierung (nach
screenshotReqC: make(chan struct{}, 16)):s := &Subscriber{ onChange: onChange, onScreenshotRequest: onScreenshotRequest, onDisplayCommand: onDisplayCommand, resetC: make(chan struct{}, 16), screenshotReqC: make(chan struct{}, 16), commandC: make(chan string, 8), stopC: make(chan struct{}), } -
Schritt 5: Command-Topic im
SetOnConnectHandlerabonnierenIn
SetOnConnectHandler(nach demc.Subscribe(screenshotTopic, ...)Block) einfügen:commandTopic := CommandTopic(screenSlug) c.Subscribe(commandTopic, 1, func(_ mqtt.Client, m mqtt.Message) { //nolint:errcheck var cmd struct { Action string `json:"action"` } if err := json.Unmarshal(m.Payload(), &cmd); err != nil { return } select { case s.commandC <- cmd.Action: default: // Kanal voll — vorheriger Befehl wartet noch } })Import
"encoding/json"am Dateianfang ergänzen. -
Schritt 6:
run()-Loop um Command-Case erweiternIm
selectinrun()(nachcase <-s.screenshotReqC:):case action := <-s.commandC: if s.onDisplayCommand != nil { go s.onDisplayCommand(action) } -
Schritt 7: Kompilieren
cd player/agent && go build ./...Erwartet: Compilefehler in
app.go(zu wenig Argumente beimqttsubscriber.New) — das ist erwartet und wird in Task 10 behoben.Tatsächlich erwartet nach Task 10 — hier nur prüfen, dass die subscriber-Datei selbst fehlerfrei kompiliert:
cd player/agent && go vet ./internal/mqttsubscriber/... -
Schritt 8: Committen
git add player/agent/internal/mqttsubscriber/subscriber.go git commit -m "feat(agent): mqttsubscriber abonniert Command-Topic"
Task 9: Agent — statusreporter um display_state erweitern
Files:
-
Modify:
player/agent/internal/statusreporter/reporter.go -
Schritt 1:
DisplayState-Feld zuSnapshothinzufügen (nachLastHeartbeatAt, Zeile 20)type Snapshot struct { Status string ServerConnectivity string ServerBaseURL string MQTTBroker string HeartbeatEverySeconds int StartedAt time.Time LastHeartbeatAt time.Time DisplayState string // "on" | "off" | "unknown" } -
Schritt 2:
DisplayState-Feld zustatusPayloadhinzufügen (nachLastHeartbeatAt, Zeile 31)type statusPayload struct { ScreenID string `json:"screen_id"` Timestamp string `json:"ts"` Status string `json:"status"` ServerConnectivity string `json:"server_connectivity"` ServerURL string `json:"server_url"` MQTTBroker string `json:"mqtt_broker"` HeartbeatEverySeconds int `json:"heartbeat_every_seconds"` StartedAt string `json:"started_at,omitempty"` LastHeartbeatAt string `json:"last_heartbeat_at,omitempty"` DisplayState string `json:"display_state,omitempty"` } -
Schritt 3:
buildPayloadumDisplayStateergänzen (nachLastHeartbeatAt-Block, Zeile 115)if snapshot.DisplayState != "" { payload.DisplayState = snapshot.DisplayState } -
Schritt 4: Kompilieren
cd player/agent && go build ./...Erwartet: Compilefehler in
app.go(neues Subscriber-Interface) — wird in Task 10 behoben. Paket selbst muss sauber sein:cd player/agent && go vet ./internal/statusreporter/... -
Schritt 5: Committen
git add player/agent/internal/statusreporter/reporter.go git commit -m "feat(agent): display_state im Status-Report"
Task 10: Agent — app.go Wiring
Files:
-
Modify:
player/agent/internal/app/app.go -
Schritt 1: Import und Feld hinzufügen
Import ergänzen:
"os" "git.az-it.net/az/morz-infoboard/player/agent/internal/displaycontroller"Im
App-Struct nachscreenshotFn(Zeile 70):displayCtrl *displaycontroller.Controller -
Schritt 2:
displayCtrlinRun()initialisieren — vor dermqttsubscriber.New()-Zeile (Zeile 206) einfügen:xDisplay := os.Getenv("DISPLAY") if xDisplay == "" { xDisplay = ":0" } a.displayCtrl = displaycontroller.New(xDisplay, a.Config.ScreenID, func(slug, state string) { a.mqttMu.Lock() pub := a.mqttPub a.mqttMu.Unlock() if pub != nil { if err := pub.SendDisplayState(slug, state); err != nil { a.logger.Printf("event=display_state_publish_error err=%v", err) } } })Hinweis:
a.Config.ScreenIDist die Screen-ID (UUID), die als MQTT-Slug verwendet wird — prüfe ob im Projekt screenSlug oder screenID für das MQTT-Topic genutzt wird. Wenn im Subscriber der Slug (a.Config.ScreenID) verwendet wird, ist das konsistent. -
Schritt 3:
mqttsubscriber.New()-Aufruf um Command-Callback erweitern (Zeile 206–215)sub := mqttsubscriber.New( a.Config.MQTTBroker, a.Config.ScreenID, a.Config.MQTTUsername, a.Config.MQTTPassword, func() { select { case a.mqttFetchC <- struct{}{}: default: } a.logger.Printf("event=mqtt_playlist_notification screen_id=%s", a.Config.ScreenID) }, a.screenshotFn, func(action string) { a.logger.Printf("event=display_command_received action=%s screen_id=%s", action, a.Config.ScreenID) a.displayCtrl.Execute(action) }, ) -
Schritt 4:
applyMQTTConfigsuchen und dort ebenfalls updatenIn
app.gonachapplyMQTTConfigsuchen (grep:func.*applyMQTTConfig). Dort gibt es einen weiterenmqttsubscriber.New()-Aufruf — denselben 7. Parameter einfügen:func(action string) { a.logger.Printf("event=display_command_received action=%s screen_id=%s", action, a.Config.ScreenID) a.displayCtrl.Execute(action) }, -
Schritt 5:
DisplayStateim Status-Snapshot mitgebenIn
app.gonachbuildSnapshotoder der Stelle suchen, wostatusreporter.Snapshotbefüllt wird (grep:statusreporter.Snapshot{). DortDisplayStateergänzen:DisplayState: a.displayCtrl.State(),Falls
displayCtrlnil sein könnte (z. B. in Tests), vorher absichern:var displayState string if a.displayCtrl != nil { displayState = a.displayCtrl.State() } // Dann: DisplayState: displayState, -
Schritt 6: Gesamtkompilierung
cd player/agent && go build ./...Erwartet: keine Ausgabe, Exit 0.
-
Schritt 7: Tests ausführen
cd player/agent && go test ./...Erwartet: alle Tests grün.
-
Schritt 8: Committen
git add player/agent/internal/app/app.go git commit -m "feat(agent): displaycontroller in app.go verdrahtet"
Task 11: Ansible — DPMS aktivieren
Files:
-
Modify:
ansible/roles/signage_display/templates/morz-kiosk.j2 -
Schritt 1: DPMS-Zeilen anpassen (Zeile 8)
Vorher:
xset -dpmsNachher:
xset +dpms xset dpms 0 0 0 # Timeouts deaktivieren — nur Backend schaltet das DisplayDie vollständige Datei sieht danach so aus (Zeilen 5-9):
# Bildschirmschoner und Energiesparen konfigurieren xset s off xset s noblank xset +dpms xset dpms 0 0 0 -
Schritt 2: Committen
git add ansible/roles/signage_display/templates/morz-kiosk.j2 git commit -m "fix(ansible): DPMS aktivieren für Display-Steuerung" -
Schritt 3: Ansible ausrollen (manuell)
ansible-playbook -i ansible/inventory.yml ansible/signage.yml --tags kioskDanach auf dem Pi prüfen:
xset q | grep DPMSErwartet:
DPMS is Enabled
Manuelle Verifikation nach Fertigstellung
- Backend starten, Agent auf dem Pi starten
- Display-Befehl senden:
curl -X POST https://<backend>/api/v1/screens/<slug>/display \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{"state":"off"}' - Monitor muss innerhalb von 2 Sekunden ausgehen
- MQTT-Topic prüfen (z. B. mit
mosquitto_sub):mosquitto_sub -t 'signage/screen/+/display-state' -v - Nächster Status-Report des Agents soll
"display_state":"off"enthalten - In der DB prüfen:
select * from screen_status;