101 lines
3.4 KiB
Go
101 lines
3.4 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"
|
|
"time"
|
|
|
|
"git.piksar.eu/piotr/groundstation/internal/display"
|
|
"git.piksar.eu/piotr/groundstation/internal/osd"
|
|
)
|
|
|
|
// osdRefresh is the OSD overlay's redraw cadence, deliberately slower than
|
|
// the video: newly published flight snapshots appear without waiting for X
|
|
// events while the video window keeps rendering priority.
|
|
const osdRefresh = time.Second
|
|
|
|
// 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. The OSD layer also
|
|
// consumes the supervisor-published flight snapshots and refreshes on its
|
|
// own slower cadence. WFB and video decode never move into this process.
|
|
func buildTarget(name string, osdState 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,
|
|
RefreshInterval: osdRefresh,
|
|
},
|
|
Template: display.NewOSDTemplate(video, osd.NewFileSource(osdState)),
|
|
},
|
|
}, 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")
|
|
osdState := flag.String("osd-state", osd.DefaultStatePath,
|
|
"OSD snapshot state file published by the groundstation supervisor")
|
|
flag.Parse()
|
|
|
|
layers, err := buildTarget(*target, *osdState)
|
|
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)
|
|
}
|