fix(rigctld): stop refusing two commands JTDX sends in Fake It

From a user's log, repeating throughout the session:

	rigctld: split off failed: cat: this radio's backend cannot set split
	rigctld: unimplemented command "X PKTUSB -1"

Both are answered with an error, and JTDX treats a rig-control error as fatal:
it abandoned a transmission 0.86 seconds into a 13.8-second frame.

"S 0" — split OFF. Fake It uses no split, and JTDX still sends this to be sure.
A backend that cannot SET split was refusing a request that was ALREADY
satisfied. It now succeeds when the rig is simplex, without touching the rig at
all. Arming still fails loudly: there the request is real and unmet, and a
client that believes it transmits up the band while the radio sits on the DX's
frequency is exactly what that refusal exists to prevent.

"X <mode>" — set_split_mode. Sent during ordinary setup, Fake It included,
where there is no second VFO to give a mode to. Accepted now: applied when a
split is in force, and a plain success otherwise, because the transmit VFO
already has that mode — it is the same VFO. get_split_mode answers to match.

Both regressions fail without the fix with the exact codes from that log,
RPRT -9 and RPRT -11.
This commit is contained in:
2026-08-13 15:57:58 +02:00
parent d44a971acf
commit 4e2a5877d6
4 changed files with 95 additions and 18904 deletions
-18902
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -4,11 +4,13 @@
"date": "",
"en": [
"Amplifiers: tick the ones sharing a combiner and ON, OFF and OPERATE act on all of them at once. Each keeps its own meters.",
"Shared CAT: with the wire trace on, every command a client sends and the answer given are logged."
"Shared CAT: with the wire trace on, every command a client sends and the answer given are logged.",
"Shared CAT: JTDX and WSJT-X no longer get an error for turning split off on a rig that has none, or for setting the transmit mode — which made JTDX abandon a transmission."
],
"fr": [
"Amplificateurs : coche ceux qui partagent un combiner et ON, OFF et OPERATE agissent sur tous à la fois. Chacun garde ses mesures.",
"CAT partagé : avec la trace activée, chaque commande envoyée par un client et la réponse donnée sont journalisées."
"CAT partagé : avec la trace activée, chaque commande envoyée par un client et la réponse donnée sont journalisées.",
"CAT partagé : JTDX et WSJT-X ne reçoivent plus d erreur en désactivant un split inexistant ni en réglant le mode d émission — ce qui faisait abandonner une émission à JTDX."
]
},
{
+39
View File
@@ -421,6 +421,17 @@ func (s *Server) handle(line string) (resp string, quit bool) {
return rprt(0), false
}
s.splitWanted.Store(false)
// Already simplex? Then there is nothing to do and the request is
// satisfied. Reporting a failure here is what broke JTDX in "Fake It":
// Fake It uses no split, JTDX still sends "S 0" to be sure, and a backend
// that cannot SET split answered an error to a request that was already
// true. JTDX read that as rig control failing and abandoned the
// transmission a second into the frame.
//
// A refusal is only honest when something actually needed doing.
if on, _ := s.rig.Split(); !on {
return rprt(0), false
}
if err := s.rig.SetSplit(false, 0); err != nil {
s.log("rigctld: split off failed: %v", err)
return rprt(-9), false
@@ -450,6 +461,34 @@ func (s *Server) handle(line string) (resp string, quit bool) {
s.log("rigctld: split ON, TX %.0f Hz", hz)
return rprt(0), false
case "X", "\\set_split_mode":
// "X <mode> <passband>" — the mode of the TRANSMIT VFO.
//
// Accepted rather than refused. WSJT-X and JTDX send it as part of their
// normal setup even in "Fake It", where there is no split and therefore no
// second VFO to give a mode to; answering "not implemented" made JTDX give
// up on rig control mid-transmission.
//
// The mode is applied when there IS a split — the transmit VFO is a real
// one then. Without split the request has no target and succeeding is the
// honest answer: the transmit VFO already has that mode, because it is the
// same VFO.
if len(args) < 1 {
return rprt(-1), false
}
if on, _ := s.rig.Split(); !on {
return rprt(0), false
}
if err := s.rig.SetMode(args[0]); err != nil {
s.log("rigctld: split mode %q failed: %v", args[0], err)
return rprt(-9), false
}
return rprt(0), false
case "x", "\\get_split_mode":
// Mirrors X: the transmit VFO's mode and passband. Without split that is
// simply the current mode.
return s.rig.Mode() + "\n2400\n", false
default:
// A frame ending in ';' is not a rigctl command at all — it is raw rig
// dialect (Kenwood/Elecraft/Yaesu), which means the client is configured
+52
View File
@@ -50,3 +50,55 @@ func TestSetSplitRefusalIsReported(t *testing.T) {
t.Errorf("a rig that cannot split answered %q — the client will transmit on the wrong frequency", got)
}
}
// "Fake It" uses NO split, and JTDX still sends "S 0" to make sure. A backend
// that cannot SET split used to answer an error to that — a refusal of a
// request that was already satisfied — and JTDX read it as rig control failing
// and abandoned the transmission a second into a 13.8-second frame.
//
// Reported against a Yaesu whose backend cannot arm split from software.
func TestSplitOffOnASimplexRigSucceeds(t *testing.T) {
rig := &fakeRig{freq: 14074000, mode: "FT8", noSplit: true}
s := New(0, rig, func(string, ...any) {})
if got, _ := s.handle("S 0 VFOA"); !strings.HasPrefix(got, "RPRT 0") {
t.Errorf("split off on a simplex rig answered %q — nothing needed doing", got)
}
// And it must not have bothered the rig at all.
if len(rig.splitCalls) != 0 {
t.Errorf("rig was asked to clear a split it did not have: %v", rig.splitCalls)
}
// Arming, however, must still fail loudly: there the request is real and
// unmet, and a client that believes it is transmitting up the band while the
// radio sits on the DX's frequency is the bug this refusal exists to prevent.
if got, _ := s.handle("S 1 VFOB"); !strings.HasPrefix(got, "RPRT 0") {
t.Fatalf("arming is deferred to set_split_freq: %q", got)
}
if got, _ := s.handle("I 14075300.000000"); strings.HasPrefix(got, "RPRT 0") {
t.Error("arming split on a backend that cannot must report a failure")
}
}
// X (set_split_mode) is sent by WSJT-X and JTDX during ordinary setup, Fake It
// included — where there is no second VFO to give a mode to. Answering "not
// implemented" made JTDX give up on rig control mid-transmission.
func TestSplitModeIsAccepted(t *testing.T) {
rig := &fakeRig{freq: 14074000, mode: "FT8"}
s := New(0, rig, func(string, ...any) {})
// No split: nothing to target, and succeeding is honest — the transmit VFO
// already has that mode because it is the same VFO.
if got, _ := s.handle("X PKTUSB -1"); !strings.HasPrefix(got, "RPRT 0") {
t.Errorf("set_split_mode answered %q", got)
}
if rig.mode != "FT8" {
t.Errorf("the mode was changed with no split in force: %q", rig.mode)
}
// Reading it back must answer a mode and a passband, never an error.
got, _ := s.handle("x")
if !strings.Contains(got, "FT8") {
t.Errorf("get_split_mode = %q, want the current mode", got)
}
}