Compare commits

..

No commits in common. "d1f2b1d0874b43246f1b04e465ecb46490c54719" and "3a2ba5d43eb540550aa933f72b0bdb481ac5ac2c" have entirely different histories.

36 changed files with 476 additions and 1126 deletions

View File

@ -1,23 +1,19 @@
package main package main
import ( import (
"io"
"log" "log"
) )
// Book maps names to repeaters
//
// It ensures that names map 1-1 to repeaters.
type Book struct { type Book struct {
entries map[string]*Repeater entries map[string]*Repeater
events chan bookEvent events chan bookEvent
makeRepeater func() *Repeater
} }
func NewBook() Book { func NewBook() Book {
return Book{ return Book{
entries: make(map[string]*Repeater), entries: make(map[string]*Repeater),
events: make(chan bookEvent, 5), events: make(chan bookEvent, 5),
makeRepeater: NewRepeater,
} }
} }
@ -32,38 +28,34 @@ const (
type bookEvent struct { type bookEvent struct {
eventType bookEventType eventType bookEventType
name string name string
sender MessageSender w io.Writer
m Message p []byte
} }
// 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{ b.events <- bookEvent{
eventType: joinEvent, eventType: joinEvent,
name: name, name: name,
sender: sender, w: w,
} }
} }
// 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{ b.events <- bookEvent{
eventType: partEvent, eventType: partEvent,
name: name, name: name,
sender: sender, w: w,
} }
} }
// Send transmits a message to the named repeater func (b Book) Send(name string, p []byte) {
func (b Book) Send(name string, m Message) {
b.events <- bookEvent{ b.events <- bookEvent{
eventType: sendEvent, eventType: sendEvent,
name: name, name: name,
m: m, p: p,
} }
} }
// Run is the endless run loop
func (b Book) Run() { func (b Book) Run() {
for { for {
b.loop() b.loop()
@ -77,16 +69,16 @@ func (b Book) loop() {
switch event.eventType { switch event.eventType {
case joinEvent: case joinEvent:
if !ok { if !ok {
repeater = b.makeRepeater() repeater = NewRepeater()
b.entries[event.name] = repeater b.entries[event.name] = repeater
} }
repeater.Join(event.sender) repeater.Join(event.w)
case partEvent: case partEvent:
if !ok { if !ok {
log.Println("WARN: Parting an empty channel:", event.name) log.Println("WARN: Parting an empty channel:", event.name)
break break
} }
repeater.Part(event.sender) repeater.Part(event.w)
if repeater.Listeners() == 0 { if repeater.Listeners() == 0 {
delete(b.entries, event.name) delete(b.entries, event.name)
} }
@ -95,6 +87,6 @@ func (b Book) loop() {
log.Println("WARN: Sending to an empty channel:", event.name) log.Println("WARN: Sending to an empty channel:", event.name)
break break
} }
repeater.Send(event.m) repeater.Send(event.p)
} }
} }

View File

@ -1,57 +1,59 @@
package main package main
import ( import (
"bytes"
"testing" "testing"
) )
func TestBook(t *testing.T) { func TestBook(t *testing.T) {
b := Book{ b := NewBook()
entries: make(map[string]*Repeater),
events: make(chan bookEvent, 5),
makeRepeater: NewTestingRepeater,
}
c1 := NewTestingClient(t) buf1 := bytes.NewBufferString("buf1")
b.Join("moo", c1) b.Join("moo", buf1)
b.loop() b.loop()
if len(b.entries) != 1 { if len(b.entries) != 1 {
t.Error("Wrong number of entries") t.Error("Wrong number of entries")
} }
c1.Expect(1)
// Send to an empty channel // Send to an empty channel
m := Message{0, 0, []uint16{22, 33}} b.Send("merf", []byte("goober"))
b.Send("merf", m)
b.loop() b.loop()
if c1.Len() > 0 { if buf1.String() != "buf1" {
t.Error("Sending to empty channel sent to non-empty channel") t.Error("Sending to empty channel sent to non-empty channel")
} }
// Send to a non-empty channel! // Send to a non-empty channel!
b.Send("moo", m) b.Send("moo", []byte("goober"))
b.loop() b.loop()
c1.Expect(1, 22, 33) if buf1.String() != "buf1goober" {
t.Error("Sending didn't work")
}
// Join another client // Join another client
c2 := NewTestingClient(t) buf2 := bytes.NewBufferString("buf2")
b.Join("moo", c2) b.Join("moo", buf2)
b.loop() b.loop()
c1.Expect(2)
c2.Expect(2)
// Send to both // Send to both
m.Duration = append(m.Duration, 44) b.Send("moo", []byte("snerk"))
b.Send("moo", m)
b.loop() b.loop()
c1.Expect(2, 22, 33, 44) if buf1.String() != "buf1goobersnerk" {
c2.Expect(2, 22, 33, 44) t.Error("Send to 2-member channel busted", buf1)
}
if buf2.String() != "buf2snerk" {
t.Error("Send to 2-member channel busted", buf2)
}
// Part a client // Part a client
b.Part("moo", c1) b.Part("moo", buf1)
b.loop() b.loop()
c2.Expect(1)
b.Send("moo", m) b.Send("moo", []byte("peanut"))
b.loop() b.loop()
c2.Expect(1, 22, 33, 44) if buf1.String() != "buf1goobersnerk" {
t.Error("Parted channel but still getting messages", buf1)
}
if buf2.String() != "buf2snerkpeanut" {
t.Error("Someone else parting somehow messed up sends", buf2)
}
} }

View File

@ -1,145 +1,49 @@
package main package main
import ( import (
"context"
"encoding/json"
"fmt" "fmt"
"time"
"os"
"log" "log"
"net/http" "net/http"
"os" "golang.org/x/net/websocket"
"time"
"nhooyr.io/websocket"
) )
var book Book var book Book
const JsonProtocol = "json.vail.woozle.org" type Client struct {
const BinaryProtocol = "binary.vail.woozle.org" repeaterName string
// 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 is a Clock which provides the actual time func (c Client) Handle(ws *websocket.Conn) {
type WallClock struct{} ws.MaxPayloadBytes = 500
book.Join(c.repeaterName, ws)
defer book.Part(c.repeaterName, ws)
func (WallClock) Now() time.Time { // Tell the client what time we think it is
return time.Now() fmt.Fprintf(ws, "[%d]", time.Now().UnixNano() / time.Millisecond.Nanoseconds())
}
// VailWebSocketConnection reads and writes Message structs for {
type VailWebSocketConnection struct { buf := make([]byte, ws.MaxPayloadBytes)
*websocket.Conn
usingJSON bool
}
func (c *VailWebSocketConnection) Receive() (Message, error) { if n, err := ws.Read(buf); err != nil {
var m Message break
messageType, buf, err := c.Read(context.Background()) } else {
if err != nil { buf = buf[:n]
return m, err }
book.Send(c.repeaterName, buf)
} }
if messageType == websocket.MessageText {
err = json.Unmarshal(buf, &m)
} else {
err = m.UnmarshalBinary(buf)
}
return m, err
}
func (c *VailWebSocketConnection) Send(m Message) error {
var err error
var buf []byte
var messageType websocket.MessageType
if c.usingJSON {
messageType = websocket.MessageText
buf, err = json.Marshal(m)
} else {
messageType = websocket.MessageBinary
buf, err = m.MarshalBinary()
}
if err != nil {
return err
}
return c.Write(context.Background(), messageType, buf)
} }
func ChatHandler(w http.ResponseWriter, r *http.Request) { func ChatHandler(w http.ResponseWriter, r *http.Request) {
forwardedFor := r.Header.Get("X-Forwarded-For") c := Client {
client := fmt.Sprintf("<%s|%s>", forwardedFor, r.RemoteAddr) repeaterName: r.FormValue("repeater"),
// 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! // This API is confusing as hell.
switch ws.Subprotocol() { // I suspect there's a better way to do this.
case JsonProtocol: websocket.Handler(c.Handle).ServeHTTP(w, r)
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)
log.Println(client, repeaterName, "connect")
for {
// Read a packet
m, err := sock.Receive()
if err != nil {
ws.Close(websocket.StatusInvalidFramePayloadData, err.Error())
break
}
// If it's empty, skip it
if len(m.Duration) == 0 {
continue
}
// If it's wildly out of time, reject it
timeDelta := time.Duration(time.Now().UnixMilli()-m.Timestamp) * time.Millisecond
if timeDelta < 0 {
timeDelta = -timeDelta
}
if timeDelta > 2*time.Second {
log.Println(err)
ws.Close(websocket.StatusInvalidFramePayloadData, "Your clock is off by too much")
break
}
book.Send(repeaterName, m)
}
log.Println(client, repeaterName, "disconnect")
} }
func main() { func main() {
@ -153,7 +57,7 @@ func main() {
port = "8080" port = "8080"
} }
log.Println("Listening on port", port) log.Println("Listening on port", port)
err := http.ListenAndServe(":"+port, nil) err := http.ListenAndServe(":" + port, nil)
if err != nil { if err != nil {
log.Fatal(err.Error()) log.Fatal(err.Error())
} }

View File

@ -6,49 +6,32 @@ import (
"time" "time"
) )
// MessageSender can send Messages // VailMessage is a single Vail message.
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
}
// Message is a single Vail message.
type Message struct { type Message struct {
// Timestamp of this message. Milliseconds since epoch. // 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 int64 Timestamp int64
// Number of connected clients. // Message timing in ms.
Clients uint16
// Message timing in milliseconds.
// Timings alternate between tone and silence. // Timings alternate between tone and silence.
// For example, `A` could be sent as [80, 80, 240] // For example, `A` could be sent as [80, 80, 240]
Duration []uint16 Duration []uint8
} }
func NewMessage(ts time.Time, durations ...time.Duration) Message { func NewMessage(ts time.Time, durations []time.Duration) Message {
msg := Message{ msg := Message{
Timestamp: ts.UnixNano() / time.Millisecond.Nanoseconds(), Timestamp: ts.UnixNano() / time.Millisecond.Nanoseconds(),
Duration: make([]uint16, len(durations)), Duration: make([]uint8, len(durations)),
} }
for i, dns := range durations { for i, dns := range durations {
ms := dns.Milliseconds() ms := dns.Milliseconds()
if ms > 255 { if (ms > 255) {
ms = 255 ms = 255
} else if ms < 0 { } else if (ms < 0) {
ms = 0 ms = 0
} }
msg.Duration[i] = uint16(ms) msg.Duration[i] = uint8(ms)
} }
return msg return msg
} }
@ -59,26 +42,20 @@ func (m Message) MarshalBinary() ([]byte, error) {
if err := binary.Write(&w, binary.BigEndian, m.Timestamp); err != nil { if err := binary.Write(&w, binary.BigEndian, m.Timestamp); err != nil {
return nil, err 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 { if err := binary.Write(&w, binary.BigEndian, m.Duration); err != nil {
return nil, err return nil, err
} }
return w.Bytes(), nil return w.Bytes(), nil
} }
// UnmarshalBinary unpacks a binary buffer into a Message. // Unmarshaling presumes something else is keeping track of lengths
func (m *Message) UnmarshalBinary(data []byte) error { func (m *Message) UnmarshalBinary(data []byte) error {
r := bytes.NewReader(data) r := bytes.NewReader(data)
if err := binary.Read(r, binary.BigEndian, &m.Timestamp); err != nil { if err := binary.Read(r, binary.BigEndian, &m.Timestamp); err != nil {
return err return err
} }
if err := binary.Read(r, binary.BigEndian, &m.Clients); err != nil { dlen := r.Len()
return err 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 { if err := binary.Read(r, binary.BigEndian, &m.Duration); err != nil {
return err return err
} }

View File

@ -6,9 +6,9 @@ import (
"time" "time"
) )
func TestMessageStruct(t *testing.T) { func TestMessage(t *testing.T) {
m := Message{0x1122334455, 0, []uint16{0xaa, 0xbb, 0xcc}} m := Message{0x1122334455, []uint8{0xaa, 0xbb, 0xcc}}
m2 := Message{12, 0, []uint16{1}} m2 := Message{12, []uint8{1}}
if !m.Equal(m) { if !m.Equal(m) {
t.Error("Equal messages did not compare equal") t.Error("Equal messages did not compare equal")
@ -16,7 +16,7 @@ func TestMessageStruct(t *testing.T) {
if m.Equal(m2) { if m.Equal(m2) {
t.Error("Unequal messages compared equal") t.Error("Unequal messages compared equal")
} }
if m.Equal(Message{m.Timestamp, 0, []uint16{1, 2, 3}}) { if m.Equal(Message{m.Timestamp, []uint8{1, 2, 3}}) {
t.Error("Messages with different payloads compared equal") t.Error("Messages with different payloads compared equal")
} }
@ -24,7 +24,7 @@ func TestMessageStruct(t *testing.T) {
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
if !bytes.Equal(bm, []byte("\x00\x00\x00\x11\x22\x33\x44\x55\x00\x00\x00\xaa\x00\xbb\x00\xcc")) { if !bytes.Equal(bm, []byte("\x00\x00\x00\x11\x22\x33\x44\x55\xaa\xbb\xcc")) {
t.Error("Encoded wrong:", bm) t.Error("Encoded wrong:", bm)
} }
@ -40,9 +40,11 @@ func TestMessageStruct(t *testing.T) {
0, 0,
m.Timestamp*time.Millisecond.Nanoseconds(), m.Timestamp*time.Millisecond.Nanoseconds(),
), ),
time.Duration(m.Duration[0])*time.Millisecond, []time.Duration{
time.Duration(m.Duration[1])*time.Millisecond, time.Duration(m.Duration[0]) * time.Millisecond,
time.Duration(m.Duration[2])*time.Millisecond, time.Duration(m.Duration[1]) * time.Millisecond,
time.Duration(m.Duration[2]) * time.Millisecond,
},
) )
if !m.Equal(m3) { if !m.Equal(m3) {
t.Error("NewMessage didn't work", m, m3) t.Error("NewMessage didn't work", m, m3)

View File

@ -1,56 +1,40 @@
package main package main
import ( import (
"time" "io"
) )
// A Repeater is just a list of senders. // A Repeater is just a list of Writers.
type Repeater struct { type Repeater struct {
clock Clock writers []io.Writer
senders []MessageSender
} }
// NewRepeater returns a newly-created repeater
func NewRepeater() *Repeater { func NewRepeater() *Repeater {
return &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) {
func (r *Repeater) Join(sender MessageSender) { r.writers = append(r.writers, w)
r.senders = append(r.senders, sender)
r.SendMessage()
} }
// Part removes a writer from this repeater func (r *Repeater) Part(w io.Writer) {
func (r *Repeater) Part(sender MessageSender) { for i, s := range r.writers {
for i, s := range r.senders { if s == w {
if s == sender { nsubs := len(r.writers)
nsubs := len(r.senders) r.writers[i] = r.writers[nsubs-1]
r.senders[i] = r.senders[nsubs-1] r.writers = r.writers[:nsubs-1]
r.senders = r.senders[:nsubs-1]
} }
} }
r.SendMessage()
} }
// Send send a message to all connected clients func (r *Repeater) Send(p []byte) {
func (r *Repeater) Send(m Message) { for _, s := range r.writers {
m.Clients = uint16(r.Listeners()) s.Write(p)
for _, s := range r.senders {
s.Send(m)
} }
} }
// 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 { func (r *Repeater) Listeners() int {
return len(r.senders) return len(r.writers)
} }

View File

@ -1,85 +1,39 @@
package main package main
import ( import (
"bytes"
"testing" "testing"
"time"
) )
type FakeClock struct{}
func (f FakeClock) Now() time.Time {
return time.UnixMilli(0)
}
type TestingClient struct {
buf []Message
expected []Message
t *testing.T
}
func NewTestingClient(t *testing.T) *TestingClient {
return &TestingClient{
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 ...uint16) {
m := Message{0, clients, payload}
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))
}
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{},
senders: make([]MessageSender, 0, 2),
}
}
func TestRepeater(t *testing.T) { func TestRepeater(t *testing.T) {
r := NewTestingRepeater() r := NewRepeater()
c1 := NewTestingClient(t) buf1 := bytes.NewBufferString("buf1")
r.Join(c1) r.Join(buf1)
c1.Expect(1) if r.Listeners() != 1 {
t.Error("Joining did nothing")
}
r.Send([]byte("moo"))
if buf1.String() != "buf1moo" {
t.Error("Client 1 not repeating", buf1)
}
r.SendMessage(15 * time.Millisecond) buf2 := bytes.NewBufferString("buf2")
c1.Expect(1, 15) r.Join(buf2)
r.Send([]byte("bar"))
if buf1.String() != "buf1moobar" {
t.Error("Client 1 not repeating", buf1)
}
if buf2.String() != "buf2bar" {
t.Error("Client 2 not repeating", buf2)
}
c2 := NewTestingClient(t) r.Part(buf1)
r.Join(c2) r.Send([]byte("baz"))
c1.Expect(2) if buf1.String() != "buf1moobar" {
c2.Expect(2) t.Error("Client 1 still getting data after part", buf1)
}
r.SendMessage(58 * time.Millisecond) if buf2.String() != "buf2barbaz" {
c1.Expect(2, 58) t.Error("Client 2 not getting data after part", buf2)
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")
} }
} }

6
go.mod
View File

@ -2,8 +2,4 @@ module github.com/nealey/vail
go 1.12 go 1.12
require ( require golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5
github.com/gin-gonic/gin v1.8.1 // indirect
github.com/klauspost/compress v1.15.6 // indirect
nhooyr.io/websocket v1.8.7
)

137
go.sum
View File

@ -1,133 +1,6 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
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/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
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/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
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/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
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/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
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/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
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/go-cmp v0.5.5/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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
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/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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 v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
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=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
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=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=

7
static/b0.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="-1 -1 15 15" xmlns="http://www.w3.org/2000/svg">
<circle cx= "6" cy="10" r="2" style="stroke: #000;"/>
<circle cx="10" cy= "6" r="2" style="stroke: #000; fill: none;"/>
<circle cx= "2" cy= "6" r="2" style="stroke: #000; fill: none;"/>
<circle cx= "6" cy= "2" r="2" style="stroke: #000; fill: none;"/>
</svg>

After

Width:  |  Height:  |  Size: 369 B

7
static/b1.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="-1 -1 15 15" xmlns="http://www.w3.org/2000/svg">
<circle cx= "6" cy="10" r="2" style="stroke: #000; fill: none;"/>
<circle cx="10" cy= "6" r="2" style="stroke: #000;"/>
<circle cx= "2" cy= "6" r="2" style="stroke: #000; fill: none;"/>
<circle cx= "6" cy= "2" r="2" style="stroke: #000; fill: none;"/>
</svg>

After

Width:  |  Height:  |  Size: 369 B

7
static/b2.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="-1 -1 15 15" xmlns="http://www.w3.org/2000/svg">
<circle cx= "6" cy="10" r="2" style="stroke: #000; fill: none;"/>
<circle cx="10" cy= "6" r="2" style="stroke: #000; fill: none;"/>
<circle cx= "2" cy= "6" r="2" style="stroke: #000;"/>
<circle cx= "6" cy= "2" r="2" style="stroke: #000; fill: none;"/>
</svg>

After

Width:  |  Height:  |  Size: 369 B

7
static/b3.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="-1 -1 15 15" xmlns="http://www.w3.org/2000/svg">
<circle cx= "6" cy="10" r="2" style="stroke: #000; fill: none;"/>
<circle cx="10" cy= "6" r="2" style="stroke: #000; fill: none;"/>
<circle cx= "2" cy= "6" r="2" style="stroke: #000; fill: none;"/>
<circle cx= "6" cy= "2" r="2" style="stroke: #000;"/>
</svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@ -27,9 +27,7 @@ const Second = 1000 * Millisecond
const OscillatorRampDuration = 5*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.") 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 * Compute the special "Audio Context" time
* *
@ -44,6 +42,13 @@ function BuzzerAudioContextTime(when) {
return Math.max(when - acOffset, 0) / Second 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 { class Oscillator {
/** /**
* Create a new oscillator, and encase it in a Gain for control. * Create a new oscillator, and encase it in a Gain for control.
@ -137,10 +142,6 @@ class Sample {
* A (mostly) virtual class defining a buzzer. * A (mostly) virtual class defining a buzzer.
*/ */
class Buzzer { class Buzzer {
constructor() {
this.connected = true
}
/** /**
* Signal an error * Signal an error
*/ */
@ -154,7 +155,7 @@ class Buzzer {
* @param {boolean} tx Transmit or receive tone * @param {boolean} tx Transmit or receive tone
* @param {number} when Time to begin, in ms (0=now) * @param {number} when Time to begin, in ms (0=now)
*/ */
async Buzz(tx, when=0) { Buzz(tx, when=0) {
console.log("Buzz", tx, when) console.log("Buzz", tx, when)
} }
@ -164,7 +165,7 @@ class Buzzer {
* @param {boolean} tx Transmit or receive tone * @param {boolean} tx Transmit or receive tone
* @param {number} when Time to end, in ms (0=now) * @param {number} when Time to end, in ms (0=now)
*/ */
async Silence(tx, when=0) { Silence(tx, when=0) {
console.log("Silence", tx, when) console.log("Silence", tx, when)
} }
@ -179,15 +180,6 @@ class Buzzer {
this.Buzz(tx, when) this.Buzz(tx, when)
this.Silence(tx, when + duration) this.Silence(tx, when + duration)
} }
/**
* Set the "connectedness" indicator.
*
* @param {boolean} connected True if connected
*/
SetConnected(connected) {
this.connected = connected
}
} }
class AudioBuzzer extends Buzzer { class AudioBuzzer extends Buzzer {
@ -218,12 +210,6 @@ class ToneBuzzer extends AudioBuzzer {
this.rxOsc = new Oscillator(lowFreq, txGain) this.rxOsc = new Oscillator(lowFreq, txGain)
this.txOsc = new Oscillator(highFreq, txGain) this.txOsc = new Oscillator(highFreq, txGain)
// Keep the speaker going always. This keeps the browser from "swapping out" our audio context.
if (false) {
this.bgOsc = new Oscillator(1, 0.001)
this.bgOsc.SoundAt()
}
} }
/** /**
@ -232,7 +218,7 @@ class ToneBuzzer extends AudioBuzzer {
* @param {boolean} tx Transmit or receive tone * @param {boolean} tx Transmit or receive tone
* @param {number} when Time to begin, in ms (0=now) * @param {number} when Time to begin, in ms (0=now)
*/ */
async Buzz(tx, when = null) { Buzz(tx, when = null) {
let osc = tx?this.txOsc:this.rxOsc let osc = tx?this.txOsc:this.rxOsc
osc.SoundAt(when) osc.SoundAt(when)
} }
@ -243,7 +229,7 @@ class ToneBuzzer extends AudioBuzzer {
* @param {boolean} tx Transmit or receive tone * @param {boolean} tx Transmit or receive tone
* @param {number} when Time to end, in ms (0=now) * @param {number} when Time to end, in ms (0=now)
*/ */
async Silence(tx, when = null) { Silence(tx, when = null) {
let osc = tx?this.txOsc:this.rxOsc let osc = tx?this.txOsc:this.rxOsc
osc.HushAt(when) osc.HushAt(when)
} }
@ -259,11 +245,11 @@ class TelegraphBuzzer extends AudioBuzzer{
this.hum = new Oscillator(140, 0.005, "sawtooth") this.hum = new Oscillator(140, 0.005, "sawtooth")
this.closeSample = new Sample("../assets/telegraph-a.mp3") this.closeSample = new Sample("telegraph-a.mp3")
this.openSample = new Sample("../assets/telegraph-b.mp3") this.openSample = new Sample("telegraph-b.mp3")
} }
async Buzz(tx, when=0) { Buzz(tx, when=0) {
if (tx) { if (tx) {
this.hum.SoundAt(when) this.hum.SoundAt(when)
} else { } else {
@ -271,7 +257,7 @@ class TelegraphBuzzer extends AudioBuzzer{
} }
} }
async Silence(tx ,when=0) { Silence(tx ,when=0) {
if (tx) { if (tx) {
this.hum.HushAt(when) this.hum.HushAt(when)
} else { } else {
@ -280,196 +266,29 @@ class TelegraphBuzzer extends AudioBuzzer{
} }
} }
class LampBuzzer extends Buzzer { class Lamp extends Buzzer {
constructor() { constructor(element) {
super() super()
this.elements = document.querySelectorAll(".recv-lamp") this.element = element
} }
async Buzz(tx, when=0) { Buzz(tx, when=0) {
if (tx) return if (tx) return
let ms = when?when - Date.now():0 let ms = when?when - Date.now():0
setTimeout( setTimeout(
() =>{ () =>{
for (let e of this.elements) { this.element.classList.add("rx")
e.classList.add("rx")
}
}, },
ms, ms,
) )
} }
async Silence(tx, when=0) { Silence(tx, when=0) {
if (tx) return if (tx) return
let ms = when?when - Date.now():0 let ms = when?when - Date.now():0
setTimeout( setTimeout(() => this.element.classList.remove("rx"), ms)
() => {
for (let e of this.elements) {
e.classList.remove("rx")
}
},
ms,
)
}
SetConnected(connected) {
for (let e of this.elements) {
if (connected) {
e.classList.add("connected")
} else {
e.classList.remove("connected")
}
}
} }
} }
class MIDIBuzzer extends Buzzer { export {Ready, ToneBuzzer, TelegraphBuzzer, Lamp}
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()) {
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)
}
}
/**
* 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}

11
static/chart.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>Chart-O-Matic</title>
<script type="module" src="chart.mjs"></script>
</head>
<body>
<h1>The Amazing Chart-O-Matic</h1>
<canvas id="chart" style="border: solid black 1px; width: 100%;"></canvas>
</body>
</html>

View File

@ -1,33 +0,0 @@
title:
repeater: Repeater
mode: Mode
notes: Notes
knobs: Knobs
key:
key: Key
dit: Dit
dah: Dah
keyer:
cootie: Straight Key / Cootie
bug: Bug
elbug: Electronic Bug
singledot: Single Dot
ultimatic: Ultimatic
iambic: Iambic (plain)
iambica: Iambic A
iambicb: Iambic B
keyahead: Keyahea
label:
wpm: WPM
ms: ms
wiki: Help
rx-delay: receive delay
telegraph-sounds: Telegraph sounds
title:
discord: Text/Voice chat on Discord
wiki: Vail Wiki
description:
ck: Send <code>CK</code> (check) to the repeater, and play when it comes back.
reset: Reset all Vail preferences to default.
notes: Enter your own notes here.

View File

@ -1,32 +0,0 @@
title:
repeater: Répéteur
mode: Mode
notes: Remarques
knobs: Réglages
key:
key: Key
dit: Dit
dah: Dah
keyer:
cootie: Straight Key / Cootie
bug: Bug
elbug: Electronic Bug
singledot: Single Dot
ultimatic: Ultimatic
iambic: Iambic (plain)
iambica: Iambic A
iambicb: Iambic B
keyahead: Keyahead
label:
wpm: WPM
ms: ms
wiki: Help
rx-delay: retard de réception
telegraph-sounds: Sons télégraphiques
title:
discord: Text/Voice chat on Discord
wiki: Vail Wiki
description:
ck: Transmettre <code>CK</code> (check) au répéteur, et le jouer quand il arrive.
reset: Réinitialiser les paramètres par défaut.
notes: Écrivez vos nôtes ici.

View File

@ -11,17 +11,17 @@
<!-- Vail stuff --> <!-- Vail stuff -->
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<link rel="icon" href="assets/vail.png" sizes="256x256" type="image/png"> <link rel="icon" href="vail.png" sizes="256x256" type="image/png">
<link rel="icon" href="assets/vail.svg" sizes="any" type="image/svg+xml"> <link rel="icon" href="vail.svg" sizes="any" type="image/svg+xml">
<script type="module" src="scripts/vail.mjs"></script> <script type="module" src="vail.mjs"></script>
<script type="module" src="scripts/ui.mjs"></script> <script type="module" src="ui.mjs"></script>
<link rel="stylesheet" href="vail.css"> <link rel="stylesheet" href="vail.css">
</head> </head>
<body> <body>
<nav class="navbar is-dark"> <nav class="navbar is-dark">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item"> <a class="navbar-item">
<img class="" src="assets/vail.svg" alt=""> <img class="" src="vail.svg" alt="">
<div class="block">Vail</div> <div class="block">Vail</div>
</a> </a>
</div> </div>
@ -46,7 +46,7 @@
<div class="level-right"> <div class="level-right">
<div class="level-item"> <div class="level-item">
<!-- This appears as a little light that turns on when someone's sending --> <!-- This appears as a little light that turns on when someone's sending -->
<span class="tag recv-lamp"> <span class="tag" id="recv">
<output class="has-text-info" id="note"></output> <output class="has-text-info" id="note"></output>
<i class="mdi mdi-volume-off" id="muted"></i> <i class="mdi mdi-volume-off" id="muted"></i>
</span> </span>

View File

@ -2,14 +2,9 @@ class Input {
constructor(keyer) { constructor(keyer) {
this.keyer = keyer this.keyer = keyer
} }
SetDitDuration(delay) { SetDitDuration(delay) {
// Nothing // Nothing
} }
SetKeyerMode(mode) {
// Nothing
}
} }
export class HTML extends Input{ export class HTML extends Input{
@ -53,7 +48,9 @@ export class Keyboard extends Input{
// Listen for keystrokes // Listen for keystrokes
document.addEventListener("keydown", e => this.keyboard(e)) document.addEventListener("keydown", e => this.keyboard(e))
document.addEventListener("keyup", e => this.keyboard(e)) document.addEventListener("keyup", e => this.keyboard(e))
window.addEventListener("blur", e => this.loseFocus(e))
// VBand: the keyboard input needs to know whether vband's "left" should be dit or straight
this.iambic = false
} }
keyboard(event) { keyboard(event) {
@ -101,28 +98,11 @@ export class Keyboard extends Input{
} }
} }
} }
loseFocus(event) {
if (this.ditDown) {
this.keyer.Key(0, false)
this.ditDown = false
}
if (this.dahDown) {
this.keyer.Key(1, false)
this.dahDown = false
}
if (this.straightDown) {
this.keyer.key(2, false)
this.straightDown = false
}
}
} }
export class MIDI extends Input{ export class MIDI extends Input{
constructor(keyer) { constructor(keyer) {
super(keyer) super(keyer)
this.ditDuration = 100
this.keyerMode = 0
this.midiAccess = {outputs: []} // stub while we wait for async stuff this.midiAccess = {outputs: []} // stub while we wait for async stuff
if (navigator.requestMIDIAccess) { if (navigator.requestMIDIAccess) {
@ -137,28 +117,12 @@ export class MIDI extends Input{
this.midiStateChange() this.midiStateChange()
} }
sendState() { SetIntervalDuration(delay) {
// Send the Vail adapter the current iambic delay setting
for (let output of this.midiAccess.outputs.values()) { for (let output of this.midiAccess.outputs.values()) {
// Turn off keyboard mode // MIDI only supports 7-bit values, so we have to divide it by two
output.send([0xB0, 0x00, 0x00]) output.send([0x8B, 0x01, delay/2])
// 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) {
this.keyerMode = mode
this.sendState()
} }
midiStateChange(event) { midiStateChange(event) {
@ -171,7 +135,9 @@ export class MIDI extends Input{
} }
// Tell the Vail adapter to disable keyboard events: we can do MIDI! // Tell the Vail adapter to disable keyboard events: we can do MIDI!
this.sendState() for (let output of this.midiAccess.outputs.values()) {
output.send([0x8B, 0x00, 0x00]) // Turn off keyboard mode
}
} }
midiMessage(event) { midiMessage(event) {
@ -263,36 +229,16 @@ export class Gamepad extends Input{
} }
} }
class Collection { /**
constructor(keyer) { * Set up all input methods
this.html =new HTML(keyer) *
this.keyboard =new Keyboard(keyer) * @param keyer Keyer object for everyone to use
this.midi =new MIDI(keyer) */
this.gamepad =new Gamepad(keyer) export function SetupAll(keyer) {
this.collection = [this.html, this.keyboard, this.midi, this.gamepad] return {
} HTML: new HTML(keyer),
Keyboard: new Keyboard(keyer),
/** MIDI: new MIDI(keyer),
* Set duration of all inputs Gamepad: new Gamepad(keyer),
*
* @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}

View File

@ -53,17 +53,10 @@ class QSet extends Set {
} }
/** /**
* Definition of a transmitter type. * A callback to start or stop transmission
* *
* The VailClient class implements this. * @callback TxControl
*/ */
class Transmitter {
/** Begin transmitting */
BeginTx() {}
/** End transmitting */
EndTx() {}
}
/** /**
* A straight keyer. * A straight keyer.
@ -74,10 +67,12 @@ class Transmitter {
*/ */
class StraightKeyer { class StraightKeyer {
/** /**
* @param {Transmitter} output Transmitter object * @param {TxControl} beginTxFunc Callback to begin transmitting
* @param {TxControl} endTxFunc Callback to end transmitting
*/ */
constructor(output) { constructor(beginTxFunc, endTxFunc) {
this.output = output this.beginTxFunc = beginTxFunc
this.endTxFunc = endTxFunc
this.Reset() this.Reset()
} }
@ -94,7 +89,7 @@ class StraightKeyer {
* Reset state and stop all transmissions. * Reset state and stop all transmissions.
*/ */
Reset() { Reset() {
this.output.EndTx() this.endTxFunc()
this.txRelays = [] this.txRelays = []
} }
@ -145,9 +140,9 @@ class StraightKeyer {
if (wasClosed != nowClosed) { if (wasClosed != nowClosed) {
if (nowClosed) { if (nowClosed) {
this.output.BeginTx() this.beginTxFunc()
} else { } else {
this.output.EndTx() this.endTxFunc()
} }
} }
} }
@ -476,19 +471,6 @@ const Keyers = {
robo: RoboKeyer.Keyer, robo: RoboKeyer.Keyer,
} }
const Numbers = {
straight: 1,
cootie: 1,
bug: 2,
elbug: 3,
singledot: 4,
ultimatic: 5,
iambic: 6,
iambica: 7,
iambicb: 8,
keyahead: 9,
}
export { export {
Keyers, Numbers, Keyers,
} }

View File

@ -7,7 +7,7 @@
"theme_color": "#009688", "theme_color": "#009688",
"description": "Internet Morse Code client", "description": "Internet Morse Code client",
"icons": [ "icons": [
{"src": "assets/vail.png", "sizes": "250x250"}, {"src": "vail.png", "sizes": "250x250"},
{"src": "assets/vail.svg", "sizes": "150x150"} {"src": "vail.svg", "sizes": "150x150"}
] ]
} }

189
static/repeaters.mjs Normal file
View File

@ -0,0 +1,189 @@
import {GetFortune} from "./fortunes.mjs"
const Millisecond = 1
const Second = 1000 * Millisecond
const Minute = 60 * Second
export class Vail {
constructor(rx, name) {
this.rx = rx
this.name = name
this.lagDurations = []
this.sent = []
this.wantConnected = true
this.wsUrl = new URL("chat", window.location)
this.wsUrl.protocol = this.wsUrl.protocol.replace("http", "ws")
this.wsUrl.pathname = this.wsUrl.pathname.replace("testing/", "") // Allow staging deploys
this.wsUrl.searchParams.set("repeater", name)
this.reopen()
}
reopen() {
if (!this.wantConnected) {
return
}
console.info("Attempting to reconnect", this.wsUrl.href)
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,
}
}
wsMessage(event) {
let now = Date.now()
let jmsg = event.data
let msg
try {
msg = JSON.parse(jmsg)
}
catch (err) {
console.error(err, jmsg)
return
}
let beginTxTime = msg[0]
let durations = msg.slice(1)
// Why is this happening?
if (beginTxTime == 0) {
return
}
let sent = this.sent.filter(e => e != jmsg)
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)
this.sent = sent
this.lagDurations.unshift(now - this.clockOffset - beginTxTime - totalDuration)
this.lagDurations.splice(20, 2)
this.rx(0, 0, this.stats())
return
}
// The very first packet is the server telling us the current time
if (durations.length == 0) {
if (this.clockOffset == 0) {
this.clockOffset = now - beginTxTime
this.rx(0, 0, this.stats())
}
return
}
// Adjust playback time to clock offset
let adjustedTxTime = beginTxTime + this.clockOffset
// Every second value is a silence duration
let tx = true
for (let duration of durations) {
duration = Number(duration)
if (tx && (duration > 0)) {
this.rx(adjustedTxTime, duration, this.stats())
}
adjustedTxTime = Number(adjustedTxTime) + duration
tx = !tx
}
}
/**
* Send a transmission
*
* @param {number} time 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]
let jmsg = JSON.stringify(msg)
if (this.socket.readyState != 1) {
// If we aren't connected, complain.
console.error("Not connected, dropping", jmsg)
return
}
this.socket.send(jmsg)
if (squelch) {
this.sent.push(jmsg)
}
}
Close() {
this.wantConnected = false
this.socket.close()
}
}
export class Null {
constructor(rx) {
this.rx = rx
this.interval = setInterval(() => this.pulse(), 3 * Second)
}
pulse() {
this.rx(0, 0, {note: "local"})
}
Transmit(time, duration, squelch=true) {
}
Close() {
clearInterval(this.interval)
}
}
export class Echo {
constructor(rx, delay=0) {
this.rx = 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 {
/**
*
* @param rx Receive callback
* @param {Keyer} keyer Keyer object
*/
constructor(rx, keyer) {
this.rx = rx
this.keyer = keyer
this.interval = setInterval(() => this.pulse(), 1 * Minute)
this.pulse()
}
pulse() {
this.rx(0, 0, {note: "local"})
if (this.keyer.Busy()) {
return
}
let fortune = GetFortune()
this.keyer.EnqueueAsciiString(`${fortune}\x04 `)
}
Transmit(time, duration, squelch=true) {
// Do nothing.
}
Close() {
this.keyer.Flush()
clearInterval(this.interval)
}
}

View File

@ -1,229 +0,0 @@
import {GetFortune} from "./fortunes.mjs"
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
this.name = name
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")
this.wsUrl.pathname = this.wsUrl.pathname.replace("testing/", "") // Allow staging deploys
this.wsUrl.searchParams.set("repeater", name)
this.reopen()
}
reopen() {
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, notice: "Repeater connected"})
}
)
this.socket.addEventListener(
"close",
msg => {
this.rx(0, 0, {connected: false, notice: `Repeater disconnected: ${msg.reason}`})
console.error("Repeater connection dropped:", msg.reason)
setTimeout(() => this.reopen(), 2*Second)
}
)
}
wsMessage(event) {
let now = Date.now()
let jmsg = event.data
let msg
try {
msg = JSON.parse(jmsg)
}
catch (err) {
console.error(err, jmsg)
return
}
let stats = {
averageLag: this.lagDurations.reduce((a,b) => (a+b), 0) / this.lagDurations.length,
clockOffset: this.clockOffset,
clients: msg.Clients,
connected: this.connected,
}
// XXX: Why is this happening?
if (msg.Timestamp == 0) {
console.debug("Got timestamp=0", msg)
return
}
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 = msg.Duration.reduce((a, b) => a + b)
this.sent = sent
this.lagDurations.unshift(now - this.clockOffset - msg.Timestamp - totalDuration)
this.lagDurations.splice(20, 2)
this.rx(0, 0, stats)
return
}
// Packets with 0 length tell us what time the server thinks it is,
// and how many clients are connected
if (msg.Duration.length == 0) {
this.clockOffset = now - msg.Timestamp
this.rx(0, 0, stats)
return
}
// Adjust playback time to clock offset
let adjustedTxTime = msg.Timestamp + this.clockOffset
// Every second value is a silence duration
let tx = true
for (let duration of msg.Duration) {
duration = Number(duration)
if (tx && (duration > 0)) {
this.rx(adjustedTxTime, duration, stats)
}
adjustedTxTime = Number(adjustedTxTime) + duration
tx = !tx
}
}
/**
* Send a 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(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)
return
}
this.socket.send(jmsg)
if (squelch) {
this.sent.push(msg)
}
}
Close() {
this.wantConnected = false
this.socket.close()
}
}
export class Null {
constructor(rx, interval=3*Second) {
this.rx = rx
this.init()
}
notice(msg) {
this.rx(0, 0, {connected: false, notice: msg})
}
init() {
this.notice("Null repeater: nobody will hear you.")
}
Transmit(time, duration, squelch=true) {}
Close() {}
}
export class Echo extends Null {
constructor(rx, delay=0) {
super(rx)
this.delay = delay
}
init () {
this.notice("Echo repeater: you can only hear yourself.")
}
Transmit(time, duration, squelch=true) {
this.rx(time + this.delay, duration, {note: "local"})
}
}
export class Fortune extends Null {
/**
*
* @param rx Receive callback
* @param {Keyer} keyer Robokeyer
*/
constructor(rx, keyer) {
super(rx)
this.keyer = keyer
}
init() {
this.notice("Say something, and I will tell you your fortune.")
}
pulse() {
this.timeout = null
if (!this.keyer || this.keyer.Busy()) {
return
}
let fortune = GetFortune()
this.keyer.EnqueueAsciiString(`${fortune} \x04 `)
}
Transmit(time, duration, squelch=true) {
if (this.timeout) {
clearTimeout(this.timeout)
}
this.timeout = setTimeout(() => this.pulse(), 3 * Second)
}
Close() {
this.keyer.Flush()
super.Close()
}
}

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -16,18 +16,8 @@
-webkit-user-select: none; /* 2022-04-26 Safari still needs this */ -webkit-user-select: none; /* 2022-04-26 Safari still needs this */
} }
.tag.recv-lamp { #recv.rx {
background-color: #444;
color: white;
}
.tag.recv-lamp.connected {
background-color: #fec;
color: black;
}
.tag.recv-lamp.rx,
.tag.recv-lamp.connected.rx {
background-color: orange; background-color: orange;
color: black;
} }
input[type=range] { input[type=range] {

View File

@ -1,5 +1,5 @@
import * as Keyers from "./keyers.mjs" import {Keyers} from "./keyers.mjs"
import * as Outputs from "./outputs.mjs" import * as Buzzer from "./buzzer.mjs"
import * as Inputs from "./inputs.mjs" import * as Inputs from "./inputs.mjs"
import * as Repeaters from "./repeaters.mjs" import * as Repeaters from "./repeaters.mjs"
import * as Chart from "./chart.mjs" import * as Chart from "./chart.mjs"
@ -10,7 +10,7 @@ const Second = 1000 * Millisecond
const Minute = 60 * Second const Minute = 60 * Second
/** /**
* Pop up a message, using an notification. * Pop up a message, using an notification..
* *
* @param {string} msg Message to display * @param {string} msg Message to display
*/ */
@ -37,24 +37,23 @@ class VailClient {
this.rxDelay = 0 * Millisecond // Time to add to incoming timestamps this.rxDelay = 0 * Millisecond // Time to add to incoming timestamps
this.beginTxTime = null // Time when we began transmitting this.beginTxTime = null // Time when we began transmitting
// Outputs // Make helpers
this.outputs = new Outputs.Collection() this.lamp = new Buzzer.Lamp(document.querySelector("#recv"))
this.buzzer = new Buzzer.ToneBuzzer()
// Keyers this.straightKeyer = new Keyers.straight(() => this.beginTx(), () => this.endTx())
this.straightKeyer = new Keyers.Keyers.straight(this) this.keyer = new Keyers.straight(() => this.beginTx(), () => this.endTx())
this.keyer = new Keyers.Keyers.straight(this) this.roboKeyer = new Keyers.robo(() => this.Buzz(), () => this.Silence())
this.roboKeyer = new Keyers.Keyers.robo(() => this.Buzz(), () => this.Silence())
// Set up various input methods // Set up various input methods
// Send this as the keyer so we can intercept dit and dah events for charts // Send this as the keyer so we can intercept dit and dah events for charts
this.inputs = new Inputs.Collection(this) this.inputs = Inputs.SetupAll(this)
// Maximize button // Maximize button
for (let e of document.querySelectorAll("button.maximize")) { for (let e of document.querySelectorAll("button.maximize")) {
e.addEventListener("click", e => this.maximize(e)) e.addEventListener("click", e => this.maximize(e))
} }
for (let e of document.querySelectorAll("#ck")) { for (let e of document.querySelectorAll("#ck")) {
e.addEventListener("click", e => this.check()) e.addEventListener("click", e => this.test())
} }
for (let e of document.querySelectorAll("#reset")) { for (let e of document.querySelectorAll("#reset")) {
e.addEventListener("click", e => this.reset()) e.addEventListener("click", e => this.reset())
@ -70,7 +69,9 @@ class VailClient {
} }
this.keyer.SetDitDuration(this.ditDuration) this.keyer.SetDitDuration(this.ditDuration)
this.roboKeyer.SetDitDuration(this.ditDuration) this.roboKeyer.SetDitDuration(this.ditDuration)
this.inputs.SetDitDuration(this.ditDuration) for (let i of Object.values(this.inputs)) {
i.SetDitDuration(this.ditDuration)
}
}) })
this.inputInit("#rx-delay", e => { this.inputInit("#rx-delay", e => {
this.rxDelay = e.target.value * Second this.rxDelay = e.target.value * Second
@ -88,7 +89,7 @@ class VailClient {
this.setTimingCharts(true) this.setTimingCharts(true)
// Turn off the "muted" symbol when we can start making noise // Turn off the "muted" symbol when we can start making noise
Outputs.AudioReady() Buzzer.Ready()
.then(() => { .then(() => {
console.log("Audio context ready") console.log("Audio context ready")
document.querySelector("#muted").classList.add("is-hidden") document.querySelector("#muted").classList.add("is-hidden")
@ -116,13 +117,12 @@ class VailClient {
} }
setKeyer(keyerName) { setKeyer(keyerName) {
let newKeyerClass = Keyers.Keyers[keyerName] let newKeyerClass = Keyers[keyerName]
let newKeyerNumber = Keyers.Numbers[keyerName]
if (!newKeyerClass) { if (!newKeyerClass) {
console.error("Keyer not found", keyerName) console.error("Keyer not found", keyerName)
return return
} }
let newKeyer = new newKeyerClass(this) let newKeyer = new newKeyerClass(() => this.beginTx(), () => this.endTx())
let i = 0 let i = 0
for (let keyName of newKeyer.KeyNames()) { for (let keyName of newKeyer.KeyNames()) {
let e = document.querySelector(`.key[data-key="${i}"]`) let e = document.querySelector(`.key[data-key="${i}"]`)
@ -132,23 +132,24 @@ class VailClient {
this.keyer.Release() this.keyer.Release()
this.keyer = newKeyer this.keyer = newKeyer
this.inputs.SetKeyerMode(newKeyerNumber)
document.querySelector("#keyer-rate").dispatchEvent(new Event("input")) document.querySelector("#keyer-rate").dispatchEvent(new Event("input"))
} }
Buzz() { Buzz() {
this.outputs.Buzz(false) this.buzzer.Buzz()
this.lamp.Buzz()
if (this.rxChart) this.rxChart.Set(1) if (this.rxChart) this.rxChart.Set(1)
} }
Silence() { Silence() {
this.outputs.Silence() this.buzzer.Silence()
this.lamp.Silence()
if (this.rxChart) this.rxChart.Set(0) if (this.rxChart) this.rxChart.Set(0)
} }
BuzzDuration(tx, when, duration) { BuzzDuration(tx, when, duration) {
this.outputs.BuzzDuration(tx, when, duration) this.buzzer.BuzzDuration(tx, when, duration)
this.lamp.BuzzDuration(tx, when, duration)
let chart = tx?this.txChart:this.rxChart let chart = tx?this.txChart:this.rxChart
if (chart) { if (chart) {
@ -162,11 +163,10 @@ class VailClient {
* *
* Called from the keyer. * Called from the keyer.
*/ */
BeginTx() { beginTx() {
this.beginTxTime = Date.now() this.beginTxTime = Date.now()
this.outputs.Buzz(true) this.buzzer.Buzz(true)
if (this.txChart) this.txChart.Set(1) if (this.txChart) this.txChart.Set(1)
} }
/** /**
@ -174,13 +174,13 @@ class VailClient {
* *
* Called from the keyer * Called from the keyer
*/ */
EndTx() { endTx() {
if (!this.beginTxTime) { if (!this.beginTxTime) {
return return
} }
let endTxTime = Date.now() let endTxTime = Date.now()
let duration = endTxTime - this.beginTxTime let duration = endTxTime - this.beginTxTime
this.outputs.Silence(true) this.buzzer.Silence(true)
this.repeater.Transmit(this.beginTxTime, duration) this.repeater.Transmit(this.beginTxTime, duration)
this.beginTxTime = null this.beginTxTime = null
if (this.txChart) this.txChart.Set(0) if (this.txChart) this.txChart.Set(0)
@ -222,10 +222,10 @@ class VailClient {
*/ */
setTelegraphBuzzer(enable) { setTelegraphBuzzer(enable) {
if (enable) { if (enable) {
this.outputs.SetAudioType("telegraph") this.buzzer = new Buzzer.TelegraphBuzzer()
toast("Telegraphs only make sound when receiving!") toast("Telegraphs only make sound when receiving!")
} else { } else {
this.outputs.SetAudioType() this.buzzer = new Buzzer.ToneBuzzer()
} }
} }
@ -291,6 +291,8 @@ class VailClient {
} else { } else {
this.repeater = new Repeaters.Vail(rx, name) this.repeater = new Repeaters.Vail(rx, name)
} }
toast(`Now using repeater: ${name}`)
} }
/** /**
@ -341,7 +343,7 @@ class VailClient {
*/ */
error(msg) { error(msg) {
toast(msg) toast(msg)
this.outputs.Error() this.buzzer.Error()
} }
/** /**
@ -369,18 +371,11 @@ class VailClient {
this.rxDurations.splice(20, 2) this.rxDurations.splice(20, 2)
} }
if (stats.notice) {
toast(stats.notice)
}
let averageLag = (stats.averageLag || 0).toFixed(2) let averageLag = (stats.averageLag || 0).toFixed(2)
let longestRxDuration = this.rxDurations.reduce((a,b) => Math.max(a,b)) let longestRxDuration = this.rxDurations.reduce((a,b) => Math.max(a,b))
let suggestedDelay = ((averageLag + longestRxDuration) * 1.2).toFixed(0) let suggestedDelay = ((averageLag + longestRxDuration) * 1.2).toFixed(0)
if (stats.connected !== undefined) { this.updateReading("#note", stats.note || "☁")
this.outputs.SetConnected(stats.connected)
}
this.updateReading("#note", stats.note || stats.clients || "😎")
this.updateReading("#lag-value", averageLag) this.updateReading("#lag-value", averageLag)
this.updateReading("#longest-rx-value", longestRxDuration) this.updateReading("#longest-rx-value", longestRxDuration)
this.updateReading("#suggested-delay-value", suggestedDelay) this.updateReading("#suggested-delay-value", suggestedDelay)
@ -421,7 +416,7 @@ class VailClient {
/** /**
* Send "CK" to server, and don't squelch the echo * Send "CK" to server, and don't squelch the echo
*/ */
check() { test() {
let when = Date.now() let when = Date.now()
let dit = this.ditDuration let dit = this.ditDuration
let dah = dit * 3 let dah = dit * 3

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB