fix(winkeyer): send the probe the way a working client sends it

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.
This commit is contained in:
2026-08-15 10:28:11 +02:00
parent 92ea98bcfa
commit 3def789742
3 changed files with 50 additions and 16 deletions
+42 -14
View File
@@ -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.