Files
OpsLog/internal/cat/yaesu_split_test.go
T
rouggy 113faede14 fix: hold the TX meters through the gaps between words
A quarter-per-poll decay covers the milliseconds between CW elements but not the
gaps that matter on the air: between the words of a CQ, in CW as in SSB, the
meters genuinely read 0 for most of a second and the bars fell with them.

A peak now stands for 1.5 s before it starts to fall, and still rises instantly —
a needle goes up fast and comes down slow. The SWR RATIO is not updated at all
from a zero reading: showing 1.0 during a word gap is worse than showing a stale
figure, because it looks like good news.

A test caught a real defect on the way: the proportional decay stalls on
integers. With the needle at 13 and the truth at 10, a quarter of the gap rounds
to zero and the meter sat three units high for ever. It now always steps down by
at least one, so it converges.
2026-07-29 13:59:47 +02:00

121 lines
4.1 KiB
Go

package cat
import (
"testing"
"time"
)
// What SPLIT means when the operator presses it.
//
// Flipping the rig's split flag alone transmits wherever the OTHER VFO happens
// to sit — reported from a real FTDX10: listening on 14.244 with VFO B left on
// 18.115 from an earlier session, pressing SPLIT threw the transmitter onto
// another band. The other VFO is stale by nature, so the transmit frequency has
// to be derived from where the operator is listening now.
func TestYaesuDefaultSplitOffset(t *testing.T) {
cases := []struct {
raw string
want int64
}{
// CW and the data modes work 1 kHz up.
{"CW-U", 1000},
{"CW-L", 1000},
{"RTTY-U", 1000},
{"RTTY-L", 1000},
{"DATA-U", 1000},
{"DATA-L", 1000},
// Phone works 5 kHz up.
{"USB", 5000},
{"LSB", 5000},
{"AM", 5000},
{"FM", 5000},
// Unknown or not yet read: the phone offset is the safer default — too
// wide is audible and obvious, too narrow lands on top of the DX.
{"", 5000},
}
for _, c := range cases {
y := &Yaesu{}
y.panel.RawMode = c.raw
if got := y.defaultSplitOffset(); got != c.want {
t.Errorf("mode %q → split offset %d Hz, want %d", c.raw, got, c.want)
}
}
}
// The SWR scale, pinned to the two measurements it was derived from.
//
// Taken on an FTDX10 (2026-07-29) against an operator watching the rig's own
// meter: raw 0 at SWR 1.1, raw 52 at SWR 1.5. The second point is what proved
// the raw value is the reflection coefficient scaled to 255 — 52/255 = 0.204,
// rho for a 1.5 SWR — rather than a percentage of meter travel, which is how the
// bar came to read 81 on a perfect antenna.
func TestSWRFromReflection(t *testing.T) {
cases := []struct {
raw int
want float64
tol float64
}{
{0, 1.0, 0.01}, // no reflected power
{52, 1.5, 0.02}, // the measured mismatch
{85, 2.0, 0.05}, // rho = 1/3
{128, 3.0, 0.1}, // rho = 0.5
{-5, 1.0, 0.01}, // nonsense reading — never below 1.0, which is physical
{255, 9.9, 0.01}, // full scale is capped rather than infinite
}
for _, c := range cases {
got := swrFromReflection(c.raw)
if got < c.want-c.tol || got > c.want+c.tol {
t.Errorf("swrFromReflection(%d) = %.2f, want %.2f ±%.2f", c.raw, got, c.want, c.tol)
}
}
// It must rise with the reflected power, or a worsening match would read
// better on the panel than on the rig.
prev := 0.0
for raw := 0; raw <= 200; raw += 20 {
v := swrFromReflection(raw)
if v < prev {
t.Fatalf("SWR fell from %.2f to %.2f at raw=%d", prev, v, raw)
}
prev = v
}
}
// Needle inertia on the TX meters.
//
// The gaps are what this is for: milliseconds between CW elements, but most of a
// second between the words of a CQ — in CW as in SSB. A meter that decays
// immediately reads 0 in every one of those gaps, so the operator sees a bar
// flashing instead of the power they are running.
func TestMeterPeakHold(t *testing.T) {
var m meterPeak
t0 := time.Now()
if got := m.update(80, t0); got != 80 {
t.Fatalf("first sample = %d, want 80 — a meter must show a reading at once", got)
}
// A gap between two words: still inside the hold, so the reading stands.
if got := m.update(0, t0.Add(400*time.Millisecond)); got != 80 {
t.Errorf("during a word gap = %d, want 80 held", got)
}
if got := m.update(0, t0.Add(1400*time.Millisecond)); got != 80 {
t.Errorf("just before the hold expires = %d, want 80 held", got)
}
// Past the hold it falls — but gradually, not to zero in one step.
after := m.update(0, t0.Add(1600*time.Millisecond))
if after >= 80 || after <= 0 {
t.Errorf("after the hold = %d, want a value falling between 80 and 0", after)
}
// A HIGHER reading is taken immediately: a needle rises fast and falls slow.
if got := m.update(95, t0.Add(1700*time.Millisecond)); got != 95 {
t.Errorf("rising sample = %d, want 95 straight away", got)
}
// And it does reach the real value eventually, or a power drop would never show.
v := 0
for i := 0; i < 60; i++ {
v = m.update(10, t0.Add(time.Duration(2000+i*250)*time.Millisecond))
}
if v != 10 {
t.Errorf("settled at %d, want 10 — the meter must converge on the truth", v)
}
}