Many client updates

This commit is contained in:
Neale Pickett 2020-05-01 15:07:09 -06:00
parent 56486979a8
commit 3a16ebb7af
3 changed files with 762 additions and 622 deletions

View File

@ -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 --> <!-- Material Design Lite -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> <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"> <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> <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
<!-- Vail stuff --> <!-- Vail stuff -->
<script src="vail.js"></script> <script src="vail.js"></script>
<link rel="stylesheet" href="vail.css"> <link rel="stylesheet" href="vail.css">
</head> </head>
<body> <body>
<div class="mdl-layout mdl-js-layout"> <div class="mdl-layout mdl-js-layout">
<header class="mdl-layout__header mdl-layout__header--scroll"> <header class="mdl-layout__header mdl-layout__header--scroll">
<div class="mdl-layout__header-row"> <div class="mdl-layout__header-row">
<!-- Title --> <!-- Title -->
<span class="mdl-layout-title">Vail</span> <span class="mdl-layout-title">Vail</span>
<!-- Add spacer, to align navigation to the right --> <!-- Add spacer, to align navigation to the right -->
<div class="mdl-layout-spacer"></div> <div class="mdl-layout-spacer"></div>
<!-- Navigation --> <!-- Navigation -->
<nav class="mdl-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">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/issues/new">Bug Report</a>
<a class="mdl-navigation__link" href="https://morse.withgoogle.com/learn/">Learn Morse</a> <a class="mdl-navigation__link" href="https://morse.withgoogle.com/learn/">Learn Morse</a>
</nav> </nav>
</div> </div>
</header> </header>
<div class="mdl-layout__drawer"> <div class="mdl-layout__drawer">
<span class="mdl-layout-title">Repeaters</span> <span class="mdl-layout-title">Repeaters</span>
<nav class="mdl-navigation"> <nav class="mdl-navigation">
<a class="mdl-navigation__link" href="?repeater=">1-15 WPM</a> <a class="mdl-navigation__link" href="?repeater=">General Chaos</a>
<a class="mdl-navigation__link" href="?repeater=int">16-20 WPM</a> <a class="mdl-navigation__link" href="?repeater=beg">1-15 WPM</a>
<a class="mdl-navigation__link" href="?repeater=adv">21-99 WPM</a> <a class="mdl-navigation__link" href="?repeater=int">16-20 WPM</a>
</nav> <a class="mdl-navigation__link" href="?repeater=adv">21-99 WPM</a>
<hr> </nav>
<nav class="mdl-navigation"> <hr>
<a class="mdl-navigation__link" href="https://morse.withgoogle.com/learn/">Learn Morse Code</a> <nav class="mdl-navigation">
</nav> <a class="mdl-navigation__link" href="https://morse.withgoogle.com/learn/">Learn Morse Code</a>
</div> </nav>
</div>
<main class="mdl-layout__content"> <main class="mdl-layout__content">
<div class="flex"> <div class="flex">
<div class="mdl-card mdl-shadow--4dp"> <div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title"> <div class="mdl-card__title">
<h2 class="mdl-card__title-text"> <h2 class="mdl-card__title-text">Input</h2>
Input </div>
</h2> <div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect">
</div> <div class="mdl-tabs__tab-bar">
<div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect"> <a href="#straight" class="mdl-tabs__tab is-active">Straight Key</a>
<div class="mdl-tabs__tab-bar"> <a href="#iambic" class="mdl-tabs__tab">Iambic</a>
<a href="#straight" class="mdl-tabs__tab is-active">Straight Key</a> <a href="#tools" class="mdl-tabs__tab">Tools</a>
<a href="#iambic" class="mdl-tabs__tab">Iambic</a> </div>
<a href="#tools" class="mdl-tabs__tab">Tools</a> <div class="mdl-tabs__panel is-active" id="straight">
</div> <table class="center wide">
<div class="mdl-tabs__panel is-active" id="straight"> <tr>
<table class="center wide"> <td>
<tr> <button id="key" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
<td> Key
<button id="key" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored"> </button>
Key </td>
</button> </tr>
</td> <tr>
</tr> <td>
<tr> <kbd>c</kbd> or <kbd>,</kbd> or <kbd>⇧ Shift</kbd>
<td> </td>
<code></code> </tr>
</td> </table>
</tr> </div>
</table> <div class="mdl-tabs__panel" id="iambic">
</div> <table class="center wide">
<div class="mdl-tabs__panel" id="iambic"> <tr>
<table class="center wide"> <td>
<tr> <button id="dit" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
<td> Dit
<button id="dit" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored"> </button>
Dit </td>
</button> <td>
</td> <button id="dah" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
<td> Dah
<button id="dah" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored"> </button>
Dah </td>
</button> </tr>
</td> <tr>
</tr> <td>
<tr> <kbd>.</kbd> or <kbd>z</kbd>
<td> <br>
<code>.</code> or <code>z</code> right-click for Dah
</td> </td>
<td> <td>
<code>/</code> or <code>x</code> <kbd>/</kbd> or <kbd>x</kbd>
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
<div class="mdl-tabs__panel" id="tools"> <div class="mdl-tabs__panel" id="tools">
<table class="center wide"> <table class="center wide">
<tr> <tr>
<td> <td>
<button id="ck" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored"> <button id="ck" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
CK CK
</button> </button>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Echo On</td> <td>Echo On</td>
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
<div class="mdl-card mdl-shadow--4dp"> <div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title"> <div class="mdl-card__title">
<h2 class="mdl-card__title-text"> <h2 class="mdl-card__title-text">Knobs</h2>
Knobs </div>
</h2> <div class="mdl-card__supporting-text">
</div> <p>
<div class="mdl-card__supporting-text"> Dit length (iambic):
<table> <output id="iambic-duration-value"></output>ms
<tbody> <input
<tr> id="iambic-duration"
<td> class="mdl-slider mdl-js-slider"
Average round-trip time: type="range"
</td> min="40"
<td> max="255"
<output id="lag-value">0</output>ms value="100">
</td> </p>
</tr> <p>
<tr> Recieve delay:
<td> <output id="rx-delay-value"></output>ms
Longest recent transmission: <input
</td> id="rx-delay"
<td> class="mdl-slider mdl-js-slider"
<output id="longest-rx-value">0</output>ms type="range"
</td> min="0"
</tr> max="9999"
<tr> value="4000">
<td> </p>
Suggested receive delay: <hr>
</td> <table>
<td> <tbody>
<output id="suggested-delay-value">0</output>ms <tr>
</td> <td>
</tr> Suggested receive delay:
<tr> </td>
</tr> <td>
</tbody> <output id="suggested-delay-value">0</output>ms
</table> </td>
<hr> </tr>
<p> <tr>
Recieve delay: <td>
<output id="rx-delay-value"></output>ms Average round-trip time:
<input </td>
id="rx-delay" <td>
class="mdl-slider mdl-js-slider" <output id="lag-value">0</output>ms
type="range" </td>
min="0" </tr>
max="5000" <tr>
value="400"> <td>
</p> Longest recent transmission:
<p> </td>
Dit length (iambic): <td>
<output id="iambic-duration-value"></output>ms <output id="longest-rx-value">0</output>ms
<input </td>
id="iambic-duration" </tr>
class="mdl-slider mdl-js-slider" <tr>
type="range" <td>
min="40" Repeater:
max="255" </td>
value="100"> <td>
</p> <span id="repeater"></span>
</div> </td>
</div> </tr>
</tbody>
</table>
<hr>
<p>Errors</p>
<div id="errors"></div>
</div>
</div>
<div class="mdl-card mdl-shadow--4dp"> <div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title"> <div class="mdl-card__title">
<h2 class="mdl-card__title-text"> <h2 class="mdl-card__title-text">Code Tree</h2>
Code Tree </div>
</h2> <div class="mdl-card__supporting-text">
</div> <img src="code-tree.png">
<div class="mdl-card__supporting-text"> </div>
<img src="code-tree.png"> </div>
</div>
</div>
<div class="mdl-card mdl-shadow--4dp"> <div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title"> <div class="mdl-card__title">
<h2 class="mdl-card__title-text"> <h2 class="mdl-card__title-text">Vail</h2>
Vail </div>
</h2> <div class="mdl-card__supporting-text">
</div> <p>
<div class="mdl-card__supporting-text"> This is a CW repeater,
<p> named after Alfred Vail,
This is a CW repeater, who may or may not have invented what's called "Morse code",
named after Alfred Vail, but clearly had some role in it.
who may or may not have invented what's called "Morse code", </p>
but clearly had some role in it.
</p>
<p> <p>
Just like a radio repeater, Just like a radio repeater,
anybody can connect and start transmitting stuff, anybody can connect and start transmitting stuff,
and this will broadcast it to everyone connected. and this will broadcast it to everyone connected.
</p> </p>
<h3 class="mdl-card__title-text">Why Does This Exist?</h3> <h3 class="mdl-card__title-text">Why Does This Exist?</h3>
<p> <p>
I need a place to practice CW with actual human beings, 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. 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. 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. Nothing else like this exists on the Internet, as far as I can tell.
</p> </p>
</div> </div>
</div> </div>
<div class="mdl-card mdl-shadow--4dp"> <div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title"> <div class="mdl-card__title">
<h2 class="mdl-card__title-text">Future plans</h2> <h2 class="mdl-card__title-text">How It Works</h2>
</div> </div>
<div class="mdl-card__supporting-text"> <div class="mdl-card__supporting-text">
<ul>
<li>Move to a more permanent URL</li> <p>
<li>Make this page less ugly</li> The Internet isn't exactly like radio waves:
<li>Arduino program to let you hook up an iambic paddle over USB</li> it still goes at near the speed of light,
<li>Document the protocol</li> but there are multiple hops between endpoints,
</ul> 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> <h3 class="mdl-card__title-text">How can I help?</h3>
<ul> <ul>
<li>Improve the <a href="https://github.com/nealey/vail/">source code</a></li> <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> <li>Email me and let me know you're using it</li>
</ul> </ul>
<p> <p>
<a href="mailto:neale@woozle.org">Neale Pickett</a> kd7oqi <a href="mailto:neale@woozle.org">Neale Pickett</a> kd7oqi
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
</div> </div>
</body> </body>
</html> </html>

View File

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

View File

@ -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) { constructor(txGain=0.3) {
this.txGain = txGain this.txGain = txGain
this.ac = new AudioContext() this.ac = new AudioContext()
this.lowGain = this.create(lowFreq) this.lowGain = this.create(lowFreq)
this.highGain = this.create(highFreq) this.highGain = this.create(highFreq)
this.errorGain = this.create(errorFreq, "square") this.errorGain = this.create(errorFreq, "square")
} }
create(frequency, type="sine") { create(frequency, type="sine") {
let gain = this.ac.createGain() let gain = this.ac.createGain()
gain.connect(this.ac.destination) gain.connect(this.ac.destination)
gain.gain.value = 0 gain.gain.value = 0
let osc = this.ac.createOscillator() let osc = this.ac.createOscillator()
osc.type = type osc.type = type
osc.connect(gain) osc.connect(gain)
osc.frequency.value = frequency osc.frequency.value = frequency
osc.start() osc.start()
return gain return gain
} }
gain(high) { gain(high) {
if (high) { if (high) {
return this.highGain.gain return this.highGain.gain
} else { } else {
return this.lowGain.gain return this.lowGain.gain
} }
} }
/** /**
* Convert clock time to AudioContext time * Convert clock time to AudioContext time
* *
* @param {number} when Clock time in ms * @param {number} when Clock time in ms
* @return {number} AudioContext offset time * @return {number} AudioContext offset time
*/ */
acTime(when) { acTime(when) {
if (! when) { if (! when) {
return this.ac.currentTime return this.ac.currentTime
} }
let acOffset = Date.now() - this.ac.currentTime*1000 let acOffset = Date.now() - this.ac.currentTime*1000
let acTime = (when - acOffset) / 1000 let acTime = (when - acOffset) / 1000
return acTime return acTime
} }
/** /**
* Set gain * Set gain
* *
* @param {number} gain Value (0-1) * @param {number} gain Value (0-1)
*/ */
SetGain(gain) { SetGain(gain) {
this.txGain = gain this.txGain = gain
} }
/** /**
* Play an error tone * Play an error tone
*/ */
ErrorTone() { ErrorTone() {
this.errorGain.gain.setTargetAtTime(this.txGain * 0.5, this.ac.currentTime, 0.001) 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) this.errorGain.gain.setTargetAtTime(0, this.ac.currentTime + 0.2, 0.001)
} }
/** /**
* Begin buzzing at time * Begin buzzing at time
* *
* @param {boolean} high High or low pitched tone * @param {boolean} high High or low pitched tone
* @param {number} when Time to begin (null=now) * @param {number} when Time to begin (null=now)
*/ */
Buzz(high, when=null) { Buzz(high, when=null) {
let gain = this.gain(high) let gain = this.gain(high)
let acWhen = this.acTime(when) let acWhen = this.acTime(when)
this.ac.resume() this.ac.resume()
gain.setTargetAtTime(this.txGain, acWhen, 0.001) gain.setTargetAtTime(this.txGain, acWhen, 0.001)
} }
/** /**
* End buzzing at time * End buzzing at time
* *
* @param {boolean} high High or low pitched tone * @param {boolean} high High or low pitched tone
* @param {number} when Time to begin (null=now) * @param {number} when Time to begin (null=now)
*/ */
Silence(high, when=null) { Silence(high, when=null) {
let gain = this.gain(high) let gain = this.gain(high)
let acWhen = this.acTime(when) let acWhen = this.acTime(when)
gain.setTargetAtTime(0, acWhen, 0.001) gain.setTargetAtTime(0, acWhen, 0.001)
} }
/** /**
* Buzz for a duration at time * Buzz for a duration at time
* *
* @param {boolean} high High or low pitched tone * @param {boolean} high High or low pitched 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(high, when, duration) {
this.Buzz(high, when) this.Buzz(high, when)
this.Silence(high, when+duration) 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 // Listen to HTML buttons
for (let e of document.querySelectorAll("button.key")) { for (let e of document.querySelectorAll("button.key")) {
e.addEventListener("contextmenu", e => {e.preventDefault(); return false}) e.addEventListener("contextmenu", e => {e.preventDefault(); return false})
e.addEventListener("mousedown", e => this.keyButton(e)) e.addEventListener("mousedown", e => this.keyButton(e))
e.addEventListener("mouseup", e => this.keyButton(e)) e.addEventListener("mouseup", e => this.keyButton(e))
} }
// Listen for keystrokes // Listen for keystrokes
document.addEventListener("keydown", e => this.key(e)) document.addEventListener("keydown", e => this.key(e))
document.addEventListener("keyup", e => this.key(e)) document.addEventListener("keyup", e => this.key(e))
// Make helpers // Make helpers
this.iambic = new Iambic(() => this.beginTx(), () => this.endTx()) this.iambic = new Iambic(() => this.beginTx(), () => this.endTx())
this.buzzer = new Buzzer() this.buzzer = new Buzzer()
// Listen for slider values // Listen for slider values
this.inputInit("#iambic-duration", e => this.iambic.SetInterval(e.target.value)) this.inputInit("#iambic-duration", e => this.iambic.SetInterval(e.target.value))
this.inputInit("#rx-delay", e => {this.rxDelay = Number(e.target.value)}) this.inputInit("#rx-delay", e => {this.rxDelay = Number(e.target.value)})
}
inputInit(selector, func) { // Show what repeater we're on
let element = document.querySelector(selector) let repeater = (new URL(location)).searchParams.get("repeater") || "Default"
let storedValue = localStorage[element.id] document.querySelector("#repeater").textContent = repeater
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() { inputInit(selector, func) {
this.beginTxTime = Date.now() let element = document.querySelector(selector)
this.buzzer.Buzz(true) 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"))
}
endTx() { error(msg) {
let endTxTime = Date.now() let now = new Date()
let duration = endTxTime - this.beginTxTime let e = document.querySelector("#errors")
this.buzzer.Silence(true) if (e) {
this.wsSend(this.beginTxTime, duration) let p = e.appendChild(document.createElement("p"))
this.beginTxTime = null p.innerText = "[" + now.toLocaleTimeString() + "] " + msg
} e.scrollTop = e.scrollHeight
}
this.buzzer.ErrorTone()
}
updateReading(selector, value) { beginTx() {
let e = document.querySelector(selector) this.beginTxTime = Date.now()
if (e) { this.buzzer.Buzz(true)
e.value = value }
}
}
updateReadings() { endTx() {
let avgLag = this.lagTimes.reduce((a,b) => (a+b)) / this.lagTimes.length let endTxTime = Date.now()
let longestRx = this.rxDurations.reduce((a,b) => Math.max(a,b)) let duration = endTxTime - this.beginTxTime
let suggestedDelay = (avgLag + longestRx) * 1.2 this.buzzer.Silence(true)
this.wsSend(this.beginTxTime, duration)
this.beginTxTime = null
}
this.updateReading("#lag-value", avgLag.toFixed()) updateReading(selector, value) {
this.updateReading("#longest-rx-value", longestRx) let e = document.querySelector(selector)
this.updateReading("#suggested-delay-value", suggestedDelay.toFixed()) if (e) {
} e.value = value
}
}
addLagReading(duration) { updateReadings() {
this.lagTimes.push(duration) let avgLag = this.lagTimes.reduce((a,b) => (a+b)) / this.lagTimes.length
while (this.lagTimes.length > 20) { let longestRx = this.rxDurations.reduce((a,b) => Math.max(a,b))
this.lagTimes.shift() let suggestedDelay = (avgLag + longestRx) * 1.2
}
this.updateReadings()
}
addRxDuration(duration) { this.updateReading("#lag-value", avgLag.toFixed())
this.rxDurations.push(duration) this.updateReading("#longest-rx-value", longestRx)
while (this.rxDurations.length > 20) { this.updateReading("#suggested-delay-value", suggestedDelay.toFixed())
this.rxDurations.shift() }
}
this.updateReadings()
}
wsSend(time, duration) { addLagReading(duration) {
let msg = [time, duration] this.lagTimes.push(duration)
let jmsg = JSON.stringify(msg) while (this.lagTimes.length > 20) {
this.socket.send(jmsg) this.lagTimes.shift()
this.sent.push(jmsg) }
} this.updateReadings()
}
wsMessage(event) { addRxDuration(duration) {
let now = Date.now() this.rxDurations.push(duration)
let jmsg = event.data while (this.rxDurations.length > 20) {
let msg = JSON.parse(jmsg) this.rxDurations.shift()
let beginTxTime = msg[0] }
let durations = msg.slice(1) this.updateReadings()
}
let sent = this.sent.filter(e => e != jmsg) wsSend(time, duration) {
if (sent.length < this.sent.length) { let msg = [time, duration]
// We're getting our own message back, which tells us our lag. let jmsg = JSON.stringify(msg)
// We shouldn't emit a tone, though. this.socket.send(jmsg)
let totalDuration = durations.reduce((a,b) => a+b) this.sent.push(jmsg)
this.sent = sent }
this.addLagReading(now - beginTxTime - totalDuration)
return 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 // Every other value is a silence duration
let tx = true let tx = true
for (let duration of durations) { for (let duration of durations) {
duration = Number(duration) duration = Number(duration)
if (tx && (duration > 0)) { if (tx && (duration > 0)) {
this.buzzer.BuzzDuration(false, adjustedTxTime, duration) this.buzzer.BuzzDuration(false, adjustedTxTime, duration)
this.addRxDuration(duration) this.addRxDuration(duration)
} }
adjustedTxTime = Number(adjustedTxTime) + duration adjustedTxTime = Number(adjustedTxTime) + duration
tx = !tx tx = !tx
} }
} }
key(event) { key(event) {
if (event.repeat) { if (event.repeat) {
// Ignore key repeats generated by the OS, we do this ourselves // Ignore key repeats generated by the OS, we do this ourselves
return return
} }
let begin = event.type.endsWith("down") let begin = event.type.endsWith("down")
if ((event.code == "KeyZ") || (event.code == "Period")) { if ((event.code == "KeyZ") || (event.code == "Period")) {
event.preventDefault() event.preventDefault()
this.iambic.Key(begin, DIT) this.iambic.Key(begin, DIT)
} }
if ((event.code == "KeyX") || (event.code == "Slash")) { if ((event.code == "KeyX") || (event.code == "Slash")) {
event.preventDefault() event.preventDefault()
this.iambic.Key(begin, DAH) this.iambic.Key(begin, DAH)
} }
if ((event.key == "Shift")) { if ((event.code == "KeyC") || (event.code == "Comma") || (event.key == "Shift")) {
event.preventDefault() event.preventDefault()
if (begin) { if (begin) {
this.beginTx() this.beginTx()
} else { } else {
this.endTx() this.endTx()
} }
} }
} }
keyButton(event) { keyButton(event) {
let begin = event.type.endsWith("down") let begin = event.type.endsWith("down")
if (event.target.id == "dah") { if (event.target.id == "dah") {
this.iambic.Key(begin, DAH) this.iambic.Key(begin, DAH)
} else if ((event.target.id == "dit") && (event.button == 2)) { } else if ((event.target.id == "dit") && (event.button == 2)) {
this.iambic.Key(begin, DAH) this.iambic.Key(begin, DAH)
} else if (event.target.id == "dit") { } else if (event.target.id == "dit") {
this.iambic.Key(begin, DIT) this.iambic.Key(begin, DIT)
} else if (event.target.id == "key") { } else if (event.target.id == "key") {
if (begin) { if (begin) {
this.beginTx() this.beginTx()
} else { } else {
this.endTx() this.endTx()
} }
} else if (event.target.id == "ck") { } else if (event.target.id == "ck") {
this.Test() this.Test()
} }
} }
/** /**
* Send "CK" to server, and don't squelch the repeat * Send "CK" to server, and don't squelch the repeat
*/ */
Test() { Test() {
let dit = Number(document.querySelector("#iambic-duration-value").value) let dit = Number(document.querySelector("#iambic-duration-value").value)
let dah = dit * 3 let dah = dit * 3
let s = dit let s = dit
let msg = [ let msg = [
Date.now(), Date.now(),
dah, s, dit, s, dah, s, dit, dah, s, dit, s, dah, s, dit,
s * 3, s * 3,
dah, s, dit, s, dah dah, s, dit, s, dah
] ]
this.wsSend(Date.now(), 0) // Get round-trip time this.wsSend(Date.now(), 0) // Get round-trip time
this.socket.send(JSON.stringify(msg)) 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()
} }