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) }