fix(flex): the satellite uplink slice was never really armed

Reported with two screenshots: both slices in USB on an inverting transponder,
slice B sitting at exactly 435.100000, and the red TX badge on the 2 m DOWNLINK.
The log named the cause in one line:

    flex: satellite armed (rx slice 0, tx slice -1)

Creating a slice is asynchronous — "slice create" is answered later with the
index — and SetSatellite returned without waiting. Everything downstream then
ran against an uplink of -1 and silently did nothing: no antenna, no CTCSS tone,
no sideband, never tuned, and never sent "tx=1". So the radio went on
transmitting on the downlink, which is the one failure here that puts a signal
where it must not go, and the frequency and mode on screen were simply the ones
the slice had been created with.

Three fixes, because the ordering can fail in more than one way:

  - Arming waits for both indices before reporting the pair armed, and says so
    plainly when the radio does not produce them.
  - The uplink is adopted from the SLICE STATUS as well as from the create
    reply. The status needs no sequence-number correlation: if satellite mode is
    armed, the uplink is unknown, and a slice is in use that is not the
    downlink, that is it — the radio is saying so.
  - What the uplink is owed is remembered — its mode, its antenna, its tone —
    and given to it when it appears. Those three are sent ONCE; only the
    frequency is re-sent every tick, so a slice that arrived late kept nothing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-09-10 00:08:43 +02:00
co-authored by Claude Opus 5
parent 3c93684b2b
commit e2fe406445
3 changed files with 98 additions and 6 deletions
+58
View File
@@ -3,6 +3,7 @@ package cat
import (
"fmt"
"strings"
"time"
"hamlog/internal/applog"
)
@@ -65,10 +66,41 @@ func (f *Flex) SetSatellite(on bool) error {
} else {
f.send(fmt.Sprintf("slice s %d tx=1", txIdx))
}
// WAIT for the slices before saying the pair is armed.
//
// Creating a slice is asynchronous: the index comes back in a later reply.
// Returning before it arrives meant everything downstream ran against an
// uplink of -1 — no antenna, no CTCSS tone, no mode, never tuned, and never
// made the transmitter, so the radio went on transmitting on the DOWNLINK.
// Seen on the air, and it is the one failure here that can put a signal
// somewhere it must not go.
rxIdx, txIdx = f.awaitSatSlices(3 * time.Second)
if rxIdx < 0 || txIdx < 0 {
applog.Printf("flex: satellite armed but the radio did not report both slices (rx %d, tx %d) — "+
"the uplink will be picked up when it does", rxIdx, txIdx)
return nil
}
applog.Printf("flex: satellite armed (rx slice %d, tx slice %d)", rxIdx, txIdx)
return nil
}
// awaitSatSlices waits for both slice indices to be known, and returns whatever
// it has when the time is up. Polled rather than signalled: the indices arrive
// on the reader goroutine by two different routes — the create reply and the
// slice status — and a poll is indifferent to which of them got there first.
func (f *Flex) awaitSatSlices(d time.Duration) (rx, tx int) {
deadline := time.Now().Add(d)
for {
f.mu.Lock()
rx, tx = f.satRX, f.satTX
f.mu.Unlock()
if (rx >= 0 && tx >= 0) || time.Now().After(deadline) {
return rx, tx
}
time.Sleep(50 * time.Millisecond)
}
}
func (f *Flex) satDisarm() error {
f.mu.Lock()
rx, tx, created := f.satRX, f.satTX, f.satCreatedTX
@@ -118,6 +150,24 @@ func (f *Flex) adoptSatSlice(role string, idx int) {
f.mu.Unlock()
if role == "tx" {
f.send(fmt.Sprintf("slice s %d tx=1", idx))
// Everything this slice was owed while nobody knew where it was. Set
// here rather than left to the next Doppler step, because the mode, the
// antenna and the tone are all sent ONCE — the step only re-sends
// frequencies.
f.mu.Lock()
mode, ant, tone := f.satUpMode, f.satTXAnt, f.satTone
f.mu.Unlock()
if strings.TrimSpace(ant) != "" {
f.send(fmt.Sprintf("slice s %d txant=%s", idx, ant))
f.send(fmt.Sprintf("slice s %d rxant=%s", idx, ant))
}
if strings.TrimSpace(mode) != "" {
f.satMode(idx, mode, 0)
}
if tone > 0 {
f.send(fmt.Sprintf("slice s %d fm_tone_value=%.1f", idx, tone))
f.send(fmt.Sprintf("slice s %d fm_tone_mode=CTCSS_TX", idx))
}
}
applog.Printf("flex: satellite %s slice is %d", role, idx)
}
@@ -146,6 +196,11 @@ func (f *Flex) TuneSatellite(downHz, upHz int64, downMode, upMode string) error
f.send(fmt.Sprintf("slice t %d %.6f", rx, float64(downHz)/1e6))
f.satMode(rx, downMode, downHz)
}
if strings.TrimSpace(upMode) != "" {
f.mu.Lock()
f.satUpMode = upMode
f.mu.Unlock()
}
if tx >= 0 && upHz > 0 {
f.send(fmt.Sprintf("slice t %d %.6f", tx, float64(upHz)/1e6))
f.satMode(tx, upMode, upHz)
@@ -223,6 +278,8 @@ func (f *Flex) SatAntennas(rxAnt, txAnt string) error {
f.mu.Lock()
rx, tx := f.satRX, f.satTX
connected := f.conn != nil
// Remembered so a slice that is reported late still gets its antenna.
f.satRXAnt, f.satTXAnt = rxAnt, txAnt
f.mu.Unlock()
if !connected {
return fmt.Errorf("flex: not connected")
@@ -256,6 +313,7 @@ func (f *Flex) SatTone(hz float64) error {
f.mu.Lock()
tx := f.satTX
connected := f.conn != nil
f.satTone = hz
f.mu.Unlock()
if !connected {
return fmt.Errorf("flex: not connected")