package winkeyer import ( "errors" "sync" "testing" "time" "go.bug.st/serial" ) // fakeKeyer is a serial.Port that behaves like a WinKeyer: it answers the echo // probe and Host Open, and records everything the host sent so the handshake // can be checked byte for byte against K1EL's documented sequence. type fakeKeyer struct { mu sync.Mutex written []byte toRead []byte version byte // deaf drops every command — the keyer that is not there, or is not // listening because RTS starved it. deaf bool // mute answers the echo but never returns a version. mute bool // needsResync ignores commands until three nulls have been seen, standing // in for a keyer left mid-command by another program. needsResync bool nulls int } func (f *fakeKeyer) Write(p []byte) (int, error) { f.mu.Lock() defer f.mu.Unlock() f.written = append(f.written, p...) if f.deaf { return len(p), nil } for i := 0; i < len(p); i++ { switch { case p[i] == cmdNull: f.nulls++ case f.needsResync && f.nulls < 3: // still confused — swallow it case p[i] == cmdAdmin && i+1 < len(p): i++ switch p[i] { case adminEcho: if i+1 < len(p) { i++ f.toRead = append(f.toRead, p[i]) } case adminOpen: if !f.mute { f.toRead = append(f.toRead, f.version) } } } } return len(p), nil } func (f *fakeKeyer) Read(p []byte) (int, error) { f.mu.Lock() defer f.mu.Unlock() if len(f.toRead) == 0 { return 0, nil // a timeout, not an error — what a real port does } n := copy(p, f.toRead) f.toRead = f.toRead[n:] return n, nil } func (f *fakeKeyer) sent() []byte { f.mu.Lock() defer f.mu.Unlock() return append([]byte(nil), f.written...) } func (f *fakeKeyer) Drain() error { return nil } func (f *fakeKeyer) ResetInputBuffer() error { return nil } func (f *fakeKeyer) ResetOutputBuffer() error { return nil } func (f *fakeKeyer) SetDTR(bool) error { return nil } func (f *fakeKeyer) SetRTS(bool) error { return nil } func (f *fakeKeyer) GetModemStatusBits() (*serial.ModemStatusBits, error) { return &serial.ModemStatusBits{}, nil } func (f *fakeKeyer) SetReadTimeout(time.Duration) error { return nil } func (f *fakeKeyer) Close() error { return nil } func (f *fakeKeyer) Break(time.Duration) error { return nil } func (f *fakeKeyer) SetMode(*serial.Mode) error { return nil } // TestHostOpenFollowsK1ELSequence checks the handshake against the order K1EL // publishes: three nulls to resync the parser, an echo probe to prove there is // a keyer, then Host Open. OpsLog used to send Host Open alone, which a keyer // left mid-command simply absorbed. func TestHostOpenFollowsK1ELSequence(t *testing.T) { f := &fakeKeyer{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) } want := []byte{ cmdNull, cmdNull, cmdNull, cmdAdmin, adminEcho, echoProbe, cmdAdmin, adminOpen, } got := f.sent() if len(got) != len(want) { t.Fatalf("sent % X, want % X", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("sent % X, want % X", got, want) } } } // A keyer left part-way through a command by another program is the everyday // cause of a silent WinKeyer. The nulls must recover it without the operator // having to unplug anything. func TestHostOpenRecoversAConfusedParser(t *testing.T) { f := &fakeKeyer{version: 30, needsResync: true} ver, err := hostOpen(f, false) if err != nil { t.Fatalf("hostOpen: %v", err) } if ver != 30 { t.Errorf("version = %d, want 30", ver) } } // Nothing on the port must FAIL the connection. Reporting success and then // writing settings and text into the void is what produced a log full of // commands and a keyer that never made a sound. func TestHostOpenFailsWhenNothingAnswers(t *testing.T) { f := &fakeKeyer{deaf: true} if _, err := hostOpen(f, false); !errors.Is(err, errNoKeyer) { t.Fatalf("want errNoKeyer, got %v", err) } } // Echoing but not returning a version is a different fault and must not be // reported as "no keyer". func TestHostOpenReportsMissingVersion(t *testing.T) { f := &fakeKeyer{mute: true} _, err := hostOpen(f, false) if err == nil { t.Fatal("want an error") } if errors.Is(err, errNoKeyer) { 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) } }