package display import ( "strconv" "git.piksar.eu/piotr/groundstation/internal/osd" ) // SnapshotSource serves the latest supervisor-published OSD snapshot while // it is fresh. The file-backed implementation lives in the osd package; the // template only consumes the contract. type SnapshotSource interface { Latest() (osd.Snapshot, bool) } // OSDTemplate is the flight OSD layout of a display target: a transparent, // input-transparent surface above pilot video. While decoded video is // absent the fallback canvas owns the screen and the OSD presents nothing; // while video is present the OSD draws deliberately minimal telemetry from // the latest published snapshot — arm state and requested CH6 WFB power. // The rendering gate is the video presence the display observes on the // display server, never the snapshot's Video field: the supervisor's field // is its launcher knowledge, while the display trusts only the presented // video output itself. 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 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() if t.video.Active() { if snapshot, ok := t.source.Latest(); ok { drawOSD(canvas, snapshot) } } canvas.Flush() } // drawOSD renders the deliberately minimal flight telemetry: the arm state // and one compact adaptive-link line — the operator's target table, the // temporary CH6 maxPower ceiling, the requested runtime level and the // applied VTX state. Values whose producers do not exist yet render at // their explicit pending marks. The full final RC state stays in the // snapshot; the pilot-facing overlay stays terse, and static configuration // is never rendered as applied VTX state. func drawOSD(canvas Canvas, snapshot osd.Snapshot) { arm := "OFF" if snapshot.Armed { arm = "ARMED" } canvas.Text(32, 32, "ARM "+arm) canvas.Text(32, 64, vtxLine(snapshot)) } // 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) }