diff --git a/publish.sh b/publish.sh index 36b841b..87ff80e 100755 --- a/publish.sh +++ b/publish.sh @@ -1,9 +1,11 @@ #! /bin/sh +cd $(dirname $0) + case "$1" in -prod|--prod) echo "Push to main branch, then update stack." - #rsync -va static melville.woozle.org:/srv/vail/ + docker -H ssh://melville.woozle.org service update --image ghcr.io/nealey/vail:main melville_vail ;; "") rsync -va static/ melville.woozle.org:/srv/vail/testing/ diff --git a/static/index.html b/static/index.html index 7f658b9..3a82ff9 100644 --- a/static/index.html +++ b/static/index.html @@ -218,7 +218,7 @@

- Dit length (iambic): + Iambic Dit length: ms / WPM @@ -230,6 +230,16 @@ max="255" value="100">

+

+ + +

Receive delay: ms diff --git a/static/inputs.mjs b/static/inputs.mjs index f64429a..be025e6 100644 --- a/static/inputs.mjs +++ b/static/inputs.mjs @@ -102,16 +102,25 @@ export class MIDI { } async midiInit(access) { + this.inputs = [] this.midiAccess = await navigator.requestMIDIAccess() - for (let input of this.midiAccess.inputs.values()) { - input.addEventListener("midimessage", e => this.midiMessage(e)) - } this.midiAccess.addEventListener("statechange", e => this.midiStateChange(e)) + this.midiStateChange() } midiStateChange(event) { - // XXX: it's not entirely clear how to handle new devices showing up. - // XXX: possibly we go through this.midiAccess.inputs and somehow only listen on new things + // Go through this.midiAccess.inputs and only listen on new things + for (let input of this.midiAccess.inputs.values()) { + if (!this.inputs.includes(input)) { + input.addEventListener("midimessage", e => this.midiMessage(e)) + this.inputs.push(input) + } + } + + // Tell the Vail adapter to disable keyboard events: we can do MIDI! + for (let output of this.midiAccess.outputs.values()) { + output.send([0x80, 0x00, 0x00]) // Stop playing low C + } } midiMessage(event) { diff --git a/static/morse.mjs b/static/morse.mjs index e14f4f6..1e66f5e 100644 --- a/static/morse.mjs +++ b/static/morse.mjs @@ -90,7 +90,7 @@ if (!window.AudioContext) { */ class Keyer { /** - * Create an Keyer + * Create a Keyer * * @param {TxControl} beginTxFunc Callback to begin transmitting * @param {TxControl} endTxFunc Callback to end transmitting @@ -104,6 +104,8 @@ class Keyer { this.pauseMultiplier = pauseMultiplier this.ditDown = false this.dahDown = false + this.typeahead = false + this.iambicModeB = true this.last = null this.queue = [] this.pulseTimer = null @@ -146,10 +148,14 @@ class Keyer { typematic() { if (this.ditDown && this.dahDown) { - if (this.last == DIT) { - this.last = DAH + if (this.iambicModeB) { + if (this.last == DIT) { + this.last = DAH + } else { + this.last = DIT + } } else { - this.last = DIT + this.last = this.last // Mode A = keep on truckin' } } else if (this.ditDown) { this.last = DIT @@ -189,6 +195,34 @@ class Keyer { this.pauseMultiplier = multiplier } + /** + * Set Iambic mode B. + * + * If true, holding both keys will alternate between dit and dah. + * If false, holding both keys sends whatever key was depressed first. + * + * @param {boolean} value True to set mode to B + */ + SetIambicModeB(value) { + this.iambicModeB = Boolean(value) + } + + /** + * Enable/disable typeahead. + * + * Typeahead maintains a key buffer, so you can key in dits and dahs faster than the + * Iambic keyer can play them out. + * + * Some people apparently expect this behavior, and have trouble if it isn't enabled. + * For others, having this enabled makes it feel like they have a "phantom keyer" + * entering keys they did not send. + * + * @param value True to enable typeahead + */ + SetTypeahead(value) { + this.typeahead = value + } + /** * Delete anything left on the queue. */ @@ -277,8 +311,8 @@ class Keyer { */ Dit(down) { this.ditDown = down - if (down) { - this.Enqueue(DIT) + if (down && this.typeahead || !this.Busy()) { + this.Enqueue(DIT, this.typeahead) } } @@ -289,8 +323,8 @@ class Keyer { */ Dah(down) { this.dahDown = down - if (down) { - this.Enqueue(DAH) + if (down && this.typeahead || !this.Busy()) { + this.Enqueue(DAH, this.typeahead) } } } diff --git a/static/repeaters.mjs b/static/repeaters.mjs index 2e72c09..fd6c5a5 100644 --- a/static/repeaters.mjs +++ b/static/repeaters.mjs @@ -45,6 +45,7 @@ export class Vail { console.error(err, jmsg) return } + let beginTxTime = msg[0] let durations = msg.slice(1) @@ -62,9 +63,16 @@ export class Vail { this.lagDurations.unshift(now - this.clockOffset - beginTxTime - totalDuration) this.lagDurations.splice(20, 2) this.rx(0, 0, this.stats()) + if (this.name == "debug") { + console.debug("Vail.wsMessage() SQUELCH", msg) + } return } + if (this.name == "debug") { + console.debug("Vail.wsMessage()", msg) + } + // The very first packet is the server telling us the current time if (durations.length == 0) { if (this.clockOffset == 0) { diff --git a/static/vail.mjs b/static/vail.mjs index 5afa22a..e3fd0f3 100644 --- a/static/vail.mjs +++ b/static/vail.mjs @@ -64,15 +64,20 @@ class VailClient { e.addEventListener("click", e => this.test()) } - // Set up sliders - this.sliderInit("#iambic-duration", e => { + // Set up inputs + this.inputInit("#iambic-duration", e => { this.keyer.SetIntervalDuration(e.target.value) this.roboKeyer.SetIntervalDuration(e.target.value) }) - this.sliderInit("#rx-delay", e => { + this.inputInit("#rx-delay", e => { this.rxDelay = Number(e.target.value) }) - + this.inputInit("#iambic-mode-b", e => { + this.keyer.SetIambicModeB(e.target.checked) + }) + this.inputInit("#iambic-typeahead", e => { + this.keyer.SetTypeahead(e.target.checked) + }) // Fill in the name of our repeater let repeaterElement = document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim())) @@ -163,33 +168,40 @@ class VailClient { } /** - * Set up a slider. + * Set up an input. * - * This reads any previously saved value and sets the slider to that. - * When the slider is updated, it saves the value it's updated to, + * This reads any previously saved value and sets the input value to that. + * When the input is updated, it saves the value it's updated to, * and calls the provided callback with the new value. * * @param {string} selector CSS path to the element * @param {function} callback Callback to call with any new value that is set */ - sliderInit(selector, callback) { + inputInit(selector, callback) { let element = document.querySelector(selector) if (!element) { return } let storedValue = localStorage[element.id] - if (storedValue) { + if (storedValue != null) { element.value = storedValue + element.checked = JSON.parse(storedValue) } let outputElement = document.querySelector(selector + "-value") let outputWpmElement = document.querySelector(selector + "-wpm") + element.addEventListener("input", e => { - localStorage[element.id] = element.value + let value = element.value + if (element.hasAttribute("checked")) { + value = element.checked + } + + localStorage[element.id] = value if (outputElement) { - outputElement.value = element.value + outputElement.value = value } if (outputWpmElement) { - outputWpmElement.value = (1200 / element.value).toFixed(1) + outputWpmElement.value = (1200 / value).toFixed(1) } if (callback) { callback(e)