fix(catshare): revert FT8 freq echo — it blocked JTDX transmit
The optimistic get_freq echo added in 0.23.5 (to stop the "Fake It" dial creep) stopped JTDX/WSJT-X from going into transmit: the client reads frequency back during its TX sequence, and echoing the commanded value instead of the rig's live value interfered with it. No-TX is far worse than a 0.5 kHz drift, so revert to reporting the rig's live frequency. get_freq/get_split_freq return s.rig.Freq() again; the echo state, noteSetFreq, reportedFreq and the drift test are removed.
This commit is contained in:
+4
-2
@@ -5,12 +5,14 @@
|
||||
"en": [
|
||||
"Log viewer: the window now keeps twice as much history (512 KB instead of 256 KB, ~3200 lines). During a busy trace the oldest lines used to scroll out of the buffer while you were still reading them; the larger window holds them.",
|
||||
"Ultrabeam over a remote link: fixed intermittent connection drops, phantom frequency jumps and wrong element-length readings. On a slow link a command that timed out left its late reply in the stream, and the next command read it as its own — crossing a status query with an element-length one, for instance. The stream is now flushed of any stale reply before each command, keeping the exchange in step.",
|
||||
"Ultrabeam: the 'moving' indicator and the Flex TX-inhibit now react the instant you click a band or pattern, instead of up to a status poll (~2 s) later. A commanded move reports motion immediately; the real motor state takes over once the next poll reads it."
|
||||
"Ultrabeam: the 'moving' indicator and the Flex TX-inhibit now react the instant you click a band or pattern, instead of up to a status poll (~2 s) later. A commanded move reports motion immediately; the real motor state takes over once the next poll reads it.",
|
||||
"CAT sharing (FT8): reverted the 0.23.5 'Fake It' drift tweak — it stopped JTDX/WSJT-X from going into transmit. Shared frequency reads report the rig's live value again. The small dial creep in 'Fake It' split can return; switching JTDX to 'Split Operation: None' (or Rig) avoids it."
|
||||
],
|
||||
"fr": [
|
||||
"Visionneuse de log : la fenêtre conserve deux fois plus d'historique (512 Ko au lieu de 256 Ko, ~3200 lignes). Lors d'une trace chargée, les plus vieilles lignes défilaient hors du buffer pendant qu'on les lisait encore ; la fenêtre agrandie les garde.",
|
||||
"Ultrabeam en remote : coupures de connexion intermittentes, sauts de fréquence fantômes et longueurs d'éléments erronées corrigés. Sur un lien lent, une commande qui expirait laissait sa réponse tardive dans le flux, et la commande suivante la lisait comme la sienne — croisant par exemple une requête de statut avec une requête de longueurs d'éléments. Le flux est désormais vidé de toute réponse périmée avant chaque commande, gardant l'échange synchronisé.",
|
||||
"Ultrabeam : l'indicateur « en mouvement » et l'inhibition d'émission Flex réagissent désormais dès que vous cliquez sur une bande ou un diagramme, au lieu d'attendre jusqu'à un poll de statut (~2 s). Un mouvement commandé signale le déplacement immédiatement ; l'état réel des moteurs prend le relais dès le poll suivant."
|
||||
"Ultrabeam : l'indicateur « en mouvement » et l'inhibition d'émission Flex réagissent désormais dès que vous cliquez sur une bande ou un diagramme, au lieu d'attendre jusqu'à un poll de statut (~2 s). Un mouvement commandé signale le déplacement immédiatement ; l'état réel des moteurs prend le relais dès le poll suivant.",
|
||||
"Partage CAT (FT8) : annulation de l'ajustement anti-drift « Fake It » de la 0.23.5 — il empêchait JTDX/WSJT-X de passer en émission. Les lectures de fréquence partagée renvoient de nouveau la valeur live de la radio. Le léger glissement du VFO en split « Fake It » peut réapparaître ; passer JTDX en « Split Operation : None » (ou Rig) l'évite."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -59,28 +59,8 @@ type Server struct {
|
||||
ln net.Listener
|
||||
conns map[net.Conn]struct{}
|
||||
closed bool
|
||||
|
||||
// Optimistic frequency echo. A sharing client that follows our dial by
|
||||
// polling get_freq (WSJT-X / JTDX in "Fake It" split) shifts the frequency on
|
||||
// TX and restores it on RX. Freq() is the last value POLLED from the rig, and
|
||||
// it lags a set_freq by up to a poll cycle (~100-200 ms). In that window the
|
||||
// client — no longer transmitting — reads back the still-shifted frequency,
|
||||
// mistakes it for a manual QSY and adopts it, so every over creeps the dial by
|
||||
// the shift amount and it never comes back. Echoing the last commanded
|
||||
// frequency until the rig confirms it (or a short deadline passes) closes the
|
||||
// window: the client always reads exactly what it just set.
|
||||
echoMu sync.Mutex
|
||||
echoHz int64
|
||||
echoAt time.Time
|
||||
echoWant bool
|
||||
}
|
||||
|
||||
// freqEchoTTL caps how long a commanded frequency is echoed when the rig never
|
||||
// reports it back (e.g. it rounded to a coarser step). Long enough to cover a
|
||||
// poll cycle with margin, short enough that a genuine knob turn during the
|
||||
// window surfaces quickly.
|
||||
const freqEchoTTL = 2 * time.Second
|
||||
|
||||
func New(port int, rig Rig, logf func(string, ...any)) *Server {
|
||||
if port <= 0 || port > 65535 {
|
||||
port = 4532 // the rigctld default every client pre-fills
|
||||
@@ -213,7 +193,7 @@ func (s *Server) handle(line string) (resp string, quit bool) {
|
||||
return "", true
|
||||
|
||||
case "f", "\\get_freq":
|
||||
return fmt.Sprintf("%d\n", s.reportedFreq()), false
|
||||
return fmt.Sprintf("%d\n", s.rig.Freq()), false
|
||||
case "F", "\\set_freq":
|
||||
if len(args) < 1 {
|
||||
return rprt(-1), false
|
||||
@@ -230,7 +210,6 @@ func (s *Server) handle(line string) (resp string, quit bool) {
|
||||
s.log("rigctld: set_freq %d failed: %v", hz, err)
|
||||
return rprt(-9), false
|
||||
}
|
||||
s.noteSetFreq(hz)
|
||||
return rprt(0), false
|
||||
|
||||
case "m", "\\get_mode":
|
||||
@@ -284,7 +263,7 @@ func (s *Server) handle(line string) (resp string, quit bool) {
|
||||
case "i", "\\get_split_freq":
|
||||
_, tx := s.rig.Split()
|
||||
if tx <= 0 {
|
||||
tx = s.reportedFreq()
|
||||
tx = s.rig.Freq()
|
||||
}
|
||||
return fmt.Sprintf("%d\n", tx), false
|
||||
case "I", "\\set_split_freq":
|
||||
@@ -300,30 +279,6 @@ func (s *Server) handle(line string) (resp string, quit bool) {
|
||||
|
||||
func rprt(code int) string { return fmt.Sprintf("RPRT %d\n", code) }
|
||||
|
||||
// noteSetFreq records a frequency a client just commanded, so the next reads
|
||||
// echo it back until the rig confirms the tune.
|
||||
func (s *Server) noteSetFreq(hz int64) {
|
||||
s.echoMu.Lock()
|
||||
s.echoHz, s.echoAt, s.echoWant = hz, time.Now(), true
|
||||
s.echoMu.Unlock()
|
||||
}
|
||||
|
||||
// reportedFreq is what get_freq answers: the last commanded frequency while the
|
||||
// rig is still catching up to it, otherwise the live polled value. See echoWant.
|
||||
func (s *Server) reportedFreq() int64 {
|
||||
live := s.rig.Freq()
|
||||
s.echoMu.Lock()
|
||||
defer s.echoMu.Unlock()
|
||||
if s.echoWant {
|
||||
if live == s.echoHz || time.Since(s.echoAt) > freqEchoTTL {
|
||||
s.echoWant = false // rig confirmed the tune, or we waited long enough
|
||||
return live
|
||||
}
|
||||
return s.echoHz
|
||||
}
|
||||
return live
|
||||
}
|
||||
|
||||
// stripVFOArg drops a leading VFO name from a command's arguments.
|
||||
//
|
||||
// Hamlib has two dialects. In the plain one a client sends "F 14074000"; in VFO
|
||||
|
||||
@@ -20,7 +20,6 @@ type fakeRig struct {
|
||||
setFreqs []int64
|
||||
setModes []string
|
||||
failSet bool
|
||||
lagSet bool // record the command but do not move freq — simulate poll lag
|
||||
}
|
||||
|
||||
func (f *fakeRig) Freq() int64 { f.mu.Lock(); defer f.mu.Unlock(); return f.freq }
|
||||
@@ -32,10 +31,8 @@ func (f *fakeRig) SetFreq(hz int64) error {
|
||||
if f.failSet {
|
||||
return fmt.Errorf("rig refused")
|
||||
}
|
||||
f.setFreqs = append(f.setFreqs, hz)
|
||||
if !f.lagSet {
|
||||
f.freq = hz
|
||||
}
|
||||
f.setFreqs = append(f.setFreqs, hz)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeRig) SetMode(m string) error {
|
||||
@@ -141,60 +138,6 @@ func TestHandleReportsBackendFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The FT8 "Fake It" drift. JTDX/WSJT-X shift the dial down on TX and restore it
|
||||
// on RX, and they follow the dial by polling get_freq. Our Freq() is the last
|
||||
// POLLED value and lags a set_freq by a poll cycle, so right after the restore
|
||||
// the client used to read the still-shifted frequency, take it for a manual QSY
|
||||
// and adopt it — the dial crept down every over and never came back. get_freq
|
||||
// now echoes the last commanded frequency until the rig confirms it.
|
||||
func TestFakeItSplitDoesNotDriftTheDial(t *testing.T) {
|
||||
// lagSet: SetFreq only records the command; we drive the poll catch-up by hand
|
||||
// to land inside the exact window the drift lived in.
|
||||
rig := &fakeRig{freq: 14074000, lagSet: true}
|
||||
s := New(0, rig, nil)
|
||||
|
||||
if got, _ := s.handle("f"); got != "14074000\n" {
|
||||
t.Fatalf("baseline get_freq = %q, want 14074000", got)
|
||||
}
|
||||
setFreq := func(hz int64) { rig.mu.Lock(); rig.freq = hz; rig.mu.Unlock() }
|
||||
|
||||
for cycle := 0; cycle < 5; cycle++ {
|
||||
// TX: the client shifts the dial down for the over.
|
||||
if got, _ := s.handle("F 14073500"); got != "RPRT 0\n" {
|
||||
t.Fatalf("cycle %d TX set_freq = %q", cycle, got)
|
||||
}
|
||||
// Mid-TX, before the rig reports the move, the client reads back exactly
|
||||
// what it commanded — not the stale 14074000.
|
||||
if got, _ := s.handle("f"); got != "14073500\n" {
|
||||
t.Fatalf("cycle %d TX get_freq = %q, want commanded 14073500", cycle, got)
|
||||
}
|
||||
setFreq(14073500) // poll catches up to the shifted dial
|
||||
|
||||
// RX: the client restores the dial.
|
||||
if got, _ := s.handle("F 14074000"); got != "RPRT 0\n" {
|
||||
t.Fatalf("cycle %d RX set_freq = %q", cycle, got)
|
||||
}
|
||||
// The rig has not yet reported the restore (still 14073500). Before the fix
|
||||
// this returned 14073500 and JTDX adopted it — the cumulative drift. Now it
|
||||
// echoes the restore, so the dial holds.
|
||||
if got, _ := s.handle("f"); got != "14074000\n" {
|
||||
t.Fatalf("cycle %d RX get_freq = %q, want restored 14074000 — dial drifted", cycle, got)
|
||||
}
|
||||
setFreq(14074000) // poll catches up to the restored dial
|
||||
}
|
||||
|
||||
// Once a poll confirms the commanded dial, the echo is released (in the field
|
||||
// the client polls continuously, so this happens within a cycle).
|
||||
if got, _ := s.handle("f"); got != "14074000\n" {
|
||||
t.Fatalf("confirming get_freq = %q, want 14074000", got)
|
||||
}
|
||||
// A genuine knob turn now surfaces at once.
|
||||
setFreq(14075000)
|
||||
if got, _ := s.handle("f"); got != "14075000\n" {
|
||||
t.Errorf("manual QSY get_freq = %q, want 14075000 — echo hid a real move", got)
|
||||
}
|
||||
}
|
||||
|
||||
// dump_state is parsed POSITIONALLY by Hamlib clients: WSJT-X reads the first
|
||||
// line as the protocol version and refuses to continue if the block is short or
|
||||
// misshapen. Pinning its shape is what stops a well-meaning edit from silently
|
||||
|
||||
Reference in New Issue
Block a user