Files
groundstation/internal/display/osd.go
T

118 lines
3.5 KiB
Go

package display
import (
"strconv"
"git.piksar.eu/piotr/groundstation/internal/osd"
)
// SnapshotSource serves the latest supervisor-published OSD snapshot and its
// state (none / fresh / stale). The file-backed implementation lives in the
// osd package; the template only consumes the contract.
type SnapshotSource interface {
Latest() (osd.Snapshot, osd.SnapshotState)
}
// OSDTemplate is the flight OSD layout of a display target: a transparent,
// input-transparent surface above everything else on the screen. It renders
// from startup — on top of the no-video fallback presentation and above
// fullscreen video alike — and never gates on video presence: the video
// lifecycle the display observes on the display server is itself displayed
// as the VIDEO line, never the snapshot's Video field. ARM and the compact
// adaptive-link line come from the latest snapshot; a stale last-known
// snapshot stays visible and marked DATA STALE, and before any snapshot has
// existed the OSD shows explicit NO DATA / unknown marks. The OSD never
// draws the fallback: it is a telemetry surface, not a video substitute,
// and WFB or video decode never move into the UI process to feed it. The
// OSD refreshes on its own slower cadence than the video and the video
// window keeps rendering priority.
type OSDTemplate struct {
video *VideoPresence
source SnapshotSource
}
// NewOSDTemplate builds the OSD surface for a target from the shared
// display-observed video presence and the supervisor-published snapshot
// source.
func NewOSDTemplate(video *VideoPresence, source SnapshotSource) OSDTemplate {
return OSDTemplate{video: video, source: source}
}
func (t OSDTemplate) Draw(canvas Canvas) {
canvas.Clear()
snapshot, state := t.source.Latest()
t.draw(canvas, snapshot, state)
canvas.Flush()
}
// draw renders the deliberately minimal telemetry lines in the top-left
// corner: arm state, the adaptive-link contract, an optional staleness or
// no-data marker, and the observed video state. The full final RC state
// stays in the snapshot; the pilot-facing overlay stays terse.
func (t OSDTemplate) draw(canvas Canvas, snapshot osd.Snapshot, state osd.SnapshotState) {
y := 32
switch state {
case osd.SnapshotNone:
canvas.Text(32, y, "NO DATA")
y += 32
canvas.Text(32, y, "VTX tbl:-- ceil:-- req:-- ack:--")
y += 32
default:
canvas.Text(32, y, armLine(snapshot))
y += 32
canvas.Text(32, y, vtxLine(snapshot))
y += 32
if state == osd.SnapshotStale {
canvas.Text(32, y, "DATA STALE")
y += 32
}
}
canvas.Text(32, y, "VIDEO "+videoState(t.video.Active()))
}
func armLine(snapshot osd.Snapshot) string {
if snapshot.Armed {
return "ARM ARMED"
}
return "ARM OFF"
}
func videoState(active bool) string {
if active {
return "ON"
}
return "OFF"
}
// vtxLine is the compact adaptive-link contract line.
func vtxLine(snapshot osd.Snapshot) string {
return "VTX tbl:" + vtxTable(snapshot.OpConfig.Table) +
" ceil:" + strconv.FormatUint(uint64(snapshot.OpConfig.MaxPower), 10) +
" req:" + vtxLevel(snapshot.Policy.Pending, snapshot.Policy.Level) +
" ack:" + vtxAck(snapshot.Ack)
}
func vtxTable(table osd.Table) string {
if !table.Selected() {
return "--"
}
return strconv.FormatUint(uint64(table.Gen), 10)
}
func vtxLevel(pending bool, level uint8) string {
if pending {
return "--"
}
return strconv.FormatUint(uint64(level), 10)
}
func vtxAck(ack osd.VTXAck) string {
if ack.Pending {
return "--"
}
if ack.Lifecycle == osd.VTXLifecycleError {
return "ERR"
}
return vtxLevel(false, ack.Level)
}