mirror of https://github.com/nealey/vail.git
Compare commits
3 Commits
15e43c28df
...
b910676539
Author | SHA1 | Date |
---|---|---|
Neale Pickett | b910676539 | |
Neale Pickett | 314994adcd | |
Neale Pickett | b45876bcf0 |
|
@ -20,7 +20,7 @@ func TestBook(t *testing.T) {
|
||||||
c1.Expect(1)
|
c1.Expect(1)
|
||||||
|
|
||||||
// Send to an empty channel
|
// Send to an empty channel
|
||||||
m := Message{0, 0, []uint8{22, 33}}
|
m := Message{0, 0, []uint16{22, 33}}
|
||||||
b.Send("merf", m)
|
b.Send("merf", m)
|
||||||
b.loop()
|
b.loop()
|
||||||
if c1.Len() > 0 {
|
if c1.Len() > 0 {
|
||||||
|
|
105
cmd/vail/main.go
105
cmd/vail/main.go
|
@ -1,17 +1,21 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/websocket"
|
"nhooyr.io/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
var book Book
|
var book Book
|
||||||
|
|
||||||
|
const JsonProtocol = "json.vail.woozle.org"
|
||||||
|
const BinaryProtocol = "binary.vail.woozle.org"
|
||||||
|
|
||||||
// Clock defines an interface for getting the current time.
|
// 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
|
// We use this in testing to provide a fixed value for the current time, so we
|
||||||
|
@ -20,7 +24,7 @@ type Clock interface {
|
||||||
Now() time.Time
|
Now() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// WallClock provides the actual time
|
// WallClock is a Clock which provides the actual time
|
||||||
type WallClock struct{}
|
type WallClock struct{}
|
||||||
|
|
||||||
func (WallClock) Now() time.Time {
|
func (WallClock) Now() time.Time {
|
||||||
|
@ -30,32 +34,87 @@ func (WallClock) Now() time.Time {
|
||||||
// VailWebSocketConnection reads and writes Message structs
|
// VailWebSocketConnection reads and writes Message structs
|
||||||
type VailWebSocketConnection struct {
|
type VailWebSocketConnection struct {
|
||||||
*websocket.Conn
|
*websocket.Conn
|
||||||
|
usingJSON bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *VailWebSocketConnection) Receive() (Message, error) {
|
func (c *VailWebSocketConnection) Receive() (Message, error) {
|
||||||
var m Message
|
var m Message
|
||||||
err := websocket.JSON.Receive(c.Conn, &m)
|
messageType, buf, err := c.Read(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if messageType == websocket.MessageText {
|
||||||
|
err = json.Unmarshal(buf, &m)
|
||||||
|
} else {
|
||||||
|
err = m.UnmarshalBinary(buf)
|
||||||
|
}
|
||||||
return m, err
|
return m, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *VailWebSocketConnection) Send(m Message) error {
|
func (c *VailWebSocketConnection) Send(m Message) error {
|
||||||
return websocket.JSON.Send(c.Conn, m)
|
var err error
|
||||||
|
var buf []byte
|
||||||
|
var messageType websocket.MessageType
|
||||||
|
|
||||||
|
log.Println("Send", m)
|
||||||
|
if c.usingJSON {
|
||||||
|
messageType = websocket.MessageText
|
||||||
|
buf, err = json.Marshal(m)
|
||||||
|
} else {
|
||||||
|
messageType = websocket.MessageBinary
|
||||||
|
buf, err = m.MarshalBinary()
|
||||||
|
}
|
||||||
|
log.Println(buf, err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
log.Println("Sending")
|
||||||
repeaterName string
|
return c.Write(context.Background(), messageType, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Client) Handle(ws *websocket.Conn) {
|
func ChatHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
sock := &VailWebSocketConnection{ws}
|
// Set up websocket
|
||||||
nowMilli := time.Now().UnixMilli()
|
ws, err := websocket.Accept(
|
||||||
ws.MaxPayloadBytes = 50
|
w, r,
|
||||||
book.Join(c.repeaterName, sock)
|
&websocket.AcceptOptions{
|
||||||
defer book.Part(c.repeaterName, sock)
|
Subprotocols: []string{JsonProtocol, BinaryProtocol},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer ws.Close(websocket.StatusInternalError, "Internal error")
|
||||||
|
|
||||||
|
// Create our Vail websocket connection for books to send to
|
||||||
|
sock := VailWebSocketConnection{
|
||||||
|
Conn: ws,
|
||||||
|
}
|
||||||
|
|
||||||
|
// websockets apparently sends a subprotocol string, so we can ignore Accept headers!
|
||||||
|
switch ws.Subprotocol() {
|
||||||
|
case JsonProtocol:
|
||||||
|
sock.usingJSON = true
|
||||||
|
case BinaryProtocol:
|
||||||
|
sock.usingJSON = false
|
||||||
|
default:
|
||||||
|
ws.Close(websocket.StatusPolicyViolation, "client must speak a vail protocol")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join the repeater
|
||||||
|
repeaterName := r.FormValue("repeater")
|
||||||
|
book.Join(repeaterName, &sock)
|
||||||
|
defer book.Part(repeaterName, &sock)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
// Read a packet
|
||||||
m, err := sock.Receive()
|
m, err := sock.Receive()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
ws.Close(websocket.StatusInvalidFramePayloadData, err.Error())
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,30 +124,20 @@ func (c Client) Handle(ws *websocket.Conn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's wildly out of time, reject it
|
// If it's wildly out of time, reject it
|
||||||
timeDelta := (nowMilli - m.Timestamp)
|
timeDelta := (time.Now().UnixMilli() - m.Timestamp)
|
||||||
if timeDelta < 0 {
|
if timeDelta < 0 {
|
||||||
timeDelta = -timeDelta
|
timeDelta = -timeDelta
|
||||||
}
|
}
|
||||||
if timeDelta > 9999 {
|
if timeDelta > 9999 {
|
||||||
fmt.Fprintln(ws, "Bad timestamp")
|
log.Println(err)
|
||||||
ws.Close()
|
ws.Close(websocket.StatusInvalidFramePayloadData, "Your clock is off by too much")
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
book.Send(c.repeaterName, m)
|
book.Send(repeaterName, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ChatHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
c := Client{
|
|
||||||
repeaterName: r.FormValue("repeater"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// This API is confusing as hell.
|
|
||||||
// I suspect there's a better way to do this.
|
|
||||||
websocket.Handler(c.Handle).ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
book = NewBook()
|
book = NewBook()
|
||||||
http.Handle("/chat", http.HandlerFunc(ChatHandler))
|
http.Handle("/chat", http.HandlerFunc(ChatHandler))
|
||||||
|
|
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,13 +33,13 @@ type Message struct {
|
||||||
// Message timing in ms.
|
// Message timing in ms.
|
||||||
// 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 []uint8
|
Duration []uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
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([]uint8, len(durations)),
|
Duration: make([]uint16, len(durations)),
|
||||||
}
|
}
|
||||||
for i, dns := range durations {
|
for i, dns := range durations {
|
||||||
ms := dns.Milliseconds()
|
ms := dns.Milliseconds()
|
||||||
|
@ -49,7 +48,7 @@ func NewMessage(ts time.Time, durations ...time.Duration) Message {
|
||||||
} else if ms < 0 {
|
} else if ms < 0 {
|
||||||
ms = 0
|
ms = 0
|
||||||
}
|
}
|
||||||
msg.Duration[i] = uint8(ms)
|
msg.Duration[i] = uint16(ms)
|
||||||
}
|
}
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
@ -69,7 +68,7 @@ func (m Message) MarshalBinary() ([]byte, error) {
|
||||||
return w.Bytes(), nil
|
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 {
|
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 {
|
||||||
|
@ -78,32 +77,14 @@ func (m *Message) UnmarshalBinary(data []byte) error {
|
||||||
if err := binary.Read(r, binary.BigEndian, &m.Clients); err != nil {
|
if err := binary.Read(r, binary.BigEndian, &m.Clients); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dlen := r.Len()
|
dlen := r.Len() / 2
|
||||||
m.Duration = make([]uint8, dlen)
|
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
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Message) MarshalJSON() ([]byte, error) {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
fmt.Fprint(buf, "{")
|
|
||||||
fmt.Fprintf(buf, "\"Timestamp\":%d,", m.Timestamp)
|
|
||||||
fmt.Fprintf(buf, "\"Clients\":%d,", m.Clients)
|
|
||||||
fmt.Fprint(buf, "\"Duration\":[")
|
|
||||||
for i := 0; i < len(m.Duration); i++ {
|
|
||||||
fmt.Fprint(buf, m.Duration[i])
|
|
||||||
if i <= len(m.Duration)-1 {
|
|
||||||
fmt.Fprint(buf, ",")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Fprint(buf)
|
|
||||||
fmt.Fprint(buf, "]")
|
|
||||||
fmt.Fprint(buf, "}")
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Message) Equal(m2 Message) bool {
|
func (m Message) Equal(m2 Message) bool {
|
||||||
if m.Timestamp != m2.Timestamp {
|
if m.Timestamp != m2.Timestamp {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMessageStruct(t *testing.T) {
|
func TestMessageStruct(t *testing.T) {
|
||||||
m := Message{0x1122334455, 0, []uint8{0xaa, 0xbb, 0xcc}}
|
m := Message{0x1122334455, 0, []uint16{0xaa, 0xbb, 0xcc}}
|
||||||
m2 := Message{12, 0, []uint8{1}}
|
m2 := Message{12, 0, []uint16{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, []uint8{1, 2, 3}}) {
|
if m.Equal(Message{m.Timestamp, 0, []uint16{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\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)
|
t.Error("Encoded wrong:", bm)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ func (tc *TestingClient) Len() int {
|
||||||
return len(tc.buf)
|
return len(tc.buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *TestingClient) Expect(clients uint16, payload ...uint8) {
|
func (tc *TestingClient) Expect(clients uint16, payload ...uint16) {
|
||||||
m := Message{0, clients, payload}
|
m := Message{0, clients, payload}
|
||||||
tc.expected = append(tc.expected, m)
|
tc.expected = append(tc.expected, m)
|
||||||
if len(tc.buf) != len(tc.expected) {
|
if len(tc.buf) != len(tc.expected) {
|
||||||
|
|
5
go.mod
5
go.mod
|
@ -2,4 +2,7 @@ module github.com/nealey/vail
|
||||||
|
|
||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
require golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5
|
require (
|
||||||
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5
|
||||||
|
nhooyr.io/websocket v1.8.7 // indirect
|
||||||
|
)
|
||||||
|
|
39
go.sum
39
go.sum
|
@ -1,6 +1,45 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||||
|
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
|
||||||
|
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/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 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
|
||||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
|
||||||
|
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||||
|
|
|
@ -72,7 +72,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" class="recv-lamp">
|
<span class="tag recv-lamp">
|
||||||
<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>
|
||||||
|
|
|
@ -137,6 +137,10 @@ 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
|
||||||
*/
|
*/
|
||||||
|
@ -175,6 +179,15 @@ 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 {
|
||||||
|
@ -297,6 +310,17 @@ class LampBuzzer extends Buzzer {
|
||||||
ms,
|
ms,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetConnected(connected) {
|
||||||
|
console.log(connected)
|
||||||
|
for (let e of this.elements) {
|
||||||
|
if (connected) {
|
||||||
|
e.classList.add("connected")
|
||||||
|
} else {
|
||||||
|
e.classList.remove("connected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MIDIBuzzer extends Buzzer {
|
class MIDIBuzzer extends Buzzer {
|
||||||
|
@ -415,7 +439,7 @@ class Collection {
|
||||||
*
|
*
|
||||||
* @param tx True if transmitting
|
* @param tx True if transmitting
|
||||||
*/
|
*/
|
||||||
Silence(tx=False) {
|
Silence(tx=false) {
|
||||||
for (let b of this.collection) {
|
for (let b of this.collection) {
|
||||||
b.Silence(tx)
|
b.Silence(tx)
|
||||||
}
|
}
|
||||||
|
@ -433,6 +457,19 @@ class Collection {
|
||||||
b.BuzzDuration(tx, when, duration)
|
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}
|
export {AudioReady, Collection}
|
||||||
|
|
|
@ -30,6 +30,7 @@ export class Vail {
|
||||||
this.lagDurations = []
|
this.lagDurations = []
|
||||||
this.sent = []
|
this.sent = []
|
||||||
this.wantConnected = true
|
this.wantConnected = true
|
||||||
|
this.connected = false
|
||||||
|
|
||||||
this.wsUrl = new URL("chat", window.location)
|
this.wsUrl = new URL("chat", window.location)
|
||||||
this.wsUrl.protocol = this.wsUrl.protocol.replace("http", "ws")
|
this.wsUrl.protocol = this.wsUrl.protocol.replace("http", "ws")
|
||||||
|
@ -43,15 +44,23 @@ export class Vail {
|
||||||
if (!this.wantConnected) {
|
if (!this.wantConnected) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.rx(0, 0, {connected: false})
|
||||||
console.info("Attempting to reconnect", this.wsUrl.href)
|
console.info("Attempting to reconnect", this.wsUrl.href)
|
||||||
this.clockOffset = 0
|
this.clockOffset = 0
|
||||||
this.socket = new WebSocket(this.wsUrl)
|
this.socket = new WebSocket(this.wsUrl, ["json.vail.woozle.org"])
|
||||||
this.socket.addEventListener("message", e => this.wsMessage(e))
|
this.socket.addEventListener("message", e => this.wsMessage(e))
|
||||||
this.socket.addEventListener(
|
this.socket.addEventListener(
|
||||||
|
"open",
|
||||||
|
msg => {
|
||||||
|
this.connected = true
|
||||||
|
this.rx(0, 0, {connected: true})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.socket.addEventListener(
|
||||||
"close",
|
"close",
|
||||||
() => {
|
msg => {
|
||||||
console.info("Repeater connection dropped.")
|
console.error("Repeater connection dropped:", msg.reason)
|
||||||
setTimeout(() => this.reopen(), 5*Second)
|
setTimeout(() => this.reopen(), 2*Second)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -71,6 +80,12 @@ export class Vail {
|
||||||
averageLag: this.lagDurations.reduce((a,b) => (a+b), 0) / this.lagDurations.length,
|
averageLag: this.lagDurations.reduce((a,b) => (a+b), 0) / this.lagDurations.length,
|
||||||
clockOffset: this.clockOffset,
|
clockOffset: this.clockOffset,
|
||||||
clients: msg.Clients,
|
clients: msg.Clients,
|
||||||
|
connected: this.connected,
|
||||||
|
}
|
||||||
|
console.log(msg)
|
||||||
|
if (typeof(msg) == "string") {
|
||||||
|
console.error(msg)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: Why is this happening?
|
// XXX: Why is this happening?
|
||||||
|
@ -147,13 +162,14 @@ export class Vail {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Null {
|
export class Null {
|
||||||
constructor(rx) {
|
constructor(rx, interval=3*Second) {
|
||||||
this.rx = rx
|
this.rx = rx
|
||||||
this.interval = setInterval(() => this.pulse(), 3 * Second)
|
this.interval = setInterval(() => this.pulse(), interval)
|
||||||
|
this.pulse()
|
||||||
}
|
}
|
||||||
|
|
||||||
pulse() {
|
pulse() {
|
||||||
this.rx(0, 0, {note: "local"})
|
this.rx(0, 0, {note: "local", connected: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
Transmit(time, duration, squelch=true) {
|
Transmit(time, duration, squelch=true) {
|
||||||
|
@ -164,38 +180,32 @@ export class Null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Echo {
|
export class Echo extends Null {
|
||||||
constructor(rx, delay=0) {
|
constructor(rx, delay=0) {
|
||||||
this.rx = rx
|
super(rx)
|
||||||
this.delay = delay
|
this.delay = delay
|
||||||
this.Transmit(0, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Transmit(time, duration, squelch=true) {
|
Transmit(time, duration, squelch=true) {
|
||||||
this.rx(time + this.delay, duration, {note: "local"})
|
this.rx(time + this.delay, duration, {note: "local"})
|
||||||
}
|
}
|
||||||
|
|
||||||
Close() {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Fortune {
|
export class Fortune extends Null {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param rx Receive callback
|
* @param rx Receive callback
|
||||||
* @param {Keyer} keyer Keyer object
|
* @param {Keyer} keyer Keyer object
|
||||||
*/
|
*/
|
||||||
constructor(rx, keyer) {
|
constructor(rx, keyer) {
|
||||||
this.rx = rx
|
super(rx, 1*Minute)
|
||||||
this.keyer = keyer
|
this.keyer = keyer
|
||||||
|
|
||||||
this.interval = setInterval(() => this.pulse(), 1 * Minute)
|
|
||||||
this.pulse()
|
this.pulse()
|
||||||
}
|
}
|
||||||
|
|
||||||
pulse() {
|
pulse() {
|
||||||
this.rx(0, 0, {note: "local"})
|
super.pulse()
|
||||||
if (this.keyer.Busy()) {
|
if (!this.keyer || this.keyer.Busy()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,12 +213,8 @@ export class Fortune {
|
||||||
this.keyer.EnqueueAsciiString(`${fortune} \x04 `)
|
this.keyer.EnqueueAsciiString(`${fortune} \x04 `)
|
||||||
}
|
}
|
||||||
|
|
||||||
Transmit(time, duration, squelch=true) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
Close() {
|
Close() {
|
||||||
this.keyer.Flush()
|
this.keyer.Flush()
|
||||||
clearInterval(this.interval)
|
super.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,7 +16,15 @@
|
||||||
-webkit-user-select: none; /* 2022-04-26 Safari still needs this */
|
-webkit-user-select: none; /* 2022-04-26 Safari still needs this */
|
||||||
}
|
}
|
||||||
|
|
||||||
.recv-lamp.rx {
|
.tag.recv-lamp {
|
||||||
|
background-color: #444;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.tag.recv-lamp.connected {
|
||||||
|
background-color: #fec;
|
||||||
|
}
|
||||||
|
.tag.recv-lamp.rx,
|
||||||
|
.tag.recv-lamp.connected.rx {
|
||||||
background-color: orange;
|
background-color: orange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -375,6 +375,9 @@ class VailClient {
|
||||||
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.outputs.SetConnected(stats.connected)
|
||||||
|
}
|
||||||
this.updateReading("#note", stats.note || "☁")
|
this.updateReading("#note", stats.note || "☁")
|
||||||
this.updateReading("#lag-value", averageLag)
|
this.updateReading("#lag-value", averageLag)
|
||||||
this.updateReading("#longest-rx-value", longestRxDuration)
|
this.updateReading("#longest-rx-value", longestRxDuration)
|
||||||
|
|
Loading…
Reference in New Issue