fix(catshare): echo PTT state so JTDX doesn't drop transmit

JTDX polls get_ptt DURING transmit to confirm the rig is keyed (WSJT-X does not,
which is why it worked and JTDX cut after ~2 s). We answered a blanket "0" (RX),
so JTDX concluded PTT had failed and dropped the over. get_ptt now echoes the
last PTT state commanded via set_ptt — always consistent with the client's own
command. Trace showed T 1 → three t→"0" polls → T 0.
This commit is contained in:
2026-08-05 09:55:27 +02:00
parent b9ea077a64
commit 9f5c482667
3 changed files with 39 additions and 6 deletions
+15 -4
View File
@@ -36,6 +36,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
)
@@ -59,6 +60,13 @@ type Server struct {
ln net.Listener
conns map[net.Conn]struct{}
closed bool
// ptt mirrors the last PTT state a client commanded via set_ptt. WSJT-X/JTDX
// poll get_ptt DURING transmit to confirm the rig is keyed; if get_ptt reads
// RX they conclude PTT failed and abort the over after a second or two. We
// don't read PTT back from every backend, so echo what the client last set —
// always consistent with its own command, and enough to satisfy the check.
ptt atomic.Bool
}
func New(port int, rig Rig, logf func(string, ...any)) *Server {
@@ -228,10 +236,12 @@ func (s *Server) handle(line string) (resp string, quit bool) {
return rprt(0), false
case "t", "\\get_ptt":
// We do not read PTT back from every backend, and answering "transmitting"
// wrongly would make a client hold off for ever. Reporting RX is the safe
// direction: the worst case is a client that transmits when we said it
// could, which is what it was going to do anyway.
// Echo the last commanded PTT state. WSJT-X/JTDX poll this WHILE
// transmitting to confirm the rig is keyed; answering a blanket "0" (RX)
// made them decide PTT had failed and abort the over after ~1-2 s.
if s.ptt.Load() {
return "1\n", false
}
return "0\n", false
case "T", "\\set_ptt":
if len(args) < 1 {
@@ -242,6 +252,7 @@ func (s *Server) handle(line string) (resp string, quit bool) {
s.log("rigctld: set_ptt %v failed: %v", on, err)
return rprt(-9), false
}
s.ptt.Store(on)
return rprt(0), false
case "v", "\\get_vfo":
+22
View File
@@ -138,6 +138,28 @@ func TestHandleReportsBackendFailure(t *testing.T) {
}
}
// get_ptt must echo the last commanded PTT state. WSJT-X/JTDX poll get_ptt while
// transmitting to confirm the rig is keyed; a blanket "0" made them decide PTT
// had failed and abort the over after a second or two.
func TestGetPTTEchoesSetPTT(t *testing.T) {
s := New(0, &fakeRig{}, nil)
if got, _ := s.handle("t"); got != "0\n" {
t.Fatalf("initial get_ptt = %q, want 0", got)
}
if got, _ := s.handle("T 1"); got != "RPRT 0\n" {
t.Fatalf("set_ptt 1 = %q, want RPRT 0", got)
}
if got, _ := s.handle("t"); got != "1\n" {
t.Fatalf("get_ptt after T 1 = %q, want 1 — client would abort TX", got)
}
if got, _ := s.handle("T 0"); got != "RPRT 0\n" {
t.Fatalf("set_ptt 0 = %q, want RPRT 0", got)
}
if got, _ := s.handle("t"); got != "0\n" {
t.Fatalf("get_ptt after T 0 = %q, want 0", 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