fix(cat): stop deasserting DTR/RTS on Kenwood and Yaesu

d4bfd30 fixed a Xiegu G90 behind a DE-19 that keyed on connect, and I
applied the same line-lowering to Kenwood and Yaesu with no evidence that
either needed it. It silenced them: many USB-serial interfaces will not
transmit with RTS low — hardware flow control, or an output stage the
line enables. A TS-990 on COM3 opened cleanly and answered nothing, which
reached us as "CAT stopped working after the update".

Xiegu keeps the behaviour: that is where the fault was reported, and that
backend now has an explicit setting for which line keys the rig. The CI-V
backend keeps its own, which predates all of this.

Kenwood also distinguishes a silent port from one sending data that never
answers, and quotes what arrived — "the rig is not answering" sent an
operator checking the power switch on a radio whose frames were visibly
on the wire.
This commit is contained in:
2026-08-01 12:09:11 +02:00
parent c62d992ad6
commit 3dd428d748
4 changed files with 81 additions and 32 deletions
+6 -2
View File
@@ -5,12 +5,16 @@
"en": [ "en": [
"Main tab: drag the divider between the two panes to resize them. The setting is remembered and travels with the data folder; double-click the divider to even them out.", "Main tab: drag the divider between the two panes to resize them. The setting is remembered and travels with the data folder; double-click the divider to even them out.",
"Kenwood: bytes arriving glued to the previous answer are no longer discarded, and the link starts from a clean buffer — a rig whose replies ran together could end up one answer behind and never report connected.", "Kenwood: bytes arriving glued to the previous answer are no longer discarded, and the link starts from a clean buffer — a rig whose replies ran together could end up one answer behind and never report connected.",
"The CAT log line now carries the error explaining a disconnection." "The CAT log line now carries the error explaining a disconnection.",
"Kenwood and Yaesu CAT connect again: 0.22.7 lowered the DTR and RTS lines on those ports, which stops many USB-serial interfaces from transmitting. Only Xiegu, where the problem was reported, still does it.",
"Kenwood: a port that opens but sends nothing is now reported differently from one sending data that does not answer, and the error quotes what arrived."
], ],
"fr": [ "fr": [
"Onglet Principal : faites glisser le séparateur entre les deux volets pour les redimensionner. Le réglage est mémorisé et suit le dossier de données ; double-clic pour les égaliser.", "Onglet Principal : faites glisser le séparateur entre les deux volets pour les redimensionner. Le réglage est mémorisé et suit le dossier de données ; double-clic pour les égaliser.",
"Kenwood : les octets reçus collés à la réponse précédente ne sont plus jetés, et la liaison démarre sur un tampon vide — une radio dont les réponses senchaînent pouvait rester une réponse en retard et ne jamais apparaître connectée.", "Kenwood : les octets reçus collés à la réponse précédente ne sont plus jetés, et la liaison démarre sur un tampon vide — une radio dont les réponses senchaînent pouvait rester une réponse en retard et ne jamais apparaître connectée.",
"La ligne de journal CAT indique désormais lerreur expliquant une déconnexion." "La ligne de journal CAT indique désormais lerreur expliquant une déconnexion.",
"Le CAT Kenwood et Yaesu se connecte de nouveau : la 0.22.7 abaissait les lignes DTR et RTS sur ces ports, ce qui empêche beaucoup dinterfaces USB-série d’émettre. Seul Xiegu, où le problème avait été signalé, le fait encore.",
"Kenwood : un port qui souvre sans rien émettre est désormais distingué dun port qui émet sans répondre, et lerreur cite ce qui est arrivé."
] ]
}, },
{ {
+45 -15
View File
@@ -83,6 +83,20 @@ type Kenwood struct {
// That is the "discarding \" 000000000010000000;\" while waiting for IF" // That is the "discarding \" 000000000010000000;\" while waiting for IF"
// a TS-480 reported, followed by connected=false for ever. // a TS-480 reported, followed by connected=false for ever.
rx []byte rx []byte
// heard is whatever arrived during Connect that was not a reply we wanted.
// Kept only to put it in the error message: "not answering" and "answering
// something unreadable" are different faults with different fixes.
heard string
}
// where names the link for a message, so an error does not read "COM @ 0 baud"
// after a network connect.
func (k *Kenwood) where() string {
if k.host != "" {
return k.host
}
return k.portName
} }
// NewKenwoodTCP builds a backend that reaches the rig over a socket instead of // NewKenwoodTCP builds a backend that reaches the rig over a socket instead of
@@ -140,6 +154,7 @@ func (k *Kenwood) Connect() error {
// its question for the rest of the session. // its question for the rest of the session.
k.rx = nil k.rx = nil
k.drain(250 * time.Millisecond) k.drain(250 * time.Millisecond)
k.heard = ""
answered := false answered := false
if id, err := k.ask("ID;"); err == nil && strings.HasPrefix(id, "ID") { if id, err := k.ask("ID;"); err == nil && strings.HasPrefix(id, "ID") {
@@ -159,10 +174,22 @@ func (k *Kenwood) Connect() error {
} }
if !answered { if !answered {
k.model = "" k.model = ""
if k.host != "" { if k.heard == "" && len(k.rx) > 0 {
return fmt.Errorf("kenwood: %s accepted the connection but the rig is not answering — check that the serial bridge points at the radio and that the radio is switched on", k.host) k.heard = string(k.rx) // an unterminated fragment is evidence too
} }
return fmt.Errorf("kenwood: %s opened but the rig is not answering — check that it is switched on and set to %d baud", k.portName, k.baud) // Distinguish silence from noise. "The rig is not answering" sent an
// operator checking the power switch and the baud rate on a radio that was
// visibly talking — its frames were arriving, they just did not match what
// was asked (wrong baud garbles them; an interface echoing our own
// commands back produces the same). Say which of the two it is, and quote
// what came back, because that is the fact that decides where to look.
if seen := k.heard; seen != "" {
return fmt.Errorf("kenwood: %s is sending data but no reply to ID; or IF; — got %q. Check the baud rate (set to %d here) and that nothing else is echoing the port", k.where(), seen, k.baud)
}
if k.host != "" {
return fmt.Errorf("kenwood: %s accepted the connection but the rig sent nothing — check that the serial bridge points at the radio and that the radio is switched on", k.host)
}
return fmt.Errorf("kenwood: %s opened but the rig sent nothing — check that it is switched on and set to %d baud", k.portName, k.baud)
} }
// Name what was actually connected to. A log reading "connected on @ 0 baud" // Name what was actually connected to. A log reading "connected on @ 0 baud"
// after a network connect is the kind of line that sends someone hunting a // after a network connect is the kind of line that sends someone hunting a
@@ -372,6 +399,11 @@ func (k *Kenwood) ask(cmd string) (string, error) {
return frame, nil return frame, nil
} }
debugLog.Printf("kenwood: discarding %q while waiting for %s", frame, want) debugLog.Printf("kenwood: discarding %q while waiting for %s", frame, want)
// Remember the first unexpected frame: if the whole handshake fails, this
// is what tells the operator the radio was talking after all.
if k.heard == "" {
k.heard = frame
}
} }
if !time.Now().Before(deadline) { if !time.Now().Before(deadline) {
return "", fmt.Errorf("kenwood: timeout answering %q", cmd) return "", fmt.Errorf("kenwood: timeout answering %q", cmd)
@@ -536,20 +568,18 @@ func (k *Kenwood) openPort() (serial.Port, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Deassert DTR and RTS. // The modem lines are LEFT ALONE.
// //
// Windows raises both when a serial port is opened, and a great many // They were briefly deasserted here, to stop an interface that reads them as
// interfaces read them as PTT: a Xiegu G90 behind a DE-19 goes into // PTT from keying the rig on connect. That silenced radios instead: a TS-990
// transmit the moment OpsLog connects and STAYS there — Xiegu's own // on COM3 opened fine and answered nothing, because a great many USB-serial
// documentation asks for RTS and DTR low. The same applies to an Icom with // interfaces will not transmit with RTS low — hardware flow control, or an
// USB SEND mapped to a line (which is why the CI-V backend has done this // output stage the line enables. "Opened but the rig sent nothing" was this,
// from the start) and to any rig keyed by a home-made cable. // and it arrived as "CAT stopped working after the update".
// //
// PTT on this backend is a CAT command, so neither line should ever be // The fault that change was written for was a Xiegu G90 behind a DE-19, and
// asserted here. A station keying by RTS/DTR configures that separately, // that backend now has an explicit setting for which line keys it. A rig
// on its own port. // whose PTT is a CAT command has no business touching DTR or RTS at all.
_ = p.SetDTR(false)
_ = p.SetRTS(false)
return p, nil return p, nil
} }
+25 -2
View File
@@ -265,8 +265,8 @@ func TestKenwoodSilentRigIsNotConnected(t *testing.T) {
if err == nil { if err == nil {
t.Fatal("a silent port was reported as a connected rig") t.Fatal("a silent port was reported as a connected rig")
} }
if !strings.Contains(err.Error(), "not answering") { if !strings.Contains(err.Error(), "sent nothing") {
t.Errorf("error was %q — it should say the rig is not answering", err) t.Errorf("error was %q — a silent port should be reported as silence", err)
} }
if k.model != "" { if k.model != "" {
t.Errorf("a stale model survived a failed connect: %q", k.model) t.Errorf("a stale model survived a failed connect: %q", k.model)
@@ -355,3 +355,26 @@ func TestKenwoodSplitWhenFRFTRejected(t *testing.T) {
t.Errorf("IF-reported split broke: split=%v tx=%d rx=%d", s.Split, s.FreqHz, s.RxFreqHz) t.Errorf("IF-reported split broke: split=%v tx=%d rx=%d", s.Split, s.FreqHz, s.RxFreqHz)
} }
} }
// A rig that talks but never answers what was asked must not be reported as a
// silent one.
//
// The two faults need opposite responses: silence means the radio is off, the
// wrong port, or a dead cable; noise means the baud rate is wrong or something
// is echoing the line. "The rig is not answering" sent an operator checking the
// power switch on a radio whose frames were visibly arriving.
func TestKenwoodNoisyRigIsReportedAsNoiseNotSilence(t *testing.T) {
k := NewKenwood("COM-TEST", 9600, "FT8")
k.dialPort = func() (serial.Port, error) {
return &fakeSerial{toRig: &strings.Builder{}, answer: func(cmd string) string {
return "XX9999;" // something, but never the reply to ID; or IF;
}}, nil
}
err := k.Connect()
if err == nil {
t.Fatal("a rig answering gibberish was reported as connected")
}
if !strings.Contains(err.Error(), "sending data") || !strings.Contains(err.Error(), "XX9999;") {
t.Errorf("error was %q — it should say data arrived, and quote it", err)
}
}
+5 -13
View File
@@ -150,20 +150,12 @@ func (y *Yaesu) Connect() error {
if err != nil { if err != nil {
return fmt.Errorf("yaesu: open %s @ %d baud: %w", y.portName, y.baud, err) return fmt.Errorf("yaesu: open %s @ %d baud: %w", y.portName, y.baud, err)
} }
// Deassert DTR and RTS. // The modem lines are LEFT ALONE — see the same note in kenwood.go.
// //
// Windows raises both when a serial port is opened, and a great many // Deasserting them to keep an interface from keying the rig silenced radios
// interfaces read them as PTT: a Xiegu G90 behind a DE-19 goes into // instead: many USB-serial interfaces will not transmit with RTS low. The
// transmit the moment OpsLog connects and STAYS there — Xiegu's own // Xiegu fault that change was written for is handled in that backend, which
// documentation asks for RTS and DTR low. The same applies to an Icom with // now has an explicit setting for which line keys the rig.
// USB SEND mapped to a line (which is why the CI-V backend has done this
// from the start) and to any rig keyed by a home-made cable.
//
// PTT on this backend is a CAT command, so neither line should ever be
// asserted here. A station keying by RTS/DTR configures that separately,
// on its own port.
_ = p.SetDTR(false)
_ = p.SetRTS(false)
p.SetReadTimeout(300 * time.Millisecond) p.SetReadTimeout(300 * time.Millisecond)
y.port = p y.port = p