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:
@@ -66,11 +66,12 @@ type Client struct {
|
||||
authTries int
|
||||
ready atomic.Bool // init commands sent → keepalive may run
|
||||
|
||||
statusMu sync.RWMutex
|
||||
status Status
|
||||
antennas map[int]string // index → name (rebuilt into status.Antennas)
|
||||
antBands map[int]int // index → band bitmask (which bands the antenna covers)
|
||||
antRawN int // one-shot: how many raw antenna lines we've logged
|
||||
statusMu sync.RWMutex
|
||||
status Status
|
||||
antennas map[int]string // index → name (rebuilt into status.Antennas)
|
||||
antBands map[int]int // index → band bitmask (which bands the antenna covers)
|
||||
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{}
|
||||
running bool
|
||||
@@ -81,13 +82,14 @@ func New(host string, port int, password string) *Client {
|
||||
port = defaultPort
|
||||
}
|
||||
return &Client{
|
||||
host: host,
|
||||
port: port,
|
||||
password: strings.TrimSpace(password),
|
||||
stop: make(chan struct{}),
|
||||
antennas: map[int]string{},
|
||||
antBands: map[int]int{},
|
||||
status: Status{Host: host},
|
||||
host: host,
|
||||
port: port,
|
||||
password: strings.TrimSpace(password),
|
||||
stop: make(chan struct{}),
|
||||
antennas: map[int]string{},
|
||||
antBands: map[int]int{},
|
||||
lastShown: map[int]int{},
|
||||
status: Status{Host: host},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,9 +400,27 @@ func (c *Client) parsePort(msg string) {
|
||||
}
|
||||
tx := kvInt(msg, "txant")
|
||||
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 {
|
||||
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
|
||||
c.setStatus(func(s *Status) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user