// Package display owns the Ground Station kiosk canvas lifecycle. The kiosk // runs independently of WFB, the transmitter and the RC video knob: it waits // for its rendering target to appear, keeps a fullscreen black canvas alive // and stops only when told to. Each target (X11/Viture today, MIPI/touch // later) provides its own opener and templates instead of one global layout. package display import ( "context" "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) } } 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 } }