feat: publish adaptive-link OSD contract and keep overlay above video (#3)
This commit is contained in:
@@ -96,7 +96,14 @@ func TestBuildTargetOSDLayerRendersPublishedSnapshots(t *testing.T) {
|
||||
publisher := osd.NewFilePublisher(state, nil)
|
||||
defer publisher.Close()
|
||||
|
||||
snapshot := osd.Snapshot{Sequence: 11, Armed: true, Power: true}
|
||||
snapshot := osd.Snapshot{
|
||||
Sequence: 11,
|
||||
Armed: true,
|
||||
OpConfig: osd.OperatorConfig{MaxPower: 95},
|
||||
Policy: osd.GSPolicy{Pending: true},
|
||||
Ack: osd.VTXAck{Pending: true, Lifecycle: osd.VTXLifecyclePending},
|
||||
Link: osd.GSObservedLink{Known: false},
|
||||
}
|
||||
publisher.Publish(snapshot)
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
@@ -117,7 +124,7 @@ func TestBuildTargetOSDLayerRendersPublishedSnapshots(t *testing.T) {
|
||||
surface := &recordingCanvas{width: 800, height: 600}
|
||||
osdLayer.Template.Draw(surface)
|
||||
|
||||
want := []string{"ARM ARMED", "PWR ON"}
|
||||
want := []string{"ARM ARMED", "VTX tbl:-- ceil:95 req:-- ack:--"}
|
||||
if len(surface.lines) != len(want) {
|
||||
t.Fatalf("OSD lines=%q want %q from the supervisor-published snapshot", surface.lines, want)
|
||||
}
|
||||
|
||||
@@ -8,17 +8,28 @@ import (
|
||||
|
||||
// newSnapshot builds the immutable flight OSD snapshot from the latest
|
||||
// valid RC frame: the final RC channels, the arm state derived from the
|
||||
// configured arm channel, the requested CH6 WFB power (the same channel
|
||||
// comparison that drives the video launcher) and the supervisor's
|
||||
// decoded-video presence (its video launcher ownership). Building it is
|
||||
// pure computation: publication never blocks the UART loop.
|
||||
// configured arm channel, and the adaptive-link contract values. CH6 feeds
|
||||
// only the temporary operator maxPower ceiling — it constrains what may be
|
||||
// requested and is never proof of applied VTX power. The policy,
|
||||
// acknowledgment and link producers (adaptive control, the VTX ack
|
||||
// transport, WFB RX metrics) do not exist yet, so the snapshot carries
|
||||
// their explicit pending/unknown states instead of fabricated data. The
|
||||
// supervisor's decoded-video presence stays its launcher ownership.
|
||||
// Building the snapshot is pure computation: publication never blocks the
|
||||
// UART loop.
|
||||
func newSnapshot(frame protocol.RCFrame, cfg config.Config, video *videoController) osd.Snapshot {
|
||||
snapshot := osd.Snapshot{
|
||||
Sequence: frame.Sequence,
|
||||
TimestampMS: frame.Timestamp,
|
||||
Channels: frame.Channels,
|
||||
Armed: frame.Channels[cfg.OSD.ArmIndex] > cfg.OSD.ArmedThreshold,
|
||||
Power: frame.Channels[cfg.Video.AuxIndex] > cfg.Video.Threshold,
|
||||
OpConfig: osd.OperatorConfig{
|
||||
Table: osd.Table{Gen: cfg.OSD.VtxTableGen, Hash: cfg.OSD.VtxTableHash},
|
||||
MaxPower: osd.CeilingFromChannel(frame.Channels[cfg.Video.AuxIndex]),
|
||||
},
|
||||
Policy: osd.GSPolicy{Pending: true},
|
||||
Ack: osd.VTXAck{Pending: true, Lifecycle: osd.VTXLifecyclePending},
|
||||
Link: osd.GSObservedLink{Known: false},
|
||||
}
|
||||
if video != nil {
|
||||
snapshot.Video = video.Running()
|
||||
|
||||
@@ -15,11 +15,14 @@ import (
|
||||
"git.piksar.eu/piotr/groundstation/internal/protocol"
|
||||
)
|
||||
|
||||
func TestNewSnapshotDerivesArmPowerAndVideoPresence(t *testing.T) {
|
||||
func TestNewSnapshotDerivesArmCeilingAndContractStates(t *testing.T) {
|
||||
cfg := config.Default()
|
||||
cfg.OSD.VtxTableGen = 3
|
||||
cfg.OSD.VtxTableHash = 0xcafe
|
||||
frame := protocol.RCFrame{Sequence: 3, Timestamp: 42}
|
||||
frame.Channels[4] = 512 // arm channel above the armed threshold
|
||||
frame.Channels[5] = -1000 // CH6 knob below the video threshold
|
||||
frame.Channels[5] = 100 // CH6 knob mid-range
|
||||
frame.Channels[15] = -999 // untouched channel must stay unchanged
|
||||
|
||||
snapshot := newSnapshot(frame, cfg, nil)
|
||||
if snapshot.Sequence != 3 || snapshot.TimestampMS != 42 {
|
||||
@@ -31,21 +34,37 @@ func TestNewSnapshotDerivesArmPowerAndVideoPresence(t *testing.T) {
|
||||
if !snapshot.Armed {
|
||||
t.Fatal("arm channel above the threshold must derive armed")
|
||||
}
|
||||
if snapshot.Power {
|
||||
t.Fatal("CH6 below the video threshold must derive no requested WFB power")
|
||||
// CH6 is only the temporary operator maxPower ceiling, never applied
|
||||
// VTX power.
|
||||
if snapshot.OpConfig.MaxPower != osd.CeilingFromChannel(100) {
|
||||
t.Fatalf("maxPower=%d want the CH6-derived ceiling", snapshot.OpConfig.MaxPower)
|
||||
}
|
||||
if snapshot.OpConfig.Table != (osd.Table{Gen: 3, Hash: 0xcafe}) {
|
||||
t.Fatalf("table=%+v want the operator-selected table", snapshot.OpConfig.Table)
|
||||
}
|
||||
// No policy, acknowledgment or link producer exists yet: the snapshot
|
||||
// must carry explicit pending/unknown states, never fabricated data.
|
||||
if !snapshot.Policy.Pending || snapshot.Policy.Level != 0 {
|
||||
t.Fatalf("policy=%+v must stay explicitly pending", snapshot.Policy)
|
||||
}
|
||||
if !snapshot.Ack.Pending || snapshot.Ack.Lifecycle != osd.VTXLifecyclePending {
|
||||
t.Fatalf("ack=%+v must stay explicitly pending", snapshot.Ack)
|
||||
}
|
||||
if snapshot.Link.Known {
|
||||
t.Fatalf("link=%+v must stay explicitly unknown", snapshot.Link)
|
||||
}
|
||||
if snapshot.Video {
|
||||
t.Fatal("without a video launcher there is no decoded-video presence")
|
||||
}
|
||||
|
||||
frame.Channels[4] = -256
|
||||
frame.Channels[5] = 100
|
||||
snapshot = newSnapshot(frame, cfg, &videoController{cmd: &exec.Cmd{}})
|
||||
frame.Channels[5] = -1000
|
||||
snapshot = newSnapshot(frame, cfg, &videoController{cmd: &exec.Cmd{}, done: make(chan error, 1)})
|
||||
if snapshot.Armed {
|
||||
t.Fatal("arm channel below the threshold must derive disarmed")
|
||||
}
|
||||
if !snapshot.Power {
|
||||
t.Fatal("CH6 above the video threshold must derive requested WFB power")
|
||||
if snapshot.OpConfig.MaxPower != osd.CeilingFromChannel(-1000) {
|
||||
t.Fatalf("maxPower=%d must follow the CH6 knob", snapshot.OpConfig.MaxPower)
|
||||
}
|
||||
if !snapshot.Video {
|
||||
t.Fatal("a running video launcher is the supervisor's decoded-video presence")
|
||||
@@ -119,8 +138,11 @@ func TestRunPublishesSnapshotsForValidFramesOnly(t *testing.T) {
|
||||
if got.Armed {
|
||||
t.Fatal("the latest frame's arm state must be disarmed")
|
||||
}
|
||||
if !got.Power {
|
||||
t.Fatal("the latest frame's CH6 must request WFB power")
|
||||
if got.OpConfig.MaxPower != osd.CeilingFromChannel(100) {
|
||||
t.Fatalf("maxPower=%d must follow the latest frame's CH6 knob", got.OpConfig.MaxPower)
|
||||
}
|
||||
if !got.Policy.Pending || !got.Ack.Pending || got.Link.Known {
|
||||
t.Fatalf("contract values %+v/%+v/%+v must stay pending/unknown", got.Policy, got.Ack, got.Link)
|
||||
}
|
||||
if got.PublishedMS == 0 {
|
||||
t.Fatal("the publisher must stamp the publication time")
|
||||
@@ -143,8 +165,8 @@ func TestRunPublishesWithoutAConfiguredVideoLauncher(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("frames must publish even without a video launcher")
|
||||
}
|
||||
if !got.Power || got.Video {
|
||||
t.Fatalf("snapshot=%+v must derive the requested power without video presence", got)
|
||||
if got.OpConfig.MaxPower != osd.CeilingFromChannel(0) || got.Video {
|
||||
t.Fatalf("snapshot=%+v must derive the ceiling without video presence", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user