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