An operator's log showed the whole fault in its first line: "connected on
COM3 — no reply — the keyer did not answer Host Open", followed by seven
configuration commands and two calls sent as Morse. Nothing was listening.
Reporting a link as up and then writing to it regardless is the part worth
fixing; the handshake is why it was down.
K1EL's Application Interface Guide gives the sequence, and we did one step
of it. Now all of it:
- DTR on, RTS OFF. K1EL's own init sets DTR_CONTROL_ENABLE with
RTS_CONTROL_DISABLE, and on a serial WinKeyer those lines ARE the power
supply — DTR feeds the 3.3 V regulator, RTS provides the negative rail.
go.bug.st/serial defaults both to true, so we drove RTS high on every
connect without a line of code saying so.
- 400 ms after the lines come up, for a WK1 still booting off DTR.
- Three 0x13 nulls to resync the command parser. A keyer left part-way
through a command by whoever spoke to it last would absorb Host Open as
a parameter — the everyday cause of a silent WinKeyer, and one the
operator can do nothing about from the outside.
- An echo test (0x00 0x04 0x55) before trusting the port at all. This is
the step that answers "is there a keyer here", and connecting now fails
on it, with the byte that came back when something else replied.
The whole handshake is retried once, since the first attempt's nulls are
what clear a confused parser. Tested against a fake port that reproduces
each failure: absent, mid-command, and echoing but versionless.
157 lines
4.3 KiB
Go
157 lines
4.3 KiB
Go
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)
|
|
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)
|
|
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); !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)
|
|
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)
|
|
}
|
|
}
|