feat(winkeyer): K3NG keyers, and wait out their reboot
The operator's keyer is a K3NG — an Arduino running an emulation of the WinKeyer protocol — which changes the diagnosis and nearly broke it. Checked against K3NG's own source before anything else, because yesterday's handshake now FAILS a connect where it used to press on regardless. It is safe: k3ng_keyer.ino implements admin echo (0x04) and echoes the byte back, 0x13 is a documented no-op, and OPTION_WINKEY_STRICT_HOST_OPEN — on by default — ignores every byte except 0x00 before host open, so the resync nulls are dropped harmlessly and the echo probe still gets through. The real cause is in K3NG's options file, beside the feature itself: "disabling Automatic Software Reset is highly recommended", and an option to "discard errant serial port bytes at startup" for when it is not. On an Arduino, DTR is wired to reset through a capacitor: opening the port reboots the board into its bootloader. Ours spoke 400 ms later, to a keyer that was not running yet. So the retry now waits 2.5 s, which recovers it without an operator pressing connect twice, and the engine list gains a K3NG entry that skips the doomed fast attempt altogether. Everything else is identical to a K1EL — same settings panel, same protocol.
This commit is contained in:
@@ -34,10 +34,25 @@ const (
|
||||
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"
|
||||
openTimeout = 2 * time.Second
|
||||
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"
|
||||
openTimeout = 2 * time.Second
|
||||
|
||||
// resetDelay is the second attempt's wait, and it is not there for a K1EL.
|
||||
//
|
||||
// Plenty of "WinKeyers" are a K3NG keyer — an Arduino running an emulation
|
||||
// of the same protocol. On an Arduino, DTR is wired to the reset pin through
|
||||
// a capacitor: raising it when the port opens REBOOTS the board, which then
|
||||
// sits in its bootloader before the sketch even starts. K3NG's own options
|
||||
// file says as much ("disabling Automatic Software Reset is highly
|
||||
// recommended", and an option to "discard errant serial port bytes at
|
||||
// startup" for when it is not). 400 ms is nowhere near long enough, so the
|
||||
// keyer misses the whole handshake and looks absent.
|
||||
//
|
||||
// Rather than make every operator wait for the slowest possible device, the
|
||||
// first attempt stays quick and only the retry allows for a reboot.
|
||||
resetDelay = 2500 * time.Millisecond
|
||||
handshakeTry = 2
|
||||
)
|
||||
|
||||
@@ -45,27 +60,38 @@ const (
|
||||
var errNoKeyer = errors.New("no WinKeyer answered on this port — check the cable, the port, and that no other program holds the keyer")
|
||||
|
||||
// hostOpen runs the full documented handshake and returns the firmware version
|
||||
// byte. It is tried twice: a keyer left mid-command by another program is the
|
||||
// common case, the nulls of the first attempt clear it, and the second then
|
||||
// succeeds.
|
||||
func hostOpen(p serial.Port) (int, error) {
|
||||
// byte. It is tried twice, and the two attempts cover the two ways a keyer that
|
||||
// is plugged in and working can miss being spoken to: a parser left mid-command
|
||||
// by whoever talked to it last (the first attempt's nulls clear that), and an
|
||||
// Arduino-based keyer still rebooting from the DTR edge (the second attempt
|
||||
// waits long enough for it).
|
||||
// slowBoot skips straight to the long wait: set when the operator has told us
|
||||
// the keyer is a K3NG, which reboots on every connect.
|
||||
func hostOpen(p serial.Port, slowBoot bool) (int, error) {
|
||||
var lastErr error
|
||||
for attempt := 1; attempt <= handshakeTry; attempt++ {
|
||||
ver, err := hostOpenOnce(p)
|
||||
wait := bootDelay
|
||||
if attempt > 1 || slowBoot {
|
||||
wait = resetDelay
|
||||
}
|
||||
ver, err := hostOpenOnce(p, wait)
|
||||
if err == nil {
|
||||
if attempt > 1 {
|
||||
applog.Printf("winkeyer: answered on attempt %d — the keyer needed %s to boot (a K3NG or other Arduino keyer with auto-reset enabled)", attempt, wait)
|
||||
}
|
||||
return ver, nil
|
||||
}
|
||||
lastErr = err
|
||||
if attempt < handshakeTry {
|
||||
applog.Printf("winkeyer: handshake attempt %d failed (%v) — retrying", attempt, err)
|
||||
applog.Printf("winkeyer: handshake attempt %d failed (%v) — retrying after %s in case the keyer is rebooting", attempt, err, resetDelay)
|
||||
}
|
||||
}
|
||||
return 0, lastErr
|
||||
}
|
||||
|
||||
func hostOpenOnce(p serial.Port) (int, error) {
|
||||
func hostOpenOnce(p serial.Port, boot time.Duration) (int, error) {
|
||||
// The keyer may still be booting off the DTR line we just raised.
|
||||
time.Sleep(bootDelay)
|
||||
time.Sleep(boot)
|
||||
drain(p)
|
||||
|
||||
// Resync the command parser before asking it anything.
|
||||
|
||||
@@ -95,7 +95,7 @@ func (f *fakeKeyer) SetMode(*serial.Mode) error { return nil }
|
||||
// left mid-command simply absorbed.
|
||||
func TestHostOpenFollowsK1ELSequence(t *testing.T) {
|
||||
f := &fakeKeyer{version: 23}
|
||||
ver, err := hostOpen(f)
|
||||
ver, err := hostOpen(f, false)
|
||||
if err != nil {
|
||||
t.Fatalf("hostOpen: %v", err)
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func TestHostOpenFollowsK1ELSequence(t *testing.T) {
|
||||
// having to unplug anything.
|
||||
func TestHostOpenRecoversAConfusedParser(t *testing.T) {
|
||||
f := &fakeKeyer{version: 30, needsResync: true}
|
||||
ver, err := hostOpen(f)
|
||||
ver, err := hostOpen(f, false)
|
||||
if err != nil {
|
||||
t.Fatalf("hostOpen: %v", err)
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func TestHostOpenRecoversAConfusedParser(t *testing.T) {
|
||||
// commands and a keyer that never made a sound.
|
||||
func TestHostOpenFailsWhenNothingAnswers(t *testing.T) {
|
||||
f := &fakeKeyer{deaf: true}
|
||||
if _, err := hostOpen(f); !errors.Is(err, errNoKeyer) {
|
||||
if _, err := hostOpen(f, false); !errors.Is(err, errNoKeyer) {
|
||||
t.Fatalf("want errNoKeyer, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func TestHostOpenFailsWhenNothingAnswers(t *testing.T) {
|
||||
// reported as "no keyer".
|
||||
func TestHostOpenReportsMissingVersion(t *testing.T) {
|
||||
f := &fakeKeyer{mute: true}
|
||||
_, err := hostOpen(f)
|
||||
_, err := hostOpen(f, false)
|
||||
if err == nil {
|
||||
t.Fatal("want an error")
|
||||
}
|
||||
@@ -154,3 +154,48 @@ func TestHostOpenReportsMissingVersion(t *testing.T) {
|
||||
t.Fatalf("a keyer that echoed was reported as absent: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// slowKeyer answers nothing until it has been "powered up" for d — a K3NG on an
|
||||
// Arduino, which the DTR edge from opening the port drops into its bootloader.
|
||||
type slowKeyer struct {
|
||||
fakeKeyer
|
||||
ready time.Time
|
||||
}
|
||||
|
||||
func (s *slowKeyer) Write(p []byte) (int, error) {
|
||||
if time.Now().Before(s.ready) {
|
||||
return len(p), nil // still in the bootloader — the bytes are lost
|
||||
}
|
||||
return s.fakeKeyer.Write(p)
|
||||
}
|
||||
|
||||
// TestHostOpenWaitsOutAnArduinoReboot is the case that started this: a K3NG
|
||||
// keyer reboots when the port opens, so it misses a handshake sent 400 ms
|
||||
// later. The retry has to wait long enough, and must not need the operator to
|
||||
// press connect twice.
|
||||
func TestHostOpenWaitsOutAnArduinoReboot(t *testing.T) {
|
||||
f := &slowKeyer{ready: time.Now().Add(1500 * time.Millisecond)}
|
||||
f.version = 23
|
||||
ver, err := hostOpen(f, false)
|
||||
if err != nil {
|
||||
t.Fatalf("hostOpen: %v", err)
|
||||
}
|
||||
if ver != 23 {
|
||||
t.Errorf("version = %d, want 23", ver)
|
||||
}
|
||||
}
|
||||
|
||||
// Telling OpsLog the keyer is a K3NG must skip the doomed fast attempt, so the
|
||||
// first try already allows for the reboot.
|
||||
func TestHostOpenSlowBootSucceedsFirstTry(t *testing.T) {
|
||||
f := &slowKeyer{ready: time.Now().Add(1500 * time.Millisecond)}
|
||||
f.version = 23
|
||||
if _, err := hostOpen(f, true); err != nil {
|
||||
t.Fatalf("hostOpen: %v", err)
|
||||
}
|
||||
// One attempt: exactly one handshake on the wire, not two.
|
||||
want := len([]byte{cmdNull, cmdNull, cmdNull, cmdAdmin, adminEcho, echoProbe, cmdAdmin, adminOpen})
|
||||
if got := len(f.sent()); got != want {
|
||||
t.Errorf("sent %d bytes, want %d — the fast attempt was not skipped", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ type Config struct {
|
||||
|
||||
// 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
|
||||
@@ -164,7 +166,9 @@ func (m *Manager) Connect(cfg Config) error {
|
||||
return fmt.Errorf("winkeyer: open %s: %w", cfg.Port, err)
|
||||
}
|
||||
|
||||
ver, err := hostOpen(p)
|
||||
// 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")
|
||||
if err != nil {
|
||||
_ = p.Close()
|
||||
return fmt.Errorf("winkeyer: %s: %w", cfg.Port, err)
|
||||
|
||||
Reference in New Issue
Block a user