package winkeyer import ( "errors" "fmt" "time" "go.bug.st/serial" "hamlog/internal/applog" ) // The opening handshake, as K1EL specifies it in the WinKeyer2 Application // Interface Guide ("WK Init Psuedo Code"). OpsLog used to send Host Open alone // and carry on whatever came back, which is how an operator ended up with a // keyer reported as connected, a full set of settings written to it, and not // one character keyed — the log said "no reply" and then behaved as if there // had been one. // // The steps exist for reasons that are not obvious from the byte values: // // 400 ms a WK1 is still powering up off the DTR line when the port opens; // WK2 and later do not need it, and it costs nothing once. // 0x13 ×3 null commands. WinKey's parser may be part-way through a command // left over from whoever spoke to it last — another logger, or us // before a crash. A command byte expecting parameters would swallow // Host Open whole. Three nulls flush that state out. // echo ask the keyer to send one known byte back. This is the only step // that answers "is there really a WinKeyer on this port", and it is // 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 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 ) // errNoKeyer is returned when nothing answers the echo probe. 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, 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++ { 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 after %s in case the keyer is rebooting", attempt, err, resetDelay) } } return 0, lastErr } 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(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 { return 0, fmt.Errorf("echo test: %w", err) } b, ok := readByte(p, echoTimeout) if !ok { return 0, errNoKeyer } if b != echoProbe { // Something replied, but not what we asked for. Say what came back — // on a wrong port that byte is the only clue to what is on the other end. 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 { return 0, fmt.Errorf("host open: %w", err) } ver, ok := readByte(p, openTimeout) if !ok { return 0, errors.New("host open: the keyer echoed but did not return its firmware version") } return int(ver), nil } // 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. func readByte(p serial.Port, d time.Duration) (byte, bool) { _ = p.SetReadTimeout(200 * time.Millisecond) deadline := time.Now().Add(d) buf := make([]byte, 1) for time.Now().Before(deadline) { n, err := p.Read(buf) if n > 0 { return buf[0], true } if err != nil { return 0, false } } return 0, false } // drain throws away anything already waiting — a status byte from a previous // session, or the tail of a reply we are no longer interested in. func drain(p serial.Port) { _ = p.SetReadTimeout(20 * time.Millisecond) buf := make([]byte, 64) for i := 0; i < 16; i++ { n, err := p.Read(buf) if n == 0 || err != nil { return } } }