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

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) {
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()
} }