Compare commits

..
3 Commits
Author SHA1 Message Date
rouggy 8113557015 chore: release v0.25.4 2026-08-15 10:29:00 +02:00
rouggy 3def789742 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.
2026-08-15 10:28:11 +02:00
rouggy 92ea98bcfa chore(changelog): open 0.25.4
0.25.3 shipped with its two entries — the verified-confirmation fix and the
native SPID rotator — and they are two separate subjects, so there was nothing
to merge this time.

Version constants untouched: the release script owns them, and it already
bumped them to 0.25.3 in the commit before this one.
2026-08-15 10:18:17 +02:00
5 changed files with 56 additions and 16 deletions
+10
View File
@@ -1,4 +1,14 @@
[
{
"version": "0.25.4",
"date": "",
"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 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",
"date": "",
+1 -1
View File
@@ -1,6 +1,6 @@
// Single source of truth for the app version shown in the UI (header + About).
// Bump this on a release (the release script updates it alongside telemetry.go).
export const APP_VERSION = '0.25.3';
export const APP_VERSION = '0.25.4';
// Author / credits, shown in Help -> About.
export const APP_AUTHOR = 'F4BPO';
+38 -10
View File
@@ -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.
+2
View File
@@ -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,
+1 -1
View File
@@ -21,7 +21,7 @@ import (
const (
// appVersion is stamped on every heartbeat (and could feed the About box).
appVersion = "0.25.3"
appVersion = "0.25.4"
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
// to https://us.i.posthog.com for a US project.