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:
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.21.9",
|
"version": "0.21.9",
|
||||||
"date": "2026-07-28",
|
"date": "2026-07-28",
|
||||||
"en": [
|
"en": [
|
||||||
"Yaesu console meters are right: power now reads the meter that follows the RF (in watts, measured, not the power setting) and SWR shows the ratio itself. Both were reading the wrong index — the SWR bar sat near 80 on a perfect match. The bars also hold their peak, so CW keying no longer makes them flash to zero.",
|
"Yaesu console meters are right: power now reads the meter that follows the RF (in watts, measured, not the power setting) and SWR shows the ratio itself. Both were reading the wrong index — the SWR bar sat near 80 on a perfect match. The bars also hold their peak for a moment before falling back, so they stay readable through the gaps between words in a CQ, in CW as in SSB.",
|
||||||
"CW speed is now one setting wherever you change it: the Yaesu console slider and the CW panel drive the keyer that is actually sending, and the rig own keyer follows so its front panel agrees.",
|
"CW speed is now one setting wherever you change it: the Yaesu console slider and the CW panel drive the keyer that is actually sending, and the rig own keyer follows so its front panel agrees.",
|
||||||
"Antenna Genius: port A no longer jumps onto port B antenna a few seconds after a change. Only one port can hold the transmit antenna, so the switch reports the same one on both — the display now follows each port own receive antenna.",
|
"Antenna Genius: port A no longer jumps onto port B antenna a few seconds after a change. Only one port can hold the transmit antenna, so the switch reports the same one on both — the display now follows each port own receive antenna.",
|
||||||
"CW through the Yaesu keyer: a fifth CW engine sends your macros with the radio own keyer (CAT \"KY\"), for the FTDX101 / FT-991A / FT-710 family. An FTDX10 does not accept it — DAKY included — and OpsLog says so, pointing to the serial DTR keyer on the rig second COM port, which works.",
|
"CW through the Yaesu keyer: a fifth CW engine sends your macros with the radio own keyer (CAT \"KY\"), for the FTDX101 / FT-991A / FT-710 family. An FTDX10 does not accept it — DAKY included — and OpsLog says so, pointing to the serial DTR keyer on the rig second COM port, which works.",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"CQ and ITU zones you correct by hand in your profile stay corrected. They are derived from cty.dat, which gives the zones of the whole entity — in a country spanning several, the automatic value is wrong and it came back at every restart. They now only fill in when empty, and recompute when the callsign or grid changes."
|
"CQ and ITU zones you correct by hand in your profile stay corrected. They are derived from cty.dat, which gives the zones of the whole entity — in a country spanning several, the automatic value is wrong and it came back at every restart. They now only fill in when empty, and recompute when the callsign or grid changes."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Les mesures de la console Yaesu sont justes : la puissance lit l'instrument qui suit la HF (en watts mesurés, et non le réglage de puissance) et le ROS affiche le rapport lui-même. Les deux lisaient le mauvais index — la barre de ROS restait vers 80 sur une antenne parfaite. Les barres retiennent aussi leur crête : la manipulation CW ne les fait plus clignoter à zéro.",
|
"Les mesures de la console Yaesu sont justes : la puissance lit l'instrument qui suit la HF (en watts mesurés, et non le réglage de puissance) et le ROS affiche le rapport lui-même. Les deux lisaient le mauvais index — la barre de ROS restait vers 80 sur une antenne parfaite. Les barres retiennent aussi leur crête un instant avant de redescendre : elles restent lisibles pendant les silences entre les mots d’un CQ, en CW comme en phonie.",
|
||||||
"La vitesse CW est désormais un réglage unique où que vous la changiez : le curseur de la console Yaesu et le panneau CW pilotent le manipulateur qui émet réellement, et le keyer interne de la radio suit pour que sa façade affiche la même valeur.",
|
"La vitesse CW est désormais un réglage unique où que vous la changiez : le curseur de la console Yaesu et le panneau CW pilotent le manipulateur qui émet réellement, et le keyer interne de la radio suit pour que sa façade affiche la même valeur.",
|
||||||
"Antenna Genius : le port A ne bascule plus sur l'antenne du port B quelques secondes après un changement. Un seul port peut porter l'antenne d'émission, le switch renvoie donc la même sur les deux — l'affichage suit désormais l'antenne de réception propre à chaque port.",
|
"Antenna Genius : le port A ne bascule plus sur l'antenne du port B quelques secondes après un changement. Un seul port peut porter l'antenne d'émission, le switch renvoie donc la même sur les deux — l'affichage suit désormais l'antenne de réception propre à chaque port.",
|
||||||
"CW par le keyer Yaesu : un cinquième moteur CW envoie vos macros avec le keyer de la radio (CAT « KY »), pour la famille FTDX101 / FT-991A / FT-710. Un FTDX10 ne l'accepte pas — DAKY compris — et OpsLog le dit en renvoyant vers le keyer série DTR sur son second port COM, qui fonctionne.",
|
"CW par le keyer Yaesu : un cinquième moteur CW envoie vos macros avec le keyer de la radio (CAT « KY »), pour la famille FTDX101 / FT-991A / FT-710. Un FTDX10 ne l'accepte pas — DAKY compris — et OpsLog le dit en renvoyant vers le keyer série DTR sur son second port COM, qui fonctionne.",
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ type Yaesu struct {
|
|||||||
panelLoaded bool
|
panelLoaded bool
|
||||||
// Commands this rig answered "?;" to — asked once, then never again.
|
// Commands this rig answered "?;" to — asked once, then never again.
|
||||||
unsupported map[string]bool
|
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
|
// metersLogged counts the RM1..RM6 samples taken during transmission, so the
|
||||||
// survey follows a real carrier instead of catching one instant of it.
|
// survey follows a real carrier instead of catching one instant of it.
|
||||||
metersLogged int
|
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
|
// 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
|
// 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.
|
// 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 {
|
if v, ok := y.askNum("RM5;", "RM5", 3); ok {
|
||||||
y.panel.PowerMeter = holdPeak(y.panel.PowerMeter, scale255(v))
|
y.panel.PowerMeter = y.powerPeak.update(scale255(v), now)
|
||||||
y.panel.PowerW = float64(holdPeak(int(y.panel.PowerW), int(yaesuWatts(v))))
|
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
|
// 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,
|
// 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
|
// the raw value is rho scaled to 255, and the ratio follows from physics
|
||||||
// rather than from a fitted curve.
|
// rather than from a fitted curve.
|
||||||
if v, ok := y.askNum("RM6;", "RM6", 3); ok {
|
if v, ok := y.askNum("RM6;", "RM6", 3); ok {
|
||||||
y.panel.SWRMeter = holdPeak(y.panel.SWRMeter, scale255(v))
|
y.panel.SWRMeter = y.swrPeak.update(scale255(v), now)
|
||||||
// SWR only means something while power is going out: between CW elements
|
// The RATIO is only meaningful while power is going out: between words
|
||||||
// the reading is 0, which would show as a perfect 1.0 match.
|
// 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 {
|
if v > 0 {
|
||||||
y.panel.SWR = swrFromReflection(v)
|
y.panel.SWR = swrFromReflection(v)
|
||||||
}
|
}
|
||||||
@@ -727,15 +729,43 @@ func yaesuWatts(raw int) float64 {
|
|||||||
return float64(raw) * 100.0 / yaesuPowerFullScale
|
return float64(raw) * 100.0 / yaesuPowerFullScale
|
||||||
}
|
}
|
||||||
|
|
||||||
// holdPeak gives a meter the inertia a needle has: it jumps to a higher reading
|
// meterPeak 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
|
// at once, HOLDS it for a moment, then falls back gradually.
|
||||||
// between every element — makes the bars flash to zero several times a second.
|
|
||||||
//
|
//
|
||||||
// The decay is a quarter of the gap per poll, so a real drop in power still
|
// The hold is the part that matters on the air. Between CW elements the gap is
|
||||||
// shows within about a second at the default rate.
|
// milliseconds, but between the words of a CQ — in CW as in SSB — it is most of
|
||||||
func holdPeak(current, sample int) int {
|
// a second, and a meter that decays straight away reads 0 in every one of those
|
||||||
if sample >= current {
|
// gaps. The operator sees a bar flashing rather than the power and SWR they are
|
||||||
return sample
|
// actually running.
|
||||||
|
type meterPeak struct {
|
||||||
|
val int
|
||||||
|
at time.Time
|
||||||
}
|
}
|
||||||
return current - (current-sample)/4
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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
|
package cat
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// What SPLIT means when the operator presses it.
|
// What SPLIT means when the operator presses it.
|
||||||
//
|
//
|
||||||
@@ -76,3 +79,42 @@ func TestSWRFromReflection(t *testing.T) {
|
|||||||
prev = v
|
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