seems to work

This commit is contained in:
Neale Pickett 2022-05-14 18:51:05 -06:00
parent 9a37907945
commit f23ea76a4f
9 changed files with 636 additions and 827 deletions

View File

@ -267,27 +267,27 @@ class TelegraphBuzzer extends AudioBuzzer{
}
class Lamp extends Buzzer {
constructor() {
constructor(element) {
super()
this.lamp = document.querySelector("#recv")
this.element = element
}
Buzz(tx, when=0) {
if (tx) return
let ms = when - Date.now()
setTimeout(e => {
recv.classList.add("rx")
}, ms)
let ms = when?when - Date.now():0
setTimeout(
() =>{
this.element.classList.add("rx")
},
ms,
)
}
Silence(tx, when=0) {
if (tx) return
let recv = document.querySelector("#recv")
let ms = when - Date.now()
setTimeout(e => {
recv.classList.remove("rx")
}, ms)
let ms = when?when - Date.now():0
setTimeout(() => this.element.classList.remove("rx"), ms)
}
}

View File

@ -8,296 +8,247 @@
<!-- 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">
<link rel="icon" href="vail.png" sizes="256x256" type="image/png">
<link rel="icon" href="vail.svg" sizes="any" type="image/svg+xml">
<script type="module" src="vail.mjs"></script>
<script type="module" src="ui.mjs"></script>
<link rel="stylesheet" href="vail.css">
</head>
<body>
<nav class="navbar">
<nav class="navbar is-dark">
<div class="navbar-brand">
<a class="navbar-item">
<img src="vail.svg" alt="">
Vail
<img class="" src="vail.svg" alt="">
<div class="block">Vail</div>
</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>
<a class="navbar-item" href="https://github.com/nealey/vail/">Source Code</a>
</div>
</div>
</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 class="columns">
<div class="column">
<div class="box" id="transciever">
<h1 class="title">Repeater</h1>
<div class="level">
<div class="level-left">
<div class="level-item">
<div class="field is-horizontal">
<div class="field-label is-hidden">
<label class="label" for="repeater">Repeater</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input class="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>
</div>
</div>
</div>
<div class="level-right">
<div class="level-item">
<!-- This appears as a little light that turns on when someone's sending -->
<span class="tag" id="recv">
<output class="has-text-info" id="note"></output>
<i class="mdi mdi-volume-off" id="muted"></i>
</span>
</div>
</div>
</div>
<div class="block">
<div class="" id="charts">
<canvas class="chart" id="rxChart" data-color="orange"></canvas>
<canvas class="chart" id="txChart" data-color="teal"></canvas>
<canvas class="chart" id="key0Chart" data-color="olive"></canvas>
<canvas class="chart" id="key1Chart" data-color="purple"></canvas>
</div>
</div>
<div class="block">
<table class="wide">
<tr>
<td>
<button class="button key is-primary" data-key="0" title="right click for Key">
Key
</button>
<div class="shortcuts">
<kbd title="keyboard button">.</kbd>
<kbd title="keyboard button">x</kbd>
<i class="mdi mdi-gamepad-circle-left" title="Gamepad Left Button"></i>
</div>
</td>
<td>
<button class="button key is-primary" data-key="1" title="right click for Key">
Key
</button>
<div class="shortcuts">
<kbd title="keyboard button">/</kbd>
<kbd title="keyboard button">z</kbd>
<i class="mdi mdi-gamepad-circle-up" title="Gamepad Top Button"></i>
</div>
</td>
</tr>
</table>
</div>
<div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Mode</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<div class="select">
<select id="keyer-mode">
<option value="cootie">Straight Key / Cootie</option>
<option value="bug">Bug</option>
<option value="elbug">ElBug</option>
<option value="singledot">Single Dot</option>
<option value="ultimatic">Ultimatic</option>
<option value="iambic">Iambic (Plain)</option>
<option value="iambica">Iambic A</option>
<option value="iambicb">Iambic B</option>
<option value="keyahead">Keyahead</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">
<output for="keyer-rate"></output> WPM
</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input
id="keyer-rate"
type="range"
min="5"
max="40"
step="1"
value="12">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="column">
<div class="box">
<h2 class="title">Knobs</h2>
<div class="block">
<div class="control">
<button id="ck" class="button is-primary">
CK
</button>
</div>
<div class="">
Send <code>CK</code> (check) to the repeater, and play when it comes back.
</div>
</div>
<div class="block">
<div class="control">
<button id="reset" class="button">
Reset
</button>
</div>
<div>
Reset all Vail preferences to default.
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">
<output for="rx-delay"></output>s
rx delay
</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input
id="rx-delay"
type="range"
min="0"
max="10"
value="4"
step="0.1"
list="rx-delays">
<datalist id="rx-delays">
<option value="0"></option>
<option value="1"></option>
<option value="2" label="2s"></option>
<option value="3"></option>
<option value="4" label="4s"></option>
<option value="5"></option>
<option value="6" label="6s"></option>
<option value="7"></option>
<option value="8" label="8s"></option>
<option value="9"></option>
<option value="10"></option>
</datalist>
</div>
</div>
</div>
</div>
<p>
<label class="checkbox">
<input type="checkbox" id="telegraph-buzzer">
Telegraph sounds
</label>
</p>
</div>
</div>
</div>
</section>
<div class="box">
<h2 class="title">Notes</h2>
<textarea class="textarea" placeholder="Enter your own notes here" id="notes"></textarea>
<a href="https://github.com/nealey/vail/wiki" target="_blank">Vail Wiki</a>
</div>
<main class="mdl-layout__content">
<div class="flex">
<div class="mdl-card mdl-shadow--4dp input-methods mashable-area">
<div id="recv">
<!-- This div appears as a little light that turns on when someone's sending -->
<i class="material-icons" id="muted">volume_off</i>
</div>
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
</div>
</h2>
</div>
<output id="note"></output>
<div id="charts">
<canvas class="chart" id="rxChart" data-color="orange"></canvas>
<canvas class="chart" id="txChart" data-color="teal"></canvas>
<canvas class="chart" id="ditChart" data-color="olive"></canvas>
<canvas class="chart" id="dahChart" data-color="purple"></canvas>
</div>
<div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect">
<div class="mdl-tabs__tab-bar">
<a href="#straight" class="mdl-tabs__tab is-active" data-singlekey="straight">Straight Key</a>
<a href="#iambic" class="mdl-tabs__tab" data-singlekey="iambic">Iambic</a>
<a href="#tools" class="mdl-tabs__tab">Tools</a>
</div>
<div class="mdl-tabs__panel is-active" id="straight">
<table class="center wide">
<tr>
<td colspan="2">
<button id="key" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Key
</button>
</td>
</tr>
<tr>
<td>
<i class="mdi mdi-keyboard" title="Keyboard"></i>
</td>
<td>
<kbd>c</kbd>
<kbd>,</kbd>
<kbd>Enter</kbd>
</td>
</tr>
<tr>
<td>
<i class="mdi mdi-controller-classic"></i>
</td>
<td>
<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>
</div>
<div class="mdl-tabs__panel" id="iambic">
<table class="center wide">
<tr>
<td colspan="2">
<button id="dit" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Dit
</button>
</td>
<td colspan="2">
<button id="dah" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Dah
</button>
</td>
</tr>
<tr>
<td>
<i class="mdi mdi-keyboard" title="Keyboard"></i>
</td>
<td>
<kbd>.</kbd>
<kbd>x</kbd>
</td>
<td>
<i class="mdi mdi-keyboard" title="Keyboard"></i>
</td>
<td>
<kbd>/</kbd>
<kbd>z</kbd>
</td>
</tr>
<tr>
<td>
<i class="mdi mdi-controller-classic"></i>
</td>
<td>
<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="mdi mdi-controller-classic"></i>
</td>
<td>
<i class="mdi mdi-gamepad-circle-up" title="Gamepad Top Button"></i>
<kbd class="gamepad" title="Gamepad Right Shoulder Button">R1</kbd>
</td>
</tr>
<tr>
<td colspan="4" class="mdl-card__supporting-text" style="text-align: center;">
Second mouse button switches dah and dit
</td>
</tr>
</table>
</div>
<div class="mdl-tabs__panel" id="tools">
<div class="flex mdl-card__supporting-text">
<button id="ck" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
CK
</button>
<p>
Send <code>CK</code> (check) to the repeater, and play when it comes back.
</p>
</div>
<div class="flex mdl-card__supporting-text">
<button id="reset" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
Reset
</button>
<p>
Reset all Vail preferences to default.
</p>
</div>
</div>
<div class="mdl-card__actions">
<button class="mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-button--colored maximize" title="maximize">
<i class="material-icons mdl-color-text--white" role="presentation">aspect_ratio</i>
</button>
</div>
</div>
</div>
<div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Notes</h2>
</div>
<div class="mdl-card__supporting-text">
<textarea class="notes" placeholder="Enter your own notes here"></textarea>
<a href="https://github.com/nealey/vail/wiki" target="_blank">Vail Wiki</a>
</div>
</div>
<div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Knobs</h2>
</div>
<div class="mdl-card__supporting-text">
<p>
Iambic Dit length:
<output id="iambic-duration-value"></output>ms
/
<output id="iambic-duration-wpm"></output> WPM
<input
id="iambic-duration"
class="mdl-slider mdl-js-slider"
type="range"
min="40"
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">
<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">
<span class="mdl-switch__label">Iambic typeahead</span>
</label>
</p>
<p>
Receive delay:
<output id="rx-delay-value"></output>ms
<input
id="rx-delay"
class="mdl-slider mdl-js-slider"
type="range"
min="0"
max="9999"
value="4000">
</p>
<p>
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="telegraph-buzzer">
<input type="checkbox" id="telegraph-buzzer" class="mdl-switch__input">
<span class="mdl-switch__label">Telegraph sounds</span>
</label>
</p>
<p>
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="timing-chart">
<input type="checkbox" id="timing-chart" class="mdl-switch__input">
<span class="mdl-switch__label">Timing chart</span>
</label>
</p>
<hr>
<table>
<tbody>
<tr>
<td>
Suggested receive delay:
</td>
<td>
<output id="suggested-delay-value">0</output>ms
</td>
</tr>
<tr>
<td>
Average round-trip time:
</td>
<td>
<output id="lag-value">0</output>ms
</td>
</tr>
<tr>
<td>
Longest recent transmission:
</td>
<td>
<output id="longest-rx-value">0</output>ms
</td>
</tr>
<tr>
<td>
Your clock is off by:
</td>
<td>
<output id="clock-off-value">??</output>ms
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
<div class="columns is-centered bottom">
<div class="column is-half" id="errors"></div>
</div>
</body>
</html>
<!-- vim: set noet ts=2 sw=2 : -->

View File

@ -27,24 +27,14 @@ export class HTML extends Input{
keyButton(event) {
let begin = event.type.endsWith("down") || event.type.endsWith("start")
if (event.target.id == "dah") {
if (event.button == 2) {
this.keyer.Dit(begin)
} else {
this.keyer.Dah(begin)
}
} else if (event.target.id == "dit") {
if (event.button == 2) {
this.keyer.Dah(begin)
} else {
this.keyer.Dit(begin)
}
} else if (event.target.id == "key") {
this.keyer.Straight(begin)
} else {
return
let key = event.target.dataset.key
// Button 2 does the other key (assuming 2 keys)
if (event.button == 2) {
key = 1 - key
}
this.keyer.Key(key, begin)
if (event.cancelable) {
event.preventDefault()
}
@ -80,7 +70,7 @@ export class Keyboard extends Input{
) {
// Dit
if (this.ditDown != down) {
this.keyer.Dit(down)
this.keyer.Key(0, down)
this.ditDown = down
}
}
@ -92,7 +82,7 @@ export class Keyboard extends Input{
|| (event.key == "]")
) {
if (this.dahDown != down) {
this.keyer.Dah(down)
this.keyer.Key(1, down)
this.dahDown = down
}
}
@ -173,10 +163,10 @@ export class MIDI extends Input{
this.keyer.Straight(begin)
break
case 1: // C#
this.keyer.Dit(begin)
this.keyer.Key(0, begin)
break
case 2: // D
this.keyer.Dah(begin)
this.keyer.Key(1, begin)
break
default:
return
@ -229,10 +219,10 @@ export class Gamepad extends Input{
this.keyer.Straight(currentButtons.key)
}
if (currentButtons.dit != this.gamepadButtons.dit) {
this.keyer.Dit(currentButtons.dit)
this.keyer.Key(0, currentButtons.dit)
}
if (currentButtons.dah != this.gamepadButtons.dah) {
this.keyer.Dah(currentButtons.dah)
this.keyer.Key(1, currentButtons.dah)
}
this.gamepadButtons = currentButtons

View File

@ -6,10 +6,8 @@
* advanced than the bug keyer.
*/
/** Silent period between words */
const PAUSE_WORD = -7
/** Silent period between letters */
const PAUSE_LETTER = -3
import * as RoboKeyer from "./robokeyer.mjs"
/** Silent period between dits and dash */
const PAUSE = -1
/** Length of a dit */
@ -34,78 +32,6 @@ const Minute = 60 * Second
/** @type {Duration} */
const Hour = 60 * Minute
const MorseMap = {
"\x04": ".-.-.", // End Of Transmission
"\x18": "........", // Cancel
"0": "-----",
"1": ".----",
"2": "..---",
"3": "...--",
"4": "....-",
"5": ".....",
"6": "-....",
"7": "--...",
"8": "---..",
"9": "----.",
"a": ".-",
"b": "-...",
"c": "-.-.",
"d": "-..",
"e": ".",
"f": "..-.",
"g": "--.",
"h": "....",
"i": "..",
"j": ".---",
"k": "-.-",
"l": ".-..",
"m": "--",
"n": "-.",
"o": "---",
"p": ".--.",
"q": "--.-",
"r": ".-.",
"s": "...",
"t": "-",
"u": "..-",
"v": "...-",
"w": ".--",
"x": "-..-",
"y": "-.--",
"z": "--..",
".": ".-.-.-",
",": "--..--",
"?": "..--..",
"'": ".----.",
"!": "-.-.--",
"/": "-..-.",
"(": "-.--.",
")": "-.--.-",
"&": ".-...",
":": "---...",
";": "---...",
"=": "-...-",
"+": ".-.-.",
"-": "-....-",
"_": "--..-.",
"\"": ".-..-.",
"$": "...-..-",
"@": ".--.-.",
}
/**
* Return the inverse of the input.
* If you give it dit, it returns dah, and vice-versa.
*
* @param ditdah What to invert
* @returns The inverse of ditdah
*/
function not(ditdah) {
if (ditdah == DIT) {
return DAH
}
return DIT
}
/**
* Queue Set: A Set you can shift and pop.
@ -167,6 +93,20 @@ class StraightKeyer {
this.txRelays = []
}
/**
* Set the duration of dit.
*
* @param {Duration} d New dit duration
*/
SetDitDuration(d) {
this.ditDuration = d
}
/**
* Clean up all timers, etc.
*/
Release() {}
/**
* Returns the state of a single transmit relay.
*
@ -235,7 +175,7 @@ class CootieKeyer extends StraightKeyer {
*/
class BugKeyer extends StraightKeyer {
KeyNames() {
return ["·", "Key"]
return ["Dit", "Key"]
}
Reset() {
@ -248,15 +188,6 @@ class BugKeyer extends StraightKeyer {
this.keyPressed = [false, false]
}
/**
* 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) {
@ -302,7 +233,7 @@ class BugKeyer extends StraightKeyer {
*/
class ElBugKeyer extends BugKeyer {
KeyNames() {
return ["· ", ""]
return ["Dit", "Dah"]
}
Reset() {
@ -499,281 +430,47 @@ class IambicBKeyer extends IambicKeyer {
}
}
/**
* Keyer class. This handles iambic and straight key input.
*
* This will handle the following things that people appear to want with iambic input:
*
* - 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 OldKeyer {
/**
* Create a Keyer
*
* @param {TxControl} beginTxFunc Callback to begin transmitting
* @param {TxControl} endTxFunc Callback to end transmitting
* @param {number} intervalDuration Dit duration (milliseconds)
* @param {number} pauseMultiplier How long to stretch out inter-letter and inter-word pauses
*/
constructor(beginTxFunc, endTxFunc, {intervalDuration=100, pauseMultiplier=1}={}) {
this.beginTxFunc = beginTxFunc
this.endTxFunc = endTxFunc
this.intervalDuration = intervalDuration
this.pauseMultiplier = pauseMultiplier
this.ditDown = false
this.dahDown = false
this.typeahead = false
this.iambicModeB = true
this.last = null
class KeyaheadKeyer extends ElBugKeyer {
Reset() {
super.Reset()
this.queue = []
this.pulseTimer = null
}
pulse() {
if (this.queue.length == 0) {
let next = this.typematic()
if (next) {
// Barkeep! Another round!
this.Enqueue(next)
} else {
// Nothing left on the queue, stop the machine
this.pulseTimer = null
return
}
Key(key, pressed) {
if (pressed) {
this.queue.push(key)
}
super.Key(key, pressed)
}
nextTx() {
let next = this.queue.shift()
if (next < 0) {
next *= -1
if (next > 1) {
// Don't adjust spacing within a letter
next *= this.pauseMultiplier
} else {
this.endTxFunc()
if (this.txChart) {
this.txChart.Add(Date.now(), 0)
}
}
} else {
this.last = next
this.beginTxFunc()
if (this.txChart) {
this.txChart.Add(Date.now(), 1)
}
if (next != null) {
return next
}
this.pulseTimer = setTimeout(() => this.pulse(), next * this.intervalDuration)
return super.nextTx()
}
}
maybePulse() {
// If there's no timer running right now, restart the pulse
if (!this.pulseTimer) {
this.pulse()
}
}
typematic() {
if (this.ditDown && this.dahDown) {
this.modeBQueue = this.last
this.last = not(this.last)
} else if (this.ditDown) {
this.modeBQueue = null
this.last = DIT
} else if (this.dahDown) {
this.modeBQueue = null
this.last = DAH
} else if (this.modeBQueue && this.iambicModeB) {
this.last = this.modeBQueue
this.modeBQueue = null
} else {
this.last = null
this.modeBQueue = null
}
return this.last
}
/**
* A dictionary of all available keyers
*/
const Keyers = {
straight: StraightKeyer,
cootie: CootieKeyer,
bug: BugKeyer,
elbug: ElBugKeyer,
singledot: SingleDotKeyer,
ultimatic: UltimaticKeyer,
iambic: IambicKeyer,
iambica: IambicAKeyer,
iambicb: IambicBKeyer,
keyahead: KeyaheadKeyer,
/**
* Return true if we are currently playing out something
*/
Busy() {
return this.pulseTimer
}
/**
* Set a new dit interval (transmission rate)
*
* @param {number} duration Dit duration (milliseconds)
*/
SetIntervalDuration(duration) {
this.intervalDuration = duration
}
/**
* Set a new pause multiplier.
*
* This slows down the inter-letter and inter-word pauses,
* which can aid in learning.
*
* @param {number} multiplier Pause multiplier
*/
SetPauseMultiplier(multiplier) {
this.pauseMultiplier = multiplier
}
/**
* Set Iambic mode B.
*
* Near as I can tell, B sends one more tone than was entered, when
* both keys are held down.
* This logic happens in the typematic code.
*
* Dit key
*
* Dah key
*
* Mode A output
*
* Mode B output
*
* @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.
*/
Flush() {
this.queue.splice(0)
}
/**
* Add to the output queue, and start processing the queue if it's not currently being processed.
*
* @param {number} key A duration, in dits. Negative durations are silent.
*/
Enqueue(key) {
this.queue.push(key)
if (key > 0) {
this.queue.push(PAUSE)
}
this.maybePulse()
}
/**
* Enqueue a morse code string (eg "... --- ...")
*
* @param {string} ms String to enqueue
*/
EnqueueMorseString(ms) {
for (let mc of ms) {
switch (mc) {
case ".":
this.Enqueue(DIT)
break
case "-":
this.Enqueue(DAH)
break
case " ":
this.Enqueue(PAUSE_LETTER)
break
}
}
}
/**
* Enqueue an ASCII string (eg "SOS help")
*
* @param {string} s String to enqueue
*/
EnqueueAsciiString(s, {pauseLetter = PAUSE_LETTER, pauseWord = PAUSE_WORD} = {}) {
for (let c of s.toLowerCase()) {
let m = MorseMap[c]
if (m) {
this.EnqueueMorseString(m)
this.Enqueue(pauseLetter)
continue
}
switch (c) {
case " ":
case "\n":
case "\t":
this.Enqueue(pauseWord)
break
default:
console.warn("Unable to encode '" + c + "'!")
break
}
}
}
/**
* Do something to the straight key
*
* @param down True if key was pressed
*/
Straight(down) {
if (down) {
this.beginTxFunc()
} else {
this.endTxFunc()
}
}
/**
* Do something to the dit key
*
* @param down True if key was pressed
*/
Dit(down) {
this.ditDown = down
if (down) {
if (this.typeahead
|| !this.Busy()
|| (this.iambicModeB && (this.last == DAH))) {
this.Enqueue(DIT)
}
}
}
/**
* Do something to the dah key
*
* @param down True if key was pressed
*/
Dah(down) {
this.dahDown = down
if (down) {
if (this.typeahead
|| !this.Busy()
|| (this.iambicModeB && (this.last == DIT))) {
this.Enqueue(DAH)
}
}
}
robo: RoboKeyer.Keyer,
}
export {
StraightKeyer,
CootieKeyer, BugKeyer, ElBugKeyer,
SingleDotKeyer, UltimaticKeyer,
IambicKeyer, IambicAKeyer, IambicBKeyer,
Keyers,
}

View File

@ -124,11 +124,10 @@ export class Vail {
export class Null {
constructor(rx) {
this.rx = rx
this.interval = setInterval(() => this.pulse(), 1 * Second)
this.interval = setInterval(() => this.pulse(), 3 * Second)
}
pulse() {
console.log("pulse")
this.rx(0, 0, {note: "local"})
}

217
static/robokeyer.mjs Normal file
View File

@ -0,0 +1,217 @@
/** Silent period between words */
const PAUSE_WORD = -7
/** Silent period between letters */
const PAUSE_LETTER = -3
/** Silent period between dits and dash */
const PAUSE = -1
/** Length of a dit */
const DIT = 1
/** Length of a dah */
const DAH = 3
const MorseMap = {
"\x04": ".-.-.", // End Of Transmission
"\x18": "........", // Cancel
"0": "-----",
"1": ".----",
"2": "..---",
"3": "...--",
"4": "....-",
"5": ".....",
"6": "-....",
"7": "--...",
"8": "---..",
"9": "----.",
"a": ".-",
"b": "-...",
"c": "-.-.",
"d": "-..",
"e": ".",
"f": "..-.",
"g": "--.",
"h": "....",
"i": "..",
"j": ".---",
"k": "-.-",
"l": ".-..",
"m": "--",
"n": "-.",
"o": "---",
"p": ".--.",
"q": "--.-",
"r": ".-.",
"s": "...",
"t": "-",
"u": "..-",
"v": "...-",
"w": ".--",
"x": "-..-",
"y": "-.--",
"z": "--..",
".": ".-.-.-",
",": "--..--",
"?": "..--..",
"'": ".----.",
"!": "-.-.--",
"/": "-..-.",
"(": "-.--.",
")": "-.--.-",
"&": ".-...",
":": "---...",
";": "---...",
"=": "-...-",
"+": ".-.-.",
"-": "-....-",
"_": "--..-.",
"\"": ".-..-.",
"$": "...-..-",
"@": ".--.-.",
}
/**
* Robo Keyer. It sends morse code so you don't have to!
*/
class Keyer {
/**
* Create a Keyer
*
* @param {TxControl} beginTxFunc Callback to begin transmitting
* @param {TxControl} endTxFunc Callback to end transmitting
*/
constructor(beginTxFunc, endTxFunc) {
this.beginTxFunc = beginTxFunc
this.endTxFunc = endTxFunc
this.ditDuration = 100
this.pauseMultiplier = 1
this.queue = []
this.pulseTimer = null
}
pulse() {
let next = this.queue.shift()
if (next == null) {
// Nothing left on the queue, stop the machine
this.pulseTimer = null
return
}
if (next < 0) {
next *= -1
if (next > 1) {
// Don't adjust spacing within a letter
next *= this.pauseMultiplier
} else {
this.endTxFunc()
}
} else {
this.beginTxFunc()
}
this.pulseTimer = setTimeout(() => this.pulse(), next * this.ditDuration)
}
maybePulse() {
// If there's no timer running right now, restart the pulse
if (!this.pulseTimer) {
this.pulse()
}
}
/**
* Return true if we are currently playing out something
*/
Busy() {
return Boolean(this.pulseTimer)
}
/**
* Set a new dit interval (transmission rate)
*
* @param {number} duration Dit duration (milliseconds)
*/
SetDitDuration(duration) {
this.ditDuration = duration
}
/**
* Set a new pause multiplier.
*
* This slows down the inter-letter and inter-word pauses,
* which can aid in learning.
*
* @param {number} multiplier Pause multiplier
*/
SetPauseMultiplier(multiplier) {
this.pauseMultiplier = multiplier
}
/**
* Delete anything left on the queue.
*/
Flush() {
this.queue.splice(0)
}
/**
* Add to the output queue, and start processing the queue if it's not currently being processed.
*
* @param {number} key A duration, in dits. Negative durations are silent.
*/
Enqueue(key) {
this.queue.push(key)
if (key > 0) {
this.queue.push(PAUSE)
}
this.maybePulse()
}
/**
* Enqueue a morse code string (eg "... --- ...")
*
* @param {string} ms String to enqueue
*/
EnqueueMorseString(ms) {
for (let mc of ms) {
switch (mc) {
case ".":
this.Enqueue(DIT)
break
case "-":
this.Enqueue(DAH)
break
case " ":
this.Enqueue(PAUSE_LETTER)
break
}
}
}
/**
* Enqueue an ASCII string (eg "SOS help")
*
* @param {string} s String to enqueue
*/
EnqueueAsciiString(s, {pauseLetter = PAUSE_LETTER, pauseWord = PAUSE_WORD} = {}) {
for (let c of s.toLowerCase()) {
let m = MorseMap[c]
if (m) {
this.EnqueueMorseString(m)
this.Enqueue(pauseLetter)
continue
}
switch (c) {
case " ":
case "\n":
case "\t":
this.Enqueue(pauseWord)
break
default:
console.warn("Unable to encode '" + c + "'!")
break
}
}
}
}
export {Keyer}

39
static/ui.mjs Normal file
View File

@ -0,0 +1,39 @@
/**
* If the user clicked on the little down arrow,
* clear the input field so all autocomplete options are shown.
*
* This kludge may not work properly on every browser.
*
* @param event Triggering event
*/
function maybeDropdown(event) {
let el = event.target
switch (event.type) {
case "click":
let offset = el.clientWidth + el.offsetLeft - event.clientX;
if (el.value) {
el.dataset.value = el.value
}
if (offset < 0) {
el.value = ""
}
break
case "mouseleave":
if (!el.value) {
el.value = el.dataset.value
}
break;
}
}
function init() {
let rep = document.querySelector("#repeater")
rep.addEventListener("click", maybeDropdown)
rep.addEventListener("mouseleave", maybeDropdown)
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}

View File

@ -1,6 +1,14 @@
.navbar-item img {
margin-right: 1em;
}
.key {
width: 100%;
height: 6em;
width: 95%;
height: 6em;
}
.wide {
width: 100%;
}
.mashable-area {
@ -8,43 +16,39 @@
-webkit-user-select: none; /* 2022-04-26 Safari still needs this */
}
.center {
text-align: center;
#recv.rx {
background-color: orange;
}
.maximize {
position: absolute;
input[type=range] {
height: 25px;
margin: 10px 0;
width: 100%;
accent-color: #00d1b2;
}
.bottom {
position: fixed;
bottom: 0;
right: 0;
width: 100vw;
}
.maximized .key {
height: 90vh;
}
.maximized {
width: 90vw;
}
.wide {
width: 100%;
}
.hidden {
visibility: none;
}
#errors {
color: rgba(127, 0, 0, .54);
max-height: 5em;
max-height: 10em;
overflow-y: scroll;
}
@keyframes yellow-fade {
0% {background: yellow;}
100% {background: none;}
0% {background-color: orange;}
100% {background-color: default;}
}
#errors p {
margin: 0;
animation: yellow-fade 2s ease-in 1;
background-color: #444;
color: white;
margin: 0.5em;
padding: 0.2em;
border-radius: 4px;
text-align: center;
animation: yellow-fade 0.3s ease-in 1;
}
kbd {
@ -72,78 +76,6 @@ code {
padding: 0.1em;
}
textarea.notes {
width: 100%;
min-height: 10em;
font-family: sans-serif;
}
img {
max-width: 20em;
}
.mdl-card__supporting-text {
max-height: 20em;
overflow-y: scroll;
}
.mdl-card__supporting-text.long {
max-height: inherit;
}
#recv {
width: 2em;
height: 1em;
line-height: 1em;
position: absolute;
top: 0.5em;
right: 1em;
border-radius: 0.3em;
text-align: center;
vertical-align: middle;
}
#recv.rx {
background-color: orange;
}
#note {
position: absolute;
top: 0.5em;
right: 5em;
font-size: 80%;
color: #888;
}
.input-methods td {
text-align: left;
}
#morse-tree table {
width: 100%;
}
#morse-tree tr {
height: 1.4em;
text-align: center;
font-family: monospace;
}
#morse-tree tr,
#morse-tree td:nth-child(n+2) {
background: #eee;
}
#morse-tree tr:nth-child(n+2),
#morse-tree td.dah {
background: #ddd;
}
#morse-list span {
font-family: monospace;
display: inline-block;
background: #eee;
margin: 1px;
padding: 0.4em;
min-width: 4em;
}
#charts {
line-height: 0;
}

View File

@ -1,4 +1,4 @@
import * as Keyer from "./keyer.mjs"
import {Keyers} from "./keyers.mjs"
import * as Buzzer from "./buzzer.mjs"
import * as Inputs from "./inputs.mjs"
import * as Repeaters from "./repeaters.mjs"
@ -7,22 +7,20 @@ import * as Chart from "./chart.mjs"
const DefaultRepeater = "General"
const Millisecond = 1
const Second = 1000 * Millisecond
const Minute = 60 * Second
/**
* Pop up a message, using an MDL snackbar.
* Pop up a message, using an notification..
*
* @param {string} msg Message to display
*/
function toast(msg) {
function toast(msg, timeout=4*Second) {
console.info(msg)
let el = document.querySelector("#snackbar")
if (!el || !el.MaterialSnackbar) {
return
}
el.MaterialSnackbar.showSnackbar({
message: msg,
timeout: 2000
})
let errors = document.querySelector("#errors")
let p = errors.appendChild(document.createElement("p"))
p.textContent = msg
setTimeout(() => p.remove(), timeout)
}
// iOS kludge
@ -40,21 +38,16 @@ class VailClient {
this.beginTxTime = null // Time when we began transmitting
// Make helpers
this.lamp = new Buzzer.Lamp()
this.lamp = new Buzzer.Lamp(document.querySelector("#recv"))
this.buzzer = new Buzzer.ToneBuzzer()
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())
this.straightKeyer = new Keyers.straight(() => this.beginTx(), () => this.endTx())
this.keyer = new Keyers.straight(() => this.beginTx(), () => this.endTx())
this.roboKeyer = new Keyers.robo(() => this.Buzz(), () => this.Silence())
// Set up various input methods
// Send this as the keyer so we can intercept dit and dah events for charts
this.inputs = Inputs.SetupAll(this)
// VBand: Keep track of how the user wants the single key to behave
for (let e of document.querySelectorAll("[data-singlekey]")) {
e.addEventListener("click", e => this.singlekeyChange(e))
}
// Maximize button
for (let e of document.querySelectorAll("button.maximize")) {
e.addEventListener("click", e => this.maximize(e))
@ -67,39 +60,36 @@ class VailClient {
}
// Set up inputs
this.inputInit("#iambic-duration", e => {
this.keyer.SetDitDuration(e.target.value)
this.roboKeyer.SetDitDuration(e.target.value)
this.inputInit("#keyer-mode", e => this.setKeyer(e.target.value))
this.inputInit("#keyer-rate", e => {
let rate = e.target.value
let ditDuration = Minute / rate / 50
this.keyer.SetDitDuration(ditDuration)
this.roboKeyer.SetDitDuration(ditDuration)
for (let i of Object.values(this.inputs)) {
i.SetDitDuration(e.target.value)
i.SetDitDuration(ditDuration)
}
})
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)
})
this.inputInit("#telegraph-buzzer", e => {
this.setTelegraphBuzzer(e.target.checked)
})
this.inputInit("#timing-chart", e => {
this.setTimingCharts(e.target.checked)
})
this.inputInit("#notes")
// Fill in the name of our repeater
document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim()))
window.addEventListener("hashchange", () => this.hashchange())
this.hashchange()
this.setTimingCharts(true)
// Turn off the "muted" symbol when we can start making noise
Buzzer.Ready()
.then(() => {
console.log("Audio context ready")
document.querySelector("#muted").classList.add("hidden")
document.querySelector("#muted").classList.add("is-hidden")
})
}
@ -110,27 +100,36 @@ class VailClient {
*/
Straight(down) {
this.straightKeyer.Key(0, down)
if (this.straightChart) this.straightChart.Set(down?1:0)
}
/**
* Dit key change (keyer shim)
* Key/paddle change
*
* @param down If the key has been depressed
* @param {Number} key Key which was pressed
* @param {Boolean} down True if key was pressed
*/
Dit(down) {
this.keyer.Key(0, down)
if (this.ditChart) this.ditChart.Set(down?1:0)
Key(key, down) {
this.keyer.Key(key, down)
if (this.keyCharts) this.keyCharts[key].Set(down?1:0)
}
/**
* Dah key change (keyer shim)
*
* @param down If the key has been depressed
*/
Dah(down) {
this.keyer.Key(1, down)
if (this.dahChart) this.dahChart.Set(down?1:0)
setKeyer(keyerName) {
let newKeyerClass = Keyers[keyerName]
if (!newKeyerClass) {
console.error("Keyer not found", keyerName)
return
}
let newKeyer = new newKeyerClass(() => this.beginTx(), () => this.endTx())
let i = 0
for (let keyName of newKeyer.KeyNames()) {
let e = document.querySelector(`.key[data-key="${i}"]`)
e.textContent = keyName
i += 1
}
this.keyer.Release()
this.keyer = newKeyer
document.querySelector("#keyer-rate").dispatchEvent(new Event("input"))
}
Buzz() {
@ -196,14 +195,15 @@ class VailClient {
let chartsContainer = document.querySelector("#charts")
if (enable) {
chartsContainer.classList.remove("hidden")
this.ditChart = Chart.FromSelector("#ditChart")
this.dahChart = Chart.FromSelector("#dahChart")
this.keyCharts = [
Chart.FromSelector("#key0Chart"),
Chart.FromSelector("#key1Chart")
]
this.txChart = Chart.FromSelector("#txChart")
this.rxChart = Chart.FromSelector("#rxChart")
} else {
chartsContainer.classList.add("hidden")
this.ditChart = null
this.dahChart = null
this.keyCharts = []
this.txChart = null
this.rxChart = null
}
@ -232,19 +232,6 @@ class VailClient {
this.setRepeater(decodeURIComponent(hashParts[1] || ""))
}
/**
* VBand: Called when something happens to change what a single key does
*
* @param {Event} event What caused this
*/
singlekeyChange(event) {
for (let e of event.composedPath()) {
if (e.dataset && e.dataset.singlekey) {
this.inputs.Keyboard.iambic = (e.dataset.singlekey == "iambic")
}
}
}
/**
* Connect to a repeater by name.
*
@ -326,8 +313,8 @@ class VailClient {
element.value = storedValue
element.checked = (storedValue == "on")
}
let outputElement = document.querySelector(selector + "-value")
let outputWpmElement = document.querySelector(selector + "-wpm")
let id = element.id
let outputElement = document.querySelector(`[for="${id}"]`)
element.addEventListener("input", e => {
let value = element.value
@ -339,9 +326,6 @@ class VailClient {
if (outputElement) {
outputElement.value = value
}
if (outputWpmElement) {
outputWpmElement.value = (1200 / value).toFixed(1)
}
if (callback) {
callback(e)
}
@ -388,7 +372,7 @@ class VailClient {
let longestRxDuration = this.rxDurations.reduce((a,b) => Math.max(a,b))
let suggestedDelay = ((averageLag + longestRxDuration) * 1.2).toFixed(0)
this.updateReading("#note", stats.note || "")
this.updateReading("#note", stats.note || "")
this.updateReading("#lag-value", averageLag)
this.updateReading("#longest-rx-value", longestRxDuration)
this.updateReading("#suggested-delay-value", suggestedDelay)
@ -457,7 +441,7 @@ class VailClient {
}
}
function vailInit() {
function init() {
if (navigator.serviceWorker) {
navigator.serviceWorker.register("sw.js")
}
@ -471,9 +455,9 @@ function vailInit() {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", vailInit)
document.addEventListener("DOMContentLoaded", init)
} else {
vailInit()
init()
}
// vim: noet sw=2 ts=2