refactor(winkeyer): learn the slow boot instead of naming the keyer

The K3NG entry added an hour ago is gone. It named one clone among many —
WKmini, home-built Arduinos, unbranded boxes — for hardware that speaks
exactly the same protocol, and it was the only line in the engine list that
picked a boot delay rather than a protocol. An operator with an unlabelled
clone would have had to guess.

The delay is now learnt per port. The first connect finds out by failing the
quick attempt and succeeding on the slow one; that fact is written to a
global setting keyed by the port, and every connect afterwards goes straight
to the slow attempt. Global rather than per profile on purpose: which keyer
is plugged into COM3 belongs to the computer, and switching profiles for a
different rig does not change the keyer on the desk.

Two tests hold the contract from both sides — a slow keyer must be reported
as slow, and a keyer that answers at once must not be, or every K1EL connect
would inherit seconds it never needed.
This commit is contained in:
2026-08-14 13:05:13 +02:00
parent aca4dc5678
commit 3ce930e9cc
7 changed files with 94 additions and 30 deletions
+20 -5
View File
@@ -51,10 +51,15 @@ type Config struct {
UsePTT bool `json:"use_ptt"` // key PTT (Key/PTT output)
SerialEcho bool `json:"serial_echo"` // device echoes sent chars back to host
// SlowBoot skips the quick opening attempt and waits for a keyer that reboots
// when the port opens — a K3NG or any other Arduino-based WinKeyer with its
// auto-reset still enabled. Not an operator setting: it is remembered per port
// the first time a keyer only answers the long attempt, so the second connect
// is as quick as a K1EL's and nobody has to know what is inside the box.
SlowBoot bool `json:"-"`
// Type selects the keyer engine on this serial port:
// "" / "k1el" → a K1EL WinKeyer chip (the default, everything above applies)
// "k3ng" → a K3NG keyer: an Arduino running the same protocol. Identical
// once open; it just needs time to reboot when the port opens.
// "serial" → the PC bit-bangs Morse on a control line (no WinKeyer chip):
// the "hardware CW keying" a Yaesu SCU-17 / generic interface
// uses. WPM / Weight / Farnsworth / LeadIn / Tail / UsePTT still
@@ -110,12 +115,20 @@ type Manager struct {
onStatus func(Status)
onEcho func(string) // chars the device echoes back as it keys them
// onSlowBoot fires when a keyer answered only after the long wait, so the
// caller can persist that for this port. See Config.SlowBoot.
onSlowBoot func(port string)
}
func NewManager(onStatus func(Status), onEcho func(string)) *Manager {
return &Manager{onStatus: onStatus, onEcho: onEcho}
}
// OnSlowBoot registers the callback that persists "this port needs the long
// opening wait". Optional: without it a slow keyer still connects, it just
// pays the failed quick attempt on every connect.
func (m *Manager) OnSlowBoot(fn func(port string)) { m.onSlowBoot = fn }
// ListPorts returns the available serial port names (COM3, COM6, …).
func ListPorts() ([]string, error) {
ports, err := serial.GetPortsList()
@@ -166,13 +179,15 @@ func (m *Manager) Connect(cfg Config) error {
return fmt.Errorf("winkeyer: open %s: %w", cfg.Port, err)
}
// A K3NG keyer reboots when the port opens (DTR is its reset line), so it is
// given the long wait from the start rather than being failed once first.
ver, err := hostOpen(p, cfg.Type == "k3ng")
ver, slowBoot, err := hostOpen(p, cfg.SlowBoot)
if err != nil {
_ = p.Close()
return fmt.Errorf("winkeyer: %s: %w", cfg.Port, err)
}
// Tell the caller to remember it, so this port opens quickly next time.
if slowBoot && !cfg.SlowBoot && m.onSlowBoot != nil {
m.onSlowBoot(cfg.Port)
}
_ = p.SetReadTimeout(200 * time.Millisecond)
m.mu.Lock()