From 3def7897429046f52b7dacbce95b1bd384076a09 Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Sat, 15 Aug 2026 10:28:11 +0200 Subject: [PATCH] fix(winkeyer): send the probe the way a working client sends it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Logger32's WinKeyer debug against the K3NG that will not answer OpsLog is a capture of the exchange working, on the same keyer and the same port: Sent: 13 13 13 00 04 55 Rcvd: 55 (72 ms) Sent: 00 02 Host open Rcvd: 23 (WK2 v23) That is the sequence I already send. The difference is the grouping: Logger32 puts the three nulls and the echo probe in ONE write, and we split them with a 50 ms pause and a buffer purge in between. On a keyer that reboots when the port opens, that pause is a window for it to come up mid-sequence and swallow half of it — and there was nothing to wait for, since a null produces no reply. The handshake bytes are now logged unconditionally, not behind the diagnostic option. "No WinKeyer answered" cannot be told apart from a wrong port, a wrong baud rate, a keyer still booting, or another program holding the line. The bytes can, and it is four lines per connect attempt. --- changelog.json | 8 +++-- internal/winkeyer/hostopen.go | 56 ++++++++++++++++++++++-------- internal/winkeyer/hostopen_test.go | 2 ++ 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/changelog.json b/changelog.json index 5f414c6..34a30b9 100644 --- a/changelog.json +++ b/changelog.json @@ -2,8 +2,12 @@ { "version": "0.25.4", "date": "", - "en": [], - "fr": [] + "en": [ + "WinKeyer: the opening probe goes out as one write, matching a capture of a client that talks to the same K3NG keyer, and the handshake bytes are always logged so a keyer that stays silent can be diagnosed." + ], + "fr": [ + "WinKeyer : la sonde d’ouverture part en un seul envoi, calquée sur la capture d’un client qui dialogue avec le même manipulateur K3NG, et les octets de la poignée de main sont toujours journalisés pour diagnostiquer un manipulateur muet." + ] }, { "version": "0.25.3", diff --git a/internal/winkeyer/hostopen.go b/internal/winkeyer/hostopen.go index 5d8f5f0..4890c4a 100644 --- a/internal/winkeyer/hostopen.go +++ b/internal/winkeyer/hostopen.go @@ -30,10 +30,10 @@ import ( // the one to fail on: everything after it assumes a listener. // open 0x00 0x02, and the keyer returns its firmware version. const ( - cmdNull = 0x13 - cmdAdmin = 0x00 - adminOpen = 0x02 - adminEcho = 0x04 + cmdNull = 0x13 + cmdAdmin = 0x00 + adminOpen = 0x02 + adminEcho = 0x04 echoProbe = 0x55 // K1EL's own choice; any byte works, this one is 0b01010101 bootDelay = 400 * time.Millisecond echoTimeout = 2 * time.Second // K1EL: "if a WK doesn't respond within 2 seconds abort" @@ -95,18 +95,25 @@ func hostOpenOnce(p serial.Port, boot time.Duration) (int, error) { time.Sleep(boot) drain(p) - // Resync the command parser before asking it anything. - if _, err := p.Write([]byte{cmdNull, cmdNull, cmdNull}); err != nil { - return 0, fmt.Errorf("resync: %w", err) - } - time.Sleep(50 * time.Millisecond) - drain(p) - - // Is anything actually there? - if _, err := p.Write([]byte{cmdAdmin, adminEcho, echoProbe}); err != nil { + // The resync nulls and the echo probe go out as ONE write. + // + // Copied byte for byte from a Logger32 capture against the K3NG keyer that + // would not answer OpsLog: "Sent: 13 13 13 00 04 55 / Rcvd: 55". Same keyer, + // same port, same six bytes — the only difference was that we sent them as + // two writes with a pause and a buffer purge in between, and Logger32 sends + // them as one. On a keyer that reboots when the port opens, that pause is a + // window for it to come up mid-sequence and swallow half of it. + // + // There is nothing to wait for between the two halves anyway: a null produces + // no reply, so the pause was only ever giving the keyer a chance to change + // its mind. + probe := []byte{cmdNull, cmdNull, cmdNull, cmdAdmin, adminEcho, echoProbe} + traceHandshake("TX", probe, 0, false) + if _, err := p.Write(probe); err != nil { return 0, fmt.Errorf("echo test: %w", err) } b, ok := readByte(p, echoTimeout) + traceHandshake("RX", nil, b, ok) if !ok { return 0, errNoKeyer } @@ -116,16 +123,37 @@ func hostOpenOnce(p serial.Port, boot time.Duration) (int, error) { return 0, fmt.Errorf("echo test: expected 0x%02X, got 0x%02X — is this the keyer's port?", echoProbe, b) } - if _, err := p.Write([]byte{cmdAdmin, adminOpen}); err != nil { + open := []byte{cmdAdmin, adminOpen} + traceHandshake("TX", open, 0, false) + if _, err := p.Write(open); err != nil { return 0, fmt.Errorf("host open: %w", err) } ver, ok := readByte(p, openTimeout) + traceHandshake("RX", nil, ver, ok) if !ok { return 0, errors.New("host open: the keyer echoed but did not return its firmware version") } return int(ver), nil } +// traceHandshake puts the opening exchange in the log, ALWAYS — unlike the +// running trace beside it, which is behind the diagnostic option. +// +// A failure that says only "no WinKeyer answered" cannot be told apart from a +// wrong port, a wrong baud rate, a keyer still rebooting, or another program +// holding the line. The bytes can. It is four lines per connect, and only when +// the connect is attempted. +func traceHandshake(dir string, b []byte, got byte, ok bool) { + switch { + case dir == "TX": + applog.Printf("winkeyer: handshake TX % 02X", b) + case ok: + applog.Printf("winkeyer: handshake RX %02X", got) + default: + applog.Printf("winkeyer: handshake RX — nothing came back") + } +} + // readByte waits up to d for one byte. The serial read timeout is per-call and // can return 0 bytes without an error, so this loops until the deadline rather // than trusting a single Read. diff --git a/internal/winkeyer/hostopen_test.go b/internal/winkeyer/hostopen_test.go index b4000dc..04ac96b 100644 --- a/internal/winkeyer/hostopen_test.go +++ b/internal/winkeyer/hostopen_test.go @@ -102,6 +102,8 @@ func TestHostOpenFollowsK1ELSequence(t *testing.T) { if ver != 23 { t.Errorf("version = %d, want 23", ver) } + // One write for the six probe bytes, then Host Open — the order and the + // grouping of a Logger32 capture against a real K3NG. want := []byte{ cmdNull, cmdNull, cmdNull, cmdAdmin, adminEcho, echoProbe,