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.
This commit is contained in:
@@ -104,6 +104,10 @@ type Yaesu struct {
|
||||
panelLoaded bool
|
||||
// Commands this rig answered "?;" to — asked once, then never again.
|
||||
unsupported map[string]bool
|
||||
// Needle inertia for the TX meters — see meterPeak.
|
||||
powerPeak meterPeak
|
||||
powerWPeak meterPeak
|
||||
swrPeak meterPeak
|
||||
// metersLogged counts the RM1..RM6 samples taken during transmission, so the
|
||||
// survey follows a real carrier instead of catching one instant of it.
|
||||
metersLogged int
|
||||
|
||||
+44
-14
@@ -173,9 +173,10 @@ func (y *Yaesu) readPanel(mode string, split bool, txHz int64) {
|
||||
// the raw value makes the bars flicker to nothing several times a second
|
||||
// and the number unreadable. A real meter has needle inertia; this is the
|
||||
// same idea, and it only ever holds a value the radio actually reported.
|
||||
now := time.Now()
|
||||
if v, ok := y.askNum("RM5;", "RM5", 3); ok {
|
||||
y.panel.PowerMeter = holdPeak(y.panel.PowerMeter, scale255(v))
|
||||
y.panel.PowerW = float64(holdPeak(int(y.panel.PowerW), int(yaesuWatts(v))))
|
||||
y.panel.PowerMeter = y.powerPeak.update(scale255(v), now)
|
||||
y.panel.PowerW = float64(y.powerWPeak.update(int(yaesuWatts(v)+0.5), now))
|
||||
}
|
||||
// SWR is RM6, and a second measurement at a KNOWN mismatch settled both the
|
||||
// index and the scale: 0 at SWR 1.1, then 52 at SWR 1.5. 52/255 = 0.204,
|
||||
@@ -183,9 +184,10 @@ func (y *Yaesu) readPanel(mode string, split bool, txHz int64) {
|
||||
// the raw value is rho scaled to 255, and the ratio follows from physics
|
||||
// rather than from a fitted curve.
|
||||
if v, ok := y.askNum("RM6;", "RM6", 3); ok {
|
||||
y.panel.SWRMeter = holdPeak(y.panel.SWRMeter, scale255(v))
|
||||
// SWR only means something while power is going out: between CW elements
|
||||
// the reading is 0, which would show as a perfect 1.0 match.
|
||||
y.panel.SWRMeter = y.swrPeak.update(scale255(v), now)
|
||||
// The RATIO is only meaningful while power is going out: between words
|
||||
// the reading is 0, which would display as a perfect 1.0 match — worse
|
||||
// than a stale figure, because it looks like good news.
|
||||
if v > 0 {
|
||||
y.panel.SWR = swrFromReflection(v)
|
||||
}
|
||||
@@ -727,15 +729,43 @@ func yaesuWatts(raw int) float64 {
|
||||
return float64(raw) * 100.0 / yaesuPowerFullScale
|
||||
}
|
||||
|
||||
// holdPeak gives a meter the inertia a needle has: it jumps to a higher reading
|
||||
// at once and falls back gradually. Without it a CW transmission — key up
|
||||
// between every element — makes the bars flash to zero several times a second.
|
||||
// meterPeak gives a meter the inertia a needle has: it jumps to a higher reading
|
||||
// at once, HOLDS it for a moment, then falls back gradually.
|
||||
//
|
||||
// The decay is a quarter of the gap per poll, so a real drop in power still
|
||||
// shows within about a second at the default rate.
|
||||
func holdPeak(current, sample int) int {
|
||||
if sample >= current {
|
||||
return sample
|
||||
// The hold is the part that matters on the air. Between CW elements the gap is
|
||||
// milliseconds, but between the words of a CQ — in CW as in SSB — it is most of
|
||||
// a second, and a meter that decays straight away reads 0 in every one of those
|
||||
// gaps. The operator sees a bar flashing rather than the power and SWR they are
|
||||
// actually running.
|
||||
type meterPeak struct {
|
||||
val int
|
||||
at time.Time
|
||||
}
|
||||
|
||||
const (
|
||||
meterHold = 1500 * time.Millisecond // how long a peak stands before it starts to fall
|
||||
meterDecay = 4 // then a quarter of the remaining gap per poll
|
||||
)
|
||||
|
||||
// update returns the value to show for this sample.
|
||||
func (m *meterPeak) update(sample int, now time.Time) int {
|
||||
if sample >= m.val {
|
||||
m.val, m.at = sample, now
|
||||
return m.val
|
||||
}
|
||||
return current - (current-sample)/4
|
||||
if now.Sub(m.at) < meterHold {
|
||||
return m.val // still inside the hold — the needle has not started to fall
|
||||
}
|
||||
// At least one step down, always. A proportional decay on integers stalls
|
||||
// near the end — with the needle at 13 and the truth at 10, a quarter of the
|
||||
// gap rounds to zero and the meter sits three units high for ever.
|
||||
step := (m.val - sample) / meterDecay
|
||||
if step < 1 {
|
||||
step = 1
|
||||
}
|
||||
m.val -= step
|
||||
if m.val <= sample {
|
||||
m.val = sample
|
||||
}
|
||||
return m.val
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package cat
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// What SPLIT means when the operator presses it.
|
||||
//
|
||||
@@ -76,3 +79,42 @@ func TestSWRFromReflection(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user