vail

Internet morse code repeater
git clone https://git.woozle.org/neale/vail.git

vail / static / scripts
Neale Pickett  ·  2023-01-17

repeaters.mjs

  1import {GetFortune} from "./fortunes.mjs"
  2import * as time from "./time.mjs"
  3
  4/**
  5 * Compare two messages
  6 * 
  7 * @param {Object} m1 First message
  8 * @param {Object} m2 Second message
  9 * @returns {Boolean} true if messages are equal
 10 */
 11function MessageEqual(m1, m2) {
 12    if ((m1.Timestamp != m2.Timestamp) || (m1.Duration.length != m2.Duration.length)) {
 13        return false
 14    }
 15    for (let i=0; i < m1.Duration.length; i++) {
 16        if (m1.Duration[i] != m2.Duration[i]) {
 17            return false
 18        }
 19    }
 20    return true    
 21}
 22
 23export class Vail {
 24    constructor(rx, name) {
 25        this.rx = rx
 26        this.name = name
 27        this.lagDurations = []
 28        this.sent = []
 29        this.wantConnected = true
 30        this.connected = false
 31        
 32		this.wsUrl = new URL("chat", window.location)
 33		this.wsUrl.protocol = this.wsUrl.protocol.replace("http", "ws")
 34        this.wsUrl.pathname = this.wsUrl.pathname.replace("testing/", "") // Allow staging deploys
 35        this.wsUrl.searchParams.set("repeater", name)
 36        
 37        this.reopen()
 38    }
 39    
 40    reopen() {
 41        if (!this.wantConnected) {
 42            return
 43        }
 44        this.rx(0, 0, {connected: false})
 45        console.info("Attempting to reconnect", this.wsUrl.href)
 46        this.clockOffset = 0
 47		this.socket = new WebSocket(this.wsUrl, ["json.vail.woozle.org"])
 48		this.socket.addEventListener("message", e => this.wsMessage(e))
 49        this.socket.addEventListener(
 50            "open",
 51            msg => {
 52                this.connected = true
 53                this.rx(0, 0, {connected: true, notice: "Repeater connected"})
 54            }
 55        )
 56		this.socket.addEventListener(
 57            "close",
 58            msg => {
 59                this.rx(0, 0, {connected: false, notice: `Repeater disconnected: ${msg.reason}`})
 60                console.error("Repeater connection dropped:", msg.reason)
 61                setTimeout(() => this.reopen(), 2*time.Second)
 62            }
 63        )
 64    }
 65
 66    wsMessage(event) {
 67        let now = Date.now()
 68        let jmsg = event.data
 69        let msg
 70        try {
 71            msg = JSON.parse(jmsg)
 72        }
 73        catch (err) {
 74            console.error(err, jmsg)
 75            return
 76        }
 77        let stats = {
 78            averageLag: this.lagDurations.reduce((a,b) => (a+b), 0) / this.lagDurations.length,
 79            clockOffset: this.clockOffset,
 80            clients: msg.Clients,
 81            connected: this.connected,
 82        }
 83
 84		// XXX: Why is this happening?
 85		if (msg.Timestamp == 0) {
 86            console.debug("Got timestamp=0", msg)
 87			return
 88        }
 89        
 90        let sent = this.sent.filter(m => !MessageEqual(msg, m))
 91		if (sent.length < this.sent.length) {
 92			// We're getting our own message back, which tells us our lag.
 93			// We shouldn't emit a tone, though.
 94			let totalDuration = msg.Duration.reduce((a, b) => a + b)
 95            this.sent = sent
 96            this.lagDurations.unshift(now - this.clockOffset - msg.Timestamp - totalDuration)
 97            this.lagDurations.splice(20, 2)
 98            this.rx(0, 0, stats)
 99			return
100		}
101
102        // Packets with 0 length tell us what time the server thinks it is,
103        // and how many clients are connected
104		if (msg.Duration.length == 0) {
105            this.clockOffset = now - msg.Timestamp
106            this.rx(0, 0, stats)
107			return
108		}
109
110		// Adjust playback time to clock offset
111        let adjustedTxTime = msg.Timestamp + this.clockOffset
112
113		// Every second value is a silence duration
114		let tx = true
115		for (let duration of msg.Duration) {
116			duration = Number(duration)
117			if (tx && (duration > 0)) {
118                this.rx(adjustedTxTime, duration, stats)
119			}
120			adjustedTxTime = Number(adjustedTxTime) + duration
121			tx = !tx
122		}
123    }
124
125    /**
126     * Send a transmission
127     * 
128     * @param {number} timestamp When to play this transmission
129     * @param {number} duration How long the transmission is
130     * @param {boolean} squelch True to mute this tone when we get it back from the repeater
131     */
132    Transmit(timestamp, duration, squelch=true) {
133        let msg = {
134            Timestamp: timestamp - this.clockOffset,
135            Duration: [duration],
136        }
137        let jmsg = JSON.stringify(msg)
138
139        if (this.socket.readyState != 1) {
140            // If we aren't connected, complain.
141            console.error("Not connected, dropping", jmsg)
142            return
143        }
144        this.socket.send(jmsg)
145        if (squelch) {
146            this.sent.push(msg)
147        }
148    }
149
150    Close() {
151        this.wantConnected = false
152        this.socket.close()
153    }
154}
155
156export class Null {
157    constructor(rx, interval=3*time.Second) {
158        this.rx = rx
159        this.init()
160    }
161
162    notice(msg) {
163        this.rx(0, 0, {connected: false, notice: msg})
164    }
165
166    init() {
167        this.notice("Null repeater: nobody will hear you.")
168    }
169
170    Transmit(time, duration, squelch=true) {}
171
172    Close() {}
173}
174
175export class Echo extends Null {
176    constructor(rx, delay=0) {
177        super(rx)
178        this.delay = delay
179    }
180
181    init () {
182        this.notice("Echo repeater: you can only hear yourself.")
183    }
184
185    Transmit(time, duration, squelch=true) {
186        this.rx(time + this.delay, duration, {note: "local"})
187    }
188}
189
190export class Fortune extends Null {
191    /**
192     * 
193     * @param rx Receive callback
194     * @param {Keyer} keyer Robokeyer
195     */
196    constructor(rx, keyer) {
197        super(rx)
198        this.keyer = keyer
199    }
200
201    init() {
202        this.notice("Say something, and I will tell you your fortune.")
203    }
204
205    pulse() {
206        this.timeout = null
207        if (!this.keyer || this.keyer.Busy()) {
208            return
209        }
210
211        let fortune = GetFortune()
212        this.keyer.EnqueueAsciiString(`${fortune} \x04    `)
213    }
214
215    Transmit(time, duration, squelch=true) {
216        if (this.timeout) {
217            clearTimeout(this.timeout)
218        }
219        this.timeout = setTimeout(() => this.pulse(), 3 * time.Second)
220    }
221
222    Close() {
223        this.keyer.Flush()
224        super.Close()
225    }
226}