// Package display owns the Ground Station kiosk layers. The kiosk runs // independently of WFB, the transmitter and the RC video knob: it waits for // its rendering target to appear, keeps its windows alive and stops only // when told to. Each target drives two independent visual layers: a // fullscreen fallback/menu canvas below video, visible while decoded pilot // video is absent, and a transparent, input-transparent OSD overlay above // video, reserved as the future flight OSD rendering surface. When video // output is presented the video window takes rendering priority and the // fallback disappears, returning when video ends. Presence is observed from // the video output itself on the display server, never from WFB or launcher // processes, and the video pipeline is never moved into, coupled to or // touched by this package. Each target (X11/Viture today, MIPI/touch later) // provides its own openers and templates instead of one global layout. package display import ( "context" "sync" "time" ) // Canvas is the minimal drawing surface a Template renders onto. type Canvas interface { Size() (width, height int) Clear() Text(x, y int, message string) Flush() } // Template renders one target-specific layout. Templates belong to a display // target; there is no shared global layout across targets. type Template interface { Draw(canvas Canvas) } // Session is one live connection to a display target. type Session interface { Canvas() Canvas // WaitEvent blocks until the target asks for a refresh, the context is // done, or the session fails. redraw reports whether the template should // be drawn again. WaitEvent(ctx context.Context) (redraw bool, err error) Close() error } // Opener connects to a display target. Implementations are called repeatedly // while the target is unavailable and must stay safe for that. type Opener interface { Open(ctx context.Context) (Session, error) } // Options tune the kiosk lifecycle loop. Zero values select the defaults. type Options struct { // RetryDelay is the first wait after a failed connect or lost session. RetryDelay time.Duration // MaxDelay bounds the exponential reconnect backoff. MaxDelay time.Duration } func (o Options) withDefaults() Options { if o.RetryDelay <= 0 { o.RetryDelay = 500 * time.Millisecond } if o.MaxDelay <= 0 { o.MaxDelay = 5 * time.Second } return o } // Run owns the kiosk display lifecycle until ctx is cancelled. Temporary // unavailability of the target, for example X11 not being up yet, never // returns an error: Run retries with bounded backoff and only stops when the // context is cancelled, so the Ground Station supervisor stays up. func Run(ctx context.Context, opener Opener, template Template, options Options) error { opts := options.withDefaults() delay := opts.RetryDelay for { if ctx.Err() != nil { return nil } session, err := opener.Open(ctx) if err != nil { if !sleep(ctx, delay) { return nil } delay = nextDelay(delay, opts.MaxDelay) continue } delay = opts.RetryDelay canvas := session.Canvas() template.Draw(canvas) for ctx.Err() == nil { redraw, err := session.WaitEvent(ctx) if err != nil { break } if redraw { template.Draw(canvas) } } _ = session.Close() if ctx.Err() != nil { return nil } if !sleep(ctx, delay) { return nil } delay = nextDelay(delay, opts.MaxDelay) } } // Layer is one independent visual layer of a display target: its own // connection, window role and template. A lost or degraded layer never // affects the others, so a missing overlay cannot disturb the fallback // canvas and neither can disturb pilot video. type Layer struct { // Name identifies the layer in diagnostics. Name string // Opener connects to the layer's own window on the target. Opener Opener // Template renders the layer's target-specific layout. Template Template // Options tune the layer's lifecycle loop. Zero values select the // defaults. Options Options } // RunLayers drives every layer of a display target until ctx is cancelled, // each on its own connection with its own reconnect backoff. A layer whose // target keeps failing keeps retrying and never stops the other layers, so // the Ground Station supervisor stays up. RunLayers returns only after every // layer has stopped. func RunLayers(ctx context.Context, layers []Layer) { var wg sync.WaitGroup for _, layer := range layers { wg.Add(1) go func(layer Layer) { defer wg.Done() _ = Run(ctx, layer.Opener, layer.Template, layer.Options) }(layer) } wg.Wait() } func nextDelay(delay, max time.Duration) time.Duration { delay *= 2 if delay > max { return max } return delay } func sleep(ctx context.Context, duration time.Duration) bool { timer := time.NewTimer(duration) defer timer.Stop() select { case <-ctx.Done(): return false case <-timer.C: return true } }