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.
121 lines
4.1 KiB
Go
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)
|
|
}
|
|
}
|