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}