Kiosk process owns its display lifecycle: waits for X11, keeps the fullscreen black canvas below video, stops deterministically on signal. Supervisor starts it before video, restarts only on process death, and shuts the whole process group down; X11 downtime never restarts the supervisor. Per-target opener/template abstractions for future MIPI.
124 lines
3.1 KiB
Go
124 lines
3.1 KiB
Go
// 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
|
|
}
|
|
}
|