Working up to single dot

This commit is contained in:
Neale Pickett 2022-05-08 11:33:25 -06:00
parent af21b30afc
commit 4950042e6c
7 changed files with 374 additions and 111 deletions

20
static/duration.mjs Normal file
View File

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

View File

@ -5,10 +5,9 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Material Design Lite -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.teal-purple.min.css">
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
<!-- Bulma CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.5.95/css/materialdesignicons.min.css">
<!-- Vail stuff -->
<link rel="manifest" href="manifest.json">
@ -18,50 +17,46 @@
<link rel="stylesheet" href="vail.css">
</head>
<body>
<div class="mdl-layout mdl-js-layout">
<header class="mdl-layout__header mdl-layout__header--scroll">
<div class="mdl-layout__header-row">
<!-- Title -->
<span class="mdl-layout-title">Vail</span>
<!-- Add spacer, to align navigation to the right -->
<div class="mdl-layout-spacer"></div>
<!-- Navigation -->
<nav class="mdl-navigation">
<a class="mdl-navigation__link" href="https://github.com/nealey/vail">Source Code</a>
</nav>
<nav class="navbar">
<div class="navbar-brand">
<a class="navbar-item">
<img src="vail.svg" alt="">
Vail
</a>
</div>
<div class="navbar-menu">
<div class="navbar-end">
<div class="navbar-item">
<a href="https://github.com/nealey/vail/">Source Code</a>
</div>
</div>
</header>
<div class="mdl-layout__drawer">
<span class="mdl-layout-title">Repeaters</span>
<nav class="mdl-navigation">
<a class="mdl-navigation__link" href="#">General</a>
<a class="mdl-navigation__link" href="#1">Channel 1</a>
<a class="mdl-navigation__link" href="#2">Channel 2</a>
<a class="mdl-navigation__link" href="#3">Channel 3</a>
</nav>
<hr>
<span class="mdl-layout-title">Local Practice</span>
<nav class="mdl-navigation">
<a class="mdl-navigation__link" href="#Echo">Echo</a>
<a class="mdl-navigation__link" href="#Fortunes">Fortunes</a>
<a class="mdl-navigation__link" href="#Fortunes: Pauses ×2">Fortunes (slow)</a>
<a class="mdl-navigation__link" href="#Fortunes: Pauses ×4">Fortunes (very slow)</a>
<a class="mdl-navigation__link" href="#Fortunes: Pauses ×6">Fortunes (very very slow)</a>
<a class="mdl-navigation__link" href="#Fortunes: Pauses ×10">Fortunes (crazy slow)</a>
</nav>
<hr>
<span class="mdl-layout-title">Resources</span>
<nav class="mdl-navigation">
<a class="mdl-navigation__link" href="https://github.com/nealey/vail/wiki" target="_blank">Wiki</a>
<a class="mdl-navigation__link" href="https://discord.gg/GBzj8cBat7" target="_blank">Discord</a>
</nav>
</div>
<div id="snackbar" class="mdl-js-snackbar mdl-snackbar">
<div class="mdl-snackbar__text"></div>
<button class="mdl-snackbar__action" type="button"></button>
</nav>
<section class="section">
<div class="container">
<div class="field">
<label class="mdl-textfield__label" for="repeater">Repeater</label>
<div class="control">
<input class="mdl-textfield__input" type="text" id="repeater" list="repeater-list">
<datalist id="repeater-list">
<option>General</option>
<option value="1">Channel 1</option>
<option value="2">Channel 2</option>
<option value="3">Channel 3</option>
<option value="Null">No transmit</option>
<option>Echo</option>
<option>Echo 5s</option>
<option>Echo 10s</option>
<option>Fortunes</option>
<option>Fortunes: Pauses ×2</option>
<option>Fortunes: Pauses ×4</option>
<option>Fortunes: Pauses ×8</option>
</datalist>
</div>
</div>
</div>
</section>
<main class="mdl-layout__content">
<div class="flex">
@ -73,22 +68,6 @@
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input class="mdl-textfield__input" type="text" id="repeater" list="repeater-list">
<datalist id="repeater-list">
<option value="">General</option>
<option value="1">Channel 1</option>
<option value="2">Channel 2</option>
<option value="3">Channel 3</option>
<option value="Null">Null (dummy load)</option>
<option>Echo</option>
<option>Echo 5s</option>
<option>Echo 10s</option>
<option>Fortunes</option>
<option>Fortunes: Pauses ×2</option>
<option>Fortunes: Pauses ×4</option>
<option>Fortunes: Pauses ×8</option>
</datalist>
<label class="mdl-textfield__label" for="repeater">Repeater</label>
</div>
</h2>
</div>
@ -118,7 +97,7 @@
</tr>
<tr>
<td>
<i class="material-icons" role="presentation">keyboard</i>
<i class="mdi mdi-keyboard" title="Keyboard"></i>
</td>
<td>
<kbd>c</kbd>
@ -128,11 +107,11 @@
</tr>
<tr>
<td>
<i class="material-icons" role="presentation">gamepad</i>
<i class="mdi mdi-controller-classic"></i>
</td>
<td>
<img class="gamepad b0" title="Gamepad Bottom Button" src="b0.svg" alt="Bottom button">
<img class="gamepad b1" title="Gamepad Right Button" src="b1.svg" alt="Right button">
<i class="mdi mdi-gamepad-circle-down" title="Gamepad Bottom Button"></i>
<i class="mdi mdi-gamepad-circle-right" title="Gamepad Right Button"></i>
</td>
</tr>
</table>
@ -153,14 +132,14 @@
</tr>
<tr>
<td>
<i class="material-icons" role="presentation">keyboard</i>
<i class="mdi mdi-keyboard" title="Keyboard"></i>
</td>
<td>
<kbd>.</kbd>
<kbd>x</kbd>
</td>
<td>
<i class="material-icons" role="presentation">keyboard</i>
<i class="mdi mdi-keyboard" title="Keyboard"></i>
</td>
<td>
<kbd>/</kbd>
@ -169,17 +148,17 @@
</tr>
<tr>
<td>
<i class="material-icons" role="presentation">gamepad</i>
<i class="mdi mdi-controller-classic"></i>
</td>
<td>
<img class="gamepad b2" title="Gamepad Left Button" src="b2.svg" alt="Left Button">
<i class="mdi mdi-gamepad-circle-left" title="Gamepad Left Button"></i>
<kbd class="gamepad" title="Gamepad Left Shoulder Button">L1</kbd>
</td>
<td>
<i class="material-icons" role="presentation">gamepad</i>
<i class="mdi mdi-controller-classic"></i>
</td>
<td>
<img class="gamepad b3" title="Gamepad Top Button" src="b3.svg" alt="Top Button">
<i class="mdi mdi-gamepad-circle-up" title="Gamepad Top Button"></i>
<kbd class="gamepad" title="Gamepad Right Shoulder Button">R1</kbd>
</td>
</tr>
@ -319,7 +298,6 @@
</div>
</main>
</div>
</body>
</html>
<!-- vim: set noet ts=2 sw=2 : -->

View File

@ -2,7 +2,7 @@ class Input {
constructor(keyer) {
this.keyer = keyer
}
SetIntervalDuration(delay) {
SetDitDuration(delay) {
// Nothing
}
}

View File

@ -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.<string>} 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}

View File

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

View File

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

View File

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