118 lines
3.5 KiB
Go
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)
|
|
}
|