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:
+36
-4
@@ -13,6 +13,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"hamlog/internal/applog"
|
||||
)
|
||||
|
||||
// Flex is a native FlexRadio (SmartSDR) CAT backend. It speaks the radio's TCP
|
||||
@@ -69,10 +71,18 @@ type Flex struct {
|
||||
// Satellite pair: slice A is the downlink, slice B the uplink. -1 when not
|
||||
// armed. satCreatedTX marks an uplink slice OpsLog opened, and is the only
|
||||
// one it will close again.
|
||||
satOn bool
|
||||
satRX int
|
||||
satTX int
|
||||
satCreatedTX bool
|
||||
satOn bool
|
||||
satRX int
|
||||
satTX int
|
||||
satCreatedTX bool
|
||||
// What the uplink slice is owed, kept so it can be given to a slice that
|
||||
// turns up LATE. Arming, the antennas, the tone and the mode all happen
|
||||
// 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.
|
||||
satUpMode string
|
||||
satRXAnt string
|
||||
satTXAnt string
|
||||
satTone float64
|
||||
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)
|
||||
spotFreq map[int]int64 // spot index → Hz, so a click can report where it was (the trigger message carries only the index)
|
||||
@@ -1034,7 +1044,29 @@ func (f *Flex) handleStatus(payload string) {
|
||||
s.filterHi = atoiDefault(val, s.filterHi)
|
||||
}
|
||||
}
|
||||
// A satellite uplink slice that arrived without our hearing about it.
|
||||
//
|
||||
// satCreate correlates the "slice create" reply by sequence number, and when
|
||||
// that correlation misses, the slice exists on the radio and OpsLog does not
|
||||
// know its index. Everything then silently does nothing: the uplink is never
|
||||
// tuned, never gets its mode, never gets its antenna or its CTCSS tone, and
|
||||
// — worst — never becomes the transmitter, so the radio goes on transmitting
|
||||
// on the DOWNLINK slice. Seen on the air: slice B sitting at the 435.100000
|
||||
// it was created with, both slices in USB, and the red TX badge on the 2 m
|
||||
// downlink.
|
||||
//
|
||||
// The status message needs no correlation. If satellite mode is armed, the
|
||||
// uplink is still unknown, and a slice is in use that is not the downlink,
|
||||
// that is the slice — the radio is telling us plainly.
|
||||
adopt := -1
|
||||
if f.satOn && f.satTX < 0 && idx != f.satRX && s.inUse {
|
||||
adopt = idx
|
||||
}
|
||||
f.mu.Unlock()
|
||||
if adopt >= 0 {
|
||||
applog.Printf("flex: adopting slice %d as the satellite uplink from its status — the create reply never came back", adopt)
|
||||
f.adoptSatSlice("tx", adopt)
|
||||
}
|
||||
}
|
||||
|
||||
// defInt returns v, or def when v is zero (so sliders show sane defaults before
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user