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 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: 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) { var lastErr error for attempt := 1; attempt <= handshakeTry; attempt++ { ver, err := hostOpenOnce(p) if err == nil { return ver, nil } lastErr = err if attempt < handshakeTry { applog.Printf("winkeyer: handshake attempt %d failed (%v) — retrying", attempt, err) } } return 0, lastErr } func hostOpenOnce(p serial.Port) (int, error) { // The keyer may still be booting off the DTR line we just raised. time.Sleep(bootDelay) 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 } } }