diff --git a/static/chart.mjs b/static/chart.mjs index 8be0718..b4faca2 100644 --- a/static/chart.mjs +++ b/static/chart.mjs @@ -14,7 +14,7 @@ class HistoryChart { * @param {string} strokeStyle strokeStyle to draw in * @param {Duration} duration Time to display history for */ - constructor(canvas, strokeStyle, duration) { + constructor(canvas, strokeStyle="black", duration=20*Second) { this.canvas = canvas this.ctx = canvas.getContext("2d") this.duration = duration @@ -29,8 +29,10 @@ class HistoryChart { this.ctx.translate(0, -canvas.height) this.ctx.strokeStyle = strokeStyle + this.ctx.fillStyle = strokeStyle this.ctx.lineWdith = 2 + this.running=true this.draw() } @@ -42,8 +44,8 @@ class HistoryChart { * This also cleans up the event list, * purging anything that is too old to be displayed. * - * @param when Time the event happened - * @param value Value for the event + * @param {Number} when Time the event happened + * @param {Number} value Value for the event */ Add(when, value) { let now = Date.now() @@ -54,8 +56,10 @@ class HistoryChart { while ((this.data.length > 1) && (this.data[1][0] < earliest)) { this.data.shift() } + } - console.log(this.data) + Stop() { + this.running = false } draw() { @@ -64,43 +68,29 @@ class HistoryChart { let xScale = this.canvas.width / this.duration let yScale = this.canvas.height * 0.95 let y = 0 + let x = 0 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) { - let x = (point[0] - earliest) * xScale - this.ctx.lineTo(x, y) - y = point[1] * yScale - this.ctx.lineTo(x, y) - } - this.ctx.lineTo(this.canvas.width, y) - this.ctx.stroke() + let x2 = (point[0] - earliest) * xScale + let y2 = point[1] * yScale - requestAnimationFrame(() => this.draw()) + 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()) + } } } 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() -} \ No newline at end of file diff --git a/static/index.html b/static/index.html index d50b532..f751869 100644 --- a/static/index.html +++ b/static/index.html @@ -27,9 +27,6 @@
@@ -54,10 +51,10 @@ Fortunes (crazy slow)
+ Resources @@ -96,6 +93,13 @@ + +
+ + + +
+
Straight Key @@ -217,6 +221,7 @@
+ Vail Wiki
@@ -265,6 +270,12 @@ Telegraph sounds

+

+ +


@@ -305,269 +316,6 @@ -
-
-

Alphabet

-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
E .I ..S ...H ....4 ....-
V ...-3 ...--
U ..-F ..-.
2 ..---
A .-R .-.L .-..
W .--P .--.
J .---1 .----
T -N -.D -..B -...6 -....
X -..-
K -.-C -.-.
Y -.--
M --G --.Z --..7 --...
Q --.-
O ---8 ---..
9 ----.
- -
- A .- - B -... - C -.-. - D -.. - E . - F ..-. - G --. - H .... - I .. - J .--- - K -.- - L .-.. - M -- - N -. - O --- - P .--. - Q --.- - R .-. - S ... - T - - U ..- - V ...- - W .-- - X -..- - Y -.-- - Z --.. -
- 0 ----- - 1 .---- - 2 ..--- - 3 ...-- - 4 ....- - 5 ..... - 6 -.... - 7 --... - 8 ---.. - 9 ----. -
- End of transmission .-.-. - Over -.- - Correction ........ - ? / Say Again ..--.. - Speak Slower --.- .-. ... -
- - - -
-
-

Documentation

-
-
-
- About - FAQ - Geek Stuff -
-
-

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

- -

- Just like a radio repeater, - anybody can connect and start transmitting stuff, - and this will broadcast it to everyone connected. -

-
- -
-

Why Does This Exist?

-

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

- -

What does "local" mean next to the repeater name?

-

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

- -

Why do I hear a low tone?

-

- This is the "drop tone", and will be accompanied by an error. -

-

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

-

- This could be happening for three reasons: -

-
    -
  1. You (the person hearing the drop tone) need a larger receive delay
  2. -
  3. The receiving computer's clock is in the future (running fast)
  4. -
  5. The sending computer's clock is in the past (running slow)
  6. -
-

- Vail attempts to correct for clock differences, - but making sure your computer has correct time, - down to the millisecond, - can help with reliability. -

- -

How can I help?

-
    -
  • Improve the source code
  • -
  • Email me and let me know you're using it
  • -
  • 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
  • -
- -

Who made this?

-

- Neale Pickett kd7oqi -

-
- - -
-

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

-

- Each Vail transmission (packet) consists of: -

-
    -
  • timestamp (milliseconds since 1 Jan 1970, 00:00:00 in Reykjavík)
  • -
  • transmission duration (milliseconds)
  • -
-

- 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 round-trip time: - the time it takes for a packet to get from your computer to the repeater and back. -

-

- When the client gets a packet it didn't send, - it adds the receive delay to the timestamp, - and schedules to play the tones and silences in the packet - at that time. -

-

- By adding the maximum round-trip time to the longest recent transmission - (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. -

-
-
-
- diff --git a/static/keyer.mjs b/static/keyer.mjs index fd9b811..acb4ea0 100644 --- a/static/keyer.mjs +++ b/static/keyer.mjs @@ -1,3 +1,5 @@ +import * as Chart from "./chart.mjs" + /** Silent period between words */ const PAUSE_WORD = -7 /** Silent period between letters */ @@ -145,10 +147,16 @@ class Keyer { next *= this.pauseMultiplier } else { this.endTxFunc() + if (this.txChart) { + this.txChart.Add(Date.now(), 0) + } } } else { this.last = next this.beginTxFunc() + if (this.txChart) { + this.txChart.Add(Date.now(), 1) + } } this.pulseTimer = setTimeout(() => this.pulse(), next * this.intervalDuration) } @@ -180,6 +188,25 @@ class Keyer { 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 */ @@ -324,6 +351,9 @@ class Keyer { } else { this.endTxFunc() } + if (this.straightChart) { + this.straightChart.Add(Date.now(), down?1:0) + } } /** @@ -340,6 +370,9 @@ class Keyer { this.Enqueue(DIT) } } + if (this.ditChart) { + this.ditChart.Add(Date.now(), down?1:0) + } } /** @@ -356,6 +389,9 @@ class Keyer { this.Enqueue(DAH) } } + if (this.dahChart) { + this.dahChart.Add(Date.now(), down?1:0) + } } } diff --git a/static/vail.css b/static/vail.css index d7b6ecd..b94e0ef 100644 --- a/static/vail.css +++ b/static/vail.css @@ -153,3 +153,11 @@ img { padding: 0.4em; min-width: 4em; } + +#charts { + line-height: 0; +} +#charts canvas { + height: 0.5em; + width: 100%; +} \ No newline at end of file diff --git a/static/vail.mjs b/static/vail.mjs index 2a44304..672ba1c 100644 --- a/static/vail.mjs +++ b/static/vail.mjs @@ -87,6 +87,10 @@ class VailClient { this.inputInit("#telegraph-buzzer", e => { 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 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. * @@ -210,6 +238,7 @@ class VailClient { inputInit(selector, callback) { let element = document.querySelector(selector) if (!element) { + console.warn("Unable to find an input to init", selector) return } let storedValue = localStorage[element.id] @@ -234,6 +263,7 @@ class VailClient { outputWpmElement.value = (1200 / value).toFixed(1) } if (callback) { + console.log("callback", selector) callback(e) } })