From f70c2aaad1017a95bb1b576f147cc7a44b0ef8d4 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 12 Apr 2020 16:37:58 -0600 Subject: [PATCH] Code, with tests, for messages and repeaters --- message.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ message_test.go | 36 +++++++++++++++++++++++++ repeater.go | 48 +++++++++++++++++++++++++++++++++ repeater_test.go | 45 +++++++++++++++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 message.go create mode 100644 message_test.go create mode 100644 repeater.go create mode 100644 repeater_test.go diff --git a/message.go b/message.go new file mode 100644 index 0000000..ee7cb87 --- /dev/null +++ b/message.go @@ -0,0 +1,70 @@ +package main + +import ( + "bytes" + "encoding/binary" + "log" +) + +// 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 uint64 + + // Message timing in ms. + // Timings alternate between tone and silence. + // For example, `A` could be sent as [80, 80, 240] + Duration []uint8 +} + +func (m Message) MarshalBinary() ([]byte, error) { + var w bytes.Buffer + if err := binary.Write(&w, binary.BigEndian, m.Timestamp); err != nil { + return nil, err + } + dlen := uint16(len(m.Duration)) + if err := binary.Write(&w, binary.BigEndian, dlen); err != nil { + return nil, err + } + if err := binary.Write(&w, binary.BigEndian, m.Duration); err != nil { + return nil, err + } + return w.Bytes(), nil +} + +func (m *Message) UnmarshalBinary(data []byte) error { + r := bytes.NewReader(data) + if err := binary.Read(r, binary.BigEndian, &m.Timestamp); err != nil { + return err + } + log.Printf("timestamp %x", m.Timestamp) + var dlen uint16 + if err := binary.Read(r, binary.BigEndian, &dlen); err != nil { + return err + } + m.Duration = make([]uint8, dlen) + if err := binary.Read(r, binary.BigEndian, &m.Duration); err != nil { + return err + } + return nil +} + +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/message_test.go b/message_test.go new file mode 100644 index 0000000..cd6f452 --- /dev/null +++ b/message_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "bytes" + "testing" +) + +func TestMessage(t *testing.T) { + m := Message{0x1122334455667788, []uint8{0xaa, 0xbb, 0xcc}} + m2 := Message{12, []uint8{1}} + + if ! m.Equal(m) { + t.Error("Equal messages did not compare equal") + } + if m.Equal(m2) { + t.Error("Unequal messages compared equal") + } + if m.Equal(Message{m.Timestamp, []uint8{1,2,3}}) { + t.Error("Messages with different payloads compared equal") + } + + bm, err := m.MarshalBinary() + if err != nil { + t.Error(err) + } + if ! bytes.Equal(bm, []byte("\x11\x22\x33\x44\x55\x66\x77\x88\x00\x03\xaa\xbb\xcc")) { + t.Error("Encoded wrong:", bm) + } + + if err := m2.UnmarshalBinary(bm); err != nil { + t.Error(err) + } + if ! m.Equal(m2) { + t.Error("Decoded wrong", m2) + } +} diff --git a/repeater.go b/repeater.go new file mode 100644 index 0000000..38854c8 --- /dev/null +++ b/repeater.go @@ -0,0 +1,48 @@ +package main + +import ( + "io" +) + +type Repeater struct { + Joins chan io.Writer + Parts chan io.Writer + Sends chan []byte + subscribers []io.Writer +} + +func NewRepeater() *Repeater { + return &Repeater{ + Joins: make(chan io.Writer, 5), + Parts: make(chan io.Writer, 5), + Sends: make(chan []byte, 5), + subscribers: make([]io.Writer, 0, 20), + } +} + +func (r *Repeater) Run() { + for { + r.loop() + } +} + +func (r *Repeater) loop() { + select { + case w := <- r.Joins: + // Add subscriber + r.subscribers = append(r.subscribers, w) + case w := <- r.Parts: + // Remove subscriber + for i, s := range r.subscribers { + if s == w { + nsubs := len(r.subscribers) + r.subscribers[i] = r.subscribers[nsubs-1] + r.subscribers = r.subscribers[:nsubs-1] + } + } + case p := <- r.Sends: + for _, s := range r.subscribers { + s.Write(p) + } + } +} diff --git a/repeater_test.go b/repeater_test.go new file mode 100644 index 0000000..828b180 --- /dev/null +++ b/repeater_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "bytes" + "testing" +) + +func TestRepeater(t *testing.T) { + r := NewRepeater() + + buf1 := bytes.NewBufferString("buf1") + r.Joins <- buf1 + r.loop() + if len(r.subscribers) != 1 { + t.Error("Joining did nothing") + } + r.Sends <- []byte("moo") + r.loop() + if buf1.String() != "buf1moo" { + t.Error("Client 1 not repeating", buf1) + } + + buf2 := bytes.NewBufferString("buf2") + r.Joins <- buf2 + r.loop() + r.Sends <- []byte("bar") + r.loop() + if buf1.String() != "buf1moobar" { + t.Error("Client 1 not repeating", buf1) + } + if buf2.String() != "buf2bar" { + t.Error("Client 2 not repeating", buf2) + } + + r.Parts <- buf1 + r.loop() + r.Sends <- []byte("baz") + r.loop() + if buf1.String() != "buf1moobar" { + t.Error("Client 1 still getting data after part", buf1) + } + if buf2.String() != "buf2barbaz" { + t.Error("Client 2 not getting data after part", buf2) + } +}