fix(sat): both antennas on both satellite slices

A slice has an rxant and a txant, and each pair belongs to that slice's
own band. Only two of the four were being set — the downlink's receive
antenna and the uplink's transmit one — so the downlink slice was left
with an empty txant. It never keys, so nothing was wrong on the air, but
the slice was half-configured: move transmit focus to it and the radio
uses whatever antenna it happened to be left on. Reported with XVTA on
2 m and XVTB on 70 cm, where the 70 cm slice showed one antenna and the
2 m slice showed two.

SatAntennas now takes all four, and the late-slice replay in
adoptSatSlice hands the uplink its own pair instead of putting the
transmit antenna on both of its ports.
This commit is contained in:
2026-09-10 17:48:28 +02:00
parent 9dcab0568b
commit 6f9b996db8
5 changed files with 58 additions and 35 deletions
+11 -7
View File
@@ -697,21 +697,25 @@ func (a *App) applySatRadio(tp sat.Transponder) {
if err != nil || len(m) == 0 { if err != nil || len(m) == 0 {
return return
} }
// The downlink is received, so it takes that band's RX antenna; the uplink // Each slice gets BOTH antennas of ITS OWN band. Only two of the four were
// is transmitted, so it takes that band's TX antenna. // set — the downlink's receive and the uplink's transmit — which left the
// downlink slice with no txant. It never keys, so nothing was wrong on the
// air, but a half-configured slice uses whatever antenna it was last left
// on the moment transmit focus moves to it.
downBand, upBand := flexBandAntKey(tp.DownLo), flexBandAntKey(tp.UpLo) downBand, upBand := flexBandAntKey(tp.DownLo), flexBandAntKey(tp.UpLo)
rxAnt := m[downBand].RX down, up := m[downBand], m[upBand]
txAnt := m[upBand].TX if strings.TrimSpace(down.RX) == "" && strings.TrimSpace(down.TX) == "" &&
if strings.TrimSpace(rxAnt) == "" && strings.TrimSpace(txAnt) == "" { strings.TrimSpace(up.RX) == "" && strings.TrimSpace(up.TX) == "" {
// Worth a line: an operator who HAS configured the pair and still sees // Worth a line: an operator who HAS configured the pair and still sees
// the wrong antenna has no other way to tell a setting he never made // the wrong antenna has no other way to tell a setting he never made
// from a lookup that missed. // from a lookup that missed.
applog.Printf("sat: no antenna configured for this pass (down %s, up %s)", downBand, upBand) applog.Printf("sat: no antenna configured for this pass (down %s, up %s)", downBand, upBand)
return return
} }
applog.Printf("sat: antennas rx=%q (%s) tx=%q (%s)", rxAnt, downBand, txAnt, upBand) applog.Printf("sat: antennas down %s rx=%q tx=%q, up %s rx=%q tx=%q",
downBand, down.RX, down.TX, upBand, up.RX, up.TX)
if err := a.cat.FlexDo(func(fc cat.FlexController) error { if err := a.cat.FlexDo(func(fc cat.FlexController) error {
return fc.SatAntennas(rxAnt, txAnt) return fc.SatAntennas(down.RX, down.TX, up.RX, up.TX)
}); err != nil { }); err != nil {
// Not fatal: a rig that is not a Flex has no such thing, and a pass with // Not fatal: a rig that is not a Flex has no such thing, and a pass with
// the wrong antenna is still a pass. // the wrong antenna is still a pass.
+6 -2
View File
@@ -8,7 +8,9 @@
"Badges on the satellite panel for the band, the Doppler offset, an inverting transponder, the passband width and a pass's peak elevation.", "Badges on the satellite panel for the band, the Doppler offset, an inverting transponder, the passband width and a pass's peak elevation.",
"The satellite ground track is visible on every basemap — it was drawn in a colour the map could not use and came out near-white.", "The satellite ground track is visible on every basemap — it was drawn in a colour the map could not use and came out near-white.",
"The decode panel no longer warns about band drift on a second slice: it now compares against every band the radio is receiving on, not just the transmit band.", "The decode panel no longer warns about band drift on a second slice: it now compares against every band the radio is receiving on, not just the transmit band.",
"OpsLog appears again after an update. The relaunch was starting the new build with its window hidden, so it ran with no window at all." "OpsLog appears again after an update. The relaunch was starting the new build with its window hidden, so it ran with no window at all.",
"Each satellite slice now gets both of its antennas, RX and TX, from its own band — the downlink slice was left with no transmit antenna at all.",
"The satellite ground track no longer draws a straight line across the map when it crosses the antimeridian."
], ],
"fr": [ "fr": [
"Le tracé du ciel et la position passent dans leur propre colonne, à gauche de la carte. Largeur réglable, et la colonne se replie comme celle de droite.", "Le tracé du ciel et la position passent dans leur propre colonne, à gauche de la carte. Largeur réglable, et la colonne se replie comme celle de droite.",
@@ -17,7 +19,9 @@
"Des pastilles sur le panneau satellite pour la bande, l’écart Doppler, un transpondeur inverseur, la largeur de bande passante et le pic d’élévation dune passe.", "Des pastilles sur le panneau satellite pour la bande, l’écart Doppler, un transpondeur inverseur, la largeur de bande passante et le pic d’élévation dune passe.",
"Le tracé au sol des satellites est visible sur tous les fonds de carte — il était dessiné dans une couleur inexploitable et sortait presque blanc.", "Le tracé au sol des satellites est visible sur tous les fonds de carte — il était dessiné dans une couleur inexploitable et sortait presque blanc.",
"Le panneau de décodages navertit plus dune dérive de bande sur une seconde tranche : la comparaison porte sur toutes les bandes reçues, pas seulement celle d’émission.", "Le panneau de décodages navertit plus dune dérive de bande sur une seconde tranche : la comparaison porte sur toutes les bandes reçues, pas seulement celle d’émission.",
"OpsLog réapparaît après une mise à jour. La relance démarrait la nouvelle version avec sa fenêtre masquée, donc sans aucune fenêtre." "OpsLog réapparaît après une mise à jour. La relance démarrait la nouvelle version avec sa fenêtre masquée, donc sans aucune fenêtre.",
"Chaque slice satellite reçoit désormais ses deux antennes, RX et TX, depuis sa propre bande — la slice de descente restait sans antenne d’émission.",
"Le tracé au sol du satellite ne trace plus une ligne droite en travers de la carte lorsquil franchit lantiméridien."
] ]
}, },
{ {
+2 -2
View File
@@ -594,9 +594,9 @@ type FlexController interface {
SetMute(bool) error SetMute(bool) error
SetRXAntenna(string) error SetRXAntenna(string) error
SetTXAntenna(string) error SetTXAntenna(string) error
// SatAntennas sets the antenna on each SATELLITE slice — they are on two // SatAntennas sets BOTH antennas on each SATELLITE slice — they are on two
// different bands and, with transverters, two different ports. // different bands and, with transverters, two different ports.
SatAntennas(rxAnt, txAnt string) error SatAntennas(downRX, downTX, upRX, upTX string) error
// SatTone sets the CTCSS tone the satellite uplink transmits (0 = off). // SatTone sets the CTCSS tone the satellite uplink transmits (0 = off).
SatTone(hz float64) error SatTone(hz float64) error
SetActiveSlice(int) error // focus slice idx so commands target it SetActiveSlice(int) error // focus slice idx so commands target it
+6 -2
View File
@@ -80,8 +80,12 @@ type Flex struct {
// before the radio has necessarily reported the slice it was asked to // before the radio has necessarily reported the slice it was asked to
// create; without this they were applied to an index of -1 and never again. // create; without this they were applied to an index of -1 and never again.
satUpMode string satUpMode string
satRXAnt string // Both antennas of both slices: a slice has an rxant and a txant, and each
satTXAnt string // pair belongs to that slice's own band.
satDownRX string
satDownTX string
satUpRX string
satUpTX string
satTone float64 satTone float64
spotCall map[int]string // spot index → callsign (to fill the call on a panadapter click) spotCall map[int]string // spot index → callsign (to fill the call on a panadapter click)
spotMode map[int]string // spot index → ADIF mode, so a click can also set the slice mode (SmartSDR tunes the spot's freq but not its mode) spotMode map[int]string // spot index → ADIF mode, so a click can also set the slice mode (SmartSDR tunes the spot's freq but not its mode)
+31 -20
View File
@@ -155,11 +155,13 @@ func (f *Flex) adoptSatSlice(role string, idx int) {
// antenna and the tone are all sent ONCE — the step only re-sends // antenna and the tone are all sent ONCE — the step only re-sends
// frequencies. // frequencies.
f.mu.Lock() f.mu.Lock()
mode, ant, tone := f.satUpMode, f.satTXAnt, f.satTone mode, rxAnt, txAnt, tone := f.satUpMode, f.satUpRX, f.satUpTX, f.satTone
f.mu.Unlock() f.mu.Unlock()
if strings.TrimSpace(ant) != "" { if strings.TrimSpace(rxAnt) != "" {
f.send(fmt.Sprintf("slice s %d txant=%s", idx, ant)) f.send(fmt.Sprintf("slice s %d rxant=%s", idx, rxAnt))
f.send(fmt.Sprintf("slice s %d rxant=%s", idx, ant)) }
if strings.TrimSpace(txAnt) != "" {
f.send(fmt.Sprintf("slice s %d txant=%s", idx, txAnt))
} }
if strings.TrimSpace(mode) != "" { if strings.TrimSpace(mode) != "" {
f.satMode(idx, mode, 0) f.satMode(idx, mode, 0)
@@ -259,7 +261,7 @@ func (f *Flex) SatReceiveHz() (int64, error) {
return s.freqHz, nil return s.freqHz, nil
} }
// SatAntennas selects the antenna each satellite slice uses. // SatAntennas selects both antennas of each satellite slice.
// //
// The two slices are on two different bands — a V/U bird receives on 70 cm and // The two slices are on two different bands — a V/U bird receives on 70 cm and
// transmits on 2 m, a U/V one does the reverse — so they cannot share one // transmits on 2 m, a U/V one does the reverse — so they cannot share one
@@ -274,30 +276,39 @@ func (f *Flex) SatReceiveHz() (int64, error) {
// Empty strings are left alone: an operator who has configured 2 m and not // Empty strings are left alone: an operator who has configured 2 m and not
// 70 cm should keep whatever the radio already had on the other side rather // 70 cm should keep whatever the radio already had on the other side rather
// than have it cleared. // than have it cleared.
func (f *Flex) SatAntennas(rxAnt, txAnt string) error { // A slice has an rxant AND a txant, and both belong to the slice's own band.
// Only two of the four were being set — the downlink's receive antenna and
// the uplink's transmit one — which left the downlink slice with an empty
// txant. It never keys, so nothing was wrong on the air, but the slice was
// half-configured: move transmit focus to it and the radio uses whatever
// antenna it was last left on.
func (f *Flex) SatAntennas(downRX, downTX, upRX, upTX string) error {
f.mu.Lock() f.mu.Lock()
rx, tx := f.satRX, f.satTX rx, tx := f.satRX, f.satTX
connected := f.conn != nil connected := f.conn != nil
// Remembered so a slice that is reported late still gets its antenna. // Remembered so a slice that is reported late still gets its antennas.
f.satRXAnt, f.satTXAnt = rxAnt, txAnt f.satDownRX, f.satDownTX = downRX, downTX
f.satUpRX, f.satUpTX = upRX, upTX
f.mu.Unlock() f.mu.Unlock()
if !connected { if !connected {
return fmt.Errorf("flex: not connected") return fmt.Errorf("flex: not connected")
} }
// The downlink slice is the one being listened to, so it takes the receive set := func(idx int, which, rxAnt, txAnt string) {
// antenna; the uplink slice is the one keyed, so it takes the transmit one. if idx < 0 {
if rx >= 0 && strings.TrimSpace(rxAnt) != "" { return
f.send(fmt.Sprintf("slice s %d rxant=%s", rx, rxAnt))
applog.Printf("flex: satellite downlink slice %d on antenna %s", rx, rxAnt)
} }
if tx >= 0 && strings.TrimSpace(txAnt) != "" { if strings.TrimSpace(rxAnt) != "" {
f.send(fmt.Sprintf("slice s %d txant=%s", tx, txAnt)) f.send(fmt.Sprintf("slice s %d rxant=%s", idx, rxAnt))
// A transmit slice also has to HEAR its own band on some radios, and a
// transverter port is the only thing connected to it. Setting the
// receive antenna to match costs nothing when it is already right.
f.send(fmt.Sprintf("slice s %d rxant=%s", tx, txAnt))
applog.Printf("flex: satellite uplink slice %d on antenna %s", tx, txAnt)
} }
if strings.TrimSpace(txAnt) != "" {
f.send(fmt.Sprintf("slice s %d txant=%s", idx, txAnt))
}
if strings.TrimSpace(rxAnt) != "" || strings.TrimSpace(txAnt) != "" {
applog.Printf("flex: satellite %s slice %d rx=%s tx=%s", which, idx, rxAnt, txAnt)
}
}
set(rx, "downlink", downRX, downTX)
set(tx, "uplink", upRX, upTX)
return nil return nil
} }