fix(catshare): stop FT8 dial drift in shared CAT (rigctld freq echo)
A digital app (JTDX/WSJT-X) sharing OpsLog's rig in "Fake It" split follows the dial by polling get_freq. Freq() is the last polled value and lags a set_freq by a poll cycle, so right after a transmission the client read the still-shifted frequency, mistook it for a manual QSY and adopted it — the dial crept down every over and never came back. get_freq now echoes the last commanded frequency until the rig confirms it (or a short deadline passes), closing the race. Backend-agnostic, so it fixes every rig, not just Kenwood.
This commit is contained in:
@@ -59,8 +59,28 @@ 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
|
||||
@@ -193,7 +213,7 @@ func (s *Server) handle(line string) (resp string, quit bool) {
|
||||
return "", true
|
||||
|
||||
case "f", "\\get_freq":
|
||||
return fmt.Sprintf("%d\n", s.rig.Freq()), false
|
||||
return fmt.Sprintf("%d\n", s.reportedFreq()), false
|
||||
case "F", "\\set_freq":
|
||||
if len(args) < 1 {
|
||||
return rprt(-1), false
|
||||
@@ -210,6 +230,7 @@ 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":
|
||||
@@ -263,7 +284,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.rig.Freq()
|
||||
tx = s.reportedFreq()
|
||||
}
|
||||
return fmt.Sprintf("%d\n", tx), false
|
||||
case "I", "\\set_split_freq":
|
||||
@@ -279,6 +300,30 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user