From 3a16ebb7afaeeffa641579dfd926665b33a010e7 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 1 May 2020 15:07:09 -0600 Subject: [PATCH] Many client updates --- static/index.html | 589 ++++++++++++++++++++--------------- static/vail.css | 28 ++ static/vail.js | 767 +++++++++++++++++++++++----------------------- 3 files changed, 762 insertions(+), 622 deletions(-) diff --git a/static/index.html b/static/index.html index 678ebee..12ca418 100644 --- a/static/index.html +++ b/static/index.html @@ -1,253 +1,350 @@ - - Vail - - - - - - - + + Vail + + - - - - - -
-
-
- - Vail - -
- - -
-
- -
- Repeaters - -
- -
- -
-
-
-
-

- Input -

-
-
- -
- - - - - - - -
- -
- -
-
-
- - - - - - - - - -
- - - -
- . or z - - / or x -
-
-
- - - - - - - -
- -
Echo On
-
-
-
+ + + + -
-
-

- Knobs -

-
-
- - - - - - - - - - - - - - - - - -
- Average round-trip time: - - 0ms -
- Longest recent transmission: - - 0ms -
- Suggested receive delay: - - 0ms -
-
-

- Recieve delay: - ms - -

-

- Dit length (iambic): - ms - -

-
-
- -
-
-

- Code Tree -

-
-
- -
-
+ + + + + +
+
+
+ + Vail + +
+ + +
+
-
-
-

- Vail -

-
-
-

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

+
+ Repeaters + +
+ +
-

Why Does This Exist?

- -

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

-
-
- -
-
-

Future plans

-
-
-
    -
  • Move to a more permanent URL
  • -
  • Make this page less ugly
  • -
  • Arduino program to let you hook up an iambic paddle over USB
  • -
  • Document the protocol
  • -
- - -

How can I help?

- -
    -
  • Improve the source code
  • -
  • Email me and let me know you're using it
  • -
- -

- Neale Pickett kd7oqi -

- -
-
-
-
-
- +
+
+
+
+

Input

+
+
+ +
+ + + + + + + +
+ +
+ c or , or ⇧ Shift +
+
+
+ + + + + + + + + +
+ + + +
+ . or z +
+ right-click for Dah +
+ / or x +
+
+
+ + + + + + + +
+ +
Echo On
+
+
+
+ +
+
+

Knobs

+
+
+

+ Dit length (iambic): + ms + +

+

+ Recieve delay: + ms + +

+
+ + + + + + + + + + + + + + + + + + + +
+ Suggested receive delay: + + 0ms +
+ Average round-trip time: + + 0ms +
+ Longest recent transmission: + + 0ms +
+ Repeater: + + +
+
+

Errors

+
+
+
+ +
+
+

Code Tree

+
+
+ +
+
+ +
+
+

Vail

+
+
+

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

+
+
+ +
+
+

How It Works

+
+
+ +

+ 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)
  • +
  • silence duration (milliseconds, optional)
  • +
  • transmission duration (milliseconds, optional)
  • +
  • silence duration (milliseconds, optional)
  • +
  • Repeat as necessary
  • +
+ +

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

+
+
+ +
+
+

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

+ Make sure your clock is synced with an Internet time server. + Accurate time is very important to how Vail works. +

+
+
+ +
+
+

Future plans

+
+
+
    +
  • Move to a more permanent URL
  • +
  • Make this page less ugly
  • +
  • Arduino program to let you hook up an iambic paddle over USB
  • +
  • Document the protocol
  • +
+ + +

How can I help?

+ +
    +
  • Improve the source code
  • +
  • Email me and let me know you're using it
  • +
+ +

+ Neale Pickett kd7oqi +

+ +
+
+
+
+ + diff --git a/static/vail.css b/static/vail.css index 83d9d0c..997631d 100644 --- a/static/vail.css +++ b/static/vail.css @@ -22,6 +22,29 @@ 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 { background-color: #333; color: #fff; @@ -31,3 +54,8 @@ code { img { max-width: 20em; } + +.mdl-card__supporting-text { + max-height: 20em; + overflow-y: scroll; +} diff --git a/static/vail.js b/static/vail.js index c46a5a2..3d4bbd8 100644 --- a/static/vail.js +++ b/static/vail.js @@ -8,407 +8,422 @@ const DIT = 1 const DAH = 3 class Iambic { - constructor(beginTxFunc, endTxFunc) { - this.beginTxFunc = beginTxFunc - this.endTxFunc = endTxFunc - this.interval = null - this.state = this.stateSpace - this.keyState = null - } - - /** - * Set a new interval (transmission rate) - * - * @param {number} duration New interval duration, in ms - */ - SetInterval(duration) { - clearInterval(this.interval) - this.interval = setInterval(e => this.pulse(), duration) - } - - // An interval has passed, call whatever the current state function is - pulse(event) { - this.state() - } - - stateSpace() { - // Don't transmit for one interval. - this.state = this.keyState || this.stateSpace - } - stateDit() { - // Send a dit - this.beginTxFunc() - this.state = this.stateEnd - } - stateDah() { - // Send a dah - this.beginTxFunc() - this.state = this.stateDah2 - } - stateDah2() { - this.state = this.stateDah3 - } - stateDah3() { - this.state = this.stateEnd - } - stateEnd() { - // Stop sending - this.endTxFunc() - this.state = this.stateSpace - this.state() - } - - /** - * Edge trigger on key press or release - * - * @param {boolean} down True if key was pressed, false if released - * @param {number} key DIT or DAH - */ - Key(down, key) { - // By setting keyState we request this state transition, - // the next time the transition is possible. - let keyState = null - if (key == DIT) { - keyState = this.stateDit - } else if (key == DAH) { - keyState = this.stateDah - } - - if (down) { - this.keyState = keyState - } else if (keyState == this.keyState) { - // Only stop when we've released the right key - this.keyState = null - } - } + constructor(beginTxFunc, endTxFunc) { + this.beginTxFunc = beginTxFunc + this.endTxFunc = endTxFunc + this.interval = null + this.state = this.stateSpace + this.keyState = null + } + + /** + * Set a new interval (transmission rate) + * + * @param {number} duration New interval duration, in ms + */ + SetInterval(duration) { + clearInterval(this.interval) + this.interval = setInterval(e => this.pulse(), duration) + } + + // An interval has passed, call whatever the current state function is + pulse(event) { + this.state() + } + + stateSpace() { + // Don't transmit for one interval. + this.state = this.keyState || this.stateSpace + } + stateDit() { + // Send a dit + this.beginTxFunc() + this.state = this.stateEnd + } + stateDah() { + // Send a dah + this.beginTxFunc() + this.state = this.stateDah2 + } + stateDah2() { + this.state = this.stateDah3 + } + stateDah3() { + this.state = this.stateEnd + } + stateEnd() { + // Stop sending + this.endTxFunc() + this.state = this.stateSpace + this.state() + } + + /** + * Edge trigger on key press or release + * + * @param {boolean} down True if key was pressed, false if released + * @param {number} key DIT or DAH + */ + Key(down, key) { + // By setting keyState we request this state transition, + // the next time the transition is possible. + let keyState = null + if (key == DIT) { + keyState = this.stateDit + } else if (key == DAH) { + keyState = this.stateDah + } + + if (down) { + this.keyState = keyState + } else if (keyState == this.keyState) { + // Only stop when we've released the right key + this.keyState = null + } + } } class Buzzer { - // Buzzers keep two oscillators: one high and one low. - // They generate a continuous waveform, - // 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, - // in order to avoid "pops" (square wave overtones) - // 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 - } - } + // Buzzers keep two oscillators: one high and one low. + // They generate a continuous waveform, + // 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, + // in order to avoid "pops" (square wave overtones) + // that happen with instant changes in gain. - /** - * Convert clock time to AudioContext time - * - * @param {number} when Clock time in ms - * @return {number} AudioContext offset time - */ - acTime(when) { - if (! when) { - return this.ac.currentTime - } + constructor(txGain=0.3) { + this.txGain = txGain - 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 - } + this.ac = new AudioContext() - /** - * 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) - } + this.lowGain = this.create(lowFreq) + this.highGain = this.create(highFreq) + this.errorGain = this.create(errorFreq, "square") + } - /** - * 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) + 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.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) - } + gain(high) { + if (high) { + return this.highGain.gain + } else { + return this.lowGain.gain + } + } + + /** + * Convert clock time to AudioContext time + * + * @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 + 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 { - constructor() { - this.sent = [] - this.lagTimes = [0] - this.rxDurations = [0] - this.rxDelay = 0 // Milliseconds to add to incoming timestamps - this.beginTxTime = null // Time when we began transmitting + constructor() { + this.sent = [] + this.lagTimes = [0] + this.rxDurations = [0] + this.rxDelay = 0 // Milliseconds to add to incoming timestamps + this.beginTxTime = null // Time when we began transmitting - // Set up WebSocket - let wsUrl = new URL(window.location) - wsUrl.protocol = "ws:" - wsUrl.pathname += "chat" - this.socket = new WebSocket(wsUrl) - 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)) - } + // Set up WebSocket + let wsUrl = new URL(window.location) + wsUrl.protocol = "ws:" + wsUrl.pathname += "chat" + this.socket = new WebSocket(wsUrl) + this.socket.addEventListener("message", e => this.wsMessage(e)) - // Listen for keystrokes - document.addEventListener("keydown", e => this.key(e)) - document.addEventListener("keyup", e => this.key(e)) - - // Make helpers - this.iambic = new Iambic(() => this.beginTx(), () => this.endTx()) - this.buzzer = new Buzzer() + // 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 slider values - this.inputInit("#iambic-duration", e => this.iambic.SetInterval(e.target.value)) - this.inputInit("#rx-delay", e => {this.rxDelay = Number(e.target.value)}) - } - - 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")) - } + // Listen for keystrokes + document.addEventListener("keydown", e => this.key(e)) + document.addEventListener("keyup", e => this.key(e)) - 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) + // Make helpers + this.iambic = new Iambic(() => this.beginTx(), () => this.endTx()) + this.buzzer = new Buzzer() - 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 - } + // Listen for slider values + this.inputInit("#iambic-duration", e => this.iambic.SetInterval(e.target.value)) + this.inputInit("#rx-delay", e => {this.rxDelay = Number(e.target.value)}) + + // Show what repeater we're on + let repeater = (new URL(location)).searchParams.get("repeater") || "Default" + document.querySelector("#repeater").textContent = repeater + } + + 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 - if (adjustedTxTime < now) { - this.buzzer.ErrorTone() - 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 - } - } + let adjustedTxTime = beginTxTime+this.rxDelay + if (adjustedTxTime < now) { + this.error("Packet requested playback " + (now - adjustedTxTime) + "ms in the past. Increase receive delay!") + return + } - key(event) { - if (event.repeat) { - // Ignore key repeats generated by the OS, we do this ourselves - return - } - - let begin = event.type.endsWith("down") - - if ((event.code == "KeyZ") || (event.code == "Period")) { - event.preventDefault() - this.iambic.Key(begin, DIT) - } - if ((event.code == "KeyX") || (event.code == "Slash")) { - event.preventDefault() - this.iambic.Key(begin, DAH) - } - if ((event.key == "Shift")) { - event.preventDefault() - if (begin) { - this.beginTx() - } else { - this.endTx() - } - } - } - - keyButton(event) { - let begin = event.type.endsWith("down") - - 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") { - this.iambic.Key(begin, DIT) - } else if (event.target.id == "key") { - if (begin) { - this.beginTx() - } else { - this.endTx() - } - } else if (event.target.id == "ck") { - this.Test() - } - } - - /** - * Send "CK" to server, and don't squelch the repeat - */ - 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)) - } + // 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) { + if (event.repeat) { + // Ignore key repeats generated by the OS, we do this ourselves + return + } + + let begin = event.type.endsWith("down") + + if ((event.code == "KeyZ") || (event.code == "Period")) { + event.preventDefault() + this.iambic.Key(begin, DIT) + } + if ((event.code == "KeyX") || (event.code == "Slash")) { + event.preventDefault() + this.iambic.Key(begin, DAH) + } + if ((event.code == "KeyC") || (event.code == "Comma") || (event.key == "Shift")) { + event.preventDefault() + if (begin) { + this.beginTx() + } else { + this.endTx() + } + } + } + + keyButton(event) { + let begin = event.type.endsWith("down") + + 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") { + this.iambic.Key(begin, DIT) + } else if (event.target.id == "key") { + if (begin) { + this.beginTx() + } else { + this.endTx() + } + } else if (event.target.id == "ck") { + this.Test() + } + } + + /** + * Send "CK" to server, and don't squelch the repeat + */ + 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() { - window.app = new Vail() + window.app = new Vail() } if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", vailInit) + document.addEventListener("DOMContentLoaded", vailInit) } else { - vailInit() + vailInit() }