Charts, pare down UI

This commit is contained in:
Neale Pickett 2022-04-24 17:13:56 -06:00
parent 2290c2ff02
commit 01ed64ad2d
5 changed files with 115 additions and 303 deletions

View File

@ -14,7 +14,7 @@ class HistoryChart {
* @param {string} strokeStyle strokeStyle to draw in * @param {string} strokeStyle strokeStyle to draw in
* @param {Duration} duration Time to display history for * @param {Duration} duration Time to display history for
*/ */
constructor(canvas, strokeStyle, duration) { constructor(canvas, strokeStyle="black", duration=20*Second) {
this.canvas = canvas this.canvas = canvas
this.ctx = canvas.getContext("2d") this.ctx = canvas.getContext("2d")
this.duration = duration this.duration = duration
@ -29,8 +29,10 @@ class HistoryChart {
this.ctx.translate(0, -canvas.height) this.ctx.translate(0, -canvas.height)
this.ctx.strokeStyle = strokeStyle this.ctx.strokeStyle = strokeStyle
this.ctx.fillStyle = strokeStyle
this.ctx.lineWdith = 2 this.ctx.lineWdith = 2
this.running=true
this.draw() this.draw()
} }
@ -42,8 +44,8 @@ class HistoryChart {
* This also cleans up the event list, * This also cleans up the event list,
* purging anything that is too old to be displayed. * purging anything that is too old to be displayed.
* *
* @param when Time the event happened * @param {Number} when Time the event happened
* @param value Value for the event * @param {Number} value Value for the event
*/ */
Add(when, value) { Add(when, value) {
let now = Date.now() let now = Date.now()
@ -54,8 +56,10 @@ class HistoryChart {
while ((this.data.length > 1) && (this.data[1][0] < earliest)) { while ((this.data.length > 1) && (this.data[1][0] < earliest)) {
this.data.shift() this.data.shift()
} }
}
console.log(this.data) Stop() {
this.running = false
} }
draw() { draw() {
@ -64,43 +68,29 @@ class HistoryChart {
let xScale = this.canvas.width / this.duration let xScale = this.canvas.width / this.duration
let yScale = this.canvas.height * 0.95 let yScale = this.canvas.height * 0.95
let y = 0 let y = 0
let x = 0
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.ctx.moveTo(0, 0)
this.ctx.beginPath()
for (let point of this.data) { for (let point of this.data) {
let x = (point[0] - earliest) * xScale let x2 = (point[0] - earliest) * xScale
this.ctx.lineTo(x, y) let y2 = point[1] * yScale
y = point[1] * yScale
this.ctx.lineTo(x, y)
}
this.ctx.lineTo(this.canvas.width, y)
this.ctx.stroke()
if (y > 0) {
this.ctx.fillRect(x, 0, x2-x, y)
}
x=x2
y=y2
}
if (y > 0) {
this.ctx.fillRect(x, 0, this.canvas.width, y)
}
if (this.running) {
requestAnimationFrame(() => this.draw()) requestAnimationFrame(() => this.draw())
} }
}
} }
export {HistoryChart} export {HistoryChart}
// XXX: remove after testing
let chart
function init() {
let canvas = document.querySelector("#chart")
chart = new HistoryChart(canvas, "red", 20 * Second)
setInterval(update, 500 * Millisecond)
}
function update() {
let now = Date.now()
chart.Add(now, Math.sin(now/Second)/2 + 0.5)
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}

View File

@ -27,9 +27,6 @@
<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://discord.gg/GBzj8cBat7">Discord</a>
<a class="mdl-navigation__link" href="https://morse.withgoogle.com/learn/">Learn Morse</a>
<a class="mdl-navigation__link" href="https://github.com/nealey/vail-adapter">USB Adapter</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">Source Code</a>
</nav> </nav>
</div> </div>
@ -54,10 +51,10 @@
<a class="mdl-navigation__link" href="#Fortunes: Pauses ×10">Fortunes (crazy slow)</a> <a class="mdl-navigation__link" href="#Fortunes: Pauses ×10">Fortunes (crazy slow)</a>
</nav> </nav>
<hr> <hr>
<span class="mdl-layout-title">Resources</span>
<nav class="mdl-navigation"> <nav class="mdl-navigation">
<a class="mdl-navigation__link" href="https://github.com/nealey/vail/wiki">Wiki</a>
<a class="mdl-navigation__link" href="https://discord.gg/GBzj8cBat7">Discord</a> <a class="mdl-navigation__link" href="https://discord.gg/GBzj8cBat7">Discord</a>
<a class="mdl-navigation__link" href="https://morse.withgoogle.com/learn/">Learn Morse Code</a>
<a class="mdl-navigation__link" href="https://github.com/nealey/vail-adapter">Use a physical key</a>
</nav> </nav>
</div> </div>
@ -96,6 +93,13 @@
</h2> </h2>
</div> </div>
<output id="note"></output> <output id="note"></output>
<div id="charts">
<canvas class="chart" id="txChart"></canvas>
<canvas class="chart" id="ditChart"></canvas>
<canvas class="chart" id="dahChart"></canvas>
</div>
<div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect"> <div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect">
<div class="mdl-tabs__tab-bar"> <div class="mdl-tabs__tab-bar">
<a href="#straight" class="mdl-tabs__tab is-active" data-singlekey="straight">Straight Key</a> <a href="#straight" class="mdl-tabs__tab is-active" data-singlekey="straight">Straight Key</a>
@ -217,6 +221,7 @@
</div> </div>
<div class="mdl-card__supporting-text"> <div class="mdl-card__supporting-text">
<textarea class="notes" placeholder="Enter your own notes here"></textarea> <textarea class="notes" placeholder="Enter your own notes here"></textarea>
<a href="https://github.com/nealey/vail/wiki">Vail Wiki</a>
</div> </div>
</div> </div>
@ -265,6 +270,12 @@
<span class="mdl-switch__label">Telegraph sounds</span> <span class="mdl-switch__label">Telegraph sounds</span>
</label> </label>
</p> </p>
<p>
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="timing-chart">
<input type="checkbox" id="timing-chart" class="mdl-switch__input">
<span class="mdl-switch__label">Timing chart</span>
</label>
</p>
<hr> <hr>
<table> <table>
<tbody> <tbody>
@ -305,269 +316,6 @@
</div> </div>
</div> </div>
<div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Alphabet</h2>
</div>
<div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect">
<div class="mdl-tabs__tab-bar">
<a href="#morse-tree" class="mdl-tabs__tab is-active">Dichotomous Key</a>
<a href="#morse-list" class="mdl-tabs__tab">List</a>
</div>
<div class="mdl-tabs__panel mdl-card__supporting-text long is-active" id="morse-tree">
<table>
<tbody>
<tr>
<td rowspan="8">E .</td>
<td rowspan="4">I ..</td>
<td rowspan="2">S ...</td>
<td rowspan="1">H ....</td>
<td class="dah">4 ....-</td>
</tr>
<tr>
<td rowspan="1">V ...-</td>
<td class="dah">3 ...--</td>
</tr>
<tr>
<td rowspan="2">U ..-</td>
<td rowspan="1">F ..-.</td>
</tr>
<tr>
<td rowspan="1"></td>
<td class="dah">2 ..---</td>
</tr>
<tr>
<td rowspan="4">A .-</td>
<td rowspan="2">R .-.</td>
<td rowspan="1">L .-..</td>
</tr>
<tr>
<td rowspan="1"></td>
</tr>
<tr>
<td rowspan="2">W .--</td>
<td rowspan="1">P .--.</td>
</tr>
<tr>
<td rowspan="1">J .---</td>
<td class="dah">1 .----</td>
</tr>
<tr>
<td rowspan="8">T -</td>
<td rowspan="4">N -.</td>
<td rowspan="2">D -..</td>
<td rowspan="1">B -...</td>
<td>6 -....</td>
</tr>
<tr>
<td rowspan="1">X -..-</td>
</tr>
<tr>
<td rowspan="2">K -.-</td>
<td rowspan="1">C -.-.</td>
</tr>
<tr>
<td rowspan="1">Y -.--</td>
</tr>
<tr>
<td rowspan="4">M --</td>
<td rowspan="2">G --.</td>
<td rowspan="1">Z --..</td>
<td>7 --...</td>
</tr>
<tr>
<td rowspan="1">Q --.-</td>
</tr>
<tr>
<td rowspan="2">O ---</td>
<td rowspan="1"></td>
<td>8 ---..</td>
</tr>
<tr>
<td rowspan="1"></td>
<td>9 ----.</td>
</tr>
</tbody>
</table>
</div>
<div class="mdl-tabs__panel mdl-card__supporting-text long" id="morse-list">
<span>A .-</span>
<span>B -...</span>
<span>C -.-.</span>
<span>D -..</span>
<span>E .</span>
<span>F ..-.</span>
<span>G --.</span>
<span>H ....</span>
<span>I ..</span>
<span>J .---</span>
<span>K -.-</span>
<span>L .-..</span>
<span>M --</span>
<span>N -.</span>
<span>O ---</span>
<span>P .--.</span>
<span>Q --.-</span>
<span>R .-.</span>
<span>S ...</span>
<span>T -</span>
<span>U ..-</span>
<span>V ...-</span>
<span>W .--</span>
<span>X -..-</span>
<span>Y -.--</span>
<span>Z --..</span>
<br>
<span>0 -----</span>
<span>1 .----</span>
<span>2 ..---</span>
<span>3 ...--</span>
<span>4 ....-</span>
<span>5 .....</span>
<span>6 -....</span>
<span>7 --...</span>
<span>8 ---..</span>
<span>9 ----.</span>
<br>
<span>End of transmission .-.-.</span>
<span>Over -.-</span>
<span>Correction ........</span>
<span>? / Say Again ..--..</span>
<span>Speak Slower --.- .-. ...</span>
</div>
</div>
</div>
<div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Documentation</h2>
</div>
<div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect">
<div class="mdl-tabs__tab-bar">
<a href="#doc-about" class="mdl-tabs__tab is-active">About</a>
<a href="#doc-faq" class="mdl-tabs__tab">FAQ</a>
<a href="#doc-geek" class="mdl-tabs__tab">Geek Stuff</a>
</div>
<div class="mdl-tabs__panel mdl-card__supporting-text long is-active" id="doc-about">
<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>
</div>
<div class="mdl-tabs__panel mdl-card__supporting-text long" id="doc-faq">
<h3 class="mdl-card__title-text">Why Does This Exist?</h3>
<p>
I needed a place to practice CW with actual human beings,
and I wanted it to be as close as possible to what I'd experience on a radio.
I also didn't have a lot of money to spend on equipment, but I did have a computer, phone, and gamepad.
Nothing else like this exists on the Internet, as far as I can tell.
</p>
<h3 class="mdl-card__title-text">What does "local" mean next to the repeater name?</h3>
<p>
It means this repeater doesn't repeat anything:
nothing you key in will be sent anywhere.
These are to help people practice and learn,
without worrying about anyone else hearing them fumble around.
</p>
<h3 class="mdl-card__title-text">Why do I hear a low tone?</h3>
<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>
Vail attempts to correct for clock differences,
but making sure your computer has correct time,
down to the millisecond,
can help with reliability.
</p>
<h3 class="mdl-card__title-text">How can I help?</h3>
<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>
<li>Vail costs me 50¢ a year to run: you could buy me a cup of coffee every 5 years or so to offset the expense</li>
</ul>
<h3 class="mdl-card__title-text">Who made this?</h3>
<p>
<a href="mailto:neale@woozle.org">Neale Pickett</a> kd7oqi
</p>
</div>
<div class="mdl-tabs__panel mdl-card__supporting-text long" id="doc-geek">
<p>
The Internet isn't exactly like radio waves:
it still goes at near the speed of light,
but there are multiple hops between endpoints,
which buffer up transmissions, and multiplex them onto a single uplink connection.
These repeaters (routers)
are also allowed to just drop things if they need to.
It's the responsibility of the communicating parties
to work out whether something needs to be retransmitted.
Because of this,
there's no telling how long it will take for a transmission to get to a destination.
</p>
<p>
Each Vail transmission (packet) consists of:
</p>
<ul>
<li>timestamp (milliseconds since 1 Jan 1970, 00:00:00 in Reykjavík)</li>
<li>transmission duration (milliseconds)</li>
</ul>
<p>
The repeater does nothing but broadcast everything it gets
to every connected Vail client,
including the one that sent the packet.
When your client gets back the exact same thing it sent,
it compares the current time to the time in the packet.
This is the <i>round-trip time</i>:
the time it takes for a packet to get from your computer to the repeater and back.
</p>
<p>
When the client gets a packet it didn't send,
it adds the <i>receive delay</i> to the timestamp,
and schedules to play the tones and silences in the packet
at that time.
</p>
<p>
By adding the maximum round-trip time to the <i>longest recent transmission</i>
(the length of a dah, hopefully),
your client can make a guess about how much time needs to be added to a received timestamp,
in order to have it play back in the future at the time it comes in.
This is just a guess.
If you're communicating with somebody with a higher round-trip time than you have,
you'll need to raise your receive delay to account for it.
</p>
</div>
</div>
</div>
</div> </div>
</main> </main>
</div> </div>

View File

@ -1,3 +1,5 @@
import * as Chart from "./chart.mjs"
/** Silent period between words */ /** Silent period between words */
const PAUSE_WORD = -7 const PAUSE_WORD = -7
/** Silent period between letters */ /** Silent period between letters */
@ -145,10 +147,16 @@ class Keyer {
next *= this.pauseMultiplier next *= this.pauseMultiplier
} else { } else {
this.endTxFunc() this.endTxFunc()
if (this.txChart) {
this.txChart.Add(Date.now(), 0)
}
} }
} else { } else {
this.last = next this.last = next
this.beginTxFunc() this.beginTxFunc()
if (this.txChart) {
this.txChart.Add(Date.now(), 1)
}
} }
this.pulseTimer = setTimeout(() => this.pulse(), next * this.intervalDuration) this.pulseTimer = setTimeout(() => this.pulse(), next * this.intervalDuration)
} }
@ -180,6 +188,25 @@ class Keyer {
return this.last return this.last
} }
/**
* Set up various charts by providing canvases for them.
*
* @param {Element} txCanvas
* @param {Element} straightCanvas
* @param {Element} ditCanvas
* @param {Element} dahCanvas
*/
SetCanvas(txCanvas=null, straightCanvas=null, ditCanvas=null, dahCanvas=null) {
for (let c of [this.txChart, this.straightChart, this.ditChart, this.dahChart]) {
if (c) c.Stop()
}
this.txChart = txCanvas?new Chart.HistoryChart(txCanvas, "red"):null
this.straightChart =straightCanvas?new Chart.HistoryChart(straightCanvas, "teal"):null
this.ditChart =ditCanvas?new Chart.HistoryChart(ditCanvas, "olive"):null
this.dahChart =dahCanvas?new Chart.HistoryChart(dahCanvas, "purple"):null
}
/** /**
* Return true if we are currently playing out something * Return true if we are currently playing out something
*/ */
@ -324,6 +351,9 @@ class Keyer {
} else { } else {
this.endTxFunc() this.endTxFunc()
} }
if (this.straightChart) {
this.straightChart.Add(Date.now(), down?1:0)
}
} }
/** /**
@ -340,6 +370,9 @@ class Keyer {
this.Enqueue(DIT) this.Enqueue(DIT)
} }
} }
if (this.ditChart) {
this.ditChart.Add(Date.now(), down?1:0)
}
} }
/** /**
@ -356,6 +389,9 @@ class Keyer {
this.Enqueue(DAH) this.Enqueue(DAH)
} }
} }
if (this.dahChart) {
this.dahChart.Add(Date.now(), down?1:0)
}
} }
} }

View File

@ -153,3 +153,11 @@ img {
padding: 0.4em; padding: 0.4em;
min-width: 4em; min-width: 4em;
} }
#charts {
line-height: 0;
}
#charts canvas {
height: 0.5em;
width: 100%;
}

View File

@ -87,6 +87,10 @@ class VailClient {
this.inputInit("#telegraph-buzzer", e => { this.inputInit("#telegraph-buzzer", e => {
this.setTelegraphBuzzer(e.target.checked) this.setTelegraphBuzzer(e.target.checked)
}) })
this.inputInit("#timing-chart", e => {
console.log("moo")
this.setTimingCharts(e.target.checked)
})
// Fill in the name of our repeater // Fill in the name of our repeater
document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim())) document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim()))
@ -101,6 +105,30 @@ class VailClient {
}) })
} }
/**
* Toggle timing charts.
*
* @param enable True to enable charts
*/
setTimingCharts(enable) {
// XXX: UI code shouldn't be in the Keyer class.
// Actually, the charts calls should be in vail
let chartsContainer = document.querySelector("#charts")
if (enable) {
chartsContainer.classList.remove("hidden")
this.keyer.SetCanvas(
document.querySelector("#txChart"),
document.querySelector("#straightChart"),
document.querySelector("#ditChart"),
document.querySelector("#dahChart"),
)
} else {
chartsContainer.classList.add("hidden")
this.keyer.SetCanvas()
}
console.log("timing chart", enable)
}
/** /**
* Toggle the clicktastic buzzer, instead of the beeptastic one. * Toggle the clicktastic buzzer, instead of the beeptastic one.
* *
@ -210,6 +238,7 @@ class VailClient {
inputInit(selector, callback) { inputInit(selector, callback) {
let element = document.querySelector(selector) let element = document.querySelector(selector)
if (!element) { if (!element) {
console.warn("Unable to find an input to init", selector)
return return
} }
let storedValue = localStorage[element.id] let storedValue = localStorage[element.id]
@ -234,6 +263,7 @@ class VailClient {
outputWpmElement.value = (1200 / value).toFixed(1) outputWpmElement.value = (1200 / value).toFixed(1)
} }
if (callback) { if (callback) {
console.log("callback", selector)
callback(e) callback(e)
} }
}) })