feat(rigctld): split that actually reaches the radio, or an honest refusal

set_split_vfo and set_split_freq both answered RPRT 0 and did nothing. WSJT-X
and JTDX in "Split Operating: Rig" send exactly that pair, believed both, and
transmitted on the RECEIVE frequency — on a pileup, straight onto the DX, while
showing the operator precisely what they had asked for. A lie that leaves no
trace in any log is the worst kind of bug this program can have.

The two commands are honoured as a PAIR. Arming alone does nothing on the radio,
because WSJT-X sends the frequency second and split armed on whatever the
transmit VFO happened to hold is worse than no split at all: it transmits
somewhere the operator never chose. The request is remembered and set_split_freq
does the work.

Kenwood gains SetSplit — FB to place the dial, then FR0/FT1 to arm, in that
order for the same reason. It writes what State() already knows how to read.

Everything else REFUSES, and that is the feature, not a shortfall. Only Flex and
Icom could even toggle split before, neither could set the transmit frequency,
and Yaesu, TCI and OmniRig have nothing at all. A refusal WSJT-X can report —
and act on, by falling back to Fake It — is worth far more than a success it has
no way to check.

Both paths are pinned: split reaching the rig as one armed call with the right
frequency, and a backend that cannot do it producing an error rather than RPRT 0.
This commit is contained in:
2026-08-11 17:53:46 +02:00
parent fa01968207
commit 43095a1d89
9 changed files with 192 additions and 7 deletions
+37
View File
@@ -513,6 +513,43 @@ func isKenwoodDataMode(mode string) bool {
return true
}
// SetSplit arms or clears split, and when arming puts txHz on the transmit VFO.
//
// Both halves in one call on purpose. WSJT-X sends "split on, VFO B" and "VFO B
// to 14075300" as two commands, and honouring only the first is worse than
// honouring neither: split would arm on whatever VFO B happened to hold, so the
// operator transmits somewhere they never chose while the software reports
// exactly what they asked for. Nothing is armed here until the frequency is on
// the dial.
//
// FR selects the receive VFO, FT the transmit one — the same pair the poll loop
// already reads to detect split, so this writes what State() knows how to read.
func (k *Kenwood) SetSplit(on bool, txHz int64) error {
k.mu.Lock()
defer k.mu.Unlock()
if k.port == nil {
return fmt.Errorf("kenwood: not connected")
}
if !on {
// Transmit follows receive again. FR is left alone: which VFO the operator
// listens on is theirs to choose, and clearing split should not move them.
return k.write("FT0;")
}
if txHz <= 0 || txHz > 99_999_999_999 {
return fmt.Errorf("kenwood: split TX frequency %d out of the 11-digit CAT range", txHz)
}
// The transmit dial FIRST, then arm. Arming first would transmit on the old
// contents of VFO B for however long the next command takes to arrive — brief,
// but on the wrong frequency, and this runs the instant before a transmission.
if err := k.write(fmt.Sprintf("FB%011d;", txHz)); err != nil {
return err
}
if err := k.write("FR0;"); err != nil { // receive on A
return err
}
return k.write("FT1;") // transmit on B
}
func (k *Kenwood) SetPTT(on bool) error {
k.mu.Lock()
defer k.mu.Unlock()