fix(kenwood): split frequencies swapped on transmit

IF reports the VFO "in use". In split that is the RECEIVE VFO while receiving
and the TRANSMIT VFO while transmitting — the backend took it as the receive VFO
in both cases. Everything therefore read correctly until the PTT closed and then
reversed, which is exactly how it was reported from a TS-590 on USB.

The IF frame has carried the transmit bit all along; parseKenwoodIF already
decoded it into f.TX. It was simply never consulted here.

This is not only a display fault. FreqHz is what a QSO is LOGGED on, so a
contact worked in split went into the log on the DX's frequency instead of the
operator's — wrong in the log, wrong in every ADIF exported from it, and
invisible until someone checked a QSO by hand.

The emulated rig in the test package can now be keyed, so the case is pinned
from both sides: verified failing without the fix (tx and rx exchanged) and
passing with it.
This commit is contained in:
2026-08-11 17:28:23 +02:00
parent 91452cefc0
commit fa01968207
3 changed files with 76 additions and 7 deletions
+24 -4
View File
@@ -377,16 +377,36 @@ func (k *Kenwood) ReadState() (RigState, error) {
f.Split = split
if f.Split {
// The transmit VFO is the other one. Read it rather than assume, and fall
// back to simplex if it cannot be read: a wrong TX frequency is written
// into the log, which is worse than showing no split at all.
// The OTHER VFO is the one IF did not report. Read it rather than assume,
// and fall back to simplex if it cannot be read: a wrong TX frequency is
// written into the log, which is worse than showing no split at all.
other := "FB;"
if f.VFO == "B" {
other = "FA;"
}
var otherHz int64
if r, err := k.ask(other); err == nil {
if hz, ok := parseKenwoodFreq(r, strings.TrimSuffix(other, ";")); ok && hz > 0 && hz != rx {
tx = hz
otherHz = hz
}
}
if otherHz > 0 {
// WHICH of the two is the transmit frequency depends on whether the rig
// is transmitting RIGHT NOW.
//
// IF reports the VFO "in use", and in split that is the RECEIVE VFO on
// receive and the TRANSMIT VFO on transmit. The code took it as the
// receive VFO always, so the moment the operator keyed up the two
// frequencies swapped: correct on receive, reversed on transmit, which
// is exactly how it was reported from a TS-590 on USB.
//
// It matters beyond the display. FreqHz is what a QSO is logged on, and
// a contact made in split would have gone into the log on the DX's
// frequency instead of the operator's.
if f.TX {
tx, rx = rx, otherHz
} else {
tx = otherHz
}
}
}
+48 -1
View File
@@ -108,6 +108,9 @@ type ts2000 struct {
mode byte
onB bool
split bool
// tx models the rig KEYED. On a real Kenwood in split, IF then reports the
// TRANSMIT VFO as the one in use — which is the whole point of the test below.
tx bool
// lazyIF models a rig that answers FR/FT correctly but never fills IF's
// split bit — the behaviour reported on a Flex through its Kenwood CAT
// emulation, where the frequency reads perfectly and split never appears.
@@ -138,8 +141,12 @@ func (r *ts2000) answer(cmd string) string {
}
// The catemu layout: IF | freq(11) | step(4) | RIT(±5) | 3 | mem(2) |
// rx/tx | mode | VFO | scan | split | tone | tone#(2) | shift | ;
txByte := 0
if r.tx {
txByte = 1
}
return fmt.Sprintf("IF%011d%04d%+06d%03d%02d%01d%c%c%01d%c%01d%02d%01d;",
cur, 0, 0, 0, 0, 0, r.mode, vfoDigit, 0, split, 0, 0, 0)
cur, 0, 0, 0, 0, txByte, r.mode, vfoDigit, 0, split, 0, 0, 0)
case strings.HasPrefix(cmd, "FA") && len(cmd) > 3:
fmt.Sscanf(cmd, "FA%d;", &r.vfoA)
return ""
@@ -456,3 +463,43 @@ func TestKenwoodBusyRigKeepsTheLink(t *testing.T) {
t.Error("a rig refusing IF; forever was still reported as healthy")
}
}
// In split, the two frequencies must not swap the moment the operator keys up.
//
// IF reports the VFO "in use", and in split that is the RECEIVE VFO on receive
// and the TRANSMIT VFO on transmit. The backend took it as the receive VFO
// always, so everything read correctly until the PTT closed and then reversed —
// reported from a TS-590 on USB.
//
// It matters beyond the display: FreqHz is what a QSO is LOGGED on, so a split
// contact would have been logged on the DX's frequency instead of the operator's.
func TestKenwoodSplitDoesNotSwapOnTransmit(t *testing.T) {
rig := &ts2000{vfoA: 14200000, vfoB: 14205000, mode: '2', split: true}
k := NewKenwood("COM-TEST", 9600, "FT8")
k.dialPort = dialTo(rig)
if err := k.Connect(); err != nil {
t.Fatalf("connect: %v", err)
}
defer k.Disconnect()
// Receiving: IF reports VFO A, the receive dial.
s, err := k.ReadState()
if err != nil {
t.Fatalf("read on receive: %v", err)
}
if !s.Split || s.FreqHz != 14205000 || s.RxFreqHz != 14200000 {
t.Fatalf("on receive: split=%v tx=%d rx=%d — want tx 14205000, rx 14200000",
s.Split, s.FreqHz, s.RxFreqHz)
}
// Keyed: the rig switches to the transmit VFO and sets IF's TX byte.
rig.onB, rig.tx = true, true
s, err = k.ReadState()
if err != nil {
t.Fatalf("read on transmit: %v", err)
}
if s.FreqHz != 14205000 || s.RxFreqHz != 14200000 {
t.Errorf("on transmit: tx=%d rx=%d — the two swapped; want tx 14205000, rx 14200000",
s.FreqHz, s.RxFreqHz)
}
}