Compare commits

...

21 Commits

Author SHA1 Message Date
Neale Pickett d1f2b1d087 rx delay in english 2022-06-06 18:16:01 -06:00
Neale Pickett 9a881b73b8 Start at internationalization 2022-06-06 18:10:42 -06:00
Neale Pickett 60c0ac0a31 Structure client files 2022-06-06 16:52:22 -06:00
Neale Pickett a3a8d9cb31 More realistic tolerance for clock skew 2022-06-06 15:57:03 -06:00
Neale Pickett e9cbdba425 De-acronym Message documentation 2022-06-06 15:28:17 -06:00
Neale Pickett 6e20cc510f Stuck key blur detection
Fixes #55
2022-06-06 15:27:54 -06:00
Neale Pickett d1edd13cc3 Update counter on join/part 2022-06-06 14:44:09 -06:00
Neale Pickett 51f89b6762 remove low-pitch oscillator, doesn't seem to help 2022-06-06 14:19:10 -06:00
Neale Pickett c64c8b619d Upgrade dependencies for security fix 2022-06-06 14:09:34 -06:00
Neale Pickett 910e9e75c0
Strict message validation
Finally, we parse incoming JSON into Message objects, check timestamps for being too far in the past, and perform other quality-of-life checks at the server, to limit avenues for abuse. This also adds a "binary" message type, which could be useful for Arduino projects.

The websocket library has been swapped out for one that makes more sense to me, bringing in a ton of dependencies.

Finally, we are now telling clients how many other clients are connected.
2022-06-06 14:03:01 -06:00
Neale Pickett 67580c2746 go mod tidy 2022-06-06 13:58:57 -06:00
Neale Pickett 406bb982e9 Merge branch 'main' into strict 2022-06-06 13:52:25 -06:00
Neale Pickett 71c108b49c Cleanup 2022-06-06 13:49:52 -06:00
Neale Pickett b910676539 Fixed fortunes 2022-06-06 10:55:11 -06:00
Neale Pickett 314994adcd It works again 2022-06-06 09:54:55 -06:00
Neale Pickett b45876bcf0 About to change to nhooyr/websocket 2022-06-05 12:34:03 -07:00
Neale Pickett 15e43c28df Send keyer state every time it's connected 2022-05-28 20:30:50 -06:00
Neale Pickett b7de5cf8cb Output MIDI notes + refactoring 2022-05-22 21:37:36 -06:00
Neale Pickett 4ef1ff7517 More work 2022-05-15 21:12:36 -06:00
Neale Pickett d6e6a268a3 More unit test junk 2022-05-15 17:38:57 -06:00
Neale Pickett db9ca5dc83 Start enforcing message structure 2022-05-15 15:57:12 -06:00
36 changed files with 1133 additions and 483 deletions

View File

@ -1,19 +1,23 @@
package main
import (
"io"
"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,
}
}
@ -28,34 +32,38 @@ const (
type bookEvent struct {
eventType bookEventType
name string
w io.Writer
p []byte
sender MessageSender
m Message
}
func (b Book) Join(name string, w io.Writer) {
// Join adds a writer to a named repeater
func (b Book) Join(name string, sender MessageSender) {
b.events <- bookEvent{
eventType: joinEvent,
name: name,
w: w,
sender: sender,
}
}
func (b Book) Part(name string, w io.Writer) {
// Part removes a writer from a named repeater
func (b Book) Part(name string, sender MessageSender) {
b.events <- bookEvent{
eventType: partEvent,
name: name,
w: w,
sender: sender,
}
}
func (b Book) Send(name string, p []byte) {
// Send transmits a message to the named repeater
func (b Book) Send(name string, m Message) {
b.events <- bookEvent{
eventType: sendEvent,
name: name,
p: p,
m: m,
}
}
// Run is the endless run loop
func (b Book) Run() {
for {
b.loop()
@ -69,16 +77,16 @@ 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)
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)
}
@ -87,6 +95,6 @@ func (b Book) loop() {
log.Println("WARN: Sending to an empty channel:", event.name)
break
}
repeater.Send(event.p)
repeater.Send(event.m)
}
}

View File

@ -1,59 +1,57 @@
package main
import (
"bytes"
"testing"
)
func TestBook(t *testing.T) {
b := NewBook()
b := Book{
entries: make(map[string]*Repeater),
events: make(chan bookEvent, 5),
makeRepeater: NewTestingRepeater,
}
buf1 := bytes.NewBufferString("buf1")
b.Join("moo", buf1)
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", []byte("goober"))
m := Message{0, 0, []uint16{22, 33}}
b.Send("merf", m)
b.loop()
if buf1.String() != "buf1" {
if c1.Len() > 0 {
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)
b.loop()
if buf1.String() != "buf1goober" {
t.Error("Sending didn't work")
}
c1.Expect(1, 22, 33)
// Join another client
buf2 := bytes.NewBufferString("buf2")
b.Join("moo", buf2)
c2 := NewTestingClient(t)
b.Join("moo", c2)
b.loop()
c1.Expect(2)
c2.Expect(2)
// Send to both
b.Send("moo", []byte("snerk"))
m.Duration = append(m.Duration, 44)
b.Send("moo", m)
b.loop()
if buf1.String() != "buf1goobersnerk" {
t.Error("Send to 2-member channel busted", buf1)
}
if buf2.String() != "buf2snerk" {
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()
c2.Expect(1)
b.Send("moo", []byte("peanut"))
b.Send("moo", m)
b.loop()
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)
}
c2.Expect(1, 22, 33, 44)
}

View File

@ -1,49 +1,145 @@
package main
import (
"context"
"encoding/json"
"fmt"
"time"
"os"
"log"
"net/http"
"golang.org/x/net/websocket"
"os"
"time"
"nhooyr.io/websocket"
)
var book Book
type Client struct {
repeaterName string
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
// can still compare clocks.
type Clock interface {
Now() time.Time
}
func (c Client) Handle(ws *websocket.Conn) {
ws.MaxPayloadBytes = 500
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())
// WallClock is a Clock which provides the actual time
type WallClock struct{}
for {
buf := make([]byte, ws.MaxPayloadBytes)
func (WallClock) Now() time.Time {
return time.Now()
}
if n, err := ws.Read(buf); err != nil {
break
} else {
buf = buf[:n]
}
book.Send(c.repeaterName, buf)
// VailWebSocketConnection reads and writes Message structs
type VailWebSocketConnection struct {
*websocket.Conn
usingJSON bool
}
func (c *VailWebSocketConnection) Receive() (Message, error) {
var m Message
messageType, buf, err := c.Read(context.Background())
if err != nil {
return m, err
}
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) {
c := Client {
repeaterName: r.FormValue("repeater"),
forwardedFor := r.Header.Get("X-Forwarded-For")
client := fmt.Sprintf("<%s|%s>", forwardedFor, r.RemoteAddr)
// 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,
}
// This API is confusing as hell.
// I suspect there's a better way to do this.
websocket.Handler(c.Handle).ServeHTTP(w, r)
// 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)
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() {
@ -57,7 +153,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())
}

View File

@ -6,32 +6,49 @@ import (
"time"
)
// 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 int64
// Message timing in ms.
// Timings alternate between tone and silence.
// For example, `A` could be sent as [80, 80, 240]
Duration []uint8
// MessageSender can send Messages
type MessageSender interface {
Send(m Message) error
}
func NewMessage(ts time.Time, durations []time.Duration) Message {
// 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 {
// Timestamp of this message. Milliseconds since epoch.
Timestamp int64
// Number of connected clients.
Clients uint16
// Message timing in milliseconds.
// Timings alternate between tone and silence.
// For example, `A` could be sent as [80, 80, 240]
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()
if (ms > 255) {
if ms > 255 {
ms = 255
} else if (ms < 0) {
} else if ms < 0 {
ms = 0
}
msg.Duration[i] = uint8(ms)
msg.Duration[i] = uint16(ms)
}
return msg
}
@ -42,20 +59,26 @@ 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
}
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 {
return err
}
dlen := r.Len()
m.Duration = make([]uint8, dlen)
if err := binary.Read(r, binary.BigEndian, &m.Clients); err != nil {
return err
}
dlen := r.Len() / 2
m.Duration = make([]uint16, dlen)
if err := binary.Read(r, binary.BigEndian, &m.Duration); err != nil {
return err
}
@ -66,16 +89,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
}

View File

@ -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, []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 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, []uint16{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\x00\xaa\x00\xbb\x00\xcc")) {
t.Error("Encoded wrong:", bm)
}
@ -40,11 +40,9 @@ func TestMessage(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)

View File

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

View File

@ -1,39 +1,85 @@
package main
import (
"bytes"
"testing"
"time"
)
func TestRepeater(t *testing.T) {
r := NewRepeater()
type FakeClock struct{}
buf1 := bytes.NewBufferString("buf1")
r.Join(buf1)
if r.Listeners() != 1 {
t.Error("Joining did nothing")
}
r.Send([]byte("moo"))
if buf1.String() != "buf1moo" {
t.Error("Client 1 not repeating", buf1)
}
func (f FakeClock) Now() time.Time {
return time.UnixMilli(0)
}
buf2 := bytes.NewBufferString("buf2")
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)
}
type TestingClient struct {
buf []Message
expected []Message
t *testing.T
}
r.Part(buf1)
r.Send([]byte("baz"))
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)
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) {
r := NewTestingRepeater()
c1 := NewTestingClient(t)
r.Join(c1)
c1.Expect(1)
r.SendMessage(15 * time.Millisecond)
c1.Expect(1, 15)
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")
}
}

6
go.mod
View File

@ -2,4 +2,8 @@ module github.com/nealey/vail
go 1.12
require golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5
require (
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,6 +1,133 @@
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=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

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

View File

@ -1,7 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 369 B

View File

@ -1,7 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 369 B

View File

@ -1,7 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 369 B

View File

@ -1,7 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 369 B

View File

@ -1,11 +0,0 @@
<!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>

33
static/i18n/en.yaml Normal file
View File

@ -0,0 +1,33 @@
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.

32
static/i18n/fr.yaml Normal file
View File

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

View File

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

View File

@ -1,189 +0,0 @@
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

@ -2,9 +2,14 @@ class Input {
constructor(keyer) {
this.keyer = keyer
}
SetDitDuration(delay) {
// Nothing
}
SetKeyerMode(mode) {
// Nothing
}
}
export class HTML extends Input{
@ -48,9 +53,7 @@ export class Keyboard extends Input{
// Listen for keystrokes
document.addEventListener("keydown", e => this.keyboard(e))
document.addEventListener("keyup", e => this.keyboard(e))
// VBand: the keyboard input needs to know whether vband's "left" should be dit or straight
this.iambic = false
window.addEventListener("blur", e => this.loseFocus(e))
}
keyboard(event) {
@ -98,11 +101,28 @@ 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{
constructor(keyer) {
super(keyer)
this.ditDuration = 100
this.keyerMode = 0
this.midiAccess = {outputs: []} // stub while we wait for async stuff
if (navigator.requestMIDIAccess) {
@ -117,12 +137,28 @@ export class MIDI extends Input{
this.midiStateChange()
}
SetIntervalDuration(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([0x8B, 0x01, delay/2])
// Turn off keyboard mode