fix(kenwood): keep the CAT link up while transmitting (K3 shared-CAT FT8)

A K3 (and other Kenwood-dialect rigs) answers "?;" to an IF; status poll while
it is transmitting. ask() latched that as "IF unsupported" and ReadState
returned an error, so the Manager dropped the whole CAT link mid-transmit — the
log shows connected=false / "IF rejected" the instant JTDX sent TX;. That tore
down the shared rigctld CAT the digital-mode software keys through.

Two fixes: (1) track PTT (SetPTT) and skip the wire poll while it's held —
ReadState hands back the cached last state, capped at 30 s so a missed
SetPTT(false) can't freeze it; (2) never latch IF/ID as unsupported — those are
universal on Kenwood/Elecraft, so a "?;" to them is a transient busy (mid-TX),
not a missing command.
This commit is contained in:
2026-08-06 17:49:46 +02:00
parent 4c1b4fd1a8
commit 761f554ac0
2 changed files with 36 additions and 6 deletions
+32 -4
View File
@@ -115,6 +115,17 @@ type Kenwood struct {
// nothing. Off by default: that is how this backend behaved for its whole
// life before the question came up.
lowerLines bool
// tx tracks whether we currently hold PTT (SetPTT true). A Kenwood/Elecraft rig
// answers "?;" to a status poll (IF;) WHILE TRANSMITTING; OpsLog used to read
// that as "the rig doesn't support IF;", latch it off and drop the whole CAT
// link — which tore down the shared-CAT PTT that WSJT-X / JTDX key through, so a
// K3 keyed but the logger's transmission fell apart. While PTT is held we skip
// the wire poll and hand back the last good state (lastState) instead. txAt caps
// the skip so a missed SetPTT(false) can't freeze the state for ever.
tx bool
txAt time.Time
lastState RigState
}
// SetLowerLines chooses whether DTR and RTS are deasserted on connect. Set
@@ -255,6 +266,15 @@ func (k *Kenwood) ReadState() (RigState, error) {
if k.port == nil {
return RigState{}, fmt.Errorf("kenwood: not connected")
}
// While transmitting, don't poll: the rig returns "?;" to IF; during TX, and
// treating that as a fault dropped the shared CAT link the digital-mode
// software keys through. Hand back the last known state. The 30 s cap resyncs
// if a SetPTT(false) was somehow missed, so a stuck TX can't freeze state.
if k.tx && !k.txAt.IsZero() && time.Since(k.txAt) < 30*time.Second {
s := k.lastState
s.Connected = true
return s, nil
}
raw, err := k.ask("IF;")
if err != nil {
return RigState{}, err
@@ -339,6 +359,7 @@ func (k *Kenwood) ReadState() (RigState, error) {
if s.Split {
k.curRXFreq = s.RxFreqHz
}
k.lastState = s // cache for the transmit window, where we can't poll
return s, nil
}
@@ -409,7 +430,9 @@ func (k *Kenwood) SetPTT(on bool) error {
if k.port == nil {
return fmt.Errorf("kenwood: not connected")
}
k.tx = on
if on {
k.txAt = time.Now()
return k.write("TX;")
}
return k.write("RX;")
@@ -467,10 +490,15 @@ func (k *Kenwood) ask(cmd string) (string, error) {
k.rx = k.rx[i+1:]
traceText("kenwood", "RX", frame)
if frame == "?;" {
// The rig rejected the command. Remember it so the poll loop stops
// paying a 600 ms timeout for it on every cycle.
k.unsupported[want] = true
debugLog.Printf("kenwood: this rig does not support %q — not asking again", cmd)
// IF; and ID; are universal on Kenwood/Elecraft — a "?;" to them is a
// transient "busy" (typically mid-transmit, or a menu open on the rig),
// NOT "unsupported". Latching them off would blind the poll loop for
// good and read as "lost the rig". Only remember the OPTIONAL commands
// (FR/FT/…) so the poll loop stops paying a 600 ms timeout for those.
if want != "IF" && want != "ID" {
k.unsupported[want] = true
debugLog.Printf("kenwood: this rig does not support %q — not asking again", cmd)
}
return "", fmt.Errorf("kenwood: %s rejected", want)
}
if strings.HasPrefix(frame, want) {