Files
groundstation/cmd/groundstation-display/main.go
T
2026-10-02 10:23:28 +02:00

87 lines
2.8 KiB
Go

// Command groundstation-display renders the Ground Station kiosk layers. It
// owns its display lifecycle independently of WFB, the transmitter and the
// video launcher: it waits for its target to appear, redraws as needed, and
// stops only when it receives SIGINT or SIGTERM.
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"git.piksar.eu/piotr/groundstation/internal/display"
)
// buildTarget selects the target-specific layers. Each target owns its own
// backend and layouts; there is no shared global template. The Viture target
// builds two independent layers — the fullscreen fallback/menu canvas below
// video and the transparent OSD overlay above it — wired to one shared video
// presence signal: the sessions observe the video output lifecycle on the
// display server and the templates draw or hide from it. WFB and video
// decode never move into this process.
func buildTarget(name string) ([]display.Layer, error) {
switch name {
case "x11":
video := display.NewVideoPresence()
return []display.Layer{
{
Name: "fallback-canvas",
Opener: display.X11Opener{Video: video},
Template: display.NewFallbackTemplate(video, "Brak połączenia WFB"),
},
{
Name: "osd-overlay",
Opener: display.X11Opener{Video: video, Role: display.RoleAbove},
Template: display.NewOSDTemplate(video),
},
}, nil
default:
return nil, fmt.Errorf("unknown display target %q (supported: x11)", name)
}
}
// wireDiagnostics gives every layer the shared structured reconnect hook so
// each layer reports its own reconnect boundaries, identified by target and
// layer name.
func wireDiagnostics(target string, layers []display.Layer) {
for i := range layers {
layers[i].Options = display.Options{
Target: target,
Layer: layers[i].Name,
Diagnostic: func(d display.Diagnostic) {
log.Printf("display reconnect: %s", formatDiagnostic(d))
},
}
}
}
// formatDiagnostic renders one reconnect diagnostic as key=value fields so
// reconnects stay greppable and machine-parseable in the journal.
func formatDiagnostic(d display.Diagnostic) string {
return fmt.Sprintf("target=%s layer=%s event=%s error=%q retry_delay=%s",
d.Target, d.Layer, d.Event, d.Err, d.RetryDelay)
}
func main() {
target := flag.String("target", "x11", "display target: x11 (Viture) today; mipi planned")
flag.Parse()
layers, err := buildTarget(*target)
if err != nil {
log.Fatal(err)
}
wireDiagnostics(*target, layers)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// Temporary X11 unavailability never exits this process; every layer
// keeps retrying internally until the context is cancelled, reporting
// each reconnect boundary through the structured diagnostic hook.
display.RunLayers(ctx, layers)
}