Compare commits

..

3 Commits

Author SHA1 Message Date
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
12 changed files with 216 additions and 90 deletions

View File

@ -20,7 +20,7 @@ func TestBook(t *testing.T) {
c1.Expect(1)
// Send to an empty channel
m := Message{0, 0, []uint8{22, 33}}
m := Message{0, 0, []uint16{22, 33}}
b.Send("merf", m)
b.loop()
if c1.Len() > 0 {

View File

@ -1,17 +1,21 @@
package main
import (
"fmt"
"context"
"encoding/json"
"log"
"net/http"
"os"
"time"
"golang.org/x/net/websocket"
"nhooyr.io/websocket"
)
var book Book
const JsonProtocol = "json.vail.woozle.org"
const BinaryProtocol = "binary.vail.woozle.org"
// Clock defines an interface for getting the current time.
//
// We use this in testing to provide a fixed value for the current time, so we
@ -20,7 +24,7 @@ type Clock interface {
Now() time.Time
}
// WallClock provides the actual time
// WallClock is a Clock which provides the actual time
type WallClock struct{}
func (WallClock) Now() time.Time {
@ -30,32 +34,87 @@ func (WallClock) Now() time.Time {
// VailWebSocketConnection reads and writes Message structs
type VailWebSocketConnection struct {
*websocket.Conn
usingJSON bool
}
func (c *VailWebSocketConnection) Receive() (Message, error) {
var m Message
err := websocket.JSON.Receive(c.Conn, &m)
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 {
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 {
repeaterName string
log.Println("Sending")
return c.Write(context.Background(), messageType, buf)
}
func (c Client) Handle(ws *websocket.Conn) {
sock := &VailWebSocketConnection{ws}
nowMilli := time.Now().UnixMilli()
ws.MaxPayloadBytes = 50
book.Join(c.repeaterName, sock)
defer book.Part(c.repeaterName, sock)
func ChatHandler(w http.ResponseWriter, r *http.Request) {
// Set up websocket
ws, err := websocket.Accept(
w, r,
&websocket.AcceptOptions{
Subprotocols: []string{JsonProtocol, BinaryProtocol},
},
)
if err != nil {
log.Println(err)
return
}
defer ws.Close(websocket.StatusInternalError, "Internal error")
// Create our Vail websocket connection for books to send to
sock := VailWebSocketConnection{
Conn: ws,
}
// websockets apparently sends a subprotocol string, so we can ignore Accept headers!
switch ws.Subprotocol() {
case JsonProtocol:
sock.usingJSON = true
case BinaryProtocol:
sock.usingJSON = false
default:
ws.Close(websocket.StatusPolicyViolation, "client must speak a vail protocol")
return
}
// Join the repeater
repeaterName := r.FormValue("repeater")
book.Join(repeaterName, &sock)
defer book.Part(repeaterName, &sock)
for {
// Read a packet
m, err := sock.Receive()
if err != nil {
log.Println(err)
ws.Close(websocket.StatusInvalidFramePayloadData, err.Error())
break
}
@ -65,30 +124,20 @@ func (c Client) Handle(ws *websocket.Conn) {
}
// If it's wildly out of time, reject it
timeDelta := (nowMilli - m.Timestamp)
timeDelta := (time.Now().UnixMilli() - m.Timestamp)
if timeDelta < 0 {
timeDelta = -timeDelta
}
if timeDelta > 9999 {
fmt.Fprintln(ws, "Bad timestamp")
ws.Close()
return
log.Println(err)
ws.Close(websocket.StatusInvalidFramePayloadData, "Your clock is off by too much")
break
}
book.Send(c.repeaterName, m)
book.Send(repeaterName, m)
}
}
func ChatHandler(w http.ResponseWriter, r *http.Request) {
c := Client{
repeaterName: r.FormValue("repeater"),
}
// This API is confusing as hell.
// I suspect there's a better way to do this.
websocket.Handler(c.Handle).ServeHTTP(w, r)
}
func main() {
book = NewBook()
http.Handle("/chat", http.HandlerFunc(ChatHandler))

View File

@ -3,7 +3,6 @@ package main
import (
"bytes"
"encoding/binary"
"fmt"
"time"
)
@ -34,13 +33,13 @@ type Message struct {
// Message timing in ms.
// Timings alternate between tone and silence.
// For example, `A` could be sent as [80, 80, 240]
Duration []uint8
Duration []uint16
}
func NewMessage(ts time.Time, durations ...time.Duration) Message {
msg := Message{
Timestamp: ts.UnixNano() / time.Millisecond.Nanoseconds(),
Duration: make([]uint8, len(durations)),
Duration: make([]uint16, len(durations)),
}
for i, dns := range durations {
ms := dns.Milliseconds()
@ -49,7 +48,7 @@ func NewMessage(ts time.Time, durations ...time.Duration) Message {
} else if ms < 0 {
ms = 0
}
msg.Duration[i] = uint8(ms)
msg.Duration[i] = uint16(ms)
}
return msg
}
@ -69,7 +68,7 @@ func (m Message) MarshalBinary() ([]byte, error) {
return w.Bytes(), nil
}
// Unmarshaling presumes something else is keeping track of lengths
// UnmarshalBinary unpacks a binary buffer into a Message.
func (m *Message) UnmarshalBinary(data []byte) error {
r := bytes.NewReader(data)
if err := binary.Read(r, binary.BigEndian, &m.Timestamp); err != nil {
@ -78,32 +77,14 @@ func (m *Message) UnmarshalBinary(data []byte) error {
if err := binary.Read(r, binary.BigEndian, &m.Clients); err != nil {
return err
}
dlen := r.Len()
m.Duration = make([]uint8, dlen)
dlen := r.Len() / 2
m.Duration = make([]uint16, dlen)
if err := binary.Read(r, binary.BigEndian, &m.Duration); err != nil {
return err
}
return nil
}
func (m Message) MarshalJSON() ([]byte, error) {
buf := new(bytes.Buffer)
fmt.Fprint(buf, "{")
fmt.Fprintf(buf, "\"Timestamp\":%d,", m.Timestamp)
fmt.Fprintf(buf, "\"Clients\":%d,", m.Clients)
fmt.Fprint(buf, "\"Duration\":[")
for i := 0; i < len(m.Duration); i++ {
fmt.Fprint(buf, m.Duration[i])
if i <= len(m.Duration)-1 {
fmt.Fprint(buf, ",")
}
}
fmt.Fprint(buf)
fmt.Fprint(buf, "]")
fmt.Fprint(buf, "}")
return buf.Bytes(), nil
}
func (m Message) Equal(m2 Message) bool {
if m.Timestamp != m2.Timestamp {
return false

View File

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

View File

@ -32,7 +32,7 @@ func (tc *TestingClient) Len() int {
return len(tc.buf)
}
func (tc *TestingClient) Expect(clients uint16, payload ...uint8) {
func (tc *TestingClient) Expect(clients uint16, payload ...uint16) {
m := Message{0, clients, payload}
tc.expected = append(tc.expected, m)
if len(tc.buf) != len(tc.expected) {

5
go.mod
View File

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

39
go.sum
View File

@ -1,6 +1,45 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=

View File

@ -72,7 +72,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" class="recv-lamp">
<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

@ -137,6 +137,10 @@ class Sample {
* A (mostly) virtual class defining a buzzer.
*/
class Buzzer {
constructor() {
this.connected = true
}
/**
* Signal an error
*/
@ -175,6 +179,15 @@ class Buzzer {
this.Buzz(tx, when)
this.Silence(tx, when + duration)
}
/**
* Set the "connectedness" indicator.
*
* @param {boolean} connected True if connected
*/
SetConnected(connected) {
this.connected = connected
}
}
class AudioBuzzer extends Buzzer {
@ -297,6 +310,17 @@ class LampBuzzer extends Buzzer {
ms,
)
}
SetConnected(connected) {
console.log(connected)
for (let e of this.elements) {
if (connected) {
e.classList.add("connected")
} else {
e.classList.remove("connected")
}
}
}
}
class MIDIBuzzer extends Buzzer {
@ -415,7 +439,7 @@ class Collection {
*
* @param tx True if transmitting
*/
Silence(tx=False) {
Silence(tx=false) {
for (let b of this.collection) {
b.Silence(tx)
}
@ -433,6 +457,19 @@ class Collection {
b.BuzzDuration(tx, when, duration)
}
}
/**
* Update the "connected" status display.
*
* For example, turn the receive light to black if the repeater is not connected.
*
* @param {boolean} connected True if we are "connected"
*/
SetConnected(connected) {
for (let b of this.collection) {
b.SetConnected(connected)
}
}
}
export {AudioReady, Collection}

View File

@ -30,6 +30,7 @@ export class Vail {
this.lagDurations = []
this.sent = []
this.wantConnected = true
this.connected = false
this.wsUrl = new URL("chat", window.location)
this.wsUrl.protocol = this.wsUrl.protocol.replace("http", "ws")
@ -43,15 +44,23 @@ export class Vail {
if (!this.wantConnected) {
return
}
this.rx(0, 0, {connected: false})
console.info("Attempting to reconnect", this.wsUrl.href)
this.clockOffset = 0
this.socket = new WebSocket(this.wsUrl)
this.socket = new WebSocket(this.wsUrl, ["json.vail.woozle.org"])
this.socket.addEventListener("message", e => this.wsMessage(e))
this.socket.addEventListener(
"open",
msg => {
this.connected = true
this.rx(0, 0, {connected: true})
}
)
this.socket.addEventListener(
"close",
() => {
console.info("Repeater connection dropped.")
setTimeout(() => this.reopen(), 5*Second)
msg => {
console.error("Repeater connection dropped:", msg.reason)
setTimeout(() => this.reopen(), 2*Second)
}
)
}
@ -71,6 +80,12 @@ export class Vail {
averageLag: this.lagDurations.reduce((a,b) => (a+b), 0) / this.lagDurations.length,
clockOffset: this.clockOffset,
clients: msg.Clients,
connected: this.connected,
}
console.log(msg)
if (typeof(msg) == "string") {
console.error(msg)
return
}
// XXX: Why is this happening?
@ -147,13 +162,14 @@ export class Vail {
}
export class Null {
constructor(rx) {
constructor(rx, interval=3*Second) {
this.rx = rx
this.interval = setInterval(() => this.pulse(), 3 * Second)
this.interval = setInterval(() => this.pulse(), interval)
this.pulse()
}
pulse() {
this.rx(0, 0, {note: "local"})
this.rx(0, 0, {note: "local", connected: false})
}
Transmit(time, duration, squelch=true) {
@ -164,38 +180,32 @@ export class Null {
}
}
export class Echo {
export class Echo extends Null {
constructor(rx, delay=0) {
this.rx = rx
super(rx)
this.delay = delay
this.Transmit(0, 0)
}
Transmit(time, duration, squelch=true) {
this.rx(time + this.delay, duration, {note: "local"})
}
Close() {
}
}
export class Fortune {
export class Fortune extends Null {
/**
*
* @param rx Receive callback
* @param {Keyer} keyer Keyer object
*/
constructor(rx, keyer) {
this.rx = rx
super(rx, 1*Minute)
this.keyer = keyer
this.interval = setInterval(() => this.pulse(), 1 * Minute)
this.pulse()
}
pulse() {
this.rx(0, 0, {note: "local"})
if (this.keyer.Busy()) {
super.pulse()
if (!this.keyer || this.keyer.Busy()) {
return
}
@ -203,12 +213,8 @@ export class Fortune {
this.keyer.EnqueueAsciiString(`${fortune} \x04 `)
}
Transmit(time, duration, squelch=true) {
// Do nothing.
}
Close() {
this.keyer.Flush()
clearInterval(this.interval)
super.Close()
}
}

View File

@ -16,7 +16,15 @@
-webkit-user-select: none; /* 2022-04-26 Safari still needs this */
}
.recv-lamp.rx {
.tag.recv-lamp {
background-color: #444;
color: white;
}
.tag.recv-lamp.connected {
background-color: #fec;
}
.tag.recv-lamp.rx,
.tag.recv-lamp.connected.rx {
background-color: orange;
}

View File

@ -375,6 +375,9 @@ class VailClient {
let longestRxDuration = this.rxDurations.reduce((a,b) => Math.max(a,b))
let suggestedDelay = ((averageLag + longestRxDuration) * 1.2).toFixed(0)
if (stats.connected !== undefined) {
this.outputs.SetConnected(stats.connected)
}
this.updateReading("#note", stats.note || "☁")
this.updateReading("#lag-value", averageLag)
this.updateReading("#longest-rx-value", longestRxDuration)