fix(kenwood): record a frequency set made while transmitting
WSJT-X "Fake It" shifts the dial for the duration of each over and puts it back afterwards, but the restore is conditional: it reads the frequency back and only moves the dial if it disagrees with where it believes the radio should be. That read lands inside the window where this backend stops polling on purpose. A Kenwood answers "?;" to IF; while it is transmitting, and treating that as a fault used to drop the shared CAT link entirely, so the cached state answers instead. SetFrequency wrote the command to the rig without touching that cache — so mid-over the cache still described the pre-over dial, WSJT-X read its own receive frequency back, concluded there was nothing to restore, and the radio stayed on the transmit frequency. Every later over started from there. Reported from a session where the dial stuck at 7075500 after a full FT8 over while a bare TUNE, which never sets a frequency, worked fine. Only simplex updates the cache. Under split, FreqHz is the transmit frequency while the write lands on whichever VFO the operator is on, and guessing which side moved would put a wrong number in front of the operator — a stale one survives until the next poll. The test reproduces the reported sequence and fails without the fix with the same frequency the log shows.
This commit is contained in:
+25
-1
@@ -440,7 +440,31 @@ func (k *Kenwood) SetFrequency(hz int64) error {
|
||||
if k.curVFO == "B" {
|
||||
cmd = "FB"
|
||||
}
|
||||
return k.write(fmt.Sprintf("%s%011d;", cmd, hz))
|
||||
if err := k.write(fmt.Sprintf("%s%011d;", cmd, hz)); err != nil {
|
||||
return err
|
||||
}
|
||||
// Remember what we just commanded.
|
||||
//
|
||||
// While PTT is held the poll is skipped and State() hands back lastState — the
|
||||
// rig answers "?;" to IF; mid-transmission, and reading that as a fault used
|
||||
// to drop the whole link. But a frequency SET during that window then went
|
||||
// unrecorded, so the cache kept describing the dial as it was before.
|
||||
//
|
||||
// WSJT-X's "Fake It" is exactly that sequence: move the dial, key, transmit,
|
||||
// and afterwards put it back. Polling during the over, it was told the rig was
|
||||
// still on the receive frequency — so there was nothing to put back, and the
|
||||
// dial stayed on the transmit frequency for good. Every following over
|
||||
// started from there, which is the drift that was reported.
|
||||
//
|
||||
// Only simplex is updated here. Under split, FreqHz means the transmit
|
||||
// frequency while this write lands on whichever VFO the operator is on, and
|
||||
// guessing which side moved would be worse than a stale value the next poll
|
||||
// corrects on its own.
|
||||
if !k.lastState.Split {
|
||||
k.curFreq = hz
|
||||
k.lastState.FreqHz = hz
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *Kenwood) SetMode(mode string) error {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package cat
|
||||
|
||||
import "testing"
|
||||
|
||||
// WSJT-X "Fake It" against the transmit-window cache.
|
||||
//
|
||||
// Fake It keeps the radio on one dial frequency and shifts it only for the
|
||||
// duration of each over: set the transmit frequency, key, transmit, unkey, set
|
||||
// it back. The restore is not unconditional — WSJT-X reads the frequency back
|
||||
// and puts the dial where it believes it should be.
|
||||
//
|
||||
// That read lands inside the window where this backend deliberately stops
|
||||
// polling, because a Kenwood answers "?;" to IF; while it is transmitting and
|
||||
// treating that as a fault used to drop the whole shared link. The cache
|
||||
// answers instead. So the cache has to account for frequency SETS made during
|
||||
// the window, or it describes the dial as it was before the over — and WSJT-X,
|
||||
// told the radio is already on the receive frequency, has nothing to restore.
|
||||
//
|
||||
// This reproduces the sequence from a reported session: the dial stayed on the
|
||||
// transmit frequency after the first over and every later one started there.
|
||||
func TestKenwoodFakeItRestoresAfterTransmit(t *testing.T) {
|
||||
const (
|
||||
rxHz = 7074000 // where the operator is listening
|
||||
txHz = 7075500 // where Fake It moves the dial to transmit
|
||||
)
|
||||
|
||||
rig := &ts2000{vfoA: rxHz, mode: '2'}
|
||||
k := NewKenwood("COM-TEST", 9600, "FT8")
|
||||
k.dialPort = dialTo(rig)
|
||||
if err := k.Connect(); err != nil {
|
||||
t.Fatalf("connect: %v", err)
|
||||
}
|
||||
defer k.Disconnect()
|
||||
|
||||
if s, err := k.ReadState(); err != nil || s.FreqHz != rxHz {
|
||||
t.Fatalf("before the over: %d (err %v) — want %d", s.FreqHz, err, rxHz)
|
||||
}
|
||||
|
||||
// The over: shift the dial, then key.
|
||||
if err := k.SetFrequency(txHz); err != nil {
|
||||
t.Fatalf("set transmit frequency: %v", err)
|
||||
}
|
||||
if err := k.SetPTT(true); err != nil {
|
||||
t.Fatalf("ptt on: %v", err)
|
||||
}
|
||||
|
||||
// WSJT-X reads back mid-over. The wire is not polled here — this is the
|
||||
// cache talking, and it must not still be saying rxHz.
|
||||
s, err := k.ReadState()
|
||||
if err != nil {
|
||||
t.Fatalf("read during the over: %v", err)
|
||||
}
|
||||
if s.FreqHz != txHz {
|
||||
t.Errorf("during the over the backend reported %d, want %d — "+
|
||||
"reporting the pre-over frequency is what stops Fake It restoring the dial", s.FreqHz, txHz)
|
||||
}
|
||||
|
||||
if err := k.SetPTT(false); err != nil {
|
||||
t.Fatalf("ptt off: %v", err)
|
||||
}
|
||||
|
||||
// The restore, once the over is done.
|
||||
if err := k.SetFrequency(rxHz); err != nil {
|
||||
t.Fatalf("restore: %v", err)
|
||||
}
|
||||
if rig.vfoA != rxHz {
|
||||
t.Errorf("dial left on %d after the over, want %d", rig.vfoA, rxHz)
|
||||
}
|
||||
if s, err := k.ReadState(); err != nil || s.FreqHz != rxHz {
|
||||
t.Errorf("after the over: %d (err %v) — want %d", s.FreqHz, err, rxHz)
|
||||
}
|
||||
}
|
||||
|
||||
// Under split the same write must NOT touch the cache: FreqHz means the
|
||||
// transmit frequency while the write lands on whichever VFO the operator is on,
|
||||
// so guessing which side moved would put a wrong number in front of the
|
||||
// operator. A stale one survives only until the next poll.
|
||||
func TestKenwoodSplitCacheLeftToThePoll(t *testing.T) {
|
||||
rig := &ts2000{vfoA: 14025000, vfoB: 14030000, mode: '3', split: true}
|
||||
k := NewKenwood("COM-TEST", 9600, "CW")
|
||||
k.dialPort = dialTo(rig)
|
||||
if err := k.Connect(); err != nil {
|
||||
t.Fatalf("connect: %v", err)
|
||||
}
|
||||
defer k.Disconnect()
|
||||
|
||||
s, err := k.ReadState()
|
||||
if err != nil || !s.Split {
|
||||
t.Fatalf("split not seen: %+v (err %v)", s, err)
|
||||
}
|
||||
before := s.FreqHz
|
||||
|
||||
if err := k.SetFrequency(14026000); err != nil {
|
||||
t.Fatalf("set: %v", err)
|
||||
}
|
||||
if k.lastState.FreqHz != before {
|
||||
t.Errorf("split cache moved to %d on a VFO write, want it left at %d for the poll",
|
||||
k.lastState.FreqHz, before)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user