fix(winkeyer): perform K1EL's full opening handshake

An operator's log showed the whole fault in its first line: "connected on
COM3 — no reply — the keyer did not answer Host Open", followed by seven
configuration commands and two calls sent as Morse. Nothing was listening.
Reporting a link as up and then writing to it regardless is the part worth
fixing; the handshake is why it was down.

K1EL's Application Interface Guide gives the sequence, and we did one step
of it. Now all of it:

  - DTR on, RTS OFF. K1EL's own init sets DTR_CONTROL_ENABLE with
    RTS_CONTROL_DISABLE, and on a serial WinKeyer those lines ARE the power
    supply — DTR feeds the 3.3 V regulator, RTS provides the negative rail.
    go.bug.st/serial defaults both to true, so we drove RTS high on every
    connect without a line of code saying so.
  - 400 ms after the lines come up, for a WK1 still booting off DTR.
  - Three 0x13 nulls to resync the command parser. A keyer left part-way
    through a command by whoever spoke to it last would absorb Host Open as
    a parameter — the everyday cause of a silent WinKeyer, and one the
    operator can do nothing about from the outside.
  - An echo test (0x00 0x04 0x55) before trusting the port at all. This is
    the step that answers "is there a keyer here", and connecting now fails
    on it, with the byte that came back when something else replied.

The whole handshake is retried once, since the first attempt's nulls are
what clear a confused parser. Tested against a fake port that reproduces
each failure: absent, mid-command, and echoing but versionless.
This commit is contained in:
2026-08-14 12:07:58 +02:00
parent 05fc8ad80c
commit 1b736272c9
4 changed files with 308 additions and 13 deletions
+14 -11
View File
@@ -152,22 +152,22 @@ func (m *Manager) Connect(cfg Config) error {
DataBits: 8,
Parity: serial.NoParity,
StopBits: serial.OneStopBit,
// DTR on, RTS OFF. K1EL's own init code sets exactly this
// (DTR_CONTROL_ENABLE / RTS_CONTROL_DISABLE), and on a serial WinKeyer
// the two lines are the chip's power supply: DTR feeds the 3.3 V
// regulator, RTS provides the negative rail for the RS-232 swing.
// Driving RTS high starves that rail. The serial library defaults BOTH
// to true, which is how this was wrong without anyone writing it.
InitialStatusBits: &serial.ModemOutputBits{DTR: true, RTS: false},
})
if err != nil {
return fmt.Errorf("winkeyer: open %s: %w", cfg.Port, err)
}
_ = p.SetReadTimeout(200 * time.Millisecond)
// Host Open: <0x00 0x02>. Device replies with its firmware version byte.
if _, err := p.Write([]byte{0x00, 0x02}); err != nil {
ver, err := hostOpen(p)
if err != nil {
_ = p.Close()
return fmt.Errorf("winkeyer: host open: %w", err)
}
ver := 0
buf := make([]byte, 16)
_ = p.SetReadTimeout(1 * time.Second)
if n, _ := p.Read(buf); n > 0 {
ver = int(buf[0])
return fmt.Errorf("winkeyer: %s: %w", cfg.Port, err)
}
_ = p.SetReadTimeout(200 * time.Millisecond)
@@ -515,7 +515,10 @@ func cmdName(b []byte) string {
func firmwareFamily(ver int) string {
switch {
case ver == 0:
return "no reply — the keyer did not answer Host Open"
// Unreachable from a successful connect — the handshake fails rather
// than returning a version of zero. Kept so a future caller that skips
// hostOpen cannot print "WK1 (v0)" and be believed.
return "unknown firmware (no version returned)"
case ver < 20:
return fmt.Sprintf("WK1 (v%d)", ver)
case ver < 30: