diff --git a/changelog.json b/changelog.json index 0908cd7..c1a537a 100644 --- a/changelog.json +++ b/changelog.json @@ -5,12 +5,16 @@ "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.", "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": [ "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 s’enchaînent pouvait rester une réponse en retard et ne jamais apparaître connectée.", - "La ligne de journal CAT indique désormais l’erreur expliquant une déconnexion." + "La ligne de journal CAT indique désormais l’erreur 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 d’interfaces USB-série d’émettre. Seul Xiegu, où le problème avait été signalé, le fait encore.", + "Kenwood : un port qui s’ouvre sans rien émettre est désormais distingué d’un port qui émet sans répondre, et l’erreur cite ce qui est arrivé." ] }, { diff --git a/internal/cat/kenwood.go b/internal/cat/kenwood.go index d4a4804..feb17f1 100644 --- a/internal/cat/kenwood.go +++ b/internal/cat/kenwood.go @@ -83,6 +83,20 @@ type Kenwood struct { // That is the "discarding \" 000000000010000000;\" while waiting for IF" // a TS-480 reported, followed by connected=false for ever. 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 @@ -140,6 +154,7 @@ func (k *Kenwood) Connect() error { // its question for the rest of the session. k.rx = nil k.drain(250 * time.Millisecond) + k.heard = "" answered := false if id, err := k.ask("ID;"); err == nil && strings.HasPrefix(id, "ID") { @@ -159,10 +174,22 @@ func (k *Kenwood) Connect() error { } if !answered { k.model = "" - if k.host != "" { - 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) + if k.heard == "" && len(k.rx) > 0 { + 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" // 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 } 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) { return "", fmt.Errorf("kenwood: timeout answering %q", cmd) @@ -536,20 +568,18 @@ func (k *Kenwood) openPort() (serial.Port, error) { if err != nil { 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 - // interfaces read them as PTT: a Xiegu G90 behind a DE-19 goes into - // transmit the moment OpsLog connects and STAYS there — Xiegu's own - // documentation asks for RTS and DTR low. The same applies to an Icom with - // 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. + // They were briefly deasserted here, to stop an interface that reads them as + // PTT from keying the rig on connect. That silenced radios instead: a TS-990 + // on COM3 opened fine and answered nothing, because a great many USB-serial + // interfaces will not transmit with RTS low — hardware flow control, or an + // output stage the line enables. "Opened but the rig sent nothing" was this, + // and it arrived as "CAT stopped working after the update". // - // 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) + // The fault that change was written for was a Xiegu G90 behind a DE-19, and + // that backend now has an explicit setting for which line keys it. A rig + // whose PTT is a CAT command has no business touching DTR or RTS at all. return p, nil } diff --git a/internal/cat/kenwood_loopback_test.go b/internal/cat/kenwood_loopback_test.go index 133d8d9..a6e8a02 100644 --- a/internal/cat/kenwood_loopback_test.go +++ b/internal/cat/kenwood_loopback_test.go @@ -265,8 +265,8 @@ func TestKenwoodSilentRigIsNotConnected(t *testing.T) { if err == nil { t.Fatal("a silent port was reported as a connected rig") } - if !strings.Contains(err.Error(), "not answering") { - t.Errorf("error was %q — it should say the rig is not answering", err) + if !strings.Contains(err.Error(), "sent nothing") { + t.Errorf("error was %q — a silent port should be reported as silence", err) } if 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) } } + +// 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) + } +} diff --git a/internal/cat/yaesu.go b/internal/cat/yaesu.go index 94eb5a6..7b175dc 100644 --- a/internal/cat/yaesu.go +++ b/internal/cat/yaesu.go @@ -150,20 +150,12 @@ func (y *Yaesu) Connect() error { if err != nil { 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 - // interfaces read them as PTT: a Xiegu G90 behind a DE-19 goes into - // transmit the moment OpsLog connects and STAYS there — Xiegu's own - // documentation asks for RTS and DTR low. The same applies to an Icom with - // 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) + // Deasserting them to keep an interface from keying the rig silenced radios + // instead: many USB-serial interfaces will not transmit with RTS low. The + // Xiegu fault that change was written for is handled in that backend, which + // now has an explicit setting for which line keys the rig. p.SetReadTimeout(300 * time.Millisecond) y.port = p