From e0cefb5c41f67688cdf6c0bb1ddaa19dec7458b7 Mon Sep 17 00:00:00 2001 From: rouggy Date: Wed, 29 Jul 2026 14:11:25 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20the=20Yaesu=20power=20meter=20is=20not?= =?UTF-8?q?=20linear=20=E2=80=94=20calibrate=20it=20on=20three=20points?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One point could not reveal the curve. Scaling 207 = 100 W straight down read 30 W where the radio showed 10, and 75 where it showed 50 — wrong everywhere except at the single point it was fitted to. Three readings taken against the rig's own display give the shape: raw 62 → 10 W raw 155 → 50 W raw 207 → 100 W Interpolating between them reproduces the radio exactly at those points and stays close in between. I did not fit a formula: three samples can be made to support several curves, and the operator can check a table against their own meter. Above the top the last segment's slope continues rather than clamping, so a rig driving an amplifier does not sit pinned at 100 W. A test pins the measured pairs and the monotonicity, so a later change that breaks them fails against the radio rather than against taste. --- changelog.json | 4 +-- internal/cat/yaesu_panel.go | 53 +++++++++++++++++++++++++------- internal/cat/yaesu_split_test.go | 43 ++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/changelog.json b/changelog.json index 05c264c..66dc1c1 100644 --- a/changelog.json +++ b/changelog.json @@ -3,7 +3,7 @@ "version": "0.21.9", "date": "2026-07-28", "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 for a moment before falling back, so they stay readable through the gaps between words in a CQ, in CW as in SSB.", + "Yaesu console meters are right: power now reads the meter that follows the RF (in watts, on a curve measured against the rig own display, 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.", "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.", @@ -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." ], "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 un instant avant de redescendre : elles restent lisibles pendant les silences entre les mots d’un CQ, en CW comme en phonie.", + "Les mesures de la console Yaesu sont justes : la puissance lit l'instrument qui suit la HF (en watts, sur une courbe relevée face à l'affichage de la radio, 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.", "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.", diff --git a/internal/cat/yaesu_panel.go b/internal/cat/yaesu_panel.go index 3e99ec7..a91b025 100644 --- a/internal/cat/yaesu_panel.go +++ b/internal/cat/yaesu_panel.go @@ -713,20 +713,52 @@ func swrFromReflection(raw int) float64 { // yaesuWatts converts the power-meter reading to watts. // -// Single-point calibration, measured rather than assumed: an FTDX10 keying CW at -// 100 W reads 207. The PO meter is close enough to linear in power for one point -// to carry the scale, and a figure taken from the METER beats the power SETTING, -// which says what was asked for and not what left the radio. +// The meter is NOT linear in power, which one calibration point could never +// reveal: scaling 207 = 100 W straight down read 30 W where the rig showed 10, +// and 75 where it showed 50. Three points measured against the radio's own +// display settle the curve: // -// A second point at low power would refine it; until then the number is right at -// 100 W and approximate below. -const yaesuPowerFullScale = 207.0 // raw reading measured at 100 W +// raw 62 → 10 W +// raw 155 → 50 W +// raw 207 → 100 W +// +// Interpolating between them reproduces the rig exactly at those points and +// stays close in between — better than fitting a formula to three samples and +// pretending it holds everywhere. Above the last point it keeps the final +// segment's slope, so an amplifier-driving rig does not flatten at 100 W. +// +// Measured on an FTDX10 (2026-07-29). Another model may well need its own row. +var yaesuPowerCurve = []struct { + raw int + watts float64 +}{ + {0, 0}, + {62, 10}, + {155, 50}, + {207, 100}, +} func yaesuWatts(raw int) float64 { if raw <= 0 { return 0 } - return float64(raw) * 100.0 / yaesuPowerFullScale + for i := 1; i < len(yaesuPowerCurve); i++ { + hi := yaesuPowerCurve[i] + if raw <= hi.raw { + lo := yaesuPowerCurve[i-1] + span := float64(hi.raw - lo.raw) + if span <= 0 { + return hi.watts + } + f := float64(raw-lo.raw) / span + return lo.watts + f*(hi.watts-lo.watts) + } + } + // Past the top of the curve: extend the last segment rather than clamp. + n := len(yaesuPowerCurve) + lo, hi := yaesuPowerCurve[n-2], yaesuPowerCurve[n-1] + slope := (hi.watts - lo.watts) / float64(hi.raw-lo.raw) + return hi.watts + float64(raw-hi.raw)*slope } // meterPeak gives a meter the inertia a needle has: it jumps to a higher reading @@ -735,8 +767,7 @@ func yaesuWatts(raw int) float64 { // 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. +// gaps: the operator sees a bar flashing rather than the power they are running. type meterPeak struct { val int at time.Time @@ -757,7 +788,7 @@ func (m *meterPeak) update(sample int, now time.Time) int { 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 + // 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 { diff --git a/internal/cat/yaesu_split_test.go b/internal/cat/yaesu_split_test.go index 52388de..28c2ec6 100644 --- a/internal/cat/yaesu_split_test.go +++ b/internal/cat/yaesu_split_test.go @@ -118,3 +118,46 @@ func TestMeterPeakHold(t *testing.T) { t.Errorf("settled at %d, want 10 — the meter must converge on the truth", v) } } + +// The power curve, pinned to the readings taken against the rig's own display. +// +// It is NOT linear, and one calibration point could not show that: scaling +// 207 = 100 W straight down read 30 W where the radio showed 10, and 75 where it +// showed 50. These are the three measured pairs, so a change to the curve that +// breaks them is a regression against the radio, not against a preference. +func TestYaesuPowerCurve(t *testing.T) { + cases := []struct { + raw int + watts float64 + tol float64 + }{ + {0, 0, 0.1}, + {62, 10, 0.5}, // measured + {155, 50, 0.5}, // measured + {207, 100, 0.5}, // measured + // Between the measured points it interpolates, so it must land inside the + // bracket rather than shooting past it. + {100, 30, 10}, + {180, 75, 10}, + } + for _, c := range cases { + got := yaesuWatts(c.raw) + if got < c.watts-c.tol || got > c.watts+c.tol { + t.Errorf("yaesuWatts(%d) = %.1f W, want %.1f ±%.1f", c.raw, got, c.watts, c.tol) + } + } + // Monotonic: more meter must never mean less power. + prev := -1.0 + for raw := 0; raw <= 255; raw++ { + v := yaesuWatts(raw) + if v < prev { + t.Fatalf("power fell from %.1f to %.1f at raw=%d", prev, v, raw) + } + prev = v + } + // Above the top of the curve it keeps rising rather than flattening at 100 W — + // a rig driving an amplifier can read past its own full scale. + if v := yaesuWatts(230); v <= 100 { + t.Errorf("yaesuWatts(230) = %.1f, want more than 100 — the curve should extend", v) + } +}