From 4950042e6c3b62457f29cfcb31984409e09b16ed Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sun, 8 May 2022 11:33:25 -0600 Subject: [PATCH] Working up to single dot --- static/duration.mjs | 20 +++ static/index.html | 122 ++++++++---------- static/inputs.mjs | 2 +- static/keyer.mjs | 296 +++++++++++++++++++++++++++++++++++++++++-- static/repeaters.mjs | 4 - static/vail.css | 19 +-- static/vail.mjs | 22 ++-- 7 files changed, 374 insertions(+), 111 deletions(-) create mode 100644 static/duration.mjs diff --git a/static/duration.mjs b/static/duration.mjs new file mode 100644 index 0000000..80de5c0 --- /dev/null +++ b/static/duration.mjs @@ -0,0 +1,20 @@ +/** + * A time duration. + * + * JavaScript uses milliseconds in most (but not all) places. + * I've found it helpful to be able to multiply by a unit, so it's clear what's going on. + * + * @typedef {number} Duration + */ + +/** @type {Duration} */ +export const Millisecond = 1 + +/** @type {Duration} */ +export const Second = 1000 * Millisecond + +/** @type {Duration} */ +export const Minute = 60 * Second + +/** @type {Duration} */ +export const Hour = 60 * Minute diff --git a/static/index.html b/static/index.html index 6d37aef..a55d415 100644 --- a/static/index.html +++ b/static/index.html @@ -5,10 +5,9 @@ - - - - + + + @@ -18,50 +17,46 @@ -
-
-
- - Vail - -
- - +
- -
- Repeaters - -
- Local Practice - -
- Resources -
- -
-
- + +
+
+
+ +
+ + + + + + + + + + + + + + + +
+
+
+
@@ -73,22 +68,6 @@

- - - - - - - - - - - - - - - -

@@ -118,7 +97,7 @@ - keyboard + c @@ -128,11 +107,11 @@ - gamepad + - Bottom button - Right button + + @@ -153,14 +132,14 @@ - keyboard + . x - keyboard + / @@ -169,17 +148,17 @@ - gamepad + - Left Button + L1 - gamepad + - Top Button + R1 @@ -319,7 +298,6 @@
-
diff --git a/static/inputs.mjs b/static/inputs.mjs index 17378a5..40677cc 100644 --- a/static/inputs.mjs +++ b/static/inputs.mjs @@ -2,7 +2,7 @@ class Input { constructor(keyer) { this.keyer = keyer } - SetIntervalDuration(delay) { + SetDitDuration(delay) { // Nothing } } diff --git a/static/keyer.mjs b/static/keyer.mjs index 34ccb6d..8db5314 100644 --- a/static/keyer.mjs +++ b/static/keyer.mjs @@ -1,3 +1,11 @@ +/** + * A number of keyers. + * + * The document "All About Squeeze-Keying" by Karl Fischer, DJ5IL, was + * absolutely instrumental in correctly (I hope) implementing everything more + * advanced than the bug keyer. + */ + /** Silent period between words */ const PAUSE_WORD = -7 /** Silent period between letters */ @@ -9,6 +17,23 @@ const DIT = 1 /** Duration of a dah */ const DAH = 3 +/** + * A time duration. + * + * JavaScript uses milliseconds in most (but not all) places. + * I've found it helpful to be able to multiply by a unit, so it's clear what's going on. + * + * @typedef {number} Duration + */ +/** @type {Duration} */ +const Millisecond = 1 +/** @type {Duration} */ +const Second = 1000 * Millisecond +/** @type {Duration} */ +const Minute = 60 * Second +/** @type {Duration} */ +const Hour = 60 * Minute + const MorseMap = { "\x04": ".-.-.", // End Of Transmission "\x18": "........", // Cancel @@ -68,11 +93,6 @@ const MorseMap = { "@": ".--.-.", } -// iOS kludge -if (!window.AudioContext) { - window.AudioContext = window.webkitAudioContext -} - /** * Return the inverse of the input. * If you give it dit, it returns dah, and vice-versa. @@ -80,7 +100,7 @@ if (!window.AudioContext) { * @param ditdah What to invert * @returns The inverse of ditdah */ -function morseNot(ditdah) { +function not(ditdah) { if (ditdah == DIT) { return DAH } @@ -93,6 +113,264 @@ function morseNot(ditdah) { * @callback TxControl */ +/** + * A straight keyer. + * + * This is one or more relays wired in parallel. Each relay has an associated + * input key. You press any key, and it starts transmitting until all keys are + * released. +*/ +class StraightKeyer { + /** + * @param {TxControl} beginTxFunc Callback to begin transmitting + * @param {TxControl} endTxFunc Callback to end transmitting + */ + constructor(beginTxFunc, endTxFunc) { + this.beginTxFunc = beginTxFunc + this.endTxFunc = endTxFunc + this.Reset() + } + + /** + * Returns a list of names for keys supported by this keyer. + * + * @returns {Array.} A list of key names + */ + KeyNames() { + return ["Key"] + } + + /** + * Reset state and stop all transmissions. + */ + Reset() { + this.endTxFunc() + this.txRelays = [] + } + + /** + * Returns the state of a single transmit relay. + * + * If n is not provided, return the state of all relays wired in parallel. + * + * @param {number} n Relay number + * @returns {bool} True if relay is closed + */ + TxClosed(n=null) { + if (n == null) { + return this.txRelays.some(Boolean) + } + return this.txRelays[n] + } + + /** + * Close a transmit relay. + * + * In most of these keyers, you have multiple things that can transmit. In + * the circuit, they'd all be wired together in parallel. We instead keep + * track of relay state here, and start or stop transmitting based on the + * logical of of all relays. + * + * @param {number} n Relay number + * @param {bool} closed True if relay should be closed + */ + Tx(n, closed) { + let wasClosed = this.TxClosed() + this.txRelays[n] = closed + let nowClosed = this.TxClosed() + + if (wasClosed != nowClosed) { + if (nowClosed) { + this.beginTxFunc() + } else { + this.endTxFunc() + } + } + } + + /** + * React to a key being pressed. + * + * @param {number} key Which key was pressed + * @param {bool} pressed True if the key was pressed + */ + Key(key, pressed) { + this.Tx(key, pressed) + } +} + +/** + * A "Cootie" or "Double Speed Key" is just two straight keys in parallel. + */ +class CootieKeyer extends StraightKeyer { + KeyNames() { + return ["Key", "Key"] + } +} + +/** + * A Vibroplex "Bug". + * + * Left key send dits over and over until you let go. + * Right key works just like a stright key. + */ +class BugKeyer extends StraightKeyer { + KeyNames() { + return ["· ", "Key"] + } + + Reset() { + super.Reset() + this.SetDitDuration(100 * Millisecond) + if (this.pulseTimer) { + clearInterval(this.pulseTimer) + this.pulseTimer = null + } + this.keyPressed = [] + } + + /** + * Set the duration of dit. + * + * @param {Duration} d New dit duration + */ + SetDitDuration(d) { + this.ditDuration = d + } + + Key(key, pressed) { + this.keyPressed[key] = pressed + if (key == 0) { + this.beginPulsing() + } else { + super.Key(key, pressed) + } + } + + /** + * Begin a pulse if it hasn't already begun + */ + beginPulsing() { + if (!this.pulseTimer) { + this.pulse() + } + } + + pulse() { + if (this.TxClosed(0)) { + // If we were transmitting, pause + this.Tx(0, false) + } else if (this.keyPressed[0]) { + // If the key was pressed, transmit + this.Tx(0, true) + } else { + // If the key wasn't pressed, stop pulsing + this.pulseTimer = null + return + } + this.pulseTimer = setTimeout(() => this.pulse(), this.ditDuration) + } +} + +/** + * Electronic Bug Keyer + * + * Repeats both dits and dahs, ensuring proper pauses. + * + * I think the original ElBug Keyers did not have two paddles, so I've taken the + * liberty of making it so that whatever you pressed last is what gets repeated, + * similar to a modern computer keyboard. + */ +class ElBugKeyer extends BugKeyer { + KeyNames() { + return ["· ", "−"] + } + + Reset() { + super.Reset() + this.lastPressed = -1 + } + + Key(key, pressed) { + this.keyPressed[key] = pressed + if (pressed) { + this.lastPressed = key + } else { + this.lastPressed = this.keyPressed.findIndex(Boolean) + } + this.beginPulsing() + } + + /** + * Calculates the duration of the next transmission to send. + * + * If there is nothing to send, returns 0. + * + * @returns {Duration} Duration of next transmission + */ + nextTxDuration() { + switch (this.lastPressed) { + case 0: + return this.ditDuration * DIT + case 1: + return this.ditDuration * DAH + } + return 0 + } + + pulse() { + let nextPulse = 0 + + // This keyer only drives one transmit relay + if (this.TxClosed()) { + // If we're transmitting at all, pause + this.Tx(0, false) + nextPulse = this.ditDuration + } else if (this.keyPressed.some(Boolean)) { + // If there's a key down, transmit. + // + // Wait until here to ask for next duration, so things with memories + // don't flush that memory for a pause. + this.Tx(0, true) + nextPulse = this.nextTxDuration() + } + + if (nextPulse) { + this.pulseTimer = setTimeout(() => this.pulse(), nextPulse) + } else { + this.pulseTimer = null + } + } +} + +/** + * Single dot memory keyer. + * + * If you tap dit while a dah is sending, it queues up a dit to send, even if + * the dit key is no longer being held at the start of the next cycle. + */ +class SingleDotKeyer extends ElBugKeyer { + Reset() { + super.Reset() + this.queue = [] + } + + Key(key, pressed) { + super.Key(key, pressed) + if (pressed && (key == 0) && this.keyPressed[1]) { + this.queue = [DIT] + } + } + + nextTxDuration() { + if (this.queue.length) { + let dits = this.queue.shift() + return dits * this.ditDuration + } + return super.nextTxDuration() + } +} + /** * Keyer class. This handles iambic and straight key input. * @@ -101,7 +379,7 @@ function morseNot(ditdah) { * - Typematic: you hold the key down and it repeats evenly-spaced tones * - Typeahead: if you hit a key while it's still transmitting the last-entered one, it queues up your next entered one */ -class Keyer { +class OldKeyer { /** * Create a Keyer * @@ -169,7 +447,7 @@ class Keyer { typematic() { if (this.ditDown && this.dahDown) { this.modeBQueue = this.last - this.last = morseNot(this.last) + this.last = not(this.last) } else if (this.ditDown) { this.modeBQueue = null this.last = DIT @@ -365,4 +643,4 @@ class Keyer { } } -export {Keyer} +export {StraightKeyer, CootieKeyer, BugKeyer, ElBugKeyer, SingleDotKeyer} diff --git a/static/repeaters.mjs b/static/repeaters.mjs index 107913e..889f104 100644 --- a/static/repeaters.mjs +++ b/static/repeaters.mjs @@ -67,12 +67,9 @@ export class Vail { this.lagDurations.unshift(now - this.clockOffset - beginTxTime - totalDuration) this.lagDurations.splice(20, 2) this.rx(0, 0, this.stats()) - console.debug("Vail.wsMessage() SQUELCH", msg) return } - console.debug("Vail.wsMessage()", this.socket, msg) - // The very first packet is the server telling us the current time if (durations.length == 0) { if (this.clockOffset == 0) { @@ -112,7 +109,6 @@ export class Vail { console.error("Not connected, dropping", jmsg) return } - console.debug("Transmit", this.socket, msg) this.socket.send(jmsg) if (squelch) { this.sent.push(jmsg) diff --git a/static/vail.css b/static/vail.css index 7fbf368..26694d2 100644 --- a/static/vail.css +++ b/static/vail.css @@ -1,14 +1,3 @@ -.flex { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - justify-content: space-around; -} - -.mdl-card { - margin: 0.5em; -} - .key { width: 100%; height: 6em; @@ -62,10 +51,10 @@ kbd { background-color: #eee; border: 1px solid #bbb; border-radius: 3px; - font-size: 9pt; + font-size: 66%; padding: .1em .6em; cursor: default; - vertical-align: top; + vertical-align: middle; } kbd.gamepad { @@ -76,10 +65,6 @@ kbd.gamepad { height: 10px; width: 10px; } -img.gamepad { - height: 1.5em; - vertical-align: baseline; -} code { background-color: #333; diff --git a/static/vail.mjs b/static/vail.mjs index 4ed288f..259a151 100644 --- a/static/vail.mjs +++ b/static/vail.mjs @@ -25,6 +25,11 @@ function toast(msg) { }) } +// iOS kludge +if (!window.AudioContext) { + window.AudioContext = window.webkitAudioContext +} + class VailClient { constructor() { this.sent = [] @@ -37,8 +42,9 @@ class VailClient { // Make helpers this.lamp = new Buzzer.Lamp() this.buzzer = new Buzzer.ToneBuzzer() - this.keyer = new Keyer.Keyer(() => this.beginTx(), () => this.endTx()) - this.roboKeyer = new Keyer.Keyer(() => this.Buzz(), () => this.Silence()) + this.straightKeyer = new Keyer.StraightKeyer(() => this.beginTx(), () => this.endTx()) + this.keyer = new Keyer.SingleDotKeyer(() => this.beginTx(), () => this.endTx()) + this.roboKeyer = new Keyer.ElBugKeyer(() => this.Buzz(), () => this.Silence()) // Set up various input methods // Send this as the keyer so we can intercept dit and dah events for charts @@ -62,10 +68,10 @@ class VailClient { // Set up inputs this.inputInit("#iambic-duration", e => { - this.keyer.SetIntervalDuration(e.target.value) - this.roboKeyer.SetIntervalDuration(e.target.value) + this.keyer.SetDitDuration(e.target.value) + this.roboKeyer.SetDitDuration(e.target.value) for (let i of Object.values(this.inputs)) { - i.SetIntervalDuration(e.target.value) + i.SetDitDuration(e.target.value) } }) this.inputInit("#rx-delay", e => { @@ -103,7 +109,7 @@ class VailClient { * @param down If key has been depressed */ Straight(down) { - this.keyer.Straight(down) + this.straightKeyer.Key(0, down) if (this.straightChart) this.straightChart.Set(down?1:0) } @@ -113,7 +119,7 @@ class VailClient { * @param down If the key has been depressed */ Dit(down) { - this.keyer.Dit(down) + this.keyer.Key(0, down) if (this.ditChart) this.ditChart.Set(down?1:0) } @@ -123,7 +129,7 @@ class VailClient { * @param down If the key has been depressed */ Dah(down) { - this.keyer.Dah(down) + this.keyer.Key(1, down) if (this.dahChart) this.dahChart.Set(down?1:0) }