From 42c88c38961718500c4b9610bd50ff2466068cdf Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Tue, 27 Apr 2021 17:30:16 -0600 Subject: [PATCH] Refactor, remove fortunes (for now) --- static/dev.html | 540 ------------------------------------------- static/dev.mjs | 411 -------------------------------- static/index.html | 50 +--- static/inputs.mjs | 201 ++++++++++++++++ static/morse.mjs | 75 +++--- static/repeaters.mjs | 115 +++++++++ static/vail.mjs | 500 +++++++++++++-------------------------- 7 files changed, 526 insertions(+), 1366 deletions(-) delete mode 100644 static/dev.html delete mode 100644 static/dev.mjs create mode 100644 static/inputs.mjs create mode 100644 static/repeaters.mjs diff --git a/static/dev.html b/static/dev.html deleted file mode 100644 index 09697e0..0000000 --- a/static/dev.html +++ /dev/null @@ -1,540 +0,0 @@ - - - - ⚠️ Vail-Dev - - - - - - - - - - - - - - - - - -
-
-
- - ⚠️ Vail - Development ⚠️ - -
- - -
-
- - - -
-
- -
- -
-
-
-
-

- - Repeater -

-
- - volume_off -
-
-
- -
- - - - - - - - - - - - -
- -
- keyboard - - c - , - Enter - ⇧ Shift -
- gamepad - - Bottom button - Right button -
-
-
- - - - - - - - - - - - - - - - - - - - -
- - - -
- keyboard - - . - x - - keyboard - - / - z -
- gamepad - - Left Button - LB - - gamepad - - Top Button - RB -
- Second mouse button: Dah -
-
-
- - - - - - - - - -
- - - -
-

- Check (CK) round-trip times and audio functionality - by sending "CK" to the repeater and playing the returned signal. -

-
-

- Fetch a fortune and play it locally. - This can help practice copying (hearing) Morse code, - without having to involve another person. -

-
-
-
- -
-
-
- -
-
-

Alphabet

-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
E .I ..S ...H ....4 ....-
V ...-3 ...--
U ..-F ..-.
2 ..---
A .-R .-.L .-..
W .--P .--.
J .---1 .----
T -N -.D -..B -...6 -....
X -..-
K -.-C -.-.
Y -.--
M --G --.Z --..7 --...
Q --.-
O ---8 ---..
9 ----.
-
-
- A .- - B -... - C -.-. - D -.. - E . - F ..-. - G --. - H .... - I .. - J .--- - K -.- - L .-.. - M -- - N -. - O --- - P .--. - Q --.- - R .-. - S ... - T - - U ..- - V ...- - W .-- - X -..- - Y -.-- - Z --.. -
- 0 ----- - 1 .---- - 2 ..--- - 3 ...-- - 4 ....- - 5 ..... - 6 -.... - 7 --... - 8 ---.. - 9 ----. -
- Over .-. - Correction ........ - ? / Say Again ..--.. -
-
-
- -
-
-

Knobs

-
-
-

- Dit length (iambic): - ms - -

-

- Recieve delay: - ms - -

-
- - - - - - - - - - - - - - - - - - - -
- Suggested receive delay: - - 0ms -
- Average round-trip time: - - 0ms -
- Longest recent transmission: - - 0ms -
- Your clock is off by: - - ??ms -
-
-
- -
-
-

Vail

-
-
-

- This is a CW repeater, - named after Alfred Vail, - who may or may not have invented what's called "Morse code", - but clearly had some role in it. -

- -

- Just like a radio repeater, - anybody can connect and start transmitting stuff, - and this will broadcast it to everyone connected. -

- -

Why Does This Exist?

- -

- I need a place to practice CW with actual human beings, - and I want it to be as close as possible to what I'd experience on a radio. - Also, I don't want to make people buy a bunch of radio hardware. - Nothing else like this exists on the Internet, as far as I can tell. -

-
-
- -
-
-

How It Works

-
-
- -

- The Internet isn't exactly like radio waves: - it still goes at near the speed of light, - but there are multiple hops between endpoints, - which buffer up transmissions, and multiplex them onto a single uplink connection. - These repeaters (routers) - are also allowed to just drop things if they need to. - It's the responsibility of the communicating parties - to work out whether something needs to be retransmitted. - Because of this, - there's no telling how long it will take for a transmission to get to a destination. -

- -

- Each Vail transmission (packet) consists of: -

- -
    -
  • timestamp (milliseconds since 1 Jan 1970, 00:00:00 in Reykjavík)
  • -
  • transmission duration (milliseconds)
  • -
- -

- The repeater does nothing but broadcast everything it gets - to every connected Vail client, - including the one that sent the packet. - When your client gets back the exact same thing it sent, - it compares the current time to the time in the packet. - This is the round-trip time: - the time it takes for a packet to get from your computer to the repeater and back. -

- -

- When the client gets a packet it didn't send, - it adds the receive delay to the timestamp, - and schedules to play the tones and silences in the packet - at that time. -

- -

- By adding the maximum round-trip time to the longest recent transmission - (the length of a dah, hopefully), - your client can make a guess about how much time needs to be added to a received timestamp, - in order to have it play back in the future at the time it comes in. - This is just a guess. - If you're communicating with somebody with a higher round-trip time than you have, - you'll need to raise your receive delay to account for it. -

-
-
- -
-
-

Why do I hear a low tone?

-
-
-

- This is the "drop tone", and will be accompanied by an error. -

- -

- This means the packet arrived so late, it can't be played in time. - In technical terms: the timestamp of the packet plus the receive delay - is less than the current time. - It can't be scheduled to play, because we can't go back in time. -

- -

- This could be happening for three reasons: -

- -
    -
  1. You (the person hearing the drop tone) need a larger receive delay
  2. -
  3. The receiving computer's clock is in the future (running fast)
  4. -
  5. The sending computer's clock is in the past (running slow)
  6. -
- -

- Make sure your clock is synced with an Internet time server. - Accurate time is very important to how Vail works. -

-
-
- -
-
-

How can I help?

-
-
-
    -
  • Improve the source code
  • -
  • Email me and let me know you're using it
  • -
- -

- Neale Pickett kd7oqi -

- -
-
-
-
-
- - - diff --git a/static/dev.mjs b/static/dev.mjs deleted file mode 100644 index 056605c..0000000 --- a/static/dev.mjs +++ /dev/null @@ -1,411 +0,0 @@ -import * as Morse from "./morse.mjs" - -class Vail { - constructor() { - this.sent = [] - this.lagTimes = [0] - this.rxDurations = [0] - this.clockOffset = 0 // How badly our clock is off of the server's - this.rxDelay = 0 // Milliseconds to add to incoming timestamps - this.beginTxTime = null // Time when we began transmitting - this.debug = localStorage.debug - - this.openSocket() - - // Listen to HTML buttons - for (let e of document.querySelectorAll("button.key")) { - e.addEventListener("contextmenu", e => { e.preventDefault(); return false }) - e.addEventListener("touchstart", e => this.keyButton(e)) - e.addEventListener("touchend", e => this.keyButton(e)) - e.addEventListener("mousedown", e => this.keyButton(e)) - e.addEventListener("mouseup", e => this.keyButton(e)) - } - for (let e of document.querySelectorAll("button.maximize")) { - e.addEventListener("click", e => this.maximize(e)) - } - - // Listen for keystrokes - document.addEventListener("keydown", e => this.keyboard(e)) - document.addEventListener("keyup", e => this.keyboard(e)) - - // Make helpers - this.iambic = new Morse.Iambic(() => this.beginTx(), () => this.endTx()) - this.buzzer = new Morse.Buzzer() - - // Listen for slider values - this.inputInit("#iambic-duration", e => this.iambic.SetIntervalDuration(e.target.value)) - this.inputInit("#rx-delay", e => { this.rxDelay = Number(e.target.value) }) - - // Show what repeater we're on - let repeater = (new URL(location)).searchParams.get("repeater") || "General Chaos" - document.querySelector("#repeater").textContent = repeater - - // Request MIDI access - if (navigator.requestMIDIAccess) { - navigator.requestMIDIAccess() - .then(a => this.midiInit(a)) - } - - // Set up for gamepad input - window.addEventListener("gamepadconnected", e => this.gamepadConnected(e)) - } - - openSocket() { - // Set up WebSocket - let wsUrl = new URL("chat", window.location) - wsUrl.protocol = wsUrl.protocol.replace("http", "ws") - this.socket = new WebSocket(wsUrl) - this.socket.addEventListener("message", e => this.wsMessage(e)) - this.socket.addEventListener("close", e => this.openSocket()) - } - - inputInit(selector, func) { - let element = document.querySelector(selector) - let storedValue = localStorage[element.id] - if (storedValue) { - element.value = storedValue - } - let outputElement = document.querySelector(selector + "-value") - element.addEventListener("input", e => { - localStorage[element.id] = element.value - if (outputElement) { - outputElement.value = element.value - } - func(e) - }) - element.dispatchEvent(new Event("input")) - } - - midiInit(access) { - this.midiAccess = access - for (let input of this.midiAccess.inputs.values()) { - input.addEventListener("midimessage", e => this.midiMessage(e)) - } - this.midiAccess.addEventListener("statechange", e => this.midiStateChange(e)) - } - - midiStateChange(event) { - // XXX: it's not entirely clear how to handle new devices showing up. - // XXX: possibly we go through this.midiAccess.inputs and somehow only listen on new things - } - - midiMessage(event) { - let data = Array.from(event.data) - - let begin - let cmd = data[0] >> 4 - let chan = data[0] & 0xf - switch (cmd) { - case 9: - begin = true - break - case 8: - begin = false - break - default: - return - } - - switch (data[1] % 12) { - case 0: // C - this.straightKey(begin) - break - case 1: // C# - this.iambic.Key(Morse.DIT, begin) - break - case 2: // D - this.iambic.Key(Morse.DAH, begin) - break - default: - return - } - } - - error(msg) { - Morse.toast(msg) - this.buzzer.ErrorTone() - } - - beginTx() { - this.beginTxTime = Date.now() - this.buzzer.Buzz(true) - } - - endTx() { - let endTxTime = Date.now() - let duration = endTxTime - this.beginTxTime - this.buzzer.Silence(true) - this.wsSend(this.beginTxTime, duration) - this.beginTxTime = null - } - - updateReading(selector, value) { - let e = document.querySelector(selector) - if (e) { - e.value = value - } - } - - updateReadings() { - let avgLag = this.lagTimes.reduce((a, b) => (a + b)) / this.lagTimes.length - let longestRx = this.rxDurations.reduce((a, b) => Math.max(a, b)) - let suggestedDelay = (avgLag + longestRx) * 1.2 - - this.updateReading("#lag-value", avgLag.toFixed()) - this.updateReading("#longest-rx-value", longestRx) - this.updateReading("#suggested-delay-value", suggestedDelay.toFixed()) - this.updateReading("#clock-off-value", this.clockOffset) - } - - addLagReading(duration) { - this.lagTimes.push(duration) - while (this.lagTimes.length > 20) { - this.lagTimes.shift() - } - this.updateReadings() - } - - addRxDuration(duration) { - this.rxDurations.push(duration) - while (this.rxDurations.length > 20) { - this.rxDurations.shift() - } - this.updateReadings() - } - - wsSend(time, duration) { - let msg = [time - this.clockOffset, duration] - let jmsg = JSON.stringify(msg) - this.socket.send(jmsg) - this.sent.push(jmsg) - } - - wsMessage(event) { - let now = Date.now() - let jmsg = event.data - let msg - try { - msg = JSON.parse(jmsg) - } - catch (err) { - console.log(err, msg) - return - } - let beginTxTime = msg[0] - let durations = msg.slice(1) - - if (this.debug) { - console.log("recv", beginTxTime, durations) - } - - 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.addLagReading(now - beginTxTime - totalDuration) - return - } - - // Server is telling us the current time - if (durations.length == 0) { - let offset = now - beginTxTime - if (this.clockOffset == 0) { - this.clockOffset = offset - this.updateReadings() - } - return - } - - // Why is this happening? - if (beginTxTime == 0) { - return - } - - // Add rxDelay - let adjustedTxTime = beginTxTime + this.rxDelay - if (adjustedTxTime < now) { - console.log("adjustedTxTime: ", adjustedTxTime, " now: ", now) - this.error("Packet requested playback " + (now - adjustedTxTime) + "ms in the past. Increase receive delay!") - return - } - - // Every other value is a silence duration - let tx = true - for (let duration of durations) { - duration = Number(duration) - if (tx && (duration > 0)) { - this.buzzer.BuzzDuration(false, adjustedTxTime, duration) - this.addRxDuration(duration) - } - adjustedTxTime = Number(adjustedTxTime) + duration - tx = !tx - } - } - - straightKey(begin) { - if (begin) { - this.beginTx() - } else { - this.endTx() - } - } - - iambicDit(begin) { - this.iambic.Key(Morse.DIT, begin) - } - - iambicDah(begin) { - this.iambic.Key(Morse.DAH, begin) - } - - keyboard(event) { - if (event.repeat) { - // Ignore key repeats generated by the OS, we do this ourselves - return - } - - let begin = event.type.endsWith("down") - - if ((event.code == "KeyX") || - (event.code == "Period") || - (event.code == "ControlLeft") || - (event.code == "BracketLeft") || - (event.key == "[")) { - event.preventDefault() - this.iambicDit(begin) - } - if ((event.code == "KeyZ") || - (event.code == "Slash") || - (event.code == "ControlRight") || - (event.code == "BracketRight") || - (event.key == "]")) { - event.preventDefault() - this.iambicDah(begin) - } - if ((event.code == "KeyC") || - (event.code == "Comma") || - (event.key == "Shift") || - (event.key == "Enter") || - (event.key == "NumpadEnter")) { - event.preventDefault() - this.straightKey(begin) - } - } - - keyButton(event) { - let begin = event.type.endsWith("down") || event.type.endsWith("start") - - event.preventDefault() - - if (event.target.id == "dah") { - this.iambicDah(begin) - } else if ((event.target.id == "dit") && (event.button == 2)) { - this.iambicDah(begin) - } else if (event.target.id == "dit") { - this.iambicDit(begin) - } else if (event.target.id == "key") { - this.straightKey(begin) - } else if ((event.target.id == "ck") && begin) { - this.Test() - } - } - - - gamepadConnected(event) { - // Polling could be computationally expensive, - // especially on devices with a power budget, like phones. - // To be considerate, we only start polling if a gamepad appears. - if (!this.gamepadButtons) { - this.gamepadButtons = {} - this.gamepadPoll(event.timeStamp) - } - } - - gamepadPoll(timestamp) { - let currentButtons = {} - for (let gp of navigator.getGamepads()) { - if (gp == null) { - continue - } - for (let i in gp.buttons) { - let pressed = gp.buttons[i].pressed - if (i < 2) { - currentButtons.key |= pressed - } else if (i % 2 == 0) { - currentButtons.dit |= pressed - } else { - currentButtons.dah |= pressed - } - } - } - - if (currentButtons.key != this.gamepadButtons.key) { - this.straightKey(currentButtons.key) - } - if (currentButtons.dit != this.gamepadButtons.dit) { - this.iambicDit(currentButtons.dit) - } - if (currentButtons.dah != this.gamepadButtons.dah) { - this.iambicDah(currentButtons.dah) - } - this.gamepadButtons = currentButtons - - requestAnimationFrame(e => this.gamepadPoll(e)) - } - - /** - * Send "CK" to server, and don't squelch the repeat - */ - Test() { - let dit = Number(document.querySelector("#iambic-duration-value").value) - let dah = dit * 3 - let s = dit - - let msg = [ - Date.now(), - dah, s, dit, s, dah, s, dit, - s * 3, - dah, s, dit, s, dah - ] - this.wsSend(Date.now(), 0) // Get round-trip time - this.socket.send(JSON.stringify(msg)) - } - - maximize(e) { - let element = e.target - while (!element.classList.contains("mdl-card")) { - element = element.parentElement - if (!element) { - console.log("Maximize button: couldn't find parent card") - return - } - } - element.classList.toggle("maximized") - console.log(element) - } - - -} - -function vailInit() { - if (navigator.serviceWorker) { - navigator.serviceWorker.register("sw.js") - } - try { - window.app = new Vail() - } catch (err) { - console.log(err) - Morse.toast(err) - } -} - - -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", vailInit) -} else { - vailInit() -} - -// vim: noet sw=2 ts=2 diff --git a/static/index.html b/static/index.html index a80d878..d621805 100644 --- a/static/index.html +++ b/static/index.html @@ -166,42 +166,14 @@
- - - - - - - - - -
- - - -
-

- Send CK (check) to the repeater, and play when it comes back. -

-
-

- Spacing: × - -

-

- Have vail tell your fortune, in English. Local only: not transmitted. -

-
+
+ +

+ Send CK (check) to the repeater, and play when it comes back. +

+