From db9ca5dc83653e28b4eafaf3110ca6b793caac5a Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 15 May 2022 15:57:12 -0600 Subject: [PATCH 01/10] Start enforcing message structure --- cmd/vail/book.go | 8 ++++---- cmd/vail/book_test.go | 30 ++++++++++++++++++---------- cmd/vail/main.go | 41 +++++++++++++++++++++++++++++---------- cmd/vail/message.go | 27 ++++++++++++++++---------- cmd/vail/message_test.go | 10 +++++----- cmd/vail/repeater.go | 10 ++++++++-- cmd/vail/repeater_test.go | 37 ++++++++++++++++++++++++++--------- 7 files changed, 113 insertions(+), 50 deletions(-) diff --git a/cmd/vail/book.go b/cmd/vail/book.go index 15c0823..6f8bafa 100644 --- a/cmd/vail/book.go +++ b/cmd/vail/book.go @@ -29,7 +29,7 @@ type bookEvent struct { eventType bookEventType name string w io.Writer - p []byte + m Message } func (b Book) Join(name string, w io.Writer) { @@ -48,11 +48,11 @@ func (b Book) Part(name string, w io.Writer) { } } -func (b Book) Send(name string, p []byte) { +func (b Book) Send(name string, m Message) { b.events <- bookEvent{ eventType: sendEvent, name: name, - p: p, + m: m, } } @@ -87,6 +87,6 @@ func (b Book) loop() { log.Println("WARN: Sending to an empty channel:", event.name) break } - repeater.Send(event.p) + repeater.Send(event.m) } } diff --git a/cmd/vail/book_test.go b/cmd/vail/book_test.go index 0eabb9f..fd59482 100644 --- a/cmd/vail/book_test.go +++ b/cmd/vail/book_test.go @@ -7,53 +7,63 @@ import ( func TestBook(t *testing.T) { b := NewBook() + m := TestMessage{Message{1, 2, []uint8{3, 4}}} buf1 := bytes.NewBufferString("buf1") + buf1Expect := bytes.NewBufferString("buf1") b.Join("moo", buf1) + m.Clients = 1 b.loop() if len(b.entries) != 1 { t.Error("Wrong number of entries") } // Send to an empty channel - b.Send("merf", []byte("goober")) + b.Send("merf", m.Message) b.loop() - if buf1.String() != "buf1" { + if buf1.String() != buf1Expect.String() { t.Error("Sending to empty channel sent to non-empty channel") } // Send to a non-empty channel! - b.Send("moo", []byte("goober")) + b.Send("moo", m.Message) b.loop() - if buf1.String() != "buf1goober" { + buf1Expect.Write(m.bytes()) + if buf1.String() != buf1Expect.String() { t.Error("Sending didn't work") } // Join another client buf2 := bytes.NewBufferString("buf2") + buf2Expect := bytes.NewBufferString("buf2") b.Join("moo", buf2) + m.Clients = 2 b.loop() // Send to both - b.Send("moo", []byte("snerk")) + b.Send("moo", m.Message) b.loop() - if buf1.String() != "buf1goobersnerk" { + buf1Expect.Write(m.bytes()) + buf2Expect.Write(m.bytes()) + if buf1.String() != buf1Expect.String() { t.Error("Send to 2-member channel busted", buf1) } - if buf2.String() != "buf2snerk" { + if buf2.String() != buf2Expect.String() { t.Error("Send to 2-member channel busted", buf2) } // Part a client b.Part("moo", buf1) b.loop() + m.Clients = 1 - b.Send("moo", []byte("peanut")) + b.Send("moo", m.Message) b.loop() - if buf1.String() != "buf1goobersnerk" { + buf2Expect.Write(m.bytes()) + if buf1.String() != buf1Expect.String() { t.Error("Parted channel but still getting messages", buf1) } - if buf2.String() != "buf2snerkpeanut" { + if buf2.String() != buf2Expect.String() { t.Error("Someone else parting somehow messed up sends", buf2) } } diff --git a/cmd/vail/main.go b/cmd/vail/main.go index b602fa7..8e305ca 100644 --- a/cmd/vail/main.go +++ b/cmd/vail/main.go @@ -2,10 +2,11 @@ package main import ( "fmt" - "time" - "os" "log" "net/http" + "os" + "time" + "golang.org/x/net/websocket" ) @@ -16,12 +17,13 @@ type Client struct { } func (c Client) Handle(ws *websocket.Conn) { - ws.MaxPayloadBytes = 500 + nowMilli := time.Now().UnixMilli() + ws.MaxPayloadBytes = 50 book.Join(c.repeaterName, ws) defer book.Part(c.repeaterName, ws) - + // Tell the client what time we think it is - fmt.Fprintf(ws, "[%d]", time.Now().UnixNano() / time.Millisecond.Nanoseconds()) + fmt.Fprintf(ws, "[%d]", time.Now().UnixNano()/time.Millisecond.Nanoseconds()) for { buf := make([]byte, ws.MaxPayloadBytes) @@ -29,15 +31,34 @@ func (c Client) Handle(ws *websocket.Conn) { if n, err := ws.Read(buf); err != nil { break } else { - buf = buf[:n] + buf = buf[:n] } - - book.Send(c.repeaterName, buf) + + // Decode into a Message + var m Message + if err := m.UnmarshalBinary(buf); err != nil { + fmt.Fprintln(ws, err) + ws.Close() + return + } + + // If it's wildly out of time, reject it + timeDelta := (nowMilli - m.Timestamp) + if timeDelta < 0 { + timeDelta = -timeDelta + } + if timeDelta > 9999 { + fmt.Fprintln(ws, "Bad timestamp") + ws.Close() + return + } + + book.Send(c.repeaterName, m) } } func ChatHandler(w http.ResponseWriter, r *http.Request) { - c := Client { + c := Client{ repeaterName: r.FormValue("repeater"), } @@ -57,7 +78,7 @@ func main() { port = "8080" } log.Println("Listening on port", port) - err := http.ListenAndServe(":" + port, nil) + err := http.ListenAndServe(":"+port, nil) if err != nil { log.Fatal(err.Error()) } diff --git a/cmd/vail/message.go b/cmd/vail/message.go index c9d80db..5cadb7f 100644 --- a/cmd/vail/message.go +++ b/cmd/vail/message.go @@ -8,11 +8,12 @@ import ( // VailMessage is a single Vail message. type Message struct { - // Relative time in ms of this message. - // These timestamps need to be consistent, but the offset can be anything. - // ECMAScript `performance.now()` is ideal. + // Timestamp of this message. Milliseconds since epoch. Timestamp int64 - + + // Number of connected clients + Clients uint16 + // Message timing in ms. // Timings alternate between tone and silence. // For example, `A` could be sent as [80, 80, 240] @@ -22,13 +23,13 @@ type Message struct { func NewMessage(ts time.Time, durations []time.Duration) Message { msg := Message{ Timestamp: ts.UnixNano() / time.Millisecond.Nanoseconds(), - Duration: make([]uint8, len(durations)), + Duration: make([]uint8, len(durations)), } for i, dns := range durations { ms := dns.Milliseconds() - if (ms > 255) { + if ms > 255 { ms = 255 - } else if (ms < 0) { + } else if ms < 0 { ms = 0 } msg.Duration[i] = uint8(ms) @@ -42,6 +43,9 @@ func (m Message) MarshalBinary() ([]byte, error) { if err := binary.Write(&w, binary.BigEndian, m.Timestamp); err != nil { return nil, err } + if err := binary.Write(&w, binary.BigEndian, m.Clients); err != nil { + return nil, err + } if err := binary.Write(&w, binary.BigEndian, m.Duration); err != nil { return nil, err } @@ -54,6 +58,9 @@ func (m *Message) UnmarshalBinary(data []byte) error { if err := binary.Read(r, binary.BigEndian, &m.Timestamp); err != nil { return err } + if err := binary.Read(r, binary.BigEndian, &m.Clients); err != nil { + return err + } dlen := r.Len() m.Duration = make([]uint8, dlen) if err := binary.Read(r, binary.BigEndian, &m.Duration); err != nil { @@ -66,16 +73,16 @@ func (m Message) Equal(m2 Message) bool { if m.Timestamp != m2.Timestamp { return false } - + if len(m.Duration) != len(m2.Duration) { return false } - + for i := range m.Duration { if m.Duration[i] != m2.Duration[i] { return false } } - + return true } diff --git a/cmd/vail/message_test.go b/cmd/vail/message_test.go index 94354c6..70ff0ec 100644 --- a/cmd/vail/message_test.go +++ b/cmd/vail/message_test.go @@ -6,9 +6,9 @@ import ( "time" ) -func TestMessage(t *testing.T) { - m := Message{0x1122334455, []uint8{0xaa, 0xbb, 0xcc}} - m2 := Message{12, []uint8{1}} +func TestMessageStruct(t *testing.T) { + m := Message{0x1122334455, 0, []uint8{0xaa, 0xbb, 0xcc}} + m2 := Message{12, 0, []uint8{1}} if !m.Equal(m) { t.Error("Equal messages did not compare equal") @@ -16,7 +16,7 @@ func TestMessage(t *testing.T) { if m.Equal(m2) { t.Error("Unequal messages compared equal") } - if m.Equal(Message{m.Timestamp, []uint8{1, 2, 3}}) { + if m.Equal(Message{m.Timestamp, 0, []uint8{1, 2, 3}}) { t.Error("Messages with different payloads compared equal") } @@ -24,7 +24,7 @@ func TestMessage(t *testing.T) { if err != nil { t.Error(err) } - if !bytes.Equal(bm, []byte("\x00\x00\x00\x11\x22\x33\x44\x55\xaa\xbb\xcc")) { + if !bytes.Equal(bm, []byte("\x00\x00\x00\x11\x22\x33\x44\x55\x00\x00\xaa\xbb\xcc")) { t.Error("Encoded wrong:", bm) } diff --git a/cmd/vail/repeater.go b/cmd/vail/repeater.go index c0366f5..1fbd0b9 100644 --- a/cmd/vail/repeater.go +++ b/cmd/vail/repeater.go @@ -2,6 +2,7 @@ package main import ( "io" + "log" ) // A Repeater is just a list of Writers. @@ -29,9 +30,14 @@ func (r *Repeater) Part(w io.Writer) { } } -func (r *Repeater) Send(p []byte) { +func (r *Repeater) Send(m Message) { + m.Clients = uint16(r.Listeners()) + buf, err := m.MarshalBinary() + if err != nil { + log.Fatal(err) + } for _, s := range r.writers { - s.Write(p) + s.Write(buf) } } diff --git a/cmd/vail/repeater_test.go b/cmd/vail/repeater_test.go index c09d5f0..6d52b0c 100644 --- a/cmd/vail/repeater_test.go +++ b/cmd/vail/repeater_test.go @@ -5,35 +5,54 @@ import ( "testing" ) +type TestMessage struct { + Message +} + +func (m TestMessage) bytes() []byte { + b, _ := m.MarshalBinary() + return b +} + func TestRepeater(t *testing.T) { r := NewRepeater() + m := TestMessage{Message{1, 3, []uint8{3, 4}}} buf1 := bytes.NewBufferString("buf1") + buf1Expect := bytes.NewBufferString("buf1") r.Join(buf1) if r.Listeners() != 1 { t.Error("Joining did nothing") } - r.Send([]byte("moo")) - if buf1.String() != "buf1moo" { + r.Send(m.Message) + m.Clients = 1 + buf1Expect.Write(m.bytes()) + if buf1.String() != buf1Expect.String() { t.Error("Client 1 not repeating", buf1) } buf2 := bytes.NewBufferString("buf2") + buf2Expect := bytes.NewBufferString("buf2") r.Join(buf2) - r.Send([]byte("bar")) - if buf1.String() != "buf1moobar" { - t.Error("Client 1 not repeating", buf1) + r.Send(m.Message) + m.Clients = 2 + buf1Expect.Write(m.bytes()) + buf2Expect.Write(m.bytes()) + if buf1.String() != buf1Expect.String() { + t.Errorf("Client 1 not repeating %#v %#v", buf1, buf1Expect) } - if buf2.String() != "buf2bar" { + if buf2.String() != buf2Expect.String() { t.Error("Client 2 not repeating", buf2) } r.Part(buf1) - r.Send([]byte("baz")) - if buf1.String() != "buf1moobar" { + r.Send(m.Message) + m.Clients = 1 + buf2Expect.Write(m.bytes()) + if buf1.String() != buf1Expect.String() { t.Error("Client 1 still getting data after part", buf1) } - if buf2.String() != "buf2barbaz" { + if buf2.String() != buf2Expect.String() { t.Error("Client 2 not getting data after part", buf2) } } From d6e6a268a3d17c9ba808a05dfe85c51ef73e0b27 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 15 May 2022 17:38:57 -0600 Subject: [PATCH 02/10] More unit test junk --- cmd/vail/book.go | 19 ++++++-- cmd/vail/book_test.go | 62 ++++++++++------------- cmd/vail/main.go | 18 +++++-- cmd/vail/message.go | 2 +- cmd/vail/message_test.go | 8 ++- cmd/vail/repeater.go | 16 ++++++ cmd/vail/repeater_test.go | 100 ++++++++++++++++++++++---------------- 7 files changed, 133 insertions(+), 92 deletions(-) diff --git a/cmd/vail/book.go b/cmd/vail/book.go index 6f8bafa..f8573b9 100644 --- a/cmd/vail/book.go +++ b/cmd/vail/book.go @@ -5,15 +5,20 @@ import ( "log" ) +// Book maps names to repeaters +// +// It ensures that names map 1-1 to repeaters. type Book struct { - entries map[string]*Repeater - events chan bookEvent + entries map[string]*Repeater + events chan bookEvent + makeRepeater func() *Repeater } func NewBook() Book { return Book{ - entries: make(map[string]*Repeater), - events: make(chan bookEvent, 5), + entries: make(map[string]*Repeater), + events: make(chan bookEvent, 5), + makeRepeater: NewRepeater, } } @@ -32,6 +37,7 @@ type bookEvent struct { m Message } +// Join adds a writer to a named repeater func (b Book) Join(name string, w io.Writer) { b.events <- bookEvent{ eventType: joinEvent, @@ -40,6 +46,7 @@ func (b Book) Join(name string, w io.Writer) { } } +// Part removes a writer from a named repeater func (b Book) Part(name string, w io.Writer) { b.events <- bookEvent{ eventType: partEvent, @@ -48,6 +55,7 @@ func (b Book) Part(name string, w io.Writer) { } } +// Send transmits a message to the named repeater func (b Book) Send(name string, m Message) { b.events <- bookEvent{ eventType: sendEvent, @@ -56,6 +64,7 @@ func (b Book) Send(name string, m Message) { } } +// Run is the endless run loop func (b Book) Run() { for { b.loop() @@ -69,7 +78,7 @@ func (b Book) loop() { switch event.eventType { case joinEvent: if !ok { - repeater = NewRepeater() + repeater = b.makeRepeater() b.entries[event.name] = repeater } repeater.Join(event.w) diff --git a/cmd/vail/book_test.go b/cmd/vail/book_test.go index fd59482..aa66412 100644 --- a/cmd/vail/book_test.go +++ b/cmd/vail/book_test.go @@ -1,69 +1,57 @@ package main import ( - "bytes" "testing" ) func TestBook(t *testing.T) { - b := NewBook() - m := TestMessage{Message{1, 2, []uint8{3, 4}}} + b := Book{ + entries: make(map[string]*Repeater), + events: make(chan bookEvent, 5), + makeRepeater: NewTestingRepeater, + } - buf1 := bytes.NewBufferString("buf1") - buf1Expect := bytes.NewBufferString("buf1") - b.Join("moo", buf1) - m.Clients = 1 + c1 := NewTestingClient(t) + b.Join("moo", c1) b.loop() if len(b.entries) != 1 { t.Error("Wrong number of entries") } + c1.Expect(1) // Send to an empty channel - b.Send("merf", m.Message) + m := Message{0, 0, []uint8{22, 33}} + b.Send("merf", m) b.loop() - if buf1.String() != buf1Expect.String() { + if c1.Len() > 0 { t.Error("Sending to empty channel sent to non-empty channel") } // Send to a non-empty channel! - b.Send("moo", m.Message) + b.Send("moo", m) b.loop() - buf1Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Error("Sending didn't work") - } + c1.Expect(1, 22, 33) // Join another client - buf2 := bytes.NewBufferString("buf2") - buf2Expect := bytes.NewBufferString("buf2") - b.Join("moo", buf2) - m.Clients = 2 + c2 := NewTestingClient(t) + b.Join("moo", c2) b.loop() + c1.Expect(2) + c2.Expect(2) // Send to both - b.Send("moo", m.Message) + m.Duration = append(m.Duration, 44) + b.Send("moo", m) b.loop() - buf1Expect.Write(m.bytes()) - buf2Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Error("Send to 2-member channel busted", buf1) - } - if buf2.String() != buf2Expect.String() { - t.Error("Send to 2-member channel busted", buf2) - } + c1.Expect(2, 22, 33, 44) + c2.Expect(2, 22, 33, 44) // Part a client - b.Part("moo", buf1) + b.Part("moo", c1) b.loop() - m.Clients = 1 + c2.Expect(1) - b.Send("moo", m.Message) + b.Send("moo", m) b.loop() - buf2Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Error("Parted channel but still getting messages", buf1) - } - if buf2.String() != buf2Expect.String() { - t.Error("Someone else parting somehow messed up sends", buf2) - } + c2.Expect(1, 22, 33, 44) } diff --git a/cmd/vail/main.go b/cmd/vail/main.go index 8e305ca..65194ee 100644 --- a/cmd/vail/main.go +++ b/cmd/vail/main.go @@ -12,6 +12,21 @@ import ( var book Book +// Clock defines an interface for getting the current time. +// +// We use this in testing to provide a fixed value for the current time, so we +// can still compare clocks. +type Clock interface { + Now() time.Time +} + +// WallClock provides the actual time +type WallClock struct{} + +func (WallClock) Now() time.Time { + return time.Now() +} + type Client struct { repeaterName string } @@ -22,9 +37,6 @@ func (c Client) Handle(ws *websocket.Conn) { book.Join(c.repeaterName, ws) defer book.Part(c.repeaterName, ws) - // Tell the client what time we think it is - fmt.Fprintf(ws, "[%d]", time.Now().UnixNano()/time.Millisecond.Nanoseconds()) - for { buf := make([]byte, ws.MaxPayloadBytes) diff --git a/cmd/vail/message.go b/cmd/vail/message.go index 5cadb7f..e35e7be 100644 --- a/cmd/vail/message.go +++ b/cmd/vail/message.go @@ -20,7 +20,7 @@ type Message struct { Duration []uint8 } -func NewMessage(ts time.Time, durations []time.Duration) Message { +func NewMessage(ts time.Time, durations ...time.Duration) Message { msg := Message{ Timestamp: ts.UnixNano() / time.Millisecond.Nanoseconds(), Duration: make([]uint8, len(durations)), diff --git a/cmd/vail/message_test.go b/cmd/vail/message_test.go index 70ff0ec..dae2d44 100644 --- a/cmd/vail/message_test.go +++ b/cmd/vail/message_test.go @@ -40,11 +40,9 @@ func TestMessageStruct(t *testing.T) { 0, m.Timestamp*time.Millisecond.Nanoseconds(), ), - []time.Duration{ - time.Duration(m.Duration[0]) * time.Millisecond, - time.Duration(m.Duration[1]) * time.Millisecond, - time.Duration(m.Duration[2]) * time.Millisecond, - }, + time.Duration(m.Duration[0])*time.Millisecond, + time.Duration(m.Duration[1])*time.Millisecond, + time.Duration(m.Duration[2])*time.Millisecond, ) if !m.Equal(m3) { t.Error("NewMessage didn't work", m, m3) diff --git a/cmd/vail/repeater.go b/cmd/vail/repeater.go index 1fbd0b9..a2d00b8 100644 --- a/cmd/vail/repeater.go +++ b/cmd/vail/repeater.go @@ -3,23 +3,30 @@ package main import ( "io" "log" + "time" ) // A Repeater is just a list of Writers. type Repeater struct { + clock Clock writers []io.Writer } +// NewRepeater returns a newly-created repeater func NewRepeater() *Repeater { return &Repeater{ + clock: WallClock{}, writers: make([]io.Writer, 0, 20), } } +// Join joins a writer to this repeater func (r *Repeater) Join(w io.Writer) { r.writers = append(r.writers, w) + r.SendMessage() } +// Part removes a writer from this repeater func (r *Repeater) Part(w io.Writer) { for i, s := range r.writers { if s == w { @@ -28,8 +35,10 @@ func (r *Repeater) Part(w io.Writer) { r.writers = r.writers[:nsubs-1] } } + r.SendMessage() } +// Send send a message to all connected clients func (r *Repeater) Send(m Message) { m.Clients = uint16(r.Listeners()) buf, err := m.MarshalBinary() @@ -41,6 +50,13 @@ func (r *Repeater) Send(m Message) { } } +// SendMessage constructs and sends a message +func (r *Repeater) SendMessage(durations ...time.Duration) { + m := NewMessage(r.clock.Now(), durations...) + r.Send(m) +} + +// Listeners returns the number of connected clients func (r *Repeater) Listeners() int { return len(r.writers) } diff --git a/cmd/vail/repeater_test.go b/cmd/vail/repeater_test.go index 6d52b0c..66d2a32 100644 --- a/cmd/vail/repeater_test.go +++ b/cmd/vail/repeater_test.go @@ -2,57 +2,75 @@ package main import ( "bytes" + "io" "testing" + "time" ) -type TestMessage struct { - Message +type FakeClock struct{} + +func (f FakeClock) Now() time.Time { + return time.UnixMilli(0) } -func (m TestMessage) bytes() []byte { - b, _ := m.MarshalBinary() - return b +type TestingClient struct { + bytes.Buffer + expected bytes.Buffer + repeater *Repeater + t *testing.T +} + +func NewTestingClient(t *testing.T) *TestingClient { + return &TestingClient{ + Buffer: bytes.Buffer{}, + expected: bytes.Buffer{}, + t: t, + } +} + +func (tc *TestingClient) Expect(clients uint16, payload ...uint8) { + m := Message{0, clients, payload} + buf, _ := m.MarshalBinary() + tc.expected.Write(buf) + if tc.String() != tc.expected.String() { + tc.t.Errorf("Client buffer mismatch. Wanted %#v, got %#v", tc.expected.String(), tc.String()) + } + tc.Reset() + tc.expected.Reset() +} + +func NewTestingRepeater() *Repeater { + return &Repeater{ + clock: FakeClock{}, + writers: make([]io.Writer, 0, 2), + } } func TestRepeater(t *testing.T) { - r := NewRepeater() - m := TestMessage{Message{1, 3, []uint8{3, 4}}} + r := NewTestingRepeater() - buf1 := bytes.NewBufferString("buf1") - buf1Expect := bytes.NewBufferString("buf1") - r.Join(buf1) - if r.Listeners() != 1 { - t.Error("Joining did nothing") - } - r.Send(m.Message) - m.Clients = 1 - buf1Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Error("Client 1 not repeating", buf1) - } + c1 := NewTestingClient(t) + r.Join(c1) + c1.Expect(1) - buf2 := bytes.NewBufferString("buf2") - buf2Expect := bytes.NewBufferString("buf2") - r.Join(buf2) - r.Send(m.Message) - m.Clients = 2 - buf1Expect.Write(m.bytes()) - buf2Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Errorf("Client 1 not repeating %#v %#v", buf1, buf1Expect) - } - if buf2.String() != buf2Expect.String() { - t.Error("Client 2 not repeating", buf2) - } + r.SendMessage(15 * time.Millisecond) + c1.Expect(1, 15) - r.Part(buf1) - r.Send(m.Message) - m.Clients = 1 - buf2Expect.Write(m.bytes()) - if buf1.String() != buf1Expect.String() { - t.Error("Client 1 still getting data after part", buf1) - } - if buf2.String() != buf2Expect.String() { - t.Error("Client 2 not getting data after part", buf2) + c2 := NewTestingClient(t) + r.Join(c2) + c1.Expect(2) + c2.Expect(2) + + r.SendMessage(58 * time.Millisecond) + c1.Expect(2, 58) + c2.Expect(2, 58) + + r.Part(c1) + c2.Expect(1) + + r.SendMessage(5 * time.Millisecond) + c2.Expect(1, 5) + if c1.Len() > 0 { + t.Error("Client 1 still getting data after part") } } From 4ef1ff7517ca7c42a54e550cfc2b65ad621f59da Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 15 May 2022 21:12:36 -0600 Subject: [PATCH 03/10] More work --- cmd/vail/book.go | 15 ++++--- cmd/vail/main.go | 36 +++++++++++------ cmd/vail/message.go | 35 +++++++++++++++++ cmd/vail/repeater.go | 34 +++++++--------- cmd/vail/repeater_test.go | 39 +++++++++++------- static/repeaters.mjs | 83 +++++++++++++++++++++++++-------------- static/vail.mjs | 4 +- 7 files changed, 159 insertions(+), 87 deletions(-) diff --git a/cmd/vail/book.go b/cmd/vail/book.go index f8573b9..95b9606 100644 --- a/cmd/vail/book.go +++ b/cmd/vail/book.go @@ -1,7 +1,6 @@ package main import ( - "io" "log" ) @@ -33,25 +32,25 @@ const ( type bookEvent struct { eventType bookEventType name string - w io.Writer + sender MessageSender m Message } // Join adds a writer to a named repeater -func (b Book) Join(name string, w io.Writer) { +func (b Book) Join(name string, sender MessageSender) { b.events <- bookEvent{ eventType: joinEvent, name: name, - w: w, + sender: sender, } } // Part removes a writer from a named repeater -func (b Book) Part(name string, w io.Writer) { +func (b Book) Part(name string, sender MessageSender) { b.events <- bookEvent{ eventType: partEvent, name: name, - w: w, + sender: sender, } } @@ -81,13 +80,13 @@ func (b Book) loop() { repeater = b.makeRepeater() b.entries[event.name] = repeater } - repeater.Join(event.w) + repeater.Join(event.sender) case partEvent: if !ok { log.Println("WARN: Parting an empty channel:", event.name) break } - repeater.Part(event.w) + repeater.Part(event.sender) if repeater.Listeners() == 0 { delete(b.entries, event.name) } diff --git a/cmd/vail/main.go b/cmd/vail/main.go index 65194ee..4d679b3 100644 --- a/cmd/vail/main.go +++ b/cmd/vail/main.go @@ -27,31 +27,41 @@ func (WallClock) Now() time.Time { return time.Now() } +// VailWebSocketConnection reads and writes Message structs +type VailWebSocketConnection struct { + *websocket.Conn +} + +func (c *VailWebSocketConnection) Receive() (Message, error) { + var m Message + err := websocket.JSON.Receive(c.Conn, &m) + return m, err +} + +func (c *VailWebSocketConnection) Send(m Message) error { + return websocket.JSON.Send(c.Conn, m) +} + type Client struct { repeaterName string } func (c Client) Handle(ws *websocket.Conn) { + sock := &VailWebSocketConnection{ws} nowMilli := time.Now().UnixMilli() ws.MaxPayloadBytes = 50 - book.Join(c.repeaterName, ws) - defer book.Part(c.repeaterName, ws) + book.Join(c.repeaterName, sock) + defer book.Part(c.repeaterName, sock) for { - buf := make([]byte, ws.MaxPayloadBytes) - - if n, err := ws.Read(buf); err != nil { + m, err := sock.Receive() + if err != nil { break - } else { - buf = buf[:n] } - // Decode into a Message - var m Message - if err := m.UnmarshalBinary(buf); err != nil { - fmt.Fprintln(ws, err) - ws.Close() - return + // If it's empty, skip it + if len(m.Duration) == 0 { + continue } // If it's wildly out of time, reject it diff --git a/cmd/vail/message.go b/cmd/vail/message.go index e35e7be..0b448b1 100644 --- a/cmd/vail/message.go +++ b/cmd/vail/message.go @@ -3,9 +3,26 @@ package main import ( "bytes" "encoding/binary" + "fmt" "time" ) +// MessageSender can send Messages +type MessageSender interface { + Send(m Message) error +} + +// MessageReceiver can receive Messages +type MessageReceiver interface { + Receive() (Message, error) +} + +// MessageSocket can send and receive Messages +type MessageSocket interface { + MessageSender + MessageReceiver +} + // VailMessage is a single Vail message. type Message struct { // Timestamp of this message. Milliseconds since epoch. @@ -69,6 +86,24 @@ func (m *Message) UnmarshalBinary(data []byte) error { return nil } +func (m Message) MarshalJSON() ([]byte, error) { + buf := new(bytes.Buffer) + fmt.Fprint(buf, "{") + fmt.Fprintf(buf, "\"Timestamp\":%d,", m.Timestamp) + fmt.Fprintf(buf, "\"Clients\":%d,", m.Clients) + fmt.Fprint(buf, "\"Duration\":[") + for i := 0; i < len(m.Duration); i++ { + fmt.Fprint(buf, m.Duration[i]) + if i <= len(m.Duration)-1 { + fmt.Fprint(buf, ",") + } + } + fmt.Fprint(buf) + fmt.Fprint(buf, "]") + fmt.Fprint(buf, "}") + return buf.Bytes(), nil +} + func (m Message) Equal(m2 Message) bool { if m.Timestamp != m2.Timestamp { return false diff --git a/cmd/vail/repeater.go b/cmd/vail/repeater.go index a2d00b8..34fad55 100644 --- a/cmd/vail/repeater.go +++ b/cmd/vail/repeater.go @@ -1,38 +1,36 @@ package main import ( - "io" - "log" "time" ) -// A Repeater is just a list of Writers. +// A Repeater is just a list of senders. type Repeater struct { clock Clock - writers []io.Writer + senders []MessageSender } // NewRepeater returns a newly-created repeater func NewRepeater() *Repeater { return &Repeater{ clock: WallClock{}, - writers: make([]io.Writer, 0, 20), + senders: make([]MessageSender, 0, 20), } } // Join joins a writer to this repeater -func (r *Repeater) Join(w io.Writer) { - r.writers = append(r.writers, w) +func (r *Repeater) Join(sender MessageSender) { + r.senders = append(r.senders, sender) r.SendMessage() } // Part removes a writer from this repeater -func (r *Repeater) Part(w io.Writer) { - for i, s := range r.writers { - if s == w { - nsubs := len(r.writers) - r.writers[i] = r.writers[nsubs-1] - r.writers = r.writers[:nsubs-1] +func (r *Repeater) Part(sender MessageSender) { + for i, s := range r.senders { + if s == sender { + nsubs := len(r.senders) + r.senders[i] = r.senders[nsubs-1] + r.senders = r.senders[:nsubs-1] } } r.SendMessage() @@ -41,12 +39,8 @@ func (r *Repeater) Part(w io.Writer) { // Send send a message to all connected clients func (r *Repeater) Send(m Message) { m.Clients = uint16(r.Listeners()) - buf, err := m.MarshalBinary() - if err != nil { - log.Fatal(err) - } - for _, s := range r.writers { - s.Write(buf) + for _, s := range r.senders { + s.Send(m) } } @@ -58,5 +52,5 @@ func (r *Repeater) SendMessage(durations ...time.Duration) { // Listeners returns the number of connected clients func (r *Repeater) Listeners() int { - return len(r.writers) + return len(r.senders) } diff --git a/cmd/vail/repeater_test.go b/cmd/vail/repeater_test.go index 66d2a32..16136fc 100644 --- a/cmd/vail/repeater_test.go +++ b/cmd/vail/repeater_test.go @@ -1,8 +1,6 @@ package main import ( - "bytes" - "io" "testing" "time" ) @@ -14,35 +12,46 @@ func (f FakeClock) Now() time.Time { } type TestingClient struct { - bytes.Buffer - expected bytes.Buffer - repeater *Repeater + buf []Message + expected []Message t *testing.T } func NewTestingClient(t *testing.T) *TestingClient { return &TestingClient{ - Buffer: bytes.Buffer{}, - expected: bytes.Buffer{}, - t: t, + t: t, } } +func (tc *TestingClient) Send(m Message) error { + tc.buf = append(tc.buf, m) + return nil +} + +func (tc *TestingClient) Len() int { + return len(tc.buf) +} + func (tc *TestingClient) Expect(clients uint16, payload ...uint8) { m := Message{0, clients, payload} - buf, _ := m.MarshalBinary() - tc.expected.Write(buf) - if tc.String() != tc.expected.String() { - tc.t.Errorf("Client buffer mismatch. Wanted %#v, got %#v", tc.expected.String(), tc.String()) + tc.expected = append(tc.expected, m) + if len(tc.buf) != len(tc.expected) { + tc.t.Errorf("Client buffer mismatch. Wanted length %d, got length %d", len(tc.expected), len(tc.buf)) } - tc.Reset() - tc.expected.Reset() + for i := 0; i < len(tc.buf); i++ { + if !tc.buf[i].Equal(tc.expected[i]) { + tc.t.Errorf("Client buffer mismatch at entry %d. Wanted %#v, got %#v", i, tc.expected[i], tc.buf[i]) + } + } + + tc.buf = []Message{} + tc.expected = []Message{} } func NewTestingRepeater() *Repeater { return &Repeater{ clock: FakeClock{}, - writers: make([]io.Writer, 0, 2), + senders: make([]MessageSender, 0, 2), } } diff --git a/static/repeaters.mjs b/static/repeaters.mjs index ac6bfeb..3717b2e 100644 --- a/static/repeaters.mjs +++ b/static/repeaters.mjs @@ -4,6 +4,25 @@ const Millisecond = 1 const Second = 1000 * Millisecond const Minute = 60 * Second +/** + * Compare two messages + * + * @param {Object} m1 First message + * @param {Object} m2 Second message + * @returns {Boolean} true if messages are equal + */ +function MessageEqual(m1, m2) { + if ((m1.Timestamp != m2.Timestamp) || (m1.Duration.length != m2.Duration.length)) { + return false + } + for (let i=0; i < m1.Duration.length; i++) { + if (m1.Duration[i] != m2.Duration[i]) { + return false + } + } + return true +} + export class Vail { constructor(rx, name) { this.rx = rx @@ -28,14 +47,13 @@ export class Vail { this.clockOffset = 0 this.socket = new WebSocket(this.wsUrl) this.socket.addEventListener("message", e => this.wsMessage(e)) - this.socket.addEventListener("close", () => this.reopen()) - } - - stats() { - return { - averageLag: this.lagDurations.reduce((a,b) => (a+b), 0) / this.lagDurations.length, - clockOffset: this.clockOffset, - } + this.socket.addEventListener( + "close", + () => { + console.info("Repeater connection dropped.") + setTimeout(() => this.reopen(), 5*Second) + } + ) } wsMessage(event) { @@ -46,48 +64,51 @@ export class Vail { msg = JSON.parse(jmsg) } catch (err) { - console.error(err, jmsg) + console.error(jmsg) return } + let stats = { + averageLag: this.lagDurations.reduce((a,b) => (a+b), 0) / this.lagDurations.length, + clockOffset: this.clockOffset, + clients: msg.Clients, + } - let beginTxTime = msg[0] - let durations = msg.slice(1) - - // Why is this happening? - if (beginTxTime == 0) { + // XXX: Why is this happening? + if (msg.Timestamp == 0) { return } - let sent = this.sent.filter(e => e != jmsg) + let sent = this.sent.filter(m => !MessageEqual(msg, m)) if (sent.length < this.sent.length) { // We're getting our own message back, which tells us our lag. // We shouldn't emit a tone, though. - let totalDuration = durations.reduce((a, b) => a + b) + let totalDuration = msg.Duration.reduce((a, b) => a + b) this.sent = sent - this.lagDurations.unshift(now - this.clockOffset - beginTxTime - totalDuration) + this.lagDurations.unshift(now - this.clockOffset - msg.Timestamp - totalDuration) this.lagDurations.splice(20, 2) - this.rx(0, 0, this.stats()) + this.rx(0, 0, stats) return } - // The very first packet is the server telling us the current time - if (durations.length == 0) { + // Packets with 0 length tell us what time the server thinks it is, + // and how many clients are connected + if (msg.Duration.length == 0) { if (this.clockOffset == 0) { - this.clockOffset = now - beginTxTime - this.rx(0, 0, this.stats()) + this.clockOffset = now - msg.Timestamp + this.rx(0, 0, stats) } return } // Adjust playback time to clock offset - let adjustedTxTime = beginTxTime + this.clockOffset + let adjustedTxTime = msg.Timestamp + this.clockOffset // Every second value is a silence duration let tx = true - for (let duration of durations) { + for (let duration of msg.Duration) { duration = Number(duration) if (tx && (duration > 0)) { - this.rx(adjustedTxTime, duration, this.stats()) + this.rx(adjustedTxTime, duration, stats) } adjustedTxTime = Number(adjustedTxTime) + duration tx = !tx @@ -97,13 +118,17 @@ export class Vail { /** * Send a transmission * - * @param {number} time When to play this transmission + * @param {number} timestamp When to play this transmission * @param {number} duration How long the transmission is * @param {boolean} squelch True to mute this tone when we get it back from the repeater */ - Transmit(time, duration, squelch=true) { - let msg = [time - this.clockOffset, duration] + Transmit(timestamp, duration, squelch=true) { + let msg = { + Timestamp: timestamp, + Duration: [duration], + } let jmsg = JSON.stringify(msg) + if (this.socket.readyState != 1) { // If we aren't connected, complain. console.error("Not connected, dropping", jmsg) @@ -111,7 +136,7 @@ export class Vail { } this.socket.send(jmsg) if (squelch) { - this.sent.push(jmsg) + this.sent.push(msg) } } diff --git a/static/vail.mjs b/static/vail.mjs index 470c707..6473e4a 100644 --- a/static/vail.mjs +++ b/static/vail.mjs @@ -53,7 +53,7 @@ class VailClient { e.addEventListener("click", e => this.maximize(e)) } for (let e of document.querySelectorAll("#ck")) { - e.addEventListener("click", e => this.test()) + e.addEventListener("click", e => this.check()) } for (let e of document.querySelectorAll("#reset")) { e.addEventListener("click", e => this.reset()) @@ -416,7 +416,7 @@ class VailClient { /** * Send "CK" to server, and don't squelch the echo */ - test() { + check() { let when = Date.now() let dit = this.ditDuration let dah = dit * 3 From b7de5cf8cb92ac654ec31628e4253458294ce817 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 22 May 2022 21:37:36 -0600 Subject: [PATCH 04/10] Output MIDI notes + refactoring --- static/index.html | 2 +- static/inputs.mjs | 59 ++++++--- static/keyers.mjs | 42 +++++-- static/{buzzer.mjs => outputs.mjs} | 188 +++++++++++++++++++++++++---- static/vail.css | 4 +- static/vail.mjs | 58 ++++----- 6 files changed, 273 insertions(+), 80 deletions(-) rename static/{buzzer.mjs => outputs.mjs} (67%) diff --git a/static/index.html b/static/index.html index 07f5d82..970e203 100644 --- a/static/index.html +++ b/static/index.html @@ -72,7 +72,7 @@
- + diff --git a/static/inputs.mjs b/static/inputs.mjs index 362f4d3..290e62c 100644 --- a/static/inputs.mjs +++ b/static/inputs.mjs @@ -2,9 +2,14 @@ class Input { constructor(keyer) { this.keyer = keyer } + SetDitDuration(delay) { // Nothing } + + SetKeyerMode(mode) { + // Nothing + } } export class HTML extends Input{ @@ -117,11 +122,17 @@ export class MIDI extends Input{ this.midiStateChange() } - SetIntervalDuration(delay) { + SetDitDuration(delay) { // Send the Vail adapter the current iambic delay setting for (let output of this.midiAccess.outputs.values()) { // MIDI only supports 7-bit values, so we have to divide it by two - output.send([0x8B, 0x01, delay/2]) + output.send([0xB0, 0x01, delay/2]) + } + } + + SetKeyerMode(mode) { + for (let output of this.midiAccess.outputs.values()) { + output.send([0xC0, mode]) } } @@ -136,7 +147,7 @@ export class MIDI extends Input{ // Tell the Vail adapter to disable keyboard events: we can do MIDI! for (let output of this.midiAccess.outputs.values()) { - output.send([0x8B, 0x00, 0x00]) // Turn off keyboard mode + output.send([0xB0, 0x00, 0x00]) // Turn off keyboard mode } } @@ -229,16 +240,36 @@ export class Gamepad extends Input{ } } -/** - * Set up all input methods - * - * @param keyer Keyer object for everyone to use - */ -export function SetupAll(keyer) { - return { - HTML: new HTML(keyer), - Keyboard: new Keyboard(keyer), - MIDI: new MIDI(keyer), - Gamepad: new Gamepad(keyer), +class Collection { + constructor(keyer) { + this.html =new HTML(keyer) + this.keyboard =new Keyboard(keyer) + this.midi =new MIDI(keyer) + this.gamepad =new Gamepad(keyer) + this.collection = [this.html, this.keyboard, this.midi, this.gamepad] + } + + /** + * Set duration of all inputs + * + * @param duration Duration to set + */ + SetDitDuration(duration) { + for (let e of this.collection) { + e.SetDitDuration(duration) + } + } + + /** + * Set keyer mode of all inputs + * + * @param mode Keyer mode to set + */ + SetKeyerMode(mode) { + for (let e of this.collection) { + e.SetKeyerMode(mode) + } } } + +export {Collection} diff --git a/static/keyers.mjs b/static/keyers.mjs index 2bed7d8..e9d3874 100644 --- a/static/keyers.mjs +++ b/static/keyers.mjs @@ -53,10 +53,17 @@ class QSet extends Set { } /** - * A callback to start or stop transmission + * Definition of a transmitter type. * - * @callback TxControl + * The VailClient class implements this. */ +class Transmitter { + /** Begin transmitting */ + BeginTx() {} + + /** End transmitting */ + EndTx() {} +} /** * A straight keyer. @@ -67,12 +74,10 @@ class QSet extends Set { */ class StraightKeyer { /** - * @param {TxControl} beginTxFunc Callback to begin transmitting - * @param {TxControl} endTxFunc Callback to end transmitting + * @param {Transmitter} output Transmitter object */ - constructor(beginTxFunc, endTxFunc) { - this.beginTxFunc = beginTxFunc - this.endTxFunc = endTxFunc + constructor(output) { + this.output = output this.Reset() } @@ -89,7 +94,7 @@ class StraightKeyer { * Reset state and stop all transmissions. */ Reset() { - this.endTxFunc() + this.output.EndTx() this.txRelays = [] } @@ -140,9 +145,9 @@ class StraightKeyer { if (wasClosed != nowClosed) { if (nowClosed) { - this.beginTxFunc() + this.output.BeginTx() } else { - this.endTxFunc() + this.output.EndTx() } } } @@ -471,6 +476,19 @@ const Keyers = { robo: RoboKeyer.Keyer, } -export { - Keyers, +const Numbers = { + straight: 1, + cootie: 1, + bug: 2, + elbug: 3, + singledot: 4, + ultimatic: 5, + iambic: 6, + iambica: 7, + iambicb: 8, + keyahead: 9, +} + +export { + Keyers, Numbers, } diff --git a/static/buzzer.mjs b/static/outputs.mjs similarity index 67% rename from static/buzzer.mjs rename to static/outputs.mjs index 925c990..6cfa56d 100644 --- a/static/buzzer.mjs +++ b/static/outputs.mjs @@ -27,7 +27,9 @@ const Second = 1000 * Millisecond const OscillatorRampDuration = 5*Millisecond console.warn("Chrome will now complain about an AudioContext not being allowed to start. This is normal, and there is no way to make Chrome stop complaining about this.") -const BuzzerAudioContext = new AudioContext() +const BuzzerAudioContext = new AudioContext({ + latencyHint: 0, +}) /** * Compute the special "Audio Context" time * @@ -42,13 +44,6 @@ function BuzzerAudioContextTime(when) { return Math.max(when - acOffset, 0) / Second } -/** - * Block until the audio system is able to start making noise. - */ -async function Ready() { - await BuzzerAudioContext.resume() -} - class Oscillator { /** * Create a new oscillator, and encase it in a Gain for control. @@ -155,7 +150,7 @@ class Buzzer { * @param {boolean} tx Transmit or receive tone * @param {number} when Time to begin, in ms (0=now) */ - Buzz(tx, when=0) { + async Buzz(tx, when=0) { console.log("Buzz", tx, when) } @@ -165,7 +160,7 @@ class Buzzer { * @param {boolean} tx Transmit or receive tone * @param {number} when Time to end, in ms (0=now) */ - Silence(tx, when=0) { + async Silence(tx, when=0) { console.log("Silence", tx, when) } @@ -210,6 +205,10 @@ class ToneBuzzer extends AudioBuzzer { this.rxOsc = new Oscillator(lowFreq, txGain) this.txOsc = new Oscillator(highFreq, txGain) + + // Keep the speaker going always. This keeps the browser from "swapping out" our audio context. + this.bgOsc = new Oscillator(1, 0.001) + this.bgOsc.SoundAt() } /** @@ -218,7 +217,7 @@ class ToneBuzzer extends AudioBuzzer { * @param {boolean} tx Transmit or receive tone * @param {number} when Time to begin, in ms (0=now) */ - Buzz(tx, when = null) { + async Buzz(tx, when = null) { let osc = tx?this.txOsc:this.rxOsc osc.SoundAt(when) } @@ -229,7 +228,7 @@ class ToneBuzzer extends AudioBuzzer { * @param {boolean} tx Transmit or receive tone * @param {number} when Time to end, in ms (0=now) */ - Silence(tx, when = null) { + async Silence(tx, when = null) { let osc = tx?this.txOsc:this.rxOsc osc.HushAt(when) } @@ -249,7 +248,7 @@ class TelegraphBuzzer extends AudioBuzzer{ this.openSample = new Sample("telegraph-b.mp3") } - Buzz(tx, when=0) { + async Buzz(tx, when=0) { if (tx) { this.hum.SoundAt(when) } else { @@ -257,7 +256,7 @@ class TelegraphBuzzer extends AudioBuzzer{ } } - Silence(tx ,when=0) { + async Silence(tx ,when=0) { if (tx) { this.hum.HushAt(when) } else { @@ -266,29 +265,174 @@ class TelegraphBuzzer extends AudioBuzzer{ } } -class Lamp extends Buzzer { - constructor(element) { +class LampBuzzer extends Buzzer { + constructor() { super() - this.element = element + this.elements = document.querySelectorAll(".recv-lamp") } - Buzz(tx, when=0) { + async Buzz(tx, when=0) { if (tx) return let ms = when?when - Date.now():0 setTimeout( () =>{ - this.element.classList.add("rx") + for (let e of this.elements) { + e.classList.add("rx") + } }, ms, ) } - Silence(tx, when=0) { + async Silence(tx, when=0) { if (tx) return let ms = when?when - Date.now():0 - setTimeout(() => this.element.classList.remove("rx"), ms) + setTimeout( + () => { + for (let e of this.elements) { + e.classList.remove("rx") + } + }, + ms, + ) } } -export {Ready, ToneBuzzer, TelegraphBuzzer, Lamp} +class MIDIBuzzer extends Buzzer { + constructor() { + super() + this.SetNote(69) // A4; 440Hz + + this.midiAccess = {outputs: []} // stub while we wait for async stuff + if (navigator.requestMIDIAccess) { + this.midiInit() + } + } + + async midiInit(access) { + this.outputs = new Set() + this.midiAccess = await navigator.requestMIDIAccess() + this.midiAccess.addEventListener("statechange", e => this.midiStateChange(e)) + this.midiStateChange() + } + + midiStateChange(event) { + let newOutputs = new Set() + for (let output of this.midiAccess.outputs.values()) { + console.log(output.state) + if ((output.state != "connected") || (output.name.includes("Through"))) { + continue + } + newOutputs.add(output) + } + this.outputs = newOutputs + } + + sendAt(when, message) { + let ms = when?when - Date.now():0 + setTimeout( + () => { + for (let output of this.outputs) { + output.send(message) + } + }, + ms, + ) + } + + async Buzz(tx, when=0) { + if (tx) { + return + } + + this.sendAt(when, [0x90, this.note, 0x7f]) + } + + async Silence(tx, when=0) { + if (tx) { + return + } + + this.sendAt(when, [0x80, this.note, 0x7f]) + } + + /* + * Set note to transmit + */ + SetNote(tx, note) { + if (tx) { + return + } + this.note = note + } +} + +/** + * Block until the audio system is able to start making noise. + */ +async function AudioReady() { + await BuzzerAudioContext.resume() +} + +class Collection { + constructor() { + this.tone = new ToneBuzzer() + this.telegraph = new TelegraphBuzzer() + this.lamp = new LampBuzzer() + this.midi = new MIDIBuzzer() + this.collection = new Set([this.tone, this.lamp, this.midi]) + } + + /** + * Set the audio output type. + * + * @param {string} audioType "telegraph" for telegraph mode, otherwise tone mode + */ + SetAudioType(audioType) { + this.collection.delete(this.telegraph) + this.collection.delete(this.tone) + if (audioType == "telegraph") { + this.collection.add(this.telegraph) + } else { + this.collection.add(this.tone) + } + } + + /** + * Buzz all outputs. + * + * @param tx True if transmitting + */ + Buzz(tx=False) { + for (let b of this.collection) { + b.Buzz(tx) + } + } + + /** + * Silence all outputs. + * + * @param tx True if transmitting + */ + Silence(tx=False) { + for (let b of this.collection) { + b.Silence(tx) + } + } + + /** + * Buzz for a certain duration at a certain time + * + * @param tx True if transmitting + * @param when Time to begin + * @param duration How long to buzz + */ + BuzzDuration(tx, when, duration) { + for (let b of this.collection) { + b.BuzzDuration(tx, when, duration) + } + } +} + +export {AudioReady, Collection} diff --git a/static/vail.css b/static/vail.css index 4723afc..f35723b 100644 --- a/static/vail.css +++ b/static/vail.css @@ -16,7 +16,7 @@ -webkit-user-select: none; /* 2022-04-26 Safari still needs this */ } -#recv.rx { +.recv-lamp.rx { background-color: orange; } @@ -83,4 +83,4 @@ code { #charts canvas { height: 0.5em; width: 100%; -} \ No newline at end of file +} diff --git a/static/vail.mjs b/static/vail.mjs index 6473e4a..508ed80 100644 --- a/static/vail.mjs +++ b/static/vail.mjs @@ -1,5 +1,5 @@ -import {Keyers} from "./keyers.mjs" -import * as Buzzer from "./buzzer.mjs" +import * as Keyers from "./keyers.mjs" +import * as Outputs from "./outputs.mjs" import * as Inputs from "./inputs.mjs" import * as Repeaters from "./repeaters.mjs" import * as Chart from "./chart.mjs" @@ -10,7 +10,7 @@ const Second = 1000 * Millisecond const Minute = 60 * Second /** - * Pop up a message, using an notification.. + * Pop up a message, using an notification. * * @param {string} msg Message to display */ @@ -37,16 +37,17 @@ class VailClient { this.rxDelay = 0 * Millisecond // Time to add to incoming timestamps this.beginTxTime = null // Time when we began transmitting - // Make helpers - this.lamp = new Buzzer.Lamp(document.querySelector("#recv")) - this.buzzer = new Buzzer.ToneBuzzer() - this.straightKeyer = new Keyers.straight(() => this.beginTx(), () => this.endTx()) - this.keyer = new Keyers.straight(() => this.beginTx(), () => this.endTx()) - this.roboKeyer = new Keyers.robo(() => this.Buzz(), () => this.Silence()) + // Outputs + this.outputs = new Outputs.Collection() + + // Keyers + this.straightKeyer = new Keyers.Keyers.straight(this) + this.keyer = new Keyers.Keyers.straight(this) + this.roboKeyer = new Keyers.Keyers.robo(() => this.Buzz(), () => this.Silence()) // Set up various input methods // Send this as the keyer so we can intercept dit and dah events for charts - this.inputs = Inputs.SetupAll(this) + this.inputs = new Inputs.Collection(this) // Maximize button for (let e of document.querySelectorAll("button.maximize")) { @@ -69,9 +70,7 @@ class VailClient { } this.keyer.SetDitDuration(this.ditDuration) this.roboKeyer.SetDitDuration(this.ditDuration) - for (let i of Object.values(this.inputs)) { - i.SetDitDuration(this.ditDuration) - } + this.inputs.SetDitDuration(this.ditDuration) }) this.inputInit("#rx-delay", e => { this.rxDelay = e.target.value * Second @@ -89,7 +88,7 @@ class VailClient { this.setTimingCharts(true) // Turn off the "muted" symbol when we can start making noise - Buzzer.Ready() + Outputs.AudioReady() .then(() => { console.log("Audio context ready") document.querySelector("#muted").classList.add("is-hidden") @@ -117,12 +116,13 @@ class VailClient { } setKeyer(keyerName) { - let newKeyerClass = Keyers[keyerName] + let newKeyerClass = Keyers.Keyers[keyerName] + let newKeyerNumber = Keyers.Numbers[keyerName] if (!newKeyerClass) { console.error("Keyer not found", keyerName) return } - let newKeyer = new newKeyerClass(() => this.beginTx(), () => this.endTx()) + let newKeyer = new newKeyerClass(this) let i = 0 for (let keyName of newKeyer.KeyNames()) { let e = document.querySelector(`.key[data-key="${i}"]`) @@ -132,24 +132,23 @@ class VailClient { this.keyer.Release() this.keyer = newKeyer + this.inputs.SetKeyerMode(newKeyerNumber) + document.querySelector("#keyer-rate").dispatchEvent(new Event("input")) } Buzz() { - this.buzzer.Buzz() - this.lamp.Buzz() + this.outputs.Buzz(false) if (this.rxChart) this.rxChart.Set(1) } Silence() { - this.buzzer.Silence() - this.lamp.Silence() + this.outputs.Silence() if (this.rxChart) this.rxChart.Set(0) } BuzzDuration(tx, when, duration) { - this.buzzer.BuzzDuration(tx, when, duration) - this.lamp.BuzzDuration(tx, when, duration) + this.outputs.BuzzDuration(tx, when, duration) let chart = tx?this.txChart:this.rxChart if (chart) { @@ -163,10 +162,11 @@ class VailClient { * * Called from the keyer. */ - beginTx() { + BeginTx() { this.beginTxTime = Date.now() - this.buzzer.Buzz(true) + this.outputs.Buzz(true) if (this.txChart) this.txChart.Set(1) + } /** @@ -174,13 +174,13 @@ class VailClient { * * Called from the keyer */ - endTx() { + EndTx() { if (!this.beginTxTime) { return } let endTxTime = Date.now() let duration = endTxTime - this.beginTxTime - this.buzzer.Silence(true) + this.outputs.Silence(true) this.repeater.Transmit(this.beginTxTime, duration) this.beginTxTime = null if (this.txChart) this.txChart.Set(0) @@ -222,10 +222,10 @@ class VailClient { */ setTelegraphBuzzer(enable) { if (enable) { - this.buzzer = new Buzzer.TelegraphBuzzer() + this.outputs.SetAudioType("telegraph") toast("Telegraphs only make sound when receiving!") } else { - this.buzzer = new Buzzer.ToneBuzzer() + this.outputs.SetAudioType() } } @@ -343,7 +343,7 @@ class VailClient { */ error(msg) { toast(msg) - this.buzzer.Error() + this.outputs.Error() } /** From 15e43c28df6119821035bd6b3e7542c998163b3f Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 28 May 2022 20:30:50 -0600 Subject: [PATCH 05/10] Send keyer state every time it's connected --- static/inputs.mjs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/static/inputs.mjs b/static/inputs.mjs index 290e62c..8106183 100644 --- a/static/inputs.mjs +++ b/static/inputs.mjs @@ -108,6 +108,8 @@ export class Keyboard extends Input{ export class MIDI extends Input{ constructor(keyer) { super(keyer) + this.ditDuration = 100 + this.keyerMode = 0 this.midiAccess = {outputs: []} // stub while we wait for async stuff if (navigator.requestMIDIAccess) { @@ -122,18 +124,28 @@ export class MIDI extends Input{ this.midiStateChange() } - SetDitDuration(delay) { - // Send the Vail adapter the current iambic delay setting + sendState() { for (let output of this.midiAccess.outputs.values()) { - // MIDI only supports 7-bit values, so we have to divide it by two - output.send([0xB0, 0x01, delay/2]) + // Turn off keyboard mode + output.send([0xB0, 0x00, 0x00]) + + // MIDI only supports 7-bit values, so we have to divide ditduration by two + output.send([0xB0, 0x01, this.ditDuration/2]) + + // Send keyer mode + output.send([0xC0, this.keyerMode]) } + + } + + SetDitDuration(duration) { + this.ditDuration = duration + this.sendState() } SetKeyerMode(mode) { - for (let output of this.midiAccess.outputs.values()) { - output.send([0xC0, mode]) - } + this.keyerMode = mode + this.sendState() } midiStateChange(event) { @@ -146,9 +158,7 @@ export class MIDI extends Input{ } // Tell the Vail adapter to disable keyboard events: we can do MIDI! - for (let output of this.midiAccess.outputs.values()) { - output.send([0xB0, 0x00, 0x00]) // Turn off keyboard mode - } + this.sendState() } midiMessage(event) { From b45876bcf05c0b4fb71799b05a77b5edfa3c7cbc Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 5 Jun 2022 12:34:03 -0700 Subject: [PATCH 06/10] About to change to nhooyr/websocket --- cmd/vail/main.go | 39 ++++++++++++++++++++++++++++++++++----- go.mod | 5 ++++- go.sum | 39 +++++++++++++++++++++++++++++++++++++++ static/repeaters.mjs | 4 ++++ 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/cmd/vail/main.go b/cmd/vail/main.go index 4d679b3..27d8a61 100644 --- a/cmd/vail/main.go +++ b/cmd/vail/main.go @@ -5,9 +5,10 @@ import ( "log" "net/http" "os" + "strings" "time" - "golang.org/x/net/websocket" + "nhooyr.io/websocket" ) var book Book @@ -20,7 +21,7 @@ type Clock interface { Now() time.Time } -// WallClock provides the actual time +// WallClock is a Clock which provides the actual time type WallClock struct{} func (WallClock) Now() time.Time { @@ -30,24 +31,47 @@ func (WallClock) Now() time.Time { // VailWebSocketConnection reads and writes Message structs type VailWebSocketConnection struct { *websocket.Conn + usingJSON bool } func (c *VailWebSocketConnection) Receive() (Message, error) { var m Message - err := websocket.JSON.Receive(c.Conn, &m) + var err error + if c.usingJSON { + err = websocket.JSON.Receive(c.Conn, &m) + } else { + buf := make([]byte, 64) + if err := websocket.Message.Receive(c.Conn, &buf); err != nil { + return m, err + } + if err := m.UnmarshalBinary(buf) + } return m, err } func (c *VailWebSocketConnection) Send(m Message) error { - return websocket.JSON.Send(c.Conn, m) + if c.usingJSON { + return websocket.JSON.Send(c.Conn, m) + } else { + return websocket.Message.Send(c.Conn, m) + } +} + +func (c *VailWebSocketConnection) Error(err error) { + msg := fmt.Sprintf("Error: %#v", err) + websocket.JSON.Send(c.Conn, msg) } type Client struct { repeaterName string + usingJSON bool } func (c Client) Handle(ws *websocket.Conn) { - sock := &VailWebSocketConnection{ws} + sock := &VailWebSocketConnection{ + Conn: ws, + usingJSON: c.usingJSON, + } nowMilli := time.Now().UnixMilli() ws.MaxPayloadBytes = 50 book.Join(c.repeaterName, sock) @@ -56,6 +80,7 @@ func (c Client) Handle(ws *websocket.Conn) { for { m, err := sock.Receive() if err != nil { + sock.Error(err) break } @@ -83,6 +108,10 @@ func ChatHandler(w http.ResponseWriter, r *http.Request) { c := Client{ repeaterName: r.FormValue("repeater"), } + accept := r.Header.Get("Accept") + if strings.Contains(accept, "json") { + c.usingJSON = true + } // This API is confusing as hell. // I suspect there's a better way to do this. diff --git a/go.mod b/go.mod index 0400ad7..ad3ae82 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/nealey/vail go 1.12 -require golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 +require ( + golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 + nhooyr.io/websocket v1.8.7 // indirect +) diff --git a/go.sum b/go.sum index e679e7c..1d99ed6 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,45 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/static/repeaters.mjs b/static/repeaters.mjs index 3717b2e..a17a32f 100644 --- a/static/repeaters.mjs +++ b/static/repeaters.mjs @@ -72,6 +72,10 @@ export class Vail { clockOffset: this.clockOffset, clients: msg.Clients, } + if (typeof(msg) == "string") { + console.error(msg) + return + } // XXX: Why is this happening? if (msg.Timestamp == 0) { From 314994adcd6531234acda757b90d8e275415c2c3 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 6 Jun 2022 09:54:55 -0600 Subject: [PATCH 07/10] It works again --- cmd/vail/book_test.go | 2 +- cmd/vail/main.go | 124 ++++++++++++++++++++++---------------- cmd/vail/message.go | 30 ++------- cmd/vail/message_test.go | 8 +-- cmd/vail/repeater_test.go | 2 +- static/repeaters.mjs | 11 ++-- 6 files changed, 90 insertions(+), 87 deletions(-) diff --git a/cmd/vail/book_test.go b/cmd/vail/book_test.go index aa66412..1026832 100644 --- a/cmd/vail/book_test.go +++ b/cmd/vail/book_test.go @@ -20,7 +20,7 @@ func TestBook(t *testing.T) { c1.Expect(1) // Send to an empty channel - m := Message{0, 0, []uint8{22, 33}} + m := Message{0, 0, []uint16{22, 33}} b.Send("merf", m) b.loop() if c1.Len() > 0 { diff --git a/cmd/vail/main.go b/cmd/vail/main.go index 27d8a61..6fcc544 100644 --- a/cmd/vail/main.go +++ b/cmd/vail/main.go @@ -1,11 +1,11 @@ package main import ( - "fmt" + "context" + "encoding/json" "log" "net/http" "os" - "strings" "time" "nhooyr.io/websocket" @@ -13,6 +13,9 @@ import ( var book Book +const JsonProtocol = "json.vail.woozle.org" +const BinaryProtocol = "binary.vail.woozle.org" + // Clock defines an interface for getting the current time. // // We use this in testing to provide a fixed value for the current time, so we @@ -36,51 +39,82 @@ type VailWebSocketConnection struct { func (c *VailWebSocketConnection) Receive() (Message, error) { var m Message - var err error - if c.usingJSON { - err = websocket.JSON.Receive(c.Conn, &m) + messageType, buf, err := c.Read(context.Background()) + if err != nil { + return m, err + } + + if messageType == websocket.MessageText { + err = json.Unmarshal(buf, &m) } else { - buf := make([]byte, 64) - if err := websocket.Message.Receive(c.Conn, &buf); err != nil { - return m, err - } - if err := m.UnmarshalBinary(buf) + err = m.UnmarshalBinary(buf) } return m, err } func (c *VailWebSocketConnection) Send(m Message) error { + var err error + var buf []byte + var messageType websocket.MessageType + + log.Println("Send", m) if c.usingJSON { - return websocket.JSON.Send(c.Conn, m) + messageType = websocket.MessageText + buf, err = json.Marshal(m) } else { - return websocket.Message.Send(c.Conn, m) + messageType = websocket.MessageBinary + buf, err = m.MarshalBinary() } -} - -func (c *VailWebSocketConnection) Error(err error) { - msg := fmt.Sprintf("Error: %#v", err) - websocket.JSON.Send(c.Conn, msg) -} - -type Client struct { - repeaterName string - usingJSON bool -} - -func (c Client) Handle(ws *websocket.Conn) { - sock := &VailWebSocketConnection{ - Conn: ws, - usingJSON: c.usingJSON, + log.Println(buf, err) + if err != nil { + return err } - nowMilli := time.Now().UnixMilli() - ws.MaxPayloadBytes = 50 - book.Join(c.repeaterName, sock) - defer book.Part(c.repeaterName, sock) + + log.Println("Sending") + return c.Write(context.Background(), messageType, buf) +} + +func ChatHandler(w http.ResponseWriter, r *http.Request) { + // Set up websocket + ws, err := websocket.Accept( + w, r, + &websocket.AcceptOptions{ + Subprotocols: []string{JsonProtocol, BinaryProtocol}, + }, + ) + if err != nil { + log.Println(err) + return + } + defer ws.Close(websocket.StatusInternalError, "Internal error") + + // Create our Vail websocket connection for books to send to + sock := VailWebSocketConnection{ + Conn: ws, + } + + // websockets apparently sends a subprotocol string, so we can ignore Accept headers! + switch ws.Subprotocol() { + case JsonProtocol: + sock.usingJSON = true + case BinaryProtocol: + sock.usingJSON = false + default: + ws.Close(websocket.StatusPolicyViolation, "client must speak a vail protocol") + return + } + + // Join the repeater + repeaterName := r.FormValue("repeater") + book.Join(repeaterName, &sock) + defer book.Part(repeaterName, &sock) for { + // Read a packet m, err := sock.Receive() if err != nil { - sock.Error(err) + log.Println(err) + ws.Close(websocket.StatusInvalidFramePayloadData, err.Error()) break } @@ -90,34 +124,20 @@ func (c Client) Handle(ws *websocket.Conn) { } // If it's wildly out of time, reject it - timeDelta := (nowMilli - m.Timestamp) + timeDelta := (time.Now().UnixMilli() - m.Timestamp) if timeDelta < 0 { timeDelta = -timeDelta } if timeDelta > 9999 { - fmt.Fprintln(ws, "Bad timestamp") - ws.Close() - return + log.Println(err) + ws.Close(websocket.StatusInvalidFramePayloadData, "Your clock is off by too much") + break } - book.Send(c.repeaterName, m) + book.Send(repeaterName, m) } } -func ChatHandler(w http.ResponseWriter, r *http.Request) { - c := Client{ - repeaterName: r.FormValue("repeater"), - } - accept := r.Header.Get("Accept") - if strings.Contains(accept, "json") { - c.usingJSON = true - } - - // This API is confusing as hell. - // I suspect there's a better way to do this. - websocket.Handler(c.Handle).ServeHTTP(w, r) -} - func main() { book = NewBook() http.Handle("/chat", http.HandlerFunc(ChatHandler)) diff --git a/cmd/vail/message.go b/cmd/vail/message.go index 0b448b1..ef036e4 100644 --- a/cmd/vail/message.go +++ b/cmd/vail/message.go @@ -34,13 +34,13 @@ type Message struct { // Message timing in ms. // Timings alternate between tone and silence. // For example, `A` could be sent as [80, 80, 240] - Duration []uint8 + Duration []uint16 } func NewMessage(ts time.Time, durations ...time.Duration) Message { msg := Message{ Timestamp: ts.UnixNano() / time.Millisecond.Nanoseconds(), - Duration: make([]uint8, len(durations)), + Duration: make([]uint16, len(durations)), } for i, dns := range durations { ms := dns.Milliseconds() @@ -49,7 +49,7 @@ func NewMessage(ts time.Time, durations ...time.Duration) Message { } else if ms < 0 { ms = 0 } - msg.Duration[i] = uint8(ms) + msg.Duration[i] = uint16(ms) } return msg } @@ -69,7 +69,7 @@ func (m Message) MarshalBinary() ([]byte, error) { return w.Bytes(), nil } -// Unmarshaling presumes something else is keeping track of lengths +// UnmarshalBinary unpacks a binary buffer into a Message. func (m *Message) UnmarshalBinary(data []byte) error { r := bytes.NewReader(data) if err := binary.Read(r, binary.BigEndian, &m.Timestamp); err != nil { @@ -78,32 +78,14 @@ func (m *Message) UnmarshalBinary(data []byte) error { if err := binary.Read(r, binary.BigEndian, &m.Clients); err != nil { return err } - dlen := r.Len() - m.Duration = make([]uint8, dlen) + dlen := r.Len() / 2 + m.Duration = make([]uint16, dlen) if err := binary.Read(r, binary.BigEndian, &m.Duration); err != nil { return err } return nil } -func (m Message) MarshalJSON() ([]byte, error) { - buf := new(bytes.Buffer) - fmt.Fprint(buf, "{") - fmt.Fprintf(buf, "\"Timestamp\":%d,", m.Timestamp) - fmt.Fprintf(buf, "\"Clients\":%d,", m.Clients) - fmt.Fprint(buf, "\"Duration\":[") - for i := 0; i < len(m.Duration); i++ { - fmt.Fprint(buf, m.Duration[i]) - if i <= len(m.Duration)-1 { - fmt.Fprint(buf, ",") - } - } - fmt.Fprint(buf) - fmt.Fprint(buf, "]") - fmt.Fprint(buf, "}") - return buf.Bytes(), nil -} - func (m Message) Equal(m2 Message) bool { if m.Timestamp != m2.Timestamp { return false diff --git a/cmd/vail/message_test.go b/cmd/vail/message_test.go index dae2d44..b88e21e 100644 --- a/cmd/vail/message_test.go +++ b/cmd/vail/message_test.go @@ -7,8 +7,8 @@ import ( ) func TestMessageStruct(t *testing.T) { - m := Message{0x1122334455, 0, []uint8{0xaa, 0xbb, 0xcc}} - m2 := Message{12, 0, []uint8{1}} + m := Message{0x1122334455, 0, []uint16{0xaa, 0xbb, 0xcc}} + m2 := Message{12, 0, []uint16{1}} if !m.Equal(m) { t.Error("Equal messages did not compare equal") @@ -16,7 +16,7 @@ func TestMessageStruct(t *testing.T) { if m.Equal(m2) { t.Error("Unequal messages compared equal") } - if m.Equal(Message{m.Timestamp, 0, []uint8{1, 2, 3}}) { + if m.Equal(Message{m.Timestamp, 0, []uint16{1, 2, 3}}) { t.Error("Messages with different payloads compared equal") } @@ -24,7 +24,7 @@ func TestMessageStruct(t *testing.T) { if err != nil { t.Error(err) } - if !bytes.Equal(bm, []byte("\x00\x00\x00\x11\x22\x33\x44\x55\x00\x00\xaa\xbb\xcc")) { + if !bytes.Equal(bm, []byte("\x00\x00\x00\x11\x22\x33\x44\x55\x00\x00\x00\xaa\x00\xbb\x00\xcc")) { t.Error("Encoded wrong:", bm) } diff --git a/cmd/vail/repeater_test.go b/cmd/vail/repeater_test.go index 16136fc..383e33b 100644 --- a/cmd/vail/repeater_test.go +++ b/cmd/vail/repeater_test.go @@ -32,7 +32,7 @@ func (tc *TestingClient) Len() int { return len(tc.buf) } -func (tc *TestingClient) Expect(clients uint16, payload ...uint8) { +func (tc *TestingClient) Expect(clients uint16, payload ...uint16) { m := Message{0, clients, payload} tc.expected = append(tc.expected, m) if len(tc.buf) != len(tc.expected) { diff --git a/static/repeaters.mjs b/static/repeaters.mjs index a17a32f..c943801 100644 --- a/static/repeaters.mjs +++ b/static/repeaters.mjs @@ -45,13 +45,13 @@ export class Vail { } console.info("Attempting to reconnect", this.wsUrl.href) this.clockOffset = 0 - this.socket = new WebSocket(this.wsUrl) + this.socket = new WebSocket(this.wsUrl, ["json.vail.woozle.org"]) this.socket.addEventListener("message", e => this.wsMessage(e)) this.socket.addEventListener( - "close", - () => { - console.info("Repeater connection dropped.") - setTimeout(() => this.reopen(), 5*Second) + "close", + msg => { + console.error("Repeater connection dropped:", msg.reason) + setTimeout(() => this.reopen(), 2*Second) } ) } @@ -72,6 +72,7 @@ export class Vail { clockOffset: this.clockOffset, clients: msg.Clients, } + console.log(msg) if (typeof(msg) == "string") { console.error(msg) return From b910676539700975b0b9004a6225079e1b6fa2c4 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 6 Jun 2022 10:55:11 -0600 Subject: [PATCH 08/10] Fixed fortunes --- cmd/vail/message.go | 1 - static/index.html | 2 +- static/outputs.mjs | 39 ++++++++++++++++++++++++++++++++++++++- static/repeaters.mjs | 43 ++++++++++++++++++++++--------------------- static/vail.css | 10 +++++++++- static/vail.mjs | 3 +++ 6 files changed, 73 insertions(+), 25 deletions(-) diff --git a/cmd/vail/message.go b/cmd/vail/message.go index ef036e4..eb98270 100644 --- a/cmd/vail/message.go +++ b/cmd/vail/message.go @@ -3,7 +3,6 @@ package main import ( "bytes" "encoding/binary" - "fmt" "time" ) diff --git a/static/index.html b/static/index.html index 970e203..43e6987 100644 --- a/static/index.html +++ b/static/index.html @@ -72,7 +72,7 @@
- + diff --git a/static/outputs.mjs b/static/outputs.mjs index 6cfa56d..f4f251c 100644 --- a/static/outputs.mjs +++ b/static/outputs.mjs @@ -137,6 +137,10 @@ class Sample { * A (mostly) virtual class defining a buzzer. */ class Buzzer { + constructor() { + this.connected = true + } + /** * Signal an error */ @@ -175,6 +179,15 @@ class Buzzer { this.Buzz(tx, when) this.Silence(tx, when + duration) } + + /** + * Set the "connectedness" indicator. + * + * @param {boolean} connected True if connected + */ + SetConnected(connected) { + this.connected = connected + } } class AudioBuzzer extends Buzzer { @@ -297,6 +310,17 @@ class LampBuzzer extends Buzzer { ms, ) } + + SetConnected(connected) { + console.log(connected) + for (let e of this.elements) { + if (connected) { + e.classList.add("connected") + } else { + e.classList.remove("connected") + } + } + } } class MIDIBuzzer extends Buzzer { @@ -415,7 +439,7 @@ class Collection { * * @param tx True if transmitting */ - Silence(tx=False) { + Silence(tx=false) { for (let b of this.collection) { b.Silence(tx) } @@ -433,6 +457,19 @@ class Collection { b.BuzzDuration(tx, when, duration) } } + + /** + * Update the "connected" status display. + * + * For example, turn the receive light to black if the repeater is not connected. + * + * @param {boolean} connected True if we are "connected" + */ + SetConnected(connected) { + for (let b of this.collection) { + b.SetConnected(connected) + } + } } export {AudioReady, Collection} diff --git a/static/repeaters.mjs b/static/repeaters.mjs index c943801..a2469ef 100644 --- a/static/repeaters.mjs +++ b/static/repeaters.mjs @@ -30,6 +30,7 @@ export class Vail { this.lagDurations = [] this.sent = [] this.wantConnected = true + this.connected = false this.wsUrl = new URL("chat", window.location) this.wsUrl.protocol = this.wsUrl.protocol.replace("http", "ws") @@ -43,10 +44,18 @@ export class Vail { if (!this.wantConnected) { return } + this.rx(0, 0, {connected: false}) console.info("Attempting to reconnect", this.wsUrl.href) this.clockOffset = 0 this.socket = new WebSocket(this.wsUrl, ["json.vail.woozle.org"]) this.socket.addEventListener("message", e => this.wsMessage(e)) + this.socket.addEventListener( + "open", + msg => { + this.connected = true + this.rx(0, 0, {connected: true}) + } + ) this.socket.addEventListener( "close", msg => { @@ -71,6 +80,7 @@ export class Vail { averageLag: this.lagDurations.reduce((a,b) => (a+b), 0) / this.lagDurations.length, clockOffset: this.clockOffset, clients: msg.Clients, + connected: this.connected, } console.log(msg) if (typeof(msg) == "string") { @@ -152,13 +162,14 @@ export class Vail { } export class Null { - constructor(rx) { + constructor(rx, interval=3*Second) { this.rx = rx - this.interval = setInterval(() => this.pulse(), 3 * Second) + this.interval = setInterval(() => this.pulse(), interval) + this.pulse() } pulse() { - this.rx(0, 0, {note: "local"}) + this.rx(0, 0, {note: "local", connected: false}) } Transmit(time, duration, squelch=true) { @@ -169,51 +180,41 @@ export class Null { } } -export class Echo { +export class Echo extends Null { constructor(rx, delay=0) { - this.rx = rx + super(rx) this.delay = delay - this.Transmit(0, 0) } Transmit(time, duration, squelch=true) { this.rx(time + this.delay, duration, {note: "local"}) } - - Close() { - } } -export class Fortune { +export class Fortune extends Null { /** * * @param rx Receive callback * @param {Keyer} keyer Keyer object */ constructor(rx, keyer) { - this.rx = rx + super(rx, 1*Minute) this.keyer = keyer - - this.interval = setInterval(() => this.pulse(), 1 * Minute) this.pulse() } pulse() { - this.rx(0, 0, {note: "local"}) - if (this.keyer.Busy()) { + super.pulse() + if (!this.keyer || this.keyer.Busy()) { return } let fortune = GetFortune() - this.keyer.EnqueueAsciiString(`${fortune}\x04 `) - } - - Transmit(time, duration, squelch=true) { - // Do nothing. + this.keyer.EnqueueAsciiString(`${fortune} \x04 `) } Close() { this.keyer.Flush() - clearInterval(this.interval) + super.Close() } } \ No newline at end of file diff --git a/static/vail.css b/static/vail.css index f35723b..1d81081 100644 --- a/static/vail.css +++ b/static/vail.css @@ -16,7 +16,15 @@ -webkit-user-select: none; /* 2022-04-26 Safari still needs this */ } -.recv-lamp.rx { +.tag.recv-lamp { + background-color: #444; + color: white; +} +.tag.recv-lamp.connected { + background-color: #fec; +} +.tag.recv-lamp.rx, +.tag.recv-lamp.connected.rx { background-color: orange; } diff --git a/static/vail.mjs b/static/vail.mjs index 508ed80..938b7da 100644 --- a/static/vail.mjs +++ b/static/vail.mjs @@ -375,6 +375,9 @@ class VailClient { let longestRxDuration = this.rxDurations.reduce((a,b) => Math.max(a,b)) let suggestedDelay = ((averageLag + longestRxDuration) * 1.2).toFixed(0) + if (stats.connected !== undefined) { + this.outputs.SetConnected(stats.connected) + } this.updateReading("#note", stats.note || "☁") this.updateReading("#lag-value", averageLag) this.updateReading("#longest-rx-value", longestRxDuration) From 71c108b49c8d55f7270a24d7397181a833ce9541 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 6 Jun 2022 13:49:52 -0600 Subject: [PATCH 09/10] Cleanup --- cmd/vail/main.go | 12 ++++++++---- static/outputs.mjs | 2 -- static/repeaters.mjs | 1 - static/vail.css | 2 ++ static/vail.mjs | 4 ++-- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/cmd/vail/main.go b/cmd/vail/main.go index 6fcc544..3d66079 100644 --- a/cmd/vail/main.go +++ b/cmd/vail/main.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/json" + "fmt" "log" "net/http" "os" @@ -57,7 +58,6 @@ func (c *VailWebSocketConnection) Send(m Message) error { var buf []byte var messageType websocket.MessageType - log.Println("Send", m) if c.usingJSON { messageType = websocket.MessageText buf, err = json.Marshal(m) @@ -65,16 +65,17 @@ func (c *VailWebSocketConnection) Send(m Message) error { messageType = websocket.MessageBinary buf, err = m.MarshalBinary() } - log.Println(buf, err) if err != nil { return err } - log.Println("Sending") return c.Write(context.Background(), messageType, buf) } func ChatHandler(w http.ResponseWriter, r *http.Request) { + forwardedFor := r.Header.Get("X-Forwarded-For") + client := fmt.Sprintf("<%s|%s>", forwardedFor, r.RemoteAddr) + // Set up websocket ws, err := websocket.Accept( w, r, @@ -109,11 +110,12 @@ func ChatHandler(w http.ResponseWriter, r *http.Request) { book.Join(repeaterName, &sock) defer book.Part(repeaterName, &sock) + log.Println(client, repeaterName, "connect") + for { // Read a packet m, err := sock.Receive() if err != nil { - log.Println(err) ws.Close(websocket.StatusInvalidFramePayloadData, err.Error()) break } @@ -136,6 +138,8 @@ func ChatHandler(w http.ResponseWriter, r *http.Request) { book.Send(repeaterName, m) } + + log.Println(client, repeaterName, "disconnect") } func main() { diff --git a/static/outputs.mjs b/static/outputs.mjs index f4f251c..43f8fef 100644 --- a/static/outputs.mjs +++ b/static/outputs.mjs @@ -312,7 +312,6 @@ class LampBuzzer extends Buzzer { } SetConnected(connected) { - console.log(connected) for (let e of this.elements) { if (connected) { e.classList.add("connected") @@ -344,7 +343,6 @@ class MIDIBuzzer extends Buzzer { midiStateChange(event) { let newOutputs = new Set() for (let output of this.midiAccess.outputs.values()) { - console.log(output.state) if ((output.state != "connected") || (output.name.includes("Through"))) { continue } diff --git a/static/repeaters.mjs b/static/repeaters.mjs index a2469ef..39504ae 100644 --- a/static/repeaters.mjs +++ b/static/repeaters.mjs @@ -82,7 +82,6 @@ export class Vail { clients: msg.Clients, connected: this.connected, } - console.log(msg) if (typeof(msg) == "string") { console.error(msg) return diff --git a/static/vail.css b/static/vail.css index 1d81081..75c3b7a 100644 --- a/static/vail.css +++ b/static/vail.css @@ -22,10 +22,12 @@ } .tag.recv-lamp.connected { background-color: #fec; + color: black; } .tag.recv-lamp.rx, .tag.recv-lamp.connected.rx { background-color: orange; + color: black; } input[type=range] { diff --git a/static/vail.mjs b/static/vail.mjs index 938b7da..e407d49 100644 --- a/static/vail.mjs +++ b/static/vail.mjs @@ -281,7 +281,7 @@ class VailClient { number = Number(numberMatch[0]) } - if (name.startsWith("Fortunes")) { + if (name.startsWith("Fortunesf")) { this.roboKeyer.SetPauseMultiplier(number || 1) this.repeater = new Repeaters.Fortune(rx, this.roboKeyer) } else if (name.startsWith("Echo")) { @@ -378,7 +378,7 @@ class VailClient { if (stats.connected !== undefined) { this.outputs.SetConnected(stats.connected) } - this.updateReading("#note", stats.note || "☁") + this.updateReading("#note", stats.note || stats.clients || "😎") this.updateReading("#lag-value", averageLag) this.updateReading("#longest-rx-value", longestRxDuration) this.updateReading("#suggested-delay-value", suggestedDelay) From 67580c2746b0bc09d77ac7c43581a2ec24412f6d Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Mon, 6 Jun 2022 13:58:57 -0600 Subject: [PATCH 10/10] go mod tidy --- go.mod | 4 ++-- go.sum | 31 ++++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index ad3ae82..ca07540 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/nealey/vail go 1.12 require ( - golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 - nhooyr.io/websocket v1.8.7 // indirect + golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect + nhooyr.io/websocket v1.8.7 ) diff --git a/go.sum b/go.sum index 1d99ed6..0ceebe3 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,66 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=