From bdda7ad07affd50227635b9e541183a0c23e8876 Mon Sep 17 00:00:00 2001 From: rouggy Date: Wed, 29 Jul 2026 13:49:09 +0200 Subject: [PATCH] fix: read the real SWR on the Yaesu, and show the ratio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second measurement at a known mismatch settled both the index and the scale. At SWR 1.1: RM6=0. At SWR 1.5: RM6=52, while RM4 kept tracking the power. 52/255 = 0.204, which is the reflection coefficient of a 1.5 SWR to three decimals. So the raw value is rho scaled to 255, and the ratio is (1+rho)/(1-rho) — physics, not a curve fitted through two points, which is why 2.0 and 3.0 fall out of it correctly without ever having been measured. The panel now shows that ratio, the number the operator reads on the rig, rather than a percentage of meter travel — and a dash while receiving, since a stale SWR from the last transmission reads as a live one. Both measurements are recorded in the code and in a test, so the mapping is evidence rather than a table borrowed from another model — which is exactly how it came to read 81 in the first place. --- changelog.json | 4 +-- frontend/src/components/YaesuPanel.tsx | 7 +++-- frontend/wailsjs/go/models.ts | 2 ++ internal/cat/yaesu_panel.go | 39 ++++++++++++++++++++++---- internal/cat/yaesu_split_test.go | 38 +++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 9 deletions(-) diff --git a/changelog.json b/changelog.json index 3caf1c3..fae8bae 100644 --- a/changelog.json +++ b/changelog.json @@ -3,7 +3,7 @@ "version": "0.21.9", "date": "2026-07-28", "en": [ - "Yaesu console: the SWR bar read about 80 on a perfect match. It was showing a meter that does not move with the antenna at all; power and SWR are now read from the indices measured on the radio.", + "Yaesu console: the SWR bar showed about 80 on a perfect match — it was reading a meter unrelated to the antenna. It now reads the right one and shows the ratio itself (1.0, 1.5…) as the rig does, not a percentage.", "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": [ - "Console Yaesu : la barre de ROS indiquait environ 80 sur une antenne parfaitement adaptée. Elle affichait un instrument qui ne suit pas l'antenne ; la puissance et le ROS sont désormais lus sur les index mesurés sur la radio.", + "Console Yaesu : la barre de ROS indiquait environ 80 sur une antenne parfaitement adaptée — elle lisait un instrument sans rapport avec l'antenne. Elle lit désormais le bon et affiche le rapport lui-même (1,0 ; 1,5…) comme la radio, au lieu d'un pourcentage.", "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/frontend/src/components/YaesuPanel.tsx b/frontend/src/components/YaesuPanel.tsx index 2c25867..af31302 100644 --- a/frontend/src/components/YaesuPanel.tsx +++ b/frontend/src/components/YaesuPanel.tsx @@ -19,7 +19,7 @@ type YaesuState = { rf_power: number; mic_gain: number; af_gain: number; rf_gain: number; squelch: number; agc?: string; preamp: number; att: number; nb: boolean; nr: boolean; nr_level: number; narrow: boolean; vox: boolean; - split_tx_hz?: number; key_speed?: number; break_in?: boolean; + split_tx_hz?: number; key_speed?: number; break_in?: boolean; swr?: number; }; const ZERO: YaesuState = { @@ -351,7 +351,10 @@ export function YaesuPanel({ onReportRST, onKeySpeed }: { onClick={onReportRST ? () => { const sp = sParts(view.s_meter); onReportRST(sMeterRST(sp.s, sp.over, view.mode)); } : undefined} title={onReportRST ? t('yaesu.sToRst') : undefined} /> - + {/* The RATIO, as the rig shows it — a percentage of meter travel is + not something an operator can act on. The bar keeps the travel. */} + = 1 ? view.swr.toFixed(1) : '1.0') : '—'} /> diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 5429bfc..9adf452 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1097,6 +1097,7 @@ export namespace cat { nr: boolean; nr_level: number; narrow: boolean; + swr: number; vox: boolean; key_speed: number; break_in: boolean; @@ -1129,6 +1130,7 @@ export namespace cat { this.nr = source["nr"]; this.nr_level = source["nr_level"]; this.narrow = source["narrow"]; + this.swr = source["swr"]; this.vox = source["vox"]; this.key_speed = source["key_speed"]; this.break_in = source["break_in"]; diff --git a/internal/cat/yaesu_panel.go b/internal/cat/yaesu_panel.go index 24ed5e9..ab5c727 100644 --- a/internal/cat/yaesu_panel.go +++ b/internal/cat/yaesu_panel.go @@ -55,7 +55,10 @@ type YaesuTXState struct { NR bool `json:"nr"` NRLevel int `json:"nr_level"` // 1-15 Narrow bool `json:"narrow"` // NAR filter - VOX bool `json:"vox"` + // SWR is the RATIO (1.0, 1.5…), computed from the reflection coefficient — + // what an operator reads on the rig, not a percentage of meter travel. + SWR float64 `json:"swr"` + VOX bool `json:"vox"` // CW-only controls. Read (and shown) only in CW, where MIC and VOX mean // nothing and these are what an operator reaches for. KeySpeed int `json:"key_speed"` // WPM @@ -157,12 +160,14 @@ func (y *Yaesu) readPanel(mode string, split bool, txHz int64) { if v, ok := y.askNum("RM4;", "RM4", 3); ok { y.panel.PowerMeter = scale255(v) } - // SWR: RM6 is the remaining candidate. It read 0 throughout, which is - // CONSISTENT with the 1.1 match but does not prove the index — only a - // deliberate mismatch would. Shown because a bar stuck at zero on a good - // antenna is honest, where 81 was actively misleading. + // 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, + // which is the reflection coefficient of a 1.5 SWR to three decimals — so + // 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 = scale255(v) + y.panel.SWR = swrFromReflection(v) } } else { // Zeroed rather than frozen: a stale SWR bar from the last transmission @@ -658,3 +663,27 @@ func (y *Yaesu) YaesuZeroIn() error { } return y.write("ZI;") } + +// swrFromReflection turns the rig's SWR meter reading into the ratio itself. +// +// The raw value is the reflection coefficient scaled to 255 — established on an +// FTDX10 by measuring at two known matches (0 at 1.1, 52 at 1.5; 52/255 = 0.204, +// which is rho for a 1.5 SWR). So SWR = (1+rho)/(1-rho), physics rather than a +// curve fitted to two points. +// +// Capped at 9.9: past that the number stops meaning anything to an operator, and +// rho approaching 1 sends the ratio to infinity. +func swrFromReflection(raw int) float64 { + if raw <= 0 { + return 1.0 + } + rho := float64(raw) / 255.0 + if rho >= 0.98 { + return 9.9 + } + swr := (1 + rho) / (1 - rho) + if swr > 9.9 { + return 9.9 + } + return swr +} diff --git a/internal/cat/yaesu_split_test.go b/internal/cat/yaesu_split_test.go index 3cab816..03a0058 100644 --- a/internal/cat/yaesu_split_test.go +++ b/internal/cat/yaesu_split_test.go @@ -38,3 +38,41 @@ func TestYaesuDefaultSplitOffset(t *testing.T) { } } } + +// 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 + } +}