mirror of https://github.com/nealey/vail.git
Refactor, remove fortunes (for now)
This commit is contained in:
parent
c725555d2c
commit
42c88c3896
540
static/dev.html
540
static/dev.html
|
@ -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 : -->
|
|
411
static/dev.mjs
411
static/dev.mjs
|
@ -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
|
|
|
@ -166,42 +166,14 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="mdl-tabs__panel" id="tools">
|
<div class="mdl-tabs__panel" id="tools">
|
||||||
<table class="center wide">
|
<div class="flex mdl-card__supporting-text">
|
||||||
<tr>
|
<button id="ck" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
|
||||||
<td>
|
CK
|
||||||
<button id="ck" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
|
</button>
|
||||||
CK
|
<p>
|
||||||
</button>
|
Send <code>CK</code> (check) to the repeater, and play when it comes back.
|
||||||
</td>
|
</p>
|
||||||
<td>
|
</div>
|
||||||
<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>
|
</div>
|
||||||
<div class="mdl-card__actions">
|
<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">
|
<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>
|
<li>The sending computer's clock is in the past (running slow)</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p>
|
<p>
|
||||||
Make sure your clock is synced with an Internet time server.
|
Vail attempts to correct for clock differences,
|
||||||
Accurate time is very important to how Vail works.
|
but making sure your computer has correct time,
|
||||||
|
down to the millisecond,
|
||||||
|
can help with reliability.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 class="mdl-card__title-text">How can I help?</h3>
|
<h3 class="mdl-card__title-text">How can I help?</h3>
|
||||||
|
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ const DIT = 1
|
||||||
/** Duration of a dah */
|
/** Duration of a dah */
|
||||||
const DAH = 3
|
const DAH = 3
|
||||||
|
|
||||||
|
|
||||||
const MorseMap = {
|
const MorseMap = {
|
||||||
"\x04": ".-.-.", // End Of Transmission
|
"\x04": ".-.-.", // End Of Transmission
|
||||||
"\x18": "........", // Cancel
|
"\x18": "........", // Cancel
|
||||||
|
@ -73,18 +74,6 @@ if (!window.AudioContext) {
|
||||||
window.AudioContext = window.webkitAudioContext
|
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
|
* 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:
|
* 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
|
* - 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
|
* - 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
|
* Create an Iambic control
|
||||||
*
|
*
|
||||||
|
@ -177,6 +166,7 @@ class Iambic {
|
||||||
Busy() {
|
Busy() {
|
||||||
return this.pulseTimer
|
return this.pulseTimer
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new dit interval (transmission rate)
|
* Set a new dit interval (transmission rate)
|
||||||
*
|
*
|
||||||
|
@ -250,22 +240,41 @@ class Iambic {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edge trigger on key press or release
|
* Do something to the straight key
|
||||||
*
|
*
|
||||||
* @param {number} key DIT or DAH
|
* @param down True if key was pressed
|
||||||
* @param {boolean} down True if key was pressed, false if released
|
*/
|
||||||
*/
|
Straight(down) {
|
||||||
Key(key, down) {
|
|
||||||
if (key == DIT) {
|
|
||||||
this.ditDown = down
|
|
||||||
} else if (key == DAH) {
|
|
||||||
this.dahDown = down
|
|
||||||
}
|
|
||||||
|
|
||||||
if (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 {
|
class Buzzer {
|
||||||
|
@ -422,15 +431,15 @@ class Buzzer {
|
||||||
/**
|
/**
|
||||||
* Buzz for a duration at time
|
* 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} when Time to begin (ms since 1970-01-01Z, null=now)
|
||||||
* @param {number} duration Duration of buzz (ms)
|
* @param {number} duration Duration of buzz (ms)
|
||||||
*/
|
*/
|
||||||
BuzzDuration(high, when, duration) {
|
BuzzDuration(tx, when, duration) {
|
||||||
this.Buzz(high, when)
|
this.Buzz(tx, when)
|
||||||
this.Silence(high, when + duration)
|
this.Silence(tx, when + duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {DIT, DAH, PAUSE, PAUSE_WORD, PAUSE_LETTER}
|
export {DIT, DAH, PAUSE, PAUSE_WORD, PAUSE_LETTER}
|
||||||
export {toast, Iambic, Buzzer}
|
export {Keyer, Buzzer}
|
||||||
|
|
|
@ -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() {
|
||||||
|
}
|
||||||
|
}
|
500
static/vail.mjs
500
static/vail.mjs
|
@ -1,10 +1,28 @@
|
||||||
import * as Morse from "./morse.mjs"
|
import * as Morse from "./morse.mjs"
|
||||||
|
import * as Inputs from "./inputs.mjs"
|
||||||
|
import * as Repeaters from "./repeaters.mjs"
|
||||||
import {getFortune} from "./fortunes.mjs"
|
import {getFortune} from "./fortunes.mjs"
|
||||||
import { toast } from "./morse.mjs"
|
|
||||||
|
|
||||||
const DefaultRepeater = "General Chaos"
|
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() {
|
constructor() {
|
||||||
this.sent = []
|
this.sent = []
|
||||||
this.lagTimes = [0]
|
this.lagTimes = [0]
|
||||||
|
@ -14,39 +32,6 @@ class Vail {
|
||||||
this.beginTxTime = null // Time when we began transmitting
|
this.beginTxTime = null // Time when we began transmitting
|
||||||
this.debug = localStorage.debug
|
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
|
// Redirect old URLs
|
||||||
if (window.location.search) {
|
if (window.location.search) {
|
||||||
let me = new URL(location)
|
let me = new URL(location)
|
||||||
|
@ -56,20 +41,44 @@ class Vail {
|
||||||
window.location = me
|
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
|
// Fill in the name of our repeater
|
||||||
let repeaterElement = document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim()))
|
let repeaterElement = document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim()))
|
||||||
this.setRepeater(decodeURI(unescape(window.location.hash.split("#")[1] || "")))
|
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) {
|
setRepeater(name) {
|
||||||
if (!name || (name == "")) {
|
if (!name || (name == "")) {
|
||||||
name = "General Chaos"
|
name = "General Chaos"
|
||||||
|
@ -94,18 +103,29 @@ class Vail {
|
||||||
window.location.hash = hash
|
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)
|
toast(`Now using repeater: ${name}`)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
let element = document.querySelector(selector)
|
||||||
|
if (!element) {
|
||||||
|
return
|
||||||
|
}
|
||||||
let storedValue = localStorage[element.id]
|
let storedValue = localStorage[element.id]
|
||||||
if (storedValue) {
|
if (storedValue) {
|
||||||
element.value = storedValue
|
element.value = storedValue
|
||||||
|
@ -116,74 +136,82 @@ class Vail {
|
||||||
if (outputElement) {
|
if (outputElement) {
|
||||||
outputElement.value = element.value
|
outputElement.value = element.value
|
||||||
}
|
}
|
||||||
func(e)
|
if (callback) {
|
||||||
|
callback(e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
element.dispatchEvent(new Event("input"))
|
element.dispatchEvent(new Event("input"))
|
||||||
}
|
}
|
||||||
|
|
||||||
midiInit(access) {
|
/**
|
||||||
this.midiAccess = access
|
* Make an error sound and pop up a message
|
||||||
for (let input of this.midiAccess.inputs.values()) {
|
*
|
||||||
input.addEventListener("midimessage", e => this.midiMessage(e))
|
* @param {string} msg The message to pop up
|
||||||
}
|
*/
|
||||||
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) {
|
error(msg) {
|
||||||
Morse.toast(msg)
|
toast(msg)
|
||||||
this.buzzer.ErrorTone()
|
this.buzzer.ErrorTone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the side tone buzzer.
|
||||||
|
*/
|
||||||
beginTx() {
|
beginTx() {
|
||||||
this.beginTxTime = Date.now()
|
this.beginTxTime = Date.now()
|
||||||
this.buzzer.Buzz(true)
|
this.buzzer.Buzz(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the side tone buzzer, and send out how long it was active.
|
||||||
|
*/
|
||||||
endTx() {
|
endTx() {
|
||||||
let endTxTime = Date.now()
|
let endTxTime = Date.now()
|
||||||
let duration = endTxTime - this.beginTxTime
|
let duration = endTxTime - this.beginTxTime
|
||||||
this.buzzer.Silence(true)
|
this.buzzer.Silence(true)
|
||||||
this.wsSend(this.beginTxTime, duration)
|
this.repeater.Transmit(this.beginTxTime, duration)
|
||||||
this.beginTxTime = null
|
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) {
|
updateReading(selector, value) {
|
||||||
let e = document.querySelector(selector)
|
let e = document.querySelector(selector)
|
||||||
if (e) {
|
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
|
* Maximize/minimize a card
|
||||||
*/
|
*
|
||||||
Test() {
|
* @param e Event
|
||||||
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(e) {
|
maximize(e) {
|
||||||
let element = e.target
|
let element = e.target
|
||||||
while (!element.classList.contains("mdl-card")) {
|
while (!element.classList.contains("mdl-card")) {
|
||||||
|
@ -447,7 +237,29 @@ class Vail {
|
||||||
console.log(element)
|
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() {
|
function vailInit() {
|
||||||
|
@ -455,10 +267,10 @@ function vailInit() {
|
||||||
navigator.serviceWorker.register("sw.js")
|
navigator.serviceWorker.register("sw.js")
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
window.app = new Vail()
|
window.app = new VailClient()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
Morse.toast(err)
|
toast(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue