fix: read the real SWR on the Yaesu, and show the ratio

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.
This commit is contained in:
2026-07-29 13:49:09 +02:00
parent 3fd6763ff0
commit bdda7ad07a
5 changed files with 81 additions and 9 deletions
+2 -2
View File
@@ -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.",
+5 -2
View File
@@ -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} />
<MeterBar label="PWR" value={view.rf_power * view.power_meter / 100} unit="W" lo={0} hi={100} accent="#0ea5e9" />
<MeterBar label="SWR" value={view.swr_meter} lo={0} hi={100} accent="#f59e0b" />
{/* 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. */}
<MeterBar label="SWR" value={view.swr_meter} lo={0} hi={100} accent="#f59e0b"
display={view.transmitting ? (view.swr && view.swr >= 1 ? view.swr.toFixed(1) : '1.0') : '—'} />
</div>
</Card>
+2
View File
@@ -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"];
+34 -5
View File
@@ -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
}
+38
View File
@@ -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
}
}