fix: Antenna Genius drew both ports on the same antenna

Reported on an 8x2: port A correctly on the 80 m vertical, then a few seconds
later both A and B shown on the same beam.

The display preferred the TX antenna, falling back to RX. On an 8x2 only ONE port
can hold the transmit antenna, so the switch reports the same txant on both — and
every keepalive poll redrew port A as whatever port B transmits through. The RX
antenna is the per-port selection, and the only thing Activate sets, so it is
what a port shows; TX stays the fallback for a port reporting no RX antenna.

A port change is now logged with the raw line — but only on CHANGE, since the
device pushes state every few seconds and logging each one would bury the rest.
Without any trace, "port A jumped to the wrong antenna" cannot be checked.

Two tests pin it: rx wins over a disagreeing tx, and a message about one port
never moves the other — which is the shape that was actually on screen.
This commit is contained in:
2026-07-29 12:46:48 +02:00
parent 08f3e54afb
commit 0cc806722e
3 changed files with 109 additions and 14 deletions
+2
View File
@@ -3,6 +3,7 @@
"version": "0.21.9", "version": "0.21.9",
"date": "2026-07-28", "date": "2026-07-28",
"en": [ "en": [
"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.",
"Yaesu control panel: with the native Yaesu backend, a console tab and Main-view pane give S/PO/SWR meters, band and mode buttons, AF/RF/squelch, AGC, IPO-AMP1-AMP2, a 6/12/18 dB attenuator, NB, DNR, narrow filter, power, mic gain, VOX, split and ATU tune. CW, RTTY, DIGI and PSK show their sideband — click an active button again to switch it. Pressing SPLIT places the transmit VFO relative to where you are listening — up 1 kHz on CW and data, up 5 kHz on phone — and shows the transmit frequency; +1k and +5k buttons cover the rest.", "Yaesu control panel: with the native Yaesu backend, a console tab and Main-view pane give S/PO/SWR meters, band and mode buttons, AF/RF/squelch, AGC, IPO-AMP1-AMP2, a 6/12/18 dB attenuator, NB, DNR, narrow filter, power, mic gain, VOX, split and ATU tune. CW, RTTY, DIGI and PSK show their sideband — click an active button again to switch it. Pressing SPLIT places the transmit VFO relative to where you are listening — up 1 kHz on CW and data, up 5 kHz on phone — and shows the transmit frequency; +1k and +5k buttons cover the rest.",
"CW through the Yaesu keyer: a fifth CW engine sends your macros with the radio own keyer (CAT \"KY\") — no WinKeyer, no second cable. Pick \"Yaesu (rig keyer)\" in Settings → CW keyer, with the Yaesu CAT backend active.", "CW through the Yaesu keyer: a fifth CW engine sends your macros with the radio own keyer (CAT \"KY\") — no WinKeyer, no second cable. Pick \"Yaesu (rig keyer)\" in Settings → CW keyer, with the Yaesu CAT backend active.",
"The Yaesu console follows the mode: in CW the microphone gain and VOX disappear and a CW card takes their place with keyer speed, break-in and ZIN (zero-in).", "The Yaesu console follows the mode: in CW the microphone gain and VOX disappear and a CW card takes their place with keyer speed, break-in and ZIN (zero-in).",
@@ -18,6 +19,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": [
"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.",
"Panneau de contrôle Yaesu : avec le backend Yaesu natif, un onglet console et un volet de la vue principale offrent les mesures S/PO/ROS, les boutons de bande et de mode, AF/RF/squelch, AGC, IPO-AMP1-AMP2, un atténuateur 6/12/18 dB, NB, DNR, filtre étroit, puissance, gain micro, VOX, split et accord d'antenne. CW, RTTY, DIGI et PSK affichent leur bande latérale — recliquez sur le bouton actif pour en changer. Appuyer sur SPLIT place le VFO d'émission par rapport à votre écoute — +1 kHz en CW et numérique, +5 kHz en phonie — et affiche la fréquence d'émission ; les boutons +1k et +5k couvrent le reste.", "Panneau de contrôle Yaesu : avec le backend Yaesu natif, un onglet console et un volet de la vue principale offrent les mesures S/PO/ROS, les boutons de bande et de mode, AF/RF/squelch, AGC, IPO-AMP1-AMP2, un atténuateur 6/12/18 dB, NB, DNR, filtre étroit, puissance, gain micro, VOX, split et accord d'antenne. CW, RTTY, DIGI et PSK affichent leur bande latérale — recliquez sur le bouton actif pour en changer. Appuyer sur SPLIT place le VFO d'émission par rapport à votre écoute — +1 kHz en CW et numérique, +5 kHz en phonie — et affiche la fréquence d'émission ; les boutons +1k et +5k couvrent le reste.",
"CW par le keyer Yaesu : un cinquième moteur CW envoie vos macros avec le keyer de la radio (CAT « KY ») — sans WinKeyer ni second câble. À choisir dans Réglages → Keyer CW sous « Yaesu (keyer de la radio) », avec le backend CAT Yaesu actif.", "CW par le keyer Yaesu : un cinquième moteur CW envoie vos macros avec le keyer de la radio (CAT « KY ») — sans WinKeyer ni second câble. À choisir dans Réglages → Keyer CW sous « Yaesu (keyer de la radio) », avec le backend CAT Yaesu actif.",
"La console Yaesu suit le mode : en CW, le gain micro et le VOX disparaissent au profit d'une carte CW avec la vitesse du manipulateur, le break-in et ZIN (zéro-in).", "La console Yaesu suit le mode : en CW, le gain micro et le VOX disparaissent au profit d'une carte CW avec la vitesse du manipulateur, le break-in et ZIN (zéro-in).",
+34 -14
View File
@@ -66,11 +66,12 @@ type Client struct {
authTries int authTries int
ready atomic.Bool // init commands sent → keepalive may run ready atomic.Bool // init commands sent → keepalive may run
statusMu sync.RWMutex statusMu sync.RWMutex
status Status status Status
antennas map[int]string // index → name (rebuilt into status.Antennas) antennas map[int]string // index → name (rebuilt into status.Antennas)
antBands map[int]int // index → band bitmask (which bands the antenna covers) antBands map[int]int // index → band bitmask (which bands the antenna covers)
antRawN int // one-shot: how many raw antenna lines we've logged antRawN int // one-shot: how many raw antenna lines we've logged
lastShown map[int]int // port id → antenna last reported, so only CHANGES are logged
stop chan struct{} stop chan struct{}
running bool running bool
@@ -81,13 +82,14 @@ func New(host string, port int, password string) *Client {
port = defaultPort port = defaultPort
} }
return &Client{ return &Client{
host: host, host: host,
port: port, port: port,
password: strings.TrimSpace(password), password: strings.TrimSpace(password),
stop: make(chan struct{}), stop: make(chan struct{}),
antennas: map[int]string{}, antennas: map[int]string{},
antBands: map[int]int{}, antBands: map[int]int{},
status: Status{Host: host}, lastShown: map[int]int{},
status: Status{Host: host},
} }
} }
@@ -398,9 +400,27 @@ func (c *Client) parsePort(msg string) {
} }
tx := kvInt(msg, "txant") tx := kvInt(msg, "txant")
rx := kvInt(msg, "rxant") rx := kvInt(msg, "rxant")
active := tx // The RX antenna is what identifies a port's selection, and it is the only
// thing Activate sets. Preferring the TX antenna made both ports converge on
// the same row a few seconds after a change: on an 8x2 only ONE port can hold
// the transmit antenna, so the device reports the same txant on both, and a
// port A genuinely on the 80 m vertical was redrawn as the beam that port B
// transmits through.
//
// TX remains the fallback for a port that reports no RX antenna at all.
active := rx
if active == 0 { if active == 0 {
active = rx active = tx
}
// Logged only when the shown antenna CHANGES: the device pushes port state
// every few seconds, and logging each one would bury everything else — but
// without any trace, "port A jumped to the wrong antenna" is unfalsifiable.
c.statusMu.Lock()
prev, seen := c.lastShown[id]
c.lastShown[id] = active
c.statusMu.Unlock()
if !seen || prev != active {
applog.Printf("antgenius: port %d → antenna %d (rxant=%d txant=%d) raw=%q", id, active, rx, tx, msg)
} }
txOn := kvInt(msg, "tx") != 0 // the standalone "tx=0|1" transmit flag txOn := kvInt(msg, "tx") != 0 // the standalone "tx=0|1" transmit flag
c.setStatus(func(s *Status) { c.setStatus(func(s *Status) {
+73
View File
@@ -0,0 +1,73 @@
package antgenius
import "testing"
// Which antenna a port SHOWS.
//
// Reported on an 8x2: port A was correctly on the 80 m vertical, then a few
// seconds later both A and B were drawn on the same beam. The cause is that only
// ONE port can hold the transmit antenna, so the device reports the same txant
// on both — and the display preferred txant. The RX antenna is the per-port
// selection, and the only thing Activate sets, so it has to win.
func TestPortShowsRXAntenna(t *testing.T) {
cases := []struct {
name string
msg string
wantPort int
wantAnt int
}{
{
// The reported case: port A listening on antenna 2 while the shared TX
// antenna is 1. Before the fix this drew port A on antenna 1.
name: "rx and tx disagree — rx wins",
msg: "port 1 rxant=2 txant=1 tx=0",
wantPort: 1, wantAnt: 2,
},
{
name: "they agree",
msg: "port 2 rxant=3 txant=3 tx=0",
wantPort: 2, wantAnt: 3,
},
{
// A port with no RX antenna still shows something meaningful.
name: "no rx antenna — fall back to tx",
msg: "port 1 rxant=0 txant=4 tx=1",
wantPort: 1, wantAnt: 4,
},
{
name: "nothing selected",
msg: "port 2 rxant=0 txant=0 tx=0",
wantPort: 2, wantAnt: 0,
},
}
for _, c := range cases {
cl := New("host", 0, "")
cl.parsePort(c.msg)
st := cl.GetStatus()
got := st.PortA
if c.wantPort == 2 {
got = st.PortB
}
if got != c.wantAnt {
t.Errorf("%s: port %d shows antenna %d, want %d (%q)", c.name, c.wantPort, got, c.wantAnt, c.msg)
}
}
}
// The two ports must stay independent: a message about one must never move the
// other, which is the shape the operator actually saw on screen.
func TestPortsAreIndependent(t *testing.T) {
c := New("host", 0, "")
c.parsePort("port 1 rxant=2 txant=0 tx=0")
c.parsePort("port 2 rxant=1 txant=1 tx=1")
st := c.GetStatus()
if st.PortA != 2 {
t.Errorf("port A = %d, want 2 — port B's message moved it", st.PortA)
}
if st.PortB != 1 {
t.Errorf("port B = %d, want 1", st.PortB)
}
if !st.TxB || st.TxA {
t.Errorf("transmit flags crossed: TxA=%v TxB=%v", st.TxA, st.TxB)
}
}