mirror of https://github.com/nealey/vail.git
Many client updates
This commit is contained in:
parent
56486979a8
commit
3a16ebb7af
|
@ -1,253 +1,350 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Vail</title>
|
<title>Vail</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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.teal-purple.min.css">
|
|
||||||
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Vail stuff -->
|
<!-- Material Design Lite -->
|
||||||
<script src="vail.js"></script>
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||||
<link rel="stylesheet" href="vail.css">
|
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.teal-purple.min.css">
|
||||||
</head>
|
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
|
||||||
<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</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://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=">1-15 WPM</a>
|
|
||||||
<a class="mdl-navigation__link" href="?repeater=int">16-20 WPM</a>
|
|
||||||
<a class="mdl-navigation__link" href="?repeater=adv">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>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main class="mdl-layout__content">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="mdl-card mdl-shadow--4dp">
|
|
||||||
<div class="mdl-card__title">
|
|
||||||
<h2 class="mdl-card__title-text">
|
|
||||||
Input
|
|
||||||
</h2>
|
|
||||||
</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>
|
|
||||||
<button id="key" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
|
|
||||||
Key
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<code>⇧</code>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="mdl-tabs__panel" id="iambic">
|
|
||||||
<table class="center wide">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<button id="dit" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
|
|
||||||
Dit
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button id="dah" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
|
|
||||||
Dah
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<code>.</code> or <code>z</code>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<code>/</code> or <code>x</code>
|
|
||||||
</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>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Echo On</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mdl-card mdl-shadow--4dp">
|
<!-- Vail stuff -->
|
||||||
<div class="mdl-card__title">
|
<script src="vail.js"></script>
|
||||||
<h2 class="mdl-card__title-text">
|
<link rel="stylesheet" href="vail.css">
|
||||||
Knobs
|
</head>
|
||||||
</h2>
|
<body>
|
||||||
</div>
|
<div class="mdl-layout mdl-js-layout">
|
||||||
<div class="mdl-card__supporting-text">
|
<header class="mdl-layout__header mdl-layout__header--scroll">
|
||||||
<table>
|
<div class="mdl-layout__header-row">
|
||||||
<tbody>
|
<!-- Title -->
|
||||||
<tr>
|
<span class="mdl-layout-title">Vail</span>
|
||||||
<td>
|
<!-- Add spacer, to align navigation to the right -->
|
||||||
Average round-trip time:
|
<div class="mdl-layout-spacer"></div>
|
||||||
</td>
|
<!-- Navigation -->
|
||||||
<td>
|
<nav class="mdl-navigation">
|
||||||
<output id="lag-value">0</output>ms
|
<a class="mdl-navigation__link" href="https://github.com/nealey/vail">Source Code</a>
|
||||||
</td>
|
<a class="mdl-navigation__link" href="https://github.com/nealey/vail/issues/new">Bug Report</a>
|
||||||
</tr>
|
<a class="mdl-navigation__link" href="https://morse.withgoogle.com/learn/">Learn Morse</a>
|
||||||
<tr>
|
</nav>
|
||||||
<td>
|
</div>
|
||||||
Longest recent transmission:
|
</header>
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<output id="longest-rx-value">0</output>ms
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
Suggested receive delay:
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<output id="suggested-delay-value">0</output>ms
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<hr>
|
|
||||||
<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="5000"
|
|
||||||
value="400">
|
|
||||||
</p>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mdl-card mdl-shadow--4dp">
|
|
||||||
<div class="mdl-card__title">
|
|
||||||
<h2 class="mdl-card__title-text">
|
|
||||||
Code Tree
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div class="mdl-card__supporting-text">
|
|
||||||
<img src="code-tree.png">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mdl-card mdl-shadow--4dp">
|
<div class="mdl-layout__drawer">
|
||||||
<div class="mdl-card__title">
|
<span class="mdl-layout-title">Repeaters</span>
|
||||||
<h2 class="mdl-card__title-text">
|
<nav class="mdl-navigation">
|
||||||
Vail
|
<a class="mdl-navigation__link" href="?repeater=">General Chaos</a>
|
||||||
</h2>
|
<a class="mdl-navigation__link" href="?repeater=beg">1-15 WPM</a>
|
||||||
</div>
|
<a class="mdl-navigation__link" href="?repeater=int">16-20 WPM</a>
|
||||||
<div class="mdl-card__supporting-text">
|
<a class="mdl-navigation__link" href="?repeater=adv">21-99 WPM</a>
|
||||||
<p>
|
</nav>
|
||||||
This is a CW repeater,
|
<hr>
|
||||||
named after Alfred Vail,
|
<nav class="mdl-navigation">
|
||||||
who may or may not have invented what's called "Morse code",
|
<a class="mdl-navigation__link" href="https://morse.withgoogle.com/learn/">Learn Morse Code</a>
|
||||||
but clearly had some role in it.
|
</nav>
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
<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>
|
<main class="mdl-layout__content">
|
||||||
|
<div class="flex">
|
||||||
<p>
|
<div class="mdl-card mdl-shadow--4dp">
|
||||||
I need a place to practice CW with actual human beings,
|
<div class="mdl-card__title">
|
||||||
and I want it to be as close as possible to what I'd experience on a radio.
|
<h2 class="mdl-card__title-text">Input</h2>
|
||||||
Also, I don't want to make people buy a bunch of radio hardware.
|
</div>
|
||||||
Nothing else like this exists on the Internet, as far as I can tell.
|
<div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect">
|
||||||
</p>
|
<div class="mdl-tabs__tab-bar">
|
||||||
</div>
|
<a href="#straight" class="mdl-tabs__tab is-active">Straight Key</a>
|
||||||
</div>
|
<a href="#iambic" class="mdl-tabs__tab">Iambic</a>
|
||||||
|
<a href="#tools" class="mdl-tabs__tab">Tools</a>
|
||||||
<div class="mdl-card mdl-shadow--4dp">
|
</div>
|
||||||
<div class="mdl-card__title">
|
<div class="mdl-tabs__panel is-active" id="straight">
|
||||||
<h2 class="mdl-card__title-text">Future plans</h2>
|
<table class="center wide">
|
||||||
</div>
|
<tr>
|
||||||
<div class="mdl-card__supporting-text">
|
<td>
|
||||||
<ul>
|
<button id="key" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
|
||||||
<li>Move to a more permanent URL</li>
|
Key
|
||||||
<li>Make this page less ugly</li>
|
</button>
|
||||||
<li>Arduino program to let you hook up an iambic paddle over USB</li>
|
</td>
|
||||||
<li>Document the protocol</li>
|
</tr>
|
||||||
</ul>
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>c</kbd> or <kbd>,</kbd> or <kbd>⇧ Shift</kbd>
|
||||||
<h3 class="mdl-card__title-text">How can I help?</h3>
|
</td>
|
||||||
|
</tr>
|
||||||
<ul>
|
</table>
|
||||||
<li>Improve the <a href="https://github.com/nealey/vail/">source code</a></li>
|
</div>
|
||||||
<li>Email me and let me know you're using it</li>
|
<div class="mdl-tabs__panel" id="iambic">
|
||||||
</ul>
|
<table class="center wide">
|
||||||
|
<tr>
|
||||||
<p>
|
<td>
|
||||||
<a href="mailto:neale@woozle.org">Neale Pickett</a> kd7oqi
|
<button id="dit" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
|
||||||
</p>
|
Dit
|
||||||
|
</button>
|
||||||
</div>
|
</td>
|
||||||
</div>
|
<td>
|
||||||
</div>
|
<button id="dah" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
|
||||||
</main>
|
Dah
|
||||||
</div>
|
</button>
|
||||||
</body>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<kbd>.</kbd> or <kbd>z</kbd>
|
||||||
|
<br>
|
||||||
|
right-click for Dah
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<kbd>/</kbd> or <kbd>x</kbd>
|
||||||
|
</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>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Echo On</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</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>
|
||||||
|
Repeater:
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span id="repeater"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<hr>
|
||||||
|
<p>Errors</p>
|
||||||
|
<div id="errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mdl-card mdl-shadow--4dp">
|
||||||
|
<div class="mdl-card__title">
|
||||||
|
<h2 class="mdl-card__title-text">Code Tree</h2>
|
||||||
|
</div>
|
||||||
|
<div class="mdl-card__supporting-text">
|
||||||
|
<img src="code-tree.png">
|
||||||
|
</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>
|
||||||
|
<li>silence duration (milliseconds, optional)</li>
|
||||||
|
<li>transmission duration (milliseconds, optional)</li>
|
||||||
|
<li>silence duration (milliseconds, optional)</li>
|
||||||
|
<li>Repeat as necessary</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">Future plans</h2>
|
||||||
|
</div>
|
||||||
|
<div class="mdl-card__supporting-text">
|
||||||
|
<ul>
|
||||||
|
<li>Move to a more permanent URL</li>
|
||||||
|
<li>Make this page less ugly</li>
|
||||||
|
<li>Arduino program to let you hook up an iambic paddle over USB</li>
|
||||||
|
<li>Document the protocol</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<h3 class="mdl-card__title-text">How can I help?</h3>
|
||||||
|
|
||||||
|
<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>
|
</html>
|
||||||
|
|
|
@ -22,6 +22,29 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#errors {
|
||||||
|
color: rgba(127, 0, 0, .54);
|
||||||
|
max-height: 5em;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes yellow-fade {
|
||||||
|
0% {background: yellow;}
|
||||||
|
100% {background: none;}
|
||||||
|
}
|
||||||
|
#errors p {
|
||||||
|
margin: 0;
|
||||||
|
animation: yellow-fade 2s ease-in 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
background-color: #eee;
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 9pt;
|
||||||
|
padding: .1em .6em;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
@ -31,3 +54,8 @@ code {
|
||||||
img {
|
img {
|
||||||
max-width: 20em;
|
max-width: 20em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mdl-card__supporting-text {
|
||||||
|
max-height: 20em;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
767
static/vail.js
767
static/vail.js
|
@ -8,407 +8,422 @@ const DIT = 1
|
||||||
const DAH = 3
|
const DAH = 3
|
||||||
|
|
||||||
class Iambic {
|
class Iambic {
|
||||||
constructor(beginTxFunc, endTxFunc) {
|
constructor(beginTxFunc, endTxFunc) {
|
||||||
this.beginTxFunc = beginTxFunc
|
this.beginTxFunc = beginTxFunc
|
||||||
this.endTxFunc = endTxFunc
|
this.endTxFunc = endTxFunc
|
||||||
this.interval = null
|
this.interval = null
|
||||||
this.state = this.stateSpace
|
this.state = this.stateSpace
|
||||||
this.keyState = null
|
this.keyState = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a new interval (transmission rate)
|
* Set a new interval (transmission rate)
|
||||||
*
|
*
|
||||||
* @param {number} duration New interval duration, in ms
|
* @param {number} duration New interval duration, in ms
|
||||||
*/
|
*/
|
||||||
SetInterval(duration) {
|
SetInterval(duration) {
|
||||||
clearInterval(this.interval)
|
clearInterval(this.interval)
|
||||||
this.interval = setInterval(e => this.pulse(), duration)
|
this.interval = setInterval(e => this.pulse(), duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// An interval has passed, call whatever the current state function is
|
// An interval has passed, call whatever the current state function is
|
||||||
pulse(event) {
|
pulse(event) {
|
||||||
this.state()
|
this.state()
|
||||||
}
|
}
|
||||||
|
|
||||||
stateSpace() {
|
stateSpace() {
|
||||||
// Don't transmit for one interval.
|
// Don't transmit for one interval.
|
||||||
this.state = this.keyState || this.stateSpace
|
this.state = this.keyState || this.stateSpace
|
||||||
}
|
}
|
||||||
stateDit() {
|
stateDit() {
|
||||||
// Send a dit
|
// Send a dit
|
||||||
this.beginTxFunc()
|
this.beginTxFunc()
|
||||||
this.state = this.stateEnd
|
this.state = this.stateEnd
|
||||||
}
|
}
|
||||||
stateDah() {
|
stateDah() {
|
||||||
// Send a dah
|
// Send a dah
|
||||||
this.beginTxFunc()
|
this.beginTxFunc()
|
||||||
this.state = this.stateDah2
|
this.state = this.stateDah2
|
||||||
}
|
}
|
||||||
stateDah2() {
|
stateDah2() {
|
||||||
this.state = this.stateDah3
|
this.state = this.stateDah3
|
||||||
}
|
}
|
||||||
stateDah3() {
|
stateDah3() {
|
||||||
this.state = this.stateEnd
|
this.state = this.stateEnd
|
||||||
}
|
}
|
||||||
stateEnd() {
|
stateEnd() {
|
||||||
// Stop sending
|
// Stop sending
|
||||||
this.endTxFunc()
|
this.endTxFunc()
|
||||||
this.state = this.stateSpace
|
this.state = this.stateSpace
|
||||||
this.state()
|
this.state()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edge trigger on key press or release
|
* Edge trigger on key press or release
|
||||||
*
|
*
|
||||||
* @param {boolean} down True if key was pressed, false if released
|
* @param {boolean} down True if key was pressed, false if released
|
||||||
* @param {number} key DIT or DAH
|
* @param {number} key DIT or DAH
|
||||||
*/
|
*/
|
||||||
Key(down, key) {
|
Key(down, key) {
|
||||||
// By setting keyState we request this state transition,
|
// By setting keyState we request this state transition,
|
||||||
// the next time the transition is possible.
|
// the next time the transition is possible.
|
||||||
let keyState = null
|
let keyState = null
|
||||||
if (key == DIT) {
|
if (key == DIT) {
|
||||||
keyState = this.stateDit
|
keyState = this.stateDit
|
||||||
} else if (key == DAH) {
|
} else if (key == DAH) {
|
||||||
keyState = this.stateDah
|
keyState = this.stateDah
|
||||||
}
|
}
|
||||||
|
|
||||||
if (down) {
|
if (down) {
|
||||||
this.keyState = keyState
|
this.keyState = keyState
|
||||||
} else if (keyState == this.keyState) {
|
} else if (keyState == this.keyState) {
|
||||||
// Only stop when we've released the right key
|
// Only stop when we've released the right key
|
||||||
this.keyState = null
|
this.keyState = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Buzzer {
|
class Buzzer {
|
||||||
// Buzzers keep two oscillators: one high and one low.
|
// Buzzers keep two oscillators: one high and one low.
|
||||||
// They generate a continuous waveform,
|
// They generate a continuous waveform,
|
||||||
// and we change the gain to turn the pitches off and on.
|
// and we change the gain to turn the pitches off and on.
|
||||||
//
|
//
|
||||||
// This also implements a very quick ramp-up and ramp-down in gain,
|
// This also implements a very quick ramp-up and ramp-down in gain,
|
||||||
// in order to avoid "pops" (square wave overtones)
|
// in order to avoid "pops" (square wave overtones)
|
||||||
// that happen with instant changes in gain.
|
// that happen with instant changes in gain.
|
||||||
|
|
||||||
constructor(txGain=0.3) {
|
|
||||||
this.txGain = txGain
|
|
||||||
|
|
||||||
this.ac = new AudioContext()
|
|
||||||
|
|
||||||
this.lowGain = this.create(lowFreq)
|
|
||||||
this.highGain = this.create(highFreq)
|
|
||||||
this.errorGain = this.create(errorFreq, "square")
|
|
||||||
}
|
|
||||||
|
|
||||||
create(frequency, type="sine") {
|
|
||||||
let gain = this.ac.createGain()
|
|
||||||
gain.connect(this.ac.destination)
|
|
||||||
gain.gain.value = 0
|
|
||||||
let osc = this.ac.createOscillator()
|
|
||||||
osc.type = type
|
|
||||||
osc.connect(gain)
|
|
||||||
osc.frequency.value = frequency
|
|
||||||
osc.start()
|
|
||||||
return gain
|
|
||||||
}
|
|
||||||
|
|
||||||
gain(high) {
|
|
||||||
if (high) {
|
|
||||||
return this.highGain.gain
|
|
||||||
} else {
|
|
||||||
return this.lowGain.gain
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
constructor(txGain=0.3) {
|
||||||
* Convert clock time to AudioContext time
|
this.txGain = txGain
|
||||||
*
|
|
||||||
* @param {number} when Clock time in ms
|
|
||||||
* @return {number} AudioContext offset time
|
|
||||||
*/
|
|
||||||
acTime(when) {
|
|
||||||
if (! when) {
|
|
||||||
return this.ac.currentTime
|
|
||||||
}
|
|
||||||
|
|
||||||
let acOffset = Date.now() - this.ac.currentTime*1000
|
this.ac = new AudioContext()
|
||||||
let acTime = (when - acOffset) / 1000
|
|
||||||
return acTime
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set gain
|
|
||||||
*
|
|
||||||
* @param {number} gain Value (0-1)
|
|
||||||
*/
|
|
||||||
SetGain(gain) {
|
|
||||||
this.txGain = gain
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.lowGain = this.create(lowFreq)
|
||||||
* Play an error tone
|
this.highGain = this.create(highFreq)
|
||||||
*/
|
this.errorGain = this.create(errorFreq, "square")
|
||||||
ErrorTone() {
|
}
|
||||||
this.errorGain.gain.setTargetAtTime(this.txGain * 0.5, this.ac.currentTime, 0.001)
|
|
||||||
this.errorGain.gain.setTargetAtTime(0, this.ac.currentTime + 0.2, 0.001)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
create(frequency, type="sine") {
|
||||||
* Begin buzzing at time
|
let gain = this.ac.createGain()
|
||||||
*
|
gain.connect(this.ac.destination)
|
||||||
* @param {boolean} high High or low pitched tone
|
gain.gain.value = 0
|
||||||
* @param {number} when Time to begin (null=now)
|
let osc = this.ac.createOscillator()
|
||||||
*/
|
osc.type = type
|
||||||
Buzz(high, when=null) {
|
osc.connect(gain)
|
||||||
let gain = this.gain(high)
|
osc.frequency.value = frequency
|
||||||
let acWhen = this.acTime(when)
|
osc.start()
|
||||||
|
return gain
|
||||||
this.ac.resume()
|
}
|
||||||
gain.setTargetAtTime(this.txGain, acWhen, 0.001)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* End buzzing at time
|
|
||||||
*
|
|
||||||
* @param {boolean} high High or low pitched tone
|
|
||||||
* @param {number} when Time to begin (null=now)
|
|
||||||
*/
|
|
||||||
Silence(high, when=null) {
|
|
||||||
let gain = this.gain(high)
|
|
||||||
let acWhen = this.acTime(when)
|
|
||||||
|
|
||||||
gain.setTargetAtTime(0, acWhen, 0.001)
|
gain(high) {
|
||||||
}
|
if (high) {
|
||||||
|
return this.highGain.gain
|
||||||
/**
|
} else {
|
||||||
* Buzz for a duration at time
|
return this.lowGain.gain
|
||||||
*
|
}
|
||||||
* @param {boolean} high High or low pitched tone
|
}
|
||||||
* @param {number} when Time to begin (ms since 1970-01-01Z, null=now)
|
|
||||||
* @param {number} duration Duration of buzz (ms)
|
/**
|
||||||
*/
|
* Convert clock time to AudioContext time
|
||||||
BuzzDuration(high, when, duration) {
|
*
|
||||||
this.Buzz(high, when)
|
* @param {number} when Clock time in ms
|
||||||
this.Silence(high, when+duration)
|
* @return {number} AudioContext offset time
|
||||||
}
|
*/
|
||||||
|
acTime(when) {
|
||||||
|
if (! when) {
|
||||||
|
return this.ac.currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
let acOffset = Date.now() - this.ac.currentTime*1000
|
||||||
|
let acTime = (when - acOffset) / 1000
|
||||||
|
return acTime
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set gain
|
||||||
|
*
|
||||||
|
* @param {number} gain Value (0-1)
|
||||||
|
*/
|
||||||
|
SetGain(gain) {
|
||||||
|
this.txGain = gain
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play an error tone
|
||||||
|
*/
|
||||||
|
ErrorTone() {
|
||||||
|
this.errorGain.gain.setTargetAtTime(this.txGain * 0.5, this.ac.currentTime, 0.001)
|
||||||
|
this.errorGain.gain.setTargetAtTime(0, this.ac.currentTime + 0.2, 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin buzzing at time
|
||||||
|
*
|
||||||
|
* @param {boolean} high High or low pitched tone
|
||||||
|
* @param {number} when Time to begin (null=now)
|
||||||
|
*/
|
||||||
|
Buzz(high, when=null) {
|
||||||
|
let gain = this.gain(high)
|
||||||
|
let acWhen = this.acTime(when)
|
||||||
|
|
||||||
|
this.ac.resume()
|
||||||
|
gain.setTargetAtTime(this.txGain, acWhen, 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End buzzing at time
|
||||||
|
*
|
||||||
|
* @param {boolean} high High or low pitched tone
|
||||||
|
* @param {number} when Time to begin (null=now)
|
||||||
|
*/
|
||||||
|
Silence(high, when=null) {
|
||||||
|
let gain = this.gain(high)
|
||||||
|
let acWhen = this.acTime(when)
|
||||||
|
|
||||||
|
gain.setTargetAtTime(0, acWhen, 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buzz for a duration at time
|
||||||
|
*
|
||||||
|
* @param {boolean} high High or low pitched 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Vail {
|
class Vail {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sent = []
|
this.sent = []
|
||||||
this.lagTimes = [0]
|
this.lagTimes = [0]
|
||||||
this.rxDurations = [0]
|
this.rxDurations = [0]
|
||||||
this.rxDelay = 0 // Milliseconds to add to incoming timestamps
|
this.rxDelay = 0 // Milliseconds to add to incoming timestamps
|
||||||
this.beginTxTime = null // Time when we began transmitting
|
this.beginTxTime = null // Time when we began transmitting
|
||||||
|
|
||||||
// Set up WebSocket
|
// Set up WebSocket
|
||||||
let wsUrl = new URL(window.location)
|
let wsUrl = new URL(window.location)
|
||||||
wsUrl.protocol = "ws:"
|
wsUrl.protocol = "ws:"
|
||||||
wsUrl.pathname += "chat"
|
wsUrl.pathname += "chat"
|
||||||
this.socket = new WebSocket(wsUrl)
|
this.socket = new WebSocket(wsUrl)
|
||||||
this.socket.addEventListener("message", e => this.wsMessage(e))
|
this.socket.addEventListener("message", e => this.wsMessage(e))
|
||||||
|
|
||||||
// Listen to HTML buttons
|
|
||||||
for (let e of document.querySelectorAll("button.key")) {
|
|
||||||
e.addEventListener("contextmenu", e => {e.preventDefault(); return false})
|
|
||||||
e.addEventListener("mousedown", e => this.keyButton(e))
|
|
||||||
e.addEventListener("mouseup", e => this.keyButton(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for keystrokes
|
// Listen to HTML buttons
|
||||||
document.addEventListener("keydown", e => this.key(e))
|
for (let e of document.querySelectorAll("button.key")) {
|
||||||
document.addEventListener("keyup", e => this.key(e))
|
e.addEventListener("contextmenu", e => {e.preventDefault(); return false})
|
||||||
|
e.addEventListener("mousedown", e => this.keyButton(e))
|
||||||
// Make helpers
|
e.addEventListener("mouseup", e => this.keyButton(e))
|
||||||
this.iambic = new Iambic(() => this.beginTx(), () => this.endTx())
|
}
|
||||||
this.buzzer = new Buzzer()
|
|
||||||
|
|
||||||
// Listen for slider values
|
// Listen for keystrokes
|
||||||
this.inputInit("#iambic-duration", e => this.iambic.SetInterval(e.target.value))
|
document.addEventListener("keydown", e => this.key(e))
|
||||||
this.inputInit("#rx-delay", e => {this.rxDelay = Number(e.target.value)})
|
document.addEventListener("keyup", e => this.key(e))
|
||||||
}
|
|
||||||
|
|
||||||
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"))
|
|
||||||
}
|
|
||||||
|
|
||||||
beginTx() {
|
// Make helpers
|
||||||
this.beginTxTime = Date.now()
|
this.iambic = new Iambic(() => this.beginTx(), () => this.endTx())
|
||||||
this.buzzer.Buzz(true)
|
this.buzzer = new Buzzer()
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 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 = JSON.parse(jmsg)
|
|
||||||
let beginTxTime = msg[0]
|
|
||||||
let durations = msg.slice(1)
|
|
||||||
|
|
||||||
let sent = this.sent.filter(e => e != jmsg)
|
// Listen for slider values
|
||||||
if (sent.length < this.sent.length) {
|
this.inputInit("#iambic-duration", e => this.iambic.SetInterval(e.target.value))
|
||||||
// We're getting our own message back, which tells us our lag.
|
this.inputInit("#rx-delay", e => {this.rxDelay = Number(e.target.value)})
|
||||||
// We shouldn't emit a tone, though.
|
|
||||||
let totalDuration = durations.reduce((a,b) => a+b)
|
// Show what repeater we're on
|
||||||
this.sent = sent
|
let repeater = (new URL(location)).searchParams.get("repeater") || "Default"
|
||||||
this.addLagReading(now - beginTxTime - totalDuration)
|
document.querySelector("#repeater").textContent = repeater
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
|
||||||
|
error(msg) {
|
||||||
|
let now = new Date()
|
||||||
|
let e = document.querySelector("#errors")
|
||||||
|
if (e) {
|
||||||
|
let p = e.appendChild(document.createElement("p"))
|
||||||
|
p.innerText = "[" + now.toLocaleTimeString() + "] " + msg
|
||||||
|
e.scrollTop = e.scrollHeight
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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 = JSON.parse(jmsg)
|
||||||
|
let beginTxTime = msg[0]
|
||||||
|
let durations = msg.slice(1)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
let adjustedTxTime = beginTxTime+this.rxDelay
|
let adjustedTxTime = beginTxTime+this.rxDelay
|
||||||
if (adjustedTxTime < now) {
|
if (adjustedTxTime < now) {
|
||||||
this.buzzer.ErrorTone()
|
this.error("Packet requested playback " + (now - adjustedTxTime) + "ms in the past. Increase receive delay!")
|
||||||
return
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
key(event) {
|
// Every other value is a silence duration
|
||||||
if (event.repeat) {
|
let tx = true
|
||||||
// Ignore key repeats generated by the OS, we do this ourselves
|
for (let duration of durations) {
|
||||||
return
|
duration = Number(duration)
|
||||||
}
|
if (tx && (duration > 0)) {
|
||||||
|
this.buzzer.BuzzDuration(false, adjustedTxTime, duration)
|
||||||
let begin = event.type.endsWith("down")
|
this.addRxDuration(duration)
|
||||||
|
}
|
||||||
if ((event.code == "KeyZ") || (event.code == "Period")) {
|
adjustedTxTime = Number(adjustedTxTime) + duration
|
||||||
event.preventDefault()
|
tx = !tx
|
||||||
this.iambic.Key(begin, DIT)
|
}
|
||||||
}
|
}
|
||||||
if ((event.code == "KeyX") || (event.code == "Slash")) {
|
|
||||||
event.preventDefault()
|
key(event) {
|
||||||
this.iambic.Key(begin, DAH)
|
if (event.repeat) {
|
||||||
}
|
// Ignore key repeats generated by the OS, we do this ourselves
|
||||||
if ((event.key == "Shift")) {
|
return
|
||||||
event.preventDefault()
|
}
|
||||||
if (begin) {
|
|
||||||
this.beginTx()
|
let begin = event.type.endsWith("down")
|
||||||
} else {
|
|
||||||
this.endTx()
|
if ((event.code == "KeyZ") || (event.code == "Period")) {
|
||||||
}
|
event.preventDefault()
|
||||||
}
|
this.iambic.Key(begin, DIT)
|
||||||
}
|
}
|
||||||
|
if ((event.code == "KeyX") || (event.code == "Slash")) {
|
||||||
keyButton(event) {
|
event.preventDefault()
|
||||||
let begin = event.type.endsWith("down")
|
this.iambic.Key(begin, DAH)
|
||||||
|
}
|
||||||
if (event.target.id == "dah") {
|
if ((event.code == "KeyC") || (event.code == "Comma") || (event.key == "Shift")) {
|
||||||
this.iambic.Key(begin, DAH)
|
event.preventDefault()
|
||||||
} else if ((event.target.id == "dit") && (event.button == 2)) {
|
if (begin) {
|
||||||
this.iambic.Key(begin, DAH)
|
this.beginTx()
|
||||||
} else if (event.target.id == "dit") {
|
} else {
|
||||||
this.iambic.Key(begin, DIT)
|
this.endTx()
|
||||||
} else if (event.target.id == "key") {
|
}
|
||||||
if (begin) {
|
}
|
||||||
this.beginTx()
|
}
|
||||||
} else {
|
|
||||||
this.endTx()
|
keyButton(event) {
|
||||||
}
|
let begin = event.type.endsWith("down")
|
||||||
} else if (event.target.id == "ck") {
|
|
||||||
this.Test()
|
if (event.target.id == "dah") {
|
||||||
}
|
this.iambic.Key(begin, DAH)
|
||||||
}
|
} else if ((event.target.id == "dit") && (event.button == 2)) {
|
||||||
|
this.iambic.Key(begin, DAH)
|
||||||
/**
|
} else if (event.target.id == "dit") {
|
||||||
* Send "CK" to server, and don't squelch the repeat
|
this.iambic.Key(begin, DIT)
|
||||||
*/
|
} else if (event.target.id == "key") {
|
||||||
Test() {
|
if (begin) {
|
||||||
let dit = Number(document.querySelector("#iambic-duration-value").value)
|
this.beginTx()
|
||||||
let dah = dit * 3
|
} else {
|
||||||
let s = dit
|
this.endTx()
|
||||||
|
}
|
||||||
let msg = [
|
} else if (event.target.id == "ck") {
|
||||||
Date.now(),
|
this.Test()
|
||||||
dah, s, dit, s, dah, s, dit,
|
}
|
||||||
s * 3,
|
}
|
||||||
dah, s, dit, s, dah
|
|
||||||
]
|
/**
|
||||||
this.wsSend(Date.now(), 0) // Get round-trip time
|
* Send "CK" to server, and don't squelch the repeat
|
||||||
this.socket.send(JSON.stringify(msg))
|
*/
|
||||||
}
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function vailInit() {
|
function vailInit() {
|
||||||
window.app = new Vail()
|
window.app = new Vail()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", vailInit)
|
document.addEventListener("DOMContentLoaded", vailInit)
|
||||||
} else {
|
} else {
|
||||||
vailInit()
|
vailInit()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue