fix: the FTDX10 refuses KY; — stop waiting on a status it will never send
The log settled it: the rig answers "?;" — its "unknown command" — to KY; and to MG;. So the keyer status query cannot work on this model, and every send spent four seconds waiting for an answer that was never coming. "?;" is now recognised as what it is. A command refused once is never asked again: models implement different subsets, and re-asking costs a 600 ms timeout on every slow beat for a control that will never answer — which is also why the panel felt sluggish. Without a buffer status there is still a real constraint: one KY command carries 24 characters and the rig DROPS the rest silently. So the send is now paced by how long the text takes to key, from PARIS timing at the rig's own speed. A long macro goes out complete instead of losing its tail. That leaves the question the log cannot answer: whether KY <text>; itself is accepted. If the rig also replies "?;" to it, the CAT menu's PC KEYING setting is the next suspect — but nothing in the code will be guessing at it.
This commit is contained in:
@@ -36,6 +36,7 @@ package cat
|
||||
// read as a hypothesis with a log line attached.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -101,6 +102,8 @@ type Yaesu struct {
|
||||
panel YaesuTXState
|
||||
panelCycle int
|
||||
panelLoaded bool
|
||||
// Commands this rig answered "?;" to — asked once, then never again.
|
||||
unsupported map[string]bool
|
||||
}
|
||||
|
||||
func NewYaesu(portName string, baud int, digital string) *Yaesu {
|
||||
@@ -324,6 +327,13 @@ func (y *Yaesu) ask(cmd string) (string, error) {
|
||||
}
|
||||
frame := string(buf[:i+1])
|
||||
buf = buf[i+1:]
|
||||
// "?;" is the rig saying it does not know this command. Confirmed on an
|
||||
// FTDX10, which answers it to KY; and MG;. Reporting it as such — rather
|
||||
// than discarding it and waiting out the timeout — is what lets callers
|
||||
// stop asking instead of paying 600 ms per poll for ever.
|
||||
if strings.TrimSpace(frame) == "?;" {
|
||||
return "", errYaesuUnsupported
|
||||
}
|
||||
if want == "" || strings.HasPrefix(strings.ToUpper(frame), want) {
|
||||
return frame, nil
|
||||
}
|
||||
@@ -336,6 +346,11 @@ func (y *Yaesu) ask(cmd string) (string, error) {
|
||||
return "", fmt.Errorf("yaesu: timeout answering %q", cmd)
|
||||
}
|
||||
|
||||
// errYaesuUnsupported is returned when the rig answers "?;" — it does not know
|
||||
// the command. Different models implement different subsets, and the only
|
||||
// reliable way to learn which is to ask once and remember the refusal.
|
||||
var errYaesuUnsupported = errors.New("yaesu: command not supported by this rig")
|
||||
|
||||
// cmdPrefix is the leading letters of a command — what its reply starts with.
|
||||
// "FA;" → "FA", "MD0;" → "MD", "KY;" → "KY".
|
||||
func cmdPrefix(cmd string) string {
|
||||
|
||||
@@ -18,6 +18,7 @@ package cat
|
||||
// Yaesu documents no way to clear the buffer, so see StopCW.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -62,10 +63,48 @@ func (y *Yaesu) SendCW(text string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// When the rig will not tell us how full its buffer is (an FTDX10 answers
|
||||
// "?;" to KY;), pace the next piece by how long this one takes to key.
|
||||
// Sending everything at once overruns the 24-character buffer and the rig
|
||||
// silently drops the rest.
|
||||
if len(msg) > 0 && y.cwStatusUnsupported() {
|
||||
time.Sleep(yaesuCWDuration(chunk, y.keyerWPM()))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cwStatusUnsupported reports whether this rig refused the KY status query.
|
||||
func (y *Yaesu) cwStatusUnsupported() bool {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
return y.unsupported["KY;"]
|
||||
}
|
||||
|
||||
// keyerWPM is the speed the rig is keying at, for pacing. Falls back to a
|
||||
// middling 20 wpm when the rig does not report it: too slow only wastes a
|
||||
// moment, too fast overruns the buffer.
|
||||
func (y *Yaesu) keyerWPM() int {
|
||||
y.mu.Lock()
|
||||
defer y.mu.Unlock()
|
||||
if y.panel.KeySpeed >= 4 {
|
||||
return y.panel.KeySpeed
|
||||
}
|
||||
return 20
|
||||
}
|
||||
|
||||
// yaesuCWDuration estimates how long a piece of text takes to key.
|
||||
//
|
||||
// PARIS timing: one word is 50 dit-lengths and a word is 5 characters, so a
|
||||
// character averages 10 dits and a dit is 1.2/wpm seconds.
|
||||
func yaesuCWDuration(text string, wpm int) time.Duration {
|
||||
if wpm < 4 {
|
||||
wpm = 20
|
||||
}
|
||||
ditMs := 1200.0 / float64(wpm)
|
||||
return time.Duration(float64(len(text))*10*ditMs) * time.Millisecond
|
||||
}
|
||||
|
||||
// StopCW aborts the message being sent.
|
||||
//
|
||||
// Yaesu documents no buffer-clear, so this drops the transmitter instead: TX0
|
||||
@@ -95,12 +134,25 @@ func (y *Yaesu) waitCWBuffer(within time.Duration) error {
|
||||
y.mu.Unlock()
|
||||
return fmt.Errorf("yaesu: not connected")
|
||||
}
|
||||
r, err := y.ask("KY;")
|
||||
if y.unsupported["KY;"] {
|
||||
y.mu.Unlock()
|
||||
return nil // this rig does not report buffer state — pacing covers it
|
||||
}
|
||||
r, err := y.ask("KY;")
|
||||
if err != nil {
|
||||
if errors.Is(err, errYaesuUnsupported) {
|
||||
if y.unsupported == nil {
|
||||
y.unsupported = map[string]bool{}
|
||||
}
|
||||
y.unsupported["KY;"] = true
|
||||
debugLog.Printf("yaesu cw: this rig does not answer KY; — pacing the send by keying time instead")
|
||||
} else {
|
||||
debugLog.Printf("yaesu cw: KY status query failed (%v) — sending anyway", err)
|
||||
}
|
||||
y.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
y.mu.Unlock()
|
||||
// Only an explicit "full" holds us back.
|
||||
//
|
||||
// The first version required the reply to be exactly "KY0;" at byte 2, and
|
||||
|
||||
@@ -3,6 +3,7 @@ package cat
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// What reaches the rig's keyer.
|
||||
@@ -92,3 +93,30 @@ func TestYaesuCWBufferFull(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pacing when the rig will not report its buffer.
|
||||
//
|
||||
// An FTDX10 answers "?;" to KY;, so there is nothing to wait on and the send has
|
||||
// to be spaced by how long the text takes to key — otherwise the second piece
|
||||
// overruns the 24-character buffer and the rig drops it silently.
|
||||
func TestYaesuCWDuration(t *testing.T) {
|
||||
// PARIS: at 20 wpm a dit is 60 ms and a character averages 10 dits, so 24
|
||||
// characters take about 14.4 s.
|
||||
got := yaesuCWDuration("ABCDEFGHIJKLMNOPQRSTUVWX", 20)
|
||||
if got < 13*time.Second || got > 16*time.Second {
|
||||
t.Errorf("24 chars at 20 wpm = %s, expected about 14.4s", got)
|
||||
}
|
||||
// Twice the speed, half the time.
|
||||
fast := yaesuCWDuration("ABCDEFGHIJKLMNOPQRSTUVWX", 40)
|
||||
if fast >= got || fast < got/3 {
|
||||
t.Errorf("40 wpm = %s vs 20 wpm = %s — should be about half", fast, got)
|
||||
}
|
||||
// A nonsense speed must not produce a zero wait (send everything at once) nor
|
||||
// an absurd one (the operator waiting minutes).
|
||||
if d := yaesuCWDuration("TEST", 0); d <= 0 || d > 10*time.Second {
|
||||
t.Errorf("4 chars at an unknown speed = %s, want a sane fallback", d)
|
||||
}
|
||||
if d := yaesuCWDuration("", 20); d != 0 {
|
||||
t.Errorf("empty text = %s, want 0", d)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package cat
|
||||
// rather than showing a zero that reads as "it reset itself".
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -199,8 +200,21 @@ func (y *Yaesu) readPanelSettings() {
|
||||
// this model lacks then keeps its previous value instead of dropping to zero,
|
||||
// which would look like a setting that reset itself.
|
||||
func (y *Yaesu) askNum(cmd, prefix string, digits int) (int, bool) {
|
||||
// A command this model refused once is never asked again. Models implement
|
||||
// different subsets — an FTDX10 answers "?;" to MG; — and re-asking costs a
|
||||
// 600 ms timeout on every slow beat, for a control that will never answer.
|
||||
if y.unsupported[cmd] {
|
||||
return 0, false
|
||||
}
|
||||
r, err := y.ask(cmd)
|
||||
if err != nil {
|
||||
if errors.Is(err, errYaesuUnsupported) {
|
||||
if y.unsupported == nil {
|
||||
y.unsupported = map[string]bool{}
|
||||
}
|
||||
y.unsupported[cmd] = true
|
||||
debugLog.Printf("yaesu: this rig does not support %q — not asking again", cmd)
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
if !strings.HasPrefix(r, prefix) || len(r) < len(prefix)+digits {
|
||||
|
||||
Reference in New Issue
Block a user