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
+6 -2
View File
@@ -2,8 +2,12 @@
{ {
"version": "0.25.4", "version": "0.25.4",
"date": "", "date": "",
"en": [], "en": [
"fr": [] "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 douverture part en un seul envoi, calquée sur la capture dun 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", "version": "0.25.3",
+42 -14
View File
@@ -30,10 +30,10 @@ import (
// the one to fail on: everything after it assumes a listener. // the one to fail on: everything after it assumes a listener.
// open 0x00 0x02, and the keyer returns its firmware version. // open 0x00 0x02, and the keyer returns its firmware version.
const ( const (
cmdNull = 0x13 cmdNull = 0x13
cmdAdmin = 0x00 cmdAdmin = 0x00
adminOpen = 0x02 adminOpen = 0x02
adminEcho = 0x04 adminEcho = 0x04
echoProbe = 0x55 // K1EL's own choice; any byte works, this one is 0b01010101 echoProbe = 0x55 // K1EL's own choice; any byte works, this one is 0b01010101
bootDelay = 400 * time.Millisecond bootDelay = 400 * time.Millisecond
echoTimeout = 2 * time.Second // K1EL: "if a WK doesn't respond within 2 seconds abort" 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) time.Sleep(boot)
drain(p) drain(p)
// Resync the command parser before asking it anything. // The resync nulls and the echo probe go out as ONE write.
if _, err := p.Write([]byte{cmdNull, cmdNull, cmdNull}); err != nil { //
return 0, fmt.Errorf("resync: %w", err) // 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,
time.Sleep(50 * time.Millisecond) // same port, same six bytes — the only difference was that we sent them as
drain(p) // 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
// Is anything actually there? // window for it to come up mid-sequence and swallow half of it.
if _, err := p.Write([]byte{cmdAdmin, adminEcho, echoProbe}); err != nil { //
// 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) return 0, fmt.Errorf("echo test: %w", err)
} }
b, ok := readByte(p, echoTimeout) b, ok := readByte(p, echoTimeout)
traceHandshake("RX", nil, b, ok)
if !ok { if !ok {
return 0, errNoKeyer 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) 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) return 0, fmt.Errorf("host open: %w", err)
} }
ver, ok := readByte(p, openTimeout) ver, ok := readByte(p, openTimeout)
traceHandshake("RX", nil, ver, ok)
if !ok { if !ok {
return 0, errors.New("host open: the keyer echoed but did not return its firmware version") return 0, errors.New("host open: the keyer echoed but did not return its firmware version")
} }
return int(ver), nil 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 // 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 // can return 0 bytes without an error, so this loops until the deadline rather
// than trusting a single Read. // than trusting a single Read.
+2
View File
@@ -102,6 +102,8 @@ func TestHostOpenFollowsK1ELSequence(t *testing.T) {
if ver != 23 { if ver != 23 {
t.Errorf("version = %d, want 23", ver) 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{ want := []byte{
cmdNull, cmdNull, cmdNull, cmdNull, cmdNull, cmdNull,
cmdAdmin, adminEcho, echoProbe, cmdAdmin, adminEcho, echoProbe,