Many changes

* MIDI
  * Send MIDI key release for v2 of vail adapter
  * Now handles hot-plugging MIDI devices!
* Iambic input
  * Typeahead is now a toggle
  * Implemented iambic mode A, also as a toggle (#31)
* If you set the repeater to "debug", it spews out debugging messages
* Made it a bit easier on myself to update my instance
This commit is contained in:
Neale Pickett 2022-04-19 22:41:09 -06:00
parent ca9f51c621
commit 74ad07174a
6 changed files with 102 additions and 27 deletions

View File

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

View File

@ -218,7 +218,7 @@
</div>
<div class="mdl-card__supporting-text">
<p>
Dit length (iambic):
Iambic Dit length:
<output id="iambic-duration-value"></output>ms
/
<output id="iambic-duration-wpm"></output> WPM
@ -230,6 +230,16 @@
max="255"
value="100">
</p>
<p>
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="iambic-mode-b">
<input type="checkbox" id="iambic-mode-b" class="mdl-switch__input" checked>
<span class="mdl-switch__label">Iambic mode B</span>
</label>
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="iambic-typeahead">
<input type="checkbox" id="iambic-typeahead" class="mdl-switch__input" checked>
<span class="mdl-switch__label">Iambic typeahead</span>
</label>
</p>
<p>
Receive delay:
<output id="rx-delay-value"></output>ms

View File

@ -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) {

View File

@ -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)
}
}
}

View File

@ -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) {

View File

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