Refactor, remove fortunes (for now)

This commit is contained in:
Neale Pickett 2021-04-27 17:30:16 -06:00
parent c725555d2c
commit 42c88c3896
7 changed files with 526 additions and 1366 deletions

View File

@ -1,540 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>⚠️ Vail-Dev</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Material Design Lite -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.brown-cyan.min.css">
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
<!-- Vail stuff -->
<link rel="manifest" href="manifest.json">
<link rel="icon" href="vail.png" sizes="256x256" type="image/png">
<link rel="icon" href="vail.svg" sizes="any" type="image/svg+xml">
<script type="module" src="dev.mjs"></script>
<script type="module" src="fortune.mjs"></script>
<link rel="stylesheet" href="vail.css">
</head>
<body>
<div class="mdl-layout mdl-js-layout">
<header class="mdl-layout__header mdl-layout__header--scroll">
<div class="mdl-layout__header-row">
<!-- Title -->
<span class="mdl-layout-title">⚠️ Vail - Development ⚠️</span>
<!-- Add spacer, to align navigation to the right -->
<div class="mdl-layout-spacer"></div>
<!-- Navigation -->
<nav class="mdl-navigation">
<a class="mdl-navigation__link" href="https://github.com/nealey/vail">Source Code</a>
<a class="mdl-navigation__link" href="https://github.com/nealey/vail/issues/new">Bug Report</a>
<a class="mdl-navigation__link" href="https://github.com/nealey/vail-adapter">USB Adapter</a>
<a class="mdl-navigation__link" href="https://morse.withgoogle.com/learn/">Learn Morse</a>
</nav>
</div>
</header>
<div class="mdl-layout__drawer">
<span class="mdl-layout-title">Repeaters</span>
<nav class="mdl-navigation">
<a class="mdl-navigation__link" href="?repeater=">General Chaos</a>
<a class="mdl-navigation__link" href="?repeater=1-15+WPM">1-15 WPM</a>
<a class="mdl-navigation__link" href="?repeater=16-20+WPM">16-20 WPM</a>
<a class="mdl-navigation__link" href="?repeater=21-99+WPM">21-99 WPM</a>
</nav>
<hr>
<nav class="mdl-navigation">
<a class="mdl-navigation__link" href="https://morse.withgoogle.com/learn/">Learn Morse Code</a>
<a class="mdl-navigation__link" href="https://github.com/nealey/vail-adapter">Use a physical key</a>
</nav>
</div>
<div id="snackbar" class="mdl-js-snackbar mdl-snackbar">
<div class="mdl-snackbar__text"></div>
<button class="mdl-snackbar__action" type="button"></button>
</div>
<main class="mdl-layout__content">
<div class="flex">
<div class="mdl-card mdl-shadow--4dp input-methods">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">
<span id="repeater"></span>
Repeater
</h2>
<div id="recv">
<!-- This div appears as a little light that turns on when someone's sending -->
<i class="material-icons" id="muted">volume_off</i>
</div>
</div>
<div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect">
<div class="mdl-tabs__tab-bar">
<a href="#straight" class="mdl-tabs__tab is-active">Straight Key</a>
<a href="#iambic" class="mdl-tabs__tab">Iambic</a>
<a href="#tools" class="mdl-tabs__tab">Tools</a>
</div>
<div class="mdl-tabs__panel is-active" id="straight">
<table class="center wide">
<tr>
<td colspan="2">
<button id="key" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Key
</button>
</td>
</tr>
<tr>
<td>
<i class="material-icons" role="presentation">keyboard</i>
</td>
<td>
<kbd>c</kbd>
<kbd>,</kbd>
<kbd>Enter</kbd>
<kbd>⇧ Shift</kbd>
</td>
</tr>
<tr>
<td>
<i class="material-icons" role="presentation">gamepad</i>
</td>
<td>
<img class="gamepad b0" title="Gamepad Bottom Button" src="b0.svg" alt="Bottom button">
<img class="gamepad b1" title="Gamepad Right Button" src="b1.svg" alt="Right button">
</td>
</tr>
</table>
</div>
<div class="mdl-tabs__panel" id="iambic">
<table class="center wide">
<tr>
<td colspan="2">
<button id="dit" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Dit
</button>
</td>
<td colspan="2">
<button id="dah" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Dah
</button>
</td>
</tr>
<tr>
<td>
<i class="material-icons" role="presentation">keyboard</i>
</td>
<td>
<kbd>.</kbd>
<kbd>x</kbd>
</td>
<td>
<i class="material-icons" role="presentation">keyboard</i>
</td>
<td>
<kbd>/</kbd>
<kbd>z</kbd>
</td>
</tr>
<tr>
<td>
<i class="material-icons" role="presentation">gamepad</i>
</td>
<td>
<img class="gamepad b2" title="Gamepad Left Button" src="b2.svg" alt="Left Button">
<kbd class="gamepad" title="Gamepad Left Shoulder Button">LB</kbd>
</td>
<td>
<i class="material-icons" role="presentation">gamepad</i>
</td>
<td>
<img class="gamepad b3" title="Gamepad Top Button" src="b3.svg" alt="Top Button">
<kbd class="gamepad" title="Gamepad Right Shoulder Button">RB</kbd>
</td>
</tr>
<tr>
<td colspan="4" class="mdl-card__supporting-text" style="text-align: center;">
Second mouse button: Dah
</td>
</tr>
</table>
</div>
<div class="mdl-tabs__panel" id="tools">
<table class="center wide">
<tr>
<td>
<button id="ck" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
CK
</button>
</td>
<td>
<button id="fortune" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Fortune
</button>
</td>
</tr>
<tr class="mdl-card__supporting-text">
<td>
<p>
Check (CK) round-trip times and audio functionality
by sending "CK" to the repeater and playing the returned signal.
</p>
</td>
<td>
<p>
Fetch a fortune and play it locally.
This can help practice copying (hearing) Morse code,
without having to involve another person.
</p>
</td>
</tr>
</table>
</div>
<div class="mdl-card__actions">
<button class="mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-button--colored maximize" title="maximize">
<i class="material-icons mdl-color-text--white" role="presentation">aspect_ratio</i>
</button>
</div>
</div>
</div>
<div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Alphabet</h2>
</div>
<div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect">
<div class="mdl-tabs__tab-bar">
<a href="#morse-tree" class="mdl-tabs__tab is-active">Dichotomous Key</a>
<a href="#morse-list" class="mdl-tabs__tab">List</a>
</div>
<div class="mdl-tabs__panel mdl-card__supporting-text long is-active" id="morse-tree">
<table>
<tbody>
<tr>
<td rowspan="8">E .</td>
<td rowspan="4">I ..</td>
<td rowspan="2">S ...</td>
<td rowspan="1">H ....</td>
<td class="dah">4 ....-</td>
</tr>
<tr>
<td rowspan="1">V ...-</td>
<td class="dah">3 ...--</td>
</tr>
<tr>
<td rowspan="2">U ..-</td>
<td rowspan="1">F ..-.</td>
</tr>
<tr>
<td rowspan="1"></td>
<td class="dah">2 ..---</td>
</tr>
<tr>
<td rowspan="4">A .-</td>
<td rowspan="2">R .-.</td>
<td rowspan="1">L .-..</td>
</tr>
<tr>
<td rowspan="1"></td>
</tr>
<tr>
<td rowspan="2">W .--</td>
<td rowspan="1">P .--.</td>
</tr>
<tr>
<td rowspan="1">J .---</td>
<td class="dah">1 .----</td>
</tr>
<tr>
<td rowspan="8">T -</td>
<td rowspan="4">N -.</td>
<td rowspan="2">D -..</td>
<td rowspan="1">B -...</td>
<td>6 -....</td>
</tr>
<tr>
<td rowspan="1">X -..-</td>
</tr>
<tr>
<td rowspan="2">K -.-</td>
<td rowspan="1">C -.-.</td>
</tr>
<tr>
<td rowspan="1">Y -.--</td>
</tr>
<tr>
<td rowspan="4">M --</td>
<td rowspan="2">G --.</td>
<td rowspan="1">Z --..</td>
<td>7 --...</td>
</tr>
<tr>
<td rowspan="1">Q --.-</td>
</tr>
<tr>
<td rowspan="2">O ---</td>
<td rowspan="1"></td>
<td>8 ---..</td>
</tr>
<tr>
<td rowspan="1"></td>
<td>9 ----.</td>
</tr>
</tbody>
</table>
</div>
<div class="mdl-tabs__panel mdl-card__supporting-text long" id="morse-list">
<span>A .-</span>
<span>B -...</span>
<span>C -.-.</span>
<span>D -..</span>
<span>E .</span>
<span>F ..-.</span>
<span>G --.</span>
<span>H ....</span>
<span>I ..</span>
<span>J .---</span>
<span>K -.-</span>
<span>L .-..</span>
<span>M --</span>
<span>N -.</span>
<span>O ---</span>
<span>P .--.</span>
<span>Q --.-</span>
<span>R .-.</span>
<span>S ...</span>
<span>T -</span>
<span>U ..-</span>
<span>V ...-</span>
<span>W .--</span>
<span>X -..-</span>
<span>Y -.--</span>
<span>Z --..</span>
<br>
<span>0 -----</span>
<span>1 .----</span>
<span>2 ..---</span>
<span>3 ...--</span>
<span>4 ....-</span>
<span>5 .....</span>
<span>6 -....</span>
<span>7 --...</span>
<span>8 ---..</span>
<span>9 ----.</span>
<br>
<span>Over .-.</span>
<span>Correction ........</span>
<span>? / Say Again ..--..</span>
</div>
</div>
</div>
<div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Knobs</h2>
</div>
<div class="mdl-card__supporting-text">
<p>
Dit length (iambic):
<output id="iambic-duration-value"></output>ms
<input
id="iambic-duration"
class="mdl-slider mdl-js-slider"
type="range"
min="40"
max="255"
value="100">
</p>
<p>
Recieve delay:
<output id="rx-delay-value"></output>ms
<input
id="rx-delay"
class="mdl-slider mdl-js-slider"
type="range"
min="0"
max="9999"
value="4000">
</p>
<hr>
<table>
<tbody>
<tr>
<td>
Suggested receive delay:
</td>
<td>
<output id="suggested-delay-value">0</output>ms
</td>
</tr>
<tr>
<td>
Average round-trip time:
</td>
<td>
<output id="lag-value">0</output>ms
</td>
</tr>
<tr>
<td>
Longest recent transmission:
</td>
<td>
<output id="longest-rx-value">0</output>ms
</td>
</tr>
<tr>
<td>
Your clock is off by:
</td>
<td>
<output id="clock-off-value">??</output>ms
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Vail</h2>
</div>
<div class="mdl-card__supporting-text">
<p>
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.
</p>
<p>
Just like a radio repeater,
anybody can connect and start transmitting stuff,
and this will broadcast it to everyone connected.
</p>
<h3 class="mdl-card__title-text">Why Does This Exist?</h3>
<p>
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.
</p>
</div>
</div>
<div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">How It Works</h2>
</div>
<div class="mdl-card__supporting-text">
<p>
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.
</p>
<p>
Each Vail transmission (packet) consists of:
</p>
<ul>
<li>timestamp (milliseconds since 1 Jan 1970, 00:00:00 in Reykjavík)</li>
<li>transmission duration (milliseconds)</li>
</ul>
<p>
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 <i>round-trip time</i>:
the time it takes for a packet to get from your computer to the repeater and back.
</p>
<p>
When the client gets a packet it didn't send,
it adds the <i>receive delay</i> to the timestamp,
and schedules to play the tones and silences in the packet
at that time.
</p>
<p>
By adding the maximum round-trip time to the <i>longest recent transmission</i>
(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.
</p>
</div>
</div>
<div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Why do I hear a low tone?</h2>
</div>
<div class="mdl-card__supporting-text">
<p>
This is the "drop tone", and will be accompanied by an error.
</p>
<p>
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.
</p>
<p>
This could be happening for three reasons:
</p>
<ol>
<li>You (the person hearing the drop tone) need a larger receive delay</li>
<li>The receiving computer's clock is in the future (running fast)</li>
<li>The sending computer's clock is in the past (running slow)</li>
</ol>
<p>
Make sure your clock is synced with an Internet time server.
Accurate time is very important to how Vail works.
</p>
</div>
</div>
<div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">How can I help?</h2>
</div>
<div class="mdl-card__supporting-text">
<ul>
<li>Improve the <a href="https://github.com/nealey/vail/">source code</a></li>
<li>Email me and let me know you're using it</li>
</ul>
<p>
<a href="mailto:neale@woozle.org">Neale Pickett</a> kd7oqi
</p>
</div>
</div>
</div>
</main>
</div>
</body>
</html>
<!-- vim: set noet ts=2 sw=2 : -->

View File

@ -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

View File

@ -166,42 +166,14 @@
</table>
</div>
<div class="mdl-tabs__panel" id="tools">
<table class="center wide">
<tr>
<td>
<button id="ck" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
CK
</button>
</td>
<td>
<button id="fortune" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Fortune
</button>
</td>
</tr>
<tr class="mdl-card__supporting-text">
<td>
<p>
Send <code>CK</code> (check) to the repeater, and play when it comes back.
</p>
</td>
<td>
<p>
Spacing: ×<output id="handicap-value"></output>
<input
id="handicap"
class="mdl-slider mdl-js-slider"
type="range"
min="1"
max="8"
value="4">
</p>
<p>
Have vail tell your fortune, in English. Local only: not transmitted.
</p>
</td>
</tr>
</table>
<div class="flex mdl-card__supporting-text">
<button id="ck" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
CK
</button>
<p>
Send <code>CK</code> (check) to the repeater, and play when it comes back.
</p>
</div>
</div>
<div class="mdl-card__actions">
<button class="mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-button--colored maximize" title="maximize">
@ -464,8 +436,10 @@
<li>The sending computer's clock is in the past (running slow)</li>
</ol>
<p>
Make sure your clock is synced with an Internet time server.
Accurate time is very important to how Vail works.
Vail attempts to correct for clock differences,
but making sure your computer has correct time,
down to the millisecond,
can help with reliability.
</p>
<h3 class="mdl-card__title-text">How can I help?</h3>

201
static/inputs.mjs Normal file
View File

@ -0,0 +1,201 @@
export class HTML {
constructor(keyer) {
this.keyer = keyer
// 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))
}
}
keyButton(event) {
let begin = event.type.endsWith("down") || event.type.endsWith("start")
event.preventDefault()
if (event.target.id == "dah") {
this.keyer.Dah(begin)
} else if ((event.target.id == "dit") && (event.button == 2)) {
this.keyer.Dah(begin)
} else if (event.target.id == "dit") {
this.keyer.Dit(begin)
} else if (event.target.id == "key") {
this.keyer.Straight(begin)
}
}
}
export class Keyboard {
constructor(keyer) {
this.keyer = keyer
// Listen for keystrokes
document.addEventListener("keydown", e => this.keyboard(e))
document.addEventListener("keyup", e => this.keyboard(e))
}
keyboard(event) {
if (["INPUT"].includes(document.activeElement.tagName)) {
// Ignore everything if the user is entering text somewhere
return
}
if (event.repeat) {
// Ignore key repeats generated by the OS, we do this ourselves
return
}
let down = event.type.endsWith("down")
if ((event.code == "KeyX") ||
(event.code == "Period") ||
(event.code == "BracketLeft") ||
(event.key == "[")) {
event.preventDefault()
this.keyer.Dit(down)
}
if ((event.code == "KeyZ") ||
(event.code == "Slash") ||
(event.code == "BracketRight") ||
(event.key == "]")) {
event.preventDefault()
this.keyer.Dah(down)
}
if ((event.code == "KeyC") ||
(event.code == "Comma") ||
(event.key == "Enter") ||
(event.key == "NumpadEnter")) {
event.preventDefault()
this.keyer.Straight(down)
}
}
}
export class MIDI {
constructor(keyer) {
this.keyer = keyer
if (navigator.requestMIDIAccess) {
this.midiInit()
}
}
async midiInit(access) {
this.midiAccess = await navigator.requestMIDIAccess()
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.keyer.Straight(begin)
break
case 1: // C#
this.keyer.Dit(begin)
break
case 2: // D
this.keyer.Dah(begin)
break
default:
return
}
}
}
export class Gamepad {
constructor(keyer) {
this.keyer = keyer
// Set up for gamepad input
window.addEventListener("gamepadconnected", e => this.gamepadConnected(e))
}
/**
* Gamepads must be polled, usually at 60fps.
* This could be really expensive,
* especially on devices with a power budget, like phones.
* To be considerate, we only start polling if a gamepad appears.
*
* @param event Gamepad Connected event
*/
gamepadConnected(event) {
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.keyer.Straight(currentButtons.key)
}
if (currentButtons.dit != this.gamepadButtons.dit) {
this.keyer.Dit(currentButtons.dit)
}
if (currentButtons.dah != this.gamepadButtons.dah) {
this.keyer.Dah(currentButtons.dah)
}
this.gamepadButtons = currentButtons
requestAnimationFrame(e => this.gamepadPoll(e))
}
}
/**
* Set up all input methods
*
* @param keyer Keyer object for everyone to use
*/
export function SetupAll(keyer) {
return {
HTML: new HTML(keyer),
Keyboard: new Keyboard(keyer),
MIDI: new MIDI(keyer),
Gamepad: new Gamepad(keyer),
}
}

View File

@ -9,6 +9,7 @@ const DIT = 1
/** Duration of a dah */
const DAH = 3
const MorseMap = {
"\x04": ".-.-.", // End Of Transmission
"\x18": "........", // Cancel
@ -73,18 +74,6 @@ if (!window.AudioContext) {
window.AudioContext = window.webkitAudioContext
}
function toast(msg) {
let el = document.querySelector("#snackbar")
if (!el || !el.MaterialSnackbar) {
console.warn(msg)
return
}
el.MaterialSnackbar.showSnackbar({
message: msg,
timeout: 2000
})
}
/**
* A callback to start or stop transmission
*
@ -92,14 +81,14 @@ function toast(msg) {
*/
/**
* Iambic input class.
* Keyer class. This handles iambic and straight key input.
*
* This will handle the following things that people appear to want with iambic input:
*
* - Typematic: you hold the key down and it repeats evenly-spaced tones
* - Typeahead: if you hit a key while it's still transmitting the last-entered one, it queues up your next entered one
*/
class Iambic {
class Keyer {
/**
* Create an Iambic control
*
@ -177,6 +166,7 @@ class Iambic {
Busy() {
return this.pulseTimer
}
/**
* Set a new dit interval (transmission rate)
*
@ -250,22 +240,41 @@ class Iambic {
}
/**
* Edge trigger on key press or release
*
* @param {number} key DIT or DAH
* @param {boolean} down True if key was pressed, false if released
*/
Key(key, down) {
if (key == DIT) {
this.ditDown = down
} else if (key == DAH) {
this.dahDown = down
}
* Do something to the straight key
*
* @param down True if key was pressed
*/
Straight(down) {
if (down) {
this.Enqueue(key)
this.beginTxFunc()
} else {
this.endTxFunc()
}
}
}
/**
* Do something to the dit key
*
* @param down True if key was pressed
*/
Dit(down) {
this.ditDown = down
if (down) {
this.Enqueue(DIT)
}
}
/**
* Do something to the dah key
*
* @param down True if key was pressed
*/
Dah(down) {
this.dahDown = down
if (down) {
this.Enqueue(DAH)
}
}
}
class Buzzer {
@ -422,15 +431,15 @@ class Buzzer {
/**
* Buzz for a duration at time
*
* @param {boolean} high High or low pitched tone
* @param {boolean} tx Transmit or receive tone
* @param {number} when Time to begin (ms since 1970-01-01Z, null=now)
* @param {number} duration Duration of buzz (ms)
*/
BuzzDuration(high, when, duration) {
this.Buzz(high, when)
this.Silence(high, when + duration)
BuzzDuration(tx, when, duration) {
this.Buzz(tx, when)
this.Silence(tx, when + duration)
}
}
export {DIT, DAH, PAUSE, PAUSE_WORD, PAUSE_LETTER}
export {toast, Iambic, Buzzer}
export {Keyer, Buzzer}

115
static/repeaters.mjs Normal file
View File

@ -0,0 +1,115 @@
export class Vail {
constructor(name, rx) {
this.name = name
this.rx = rx
this.lagDurations = []
this.sent = []
this.wsUrl = new URL("chat", window.location)
this.wsUrl.protocol = this.wsUrl.protocol.replace("http", "ws")
this.wsUrl.searchParams.set("repeater", name)
this.reopen()
}
reopen() {
console.info("Attempting to reconnect", this.wsUrl)
this.clockOffset = 0
this.socket = new WebSocket(this.wsUrl)
this.socket.addEventListener("message", e => this.wsMessage(e))
this.socket.addEventListener("close", () => this.reopen())
}
stats() {
return {
averageLag: this.lagDurations.reduce((a,b) => (a+b), 0) / this.lagDurations.length,
clockOffset: this.clockOffset,
}
}
wsMessage(event) {
let now = Date.now()
let jmsg = event.data
let msg
try {
msg = JSON.parse(jmsg)
}
catch (err) {
console.error(err, jmsg)
return
}
let beginTxTime = msg[0]
let durations = msg.slice(1)
// Why is this happening?
if (beginTxTime == 0) {
return
}
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.lagDurations.unshift(now - this.clockOffset - beginTxTime - totalDuration)
this.lagDurations.splice(20, 2)
this.rx(0, 0, this.stats())
return
}
// The very first packet is the server telling us the current time
if (durations.length == 0) {
if (this.clockOffset == 0) {
this.clockOffset = now - beginTxTime
this.rx(0, 0, this.stats())
}
return
}
// Adjust playback time to clock offset
let adjustedTxTime = beginTxTime + this.clockOffset
// Every second value is a silence duration
let tx = true
for (let duration of durations) {
duration = Number(duration)
if (tx && (duration > 0)) {
this.rx(adjustedTxTime, duration, this.stats())
}
adjustedTxTime = Number(adjustedTxTime) + duration
tx = !tx
}
}
/**
* Send a transmission
*
* @param {number} time When to play this transmission
* @param {number} duration How long the transmission is
* @param {boolean} squelch True to mute this tone when we get it back from the repeater
*/
Transmit(time, duration, squelch=true) {
let msg = [time - this.clockOffset, duration]
let jmsg = JSON.stringify(msg)
this.socket.send(jmsg)
if (squelch) {
this.sent.push(jmsg)
}
}
Close() {
this.socket.close()
}
}
export class Null {
constructor(name, rx) {
}
Transmit(time, duration, squelch=True) {
}
Close() {
}
}

View File

@ -1,10 +1,28 @@
import * as Morse from "./morse.mjs"
import * as Inputs from "./inputs.mjs"
import * as Repeaters from "./repeaters.mjs"
import {getFortune} from "./fortunes.mjs"
import { toast } from "./morse.mjs"
const DefaultRepeater = "General Chaos"
class Vail {
/**
* Pop up a message, using an MDL snackbar.
*
* @param {string} msg Message to display
*/
function toast(msg) {
let el = document.querySelector("#snackbar")
if (!el || !el.MaterialSnackbar) {
console.warn(msg)
return
}
el.MaterialSnackbar.showSnackbar({
message: msg,
timeout: 2000
})
}
class VailClient {
constructor() {
this.sent = []
this.lagTimes = [0]
@ -14,39 +32,6 @@ class Vail {
this.beginTxTime = null // Time when we began transmitting
this.debug = localStorage.debug
// 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.buzzer = new Morse.Buzzer()
this.iambic = new Morse.Iambic(() => this.beginTx(), () => this.endTx())
this.fortuneIambic = new Morse.Iambic(() => this.buzzer.Buzz(), () => this.buzzer.Silence())
// Listen for slider values
this.inputInit("#iambic-duration", e => {
this.iambic.SetIntervalDuration(e.target.value)
this.fortuneIambic.SetIntervalDuration(e.target.value)
})
this.inputInit("#rx-delay", e => {
this.rxDelay = Number(e.target.value)
})
this.inputInit("#handicap", e => {
this.fortuneIambic.SetPauseMultiplier(e.target.value)
})
// Redirect old URLs
if (window.location.search) {
let me = new URL(location)
@ -56,20 +41,44 @@ class Vail {
window.location = me
}
// Make helpers
this.buzzer = new Morse.Buzzer()
this.keyer = new Morse.Keyer(() => this.beginTx(), () => this.endTx())
this.iambicKeyer = new Morse.Keyer(() => this.buzzer.Buzz(), () => this.buzzer.Silence())
// Set up various input methods
this.inputs = Inputs.SetupAll(this.keyer)
// Maximize button
for (let e of document.querySelectorAll("button.maximize")) {
e.addEventListener("click", e => this.maximize(e))
}
for (let e of document.querySelectorAll("#ck")) {
e.addEventListener("click", e => this.test())
}
// Set up sliders
this.sliderInit("#iambic-duration", e => {
this.keyer.SetIntervalDuration(e.target.value)
this.iambicKeyer.SetIntervalDuration(e.target.value)
})
this.sliderInit("#rx-delay", e => {
this.rxDelay = Number(e.target.value)
})
// Fill in the name of our repeater
let repeaterElement = document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim()))
this.setRepeater(decodeURI(unescape(window.location.hash.split("#")[1] || "")))
// 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))
}
/**
* Connect to a repeater by name.
*
* In the future this may do some fancy switching logic to provide multiple types of repeaters.
* For instance, I'd like to create a set of repeaters that run locally, for practice.
*
* @param {string} name Repeater name
*/
setRepeater(name) {
if (!name || (name == "")) {
name = "General Chaos"
@ -94,18 +103,29 @@ class Vail {
window.location.hash = hash
}
toast(`Now using repeater: ${name}`)
if (this.repeater) {
this.repeater.Close()
}
this.repeater = new Repeaters.Vail(name, (w,d,s) => this.receive(w,d,s))
let wsUrl = new URL("chat", window.location)
wsUrl.protocol = wsUrl.protocol.replace("http", "ws")
wsUrl.searchParams.set("repeater", name)
this.socket = new WebSocket(wsUrl)
this.socket.addEventListener("message", e => this.wsMessage(e))
this.socket.addEventListener("close", () => this.setRepeater(name))
toast(`Now using repeater: ${name}`)
}
inputInit(selector, func) {
/**
* Set up a slider.
*
* This reads any previously saved value and sets the slider to that.
* When the slider is updated, it saves the value it's updated to,
* and calls the provided callback with the new value.
*
* @param {string} selector CSS path to the element
* @param {function} callback Callback to call with any new value that is set
*/
sliderInit(selector, callback) {
let element = document.querySelector(selector)
if (!element) {
return
}
let storedValue = localStorage[element.id]
if (storedValue) {
element.value = storedValue
@ -116,74 +136,82 @@ class Vail {
if (outputElement) {
outputElement.value = element.value
}
func(e)
if (callback) {
callback(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
}
}
/**
* Make an error sound and pop up a message
*
* @param {string} msg The message to pop up
*/
error(msg) {
Morse.toast(msg)
toast(msg)
this.buzzer.ErrorTone()
}
/**
* Start the side tone buzzer.
*/
beginTx() {
this.beginTxTime = Date.now()
this.buzzer.Buzz(true)
}
/**
* Stop the side tone buzzer, and send out how long it was active.
*/
endTx() {
let endTxTime = Date.now()
let duration = endTxTime - this.beginTxTime
this.buzzer.Silence(true)
this.wsSend(this.beginTxTime, duration)
this.repeater.Transmit(this.beginTxTime, duration)
this.beginTxTime = null
}
/**
* Called by a repeater class when there's something received.
*
* @param {number} when When to play the tone
* @param {number} duration How long to play the tone
* @param {dict} stats Stuff the repeater class would like us to know about
*/
receive(when, duration, stats) {
this.clockOffset = stats.clockOffset
let now = Date.now()
when += this.rxDelay
if (duration > 0) {
if (when < now) {
this.error("Packet requested playback " + (now - when) + "ms in the past. Increase receive delay!")
return
}
this.buzzer.BuzzDuration(false, when, duration)
this.rxDurations.unshift(duration)
this.rxDurations.splice(20, 2)
}
let averageLag = (stats.averageLag || 0).toFixed(2)
let longestRxDuration = this.rxDurations.reduce((a,b) => Math.max(a,b))
let suggestedDelay = ((averageLag + longestRxDuration) * 1.2).toFixed(0)
this.updateReading("#lag-value", averageLag)
this.updateReading("#longest-rx-value", longestRxDuration)
this.updateReading("#suggested-delay-value", suggestedDelay)
this.updateReading("#clock-off-value", this.clockOffset)
}
/**
* Update an element with a value, if that element exists
*
* @param {string} selector CSS path to the element
* @param value Value to set
*/
updateReading(selector, value) {
let e = document.querySelector(selector)
if (e) {
@ -191,249 +219,11 @@ class Vail {
}
}
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 (["INPUT"].includes(document.activeElement.tagName)) {
// Ignore everything if the user is entering text somewhere
return
}
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 == "BracketLeft") ||
(event.key == "[")) {
event.preventDefault()
this.iambicDit(begin)
}
if ((event.code == "KeyZ") ||
(event.code == "Slash") ||
(event.code == "BracketRight") ||
(event.key == "]")) {
event.preventDefault()
this.iambicDah(begin)
}
if ((event.code == "KeyC") ||
(event.code == "Comma") ||
(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()
} else if ((event.target.id == "fortune") && begin) {
this.PlayFortune()
}
}
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))
}
/**
* Play a randomly-chosen fortune
&& begin */
PlayFortune() {
if (this.fortuneIambic.Busy()) {
toast("I am already telling your fortune!")
} else {
let fortune = getFortune()
this.fortuneIambic.EnqueueAsciiString(`${fortune}\x04`)
}
}
* Maximize/minimize a card
*
* @param e Event
*/
maximize(e) {
let element = e.target
while (!element.classList.contains("mdl-card")) {
@ -447,7 +237,29 @@ class Vail {
console.log(element)
}
/**
* Send "CK" to server, and don't squelch the echo
*/
test() {
let when = Date.now()
let dit = Number(document.querySelector("#iambic-duration-value").value)
let dah = dit * 3
let s = dit
let message = [
dah, s, dit, s, dah, s, dit,
s * 3,
dah, s, dit, s, dah
]
this.repeater.Transmit(when, 0) // Get round-trip time
for (let i in message) {
let duration = message[i]
if (i % 2 == 0) {
this.repeater.Transmit(when, duration, false)
}
when += duration
}
}
}
function vailInit() {
@ -455,10 +267,10 @@ function vailInit() {
navigator.serviceWorker.register("sw.js")
}
try {
window.app = new Vail()
window.app = new VailClient()
} catch (err) {
console.log(err)
Morse.toast(err)
toast(err)
}
}