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 { class Lamp extends Buzzer {
constructor() { constructor(element) {
super() super()
this.lamp = document.querySelector("#recv") this.element = element
} }
Buzz(tx, when=0) { Buzz(tx, when=0) {
if (tx) return if (tx) return
let ms = when - Date.now() let ms = when?when - Date.now():0
setTimeout(e => { setTimeout(
recv.classList.add("rx") () =>{
}, ms) this.element.classList.add("rx")
},
ms,
)
} }
Silence(tx, when=0) { Silence(tx, when=0) {
if (tx) return if (tx) return
let recv = document.querySelector("#recv") let ms = when?when - Date.now():0
let ms = when - Date.now() setTimeout(() => this.element.classList.remove("rx"), ms)
setTimeout(e => {
recv.classList.remove("rx")
}, ms)
} }
} }

View File

@ -14,30 +14,41 @@
<link rel="icon" href="vail.png" sizes="256x256" type="image/png"> <link rel="icon" href="vail.png" sizes="256x256" type="image/png">
<link rel="icon" href="vail.svg" sizes="any" type="image/svg+xml"> <link rel="icon" href="vail.svg" sizes="any" type="image/svg+xml">
<script type="module" src="vail.mjs"></script> <script type="module" src="vail.mjs"></script>
<script type="module" src="ui.mjs"></script>
<link rel="stylesheet" href="vail.css"> <link rel="stylesheet" href="vail.css">
</head> </head>
<body> <body>
<nav class="navbar"> <nav class="navbar is-dark">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item"> <a class="navbar-item">
<img src="vail.svg" alt=""> <img class="" src="vail.svg" alt="">
Vail <div class="block">Vail</div>
</a> </a>
</div> </div>
<div class="navbar-menu"> <div class="navbar-menu">
<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item"> <a class="navbar-item" href="https://github.com/nealey/vail/">Source Code</a>
<a href="https://github.com/nealey/vail/">Source Code</a>
</div>
</div> </div>
</div> </div>
</nav> </nav>
<section class="section"> <section class="section">
<div class="container"> <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="field">
<label class="mdl-textfield__label" for="repeater">Repeater</label>
<div class="control"> <div class="control">
<input class="mdl-textfield__input" type="text" id="repeater" list="repeater-list"> <input class="input" type="text" id="repeater" list="repeater-list">
<datalist id="repeater-list"> <datalist id="repeater-list">
<option>General</option> <option>General</option>
<option value="1">Channel 1</option> <option value="1">Channel 1</option>
@ -55,249 +66,189 @@
</div> </div>
</div> </div>
</div> </div>
</section>
<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>
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
</div> </div>
</h2>
</div> </div>
<output id="note"></output> <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 id="charts"> <div class="block">
<div class="" id="charts">
<canvas class="chart" id="rxChart" data-color="orange"></canvas> <canvas class="chart" id="rxChart" data-color="orange"></canvas>
<canvas class="chart" id="txChart" data-color="teal"></canvas> <canvas class="chart" id="txChart" data-color="teal"></canvas>
<canvas class="chart" id="ditChart" data-color="olive"></canvas> <canvas class="chart" id="key0Chart" data-color="olive"></canvas>
<canvas class="chart" id="dahChart" data-color="purple"></canvas> <canvas class="chart" id="key1Chart" data-color="purple"></canvas>
</div>
</div> </div>
<div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect"> <div class="block">
<div class="mdl-tabs__tab-bar"> <table class="wide">
<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> <tr>
<td colspan="2"> <td>
<button id="key" class="key mdl-button mdl-js-button mdl-button--raised mdl-button--colored"> <button class="button key is-primary" data-key="0" title="right click for Key">
Key Key
</button> </button>
</td> <div class="shortcuts">
</tr> <kbd title="keyboard button">.</kbd>
<tr> <kbd title="keyboard button">x</kbd>
<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> <i class="mdi mdi-gamepad-circle-left" title="Gamepad Left Button"></i>
<kbd class="gamepad" title="Gamepad Left Shoulder Button">L1</kbd> </div>
</td>
<td>
<i class="mdi mdi-controller-classic"></i>
</td> </td>
<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> <i class="mdi mdi-gamepad-circle-up" title="Gamepad Top Button"></i>
<kbd class="gamepad" title="Gamepad Right Shoulder Button">R1</kbd> </div>
</td>
</tr>
<tr>
<td colspan="4" class="mdl-card__supporting-text" style="text-align: center;">
Second mouse button switches dah and dit
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
<div class="mdl-tabs__panel" id="tools">
<div class="flex mdl-card__supporting-text"> <div>
<button id="ck" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored"> <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 CK
</button> </button>
<p>
Send <code>CK</code> (check) to the repeater, and play when it comes back.
</p>
</div> </div>
<div class="flex mdl-card__supporting-text"> <div class="">
<button id="reset" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored"> 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 Reset
</button> </button>
<p> </div>
<div>
Reset all Vail preferences to default. 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> </div>
<div class="mdl-card mdl-shadow--4dp">
<div class="mdl-card__title"> <div class="field is-horizontal">
<h2 class="mdl-card__title-text">Knobs</h2> <div class="field-label">
<label class="label">
<output for="rx-delay"></output>s
rx delay
</label>
</div> </div>
<div class="mdl-card__supporting-text"> <div class="field-body">
<p> <div class="field">
Iambic Dit length: <div class="control">
<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 <input
id="rx-delay" id="rx-delay"
class="mdl-slider mdl-js-slider"
type="range" type="range"
min="0" min="0"
max="9999" max="10"
value="4000"> value="4"
</p> step="0.1"
<p> list="rx-delays">
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="telegraph-buzzer"> <datalist id="rx-delays">
<input type="checkbox" id="telegraph-buzzer" class="mdl-switch__input"> <option value="0"></option>
<span class="mdl-switch__label">Telegraph sounds</span> <option value="1"></option>
</label> <option value="2" label="2s"></option>
</p> <option value="3"></option>
<p> <option value="4" label="4s"></option>
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="timing-chart"> <option value="5"></option>
<input type="checkbox" id="timing-chart" class="mdl-switch__input"> <option value="6" label="6s"></option>
<span class="mdl-switch__label">Timing chart</span> <option value="7"></option>
</label> <option value="8" label="8s"></option>
</p> <option value="9"></option>
<hr> <option value="10"></option>
<table> </datalist>
<tbody> </div>
<tr> </div>
<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> </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>
<div class="columns is-centered bottom">
<div class="column is-half" id="errors"></div>
</div> </div>
</main>
</body> </body>
</html> </html>
<!-- vim: set noet ts=2 sw=2 : --> <!-- vim: set noet ts=2 sw=2 : -->

View File

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

View File

@ -6,10 +6,8 @@
* advanced than the bug keyer. * advanced than the bug keyer.
*/ */
/** Silent period between words */ import * as RoboKeyer from "./robokeyer.mjs"
const PAUSE_WORD = -7
/** Silent period between letters */
const PAUSE_LETTER = -3
/** Silent period between dits and dash */ /** Silent period between dits and dash */
const PAUSE = -1 const PAUSE = -1
/** Length of a dit */ /** Length of a dit */
@ -34,78 +32,6 @@ const Minute = 60 * Second
/** @type {Duration} */ /** @type {Duration} */
const Hour = 60 * Minute 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. * Queue Set: A Set you can shift and pop.
@ -167,6 +93,20 @@ class StraightKeyer {
this.txRelays = [] 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. * Returns the state of a single transmit relay.
* *
@ -235,7 +175,7 @@ class CootieKeyer extends StraightKeyer {
*/ */
class BugKeyer extends StraightKeyer { class BugKeyer extends StraightKeyer {
KeyNames() { KeyNames() {
return ["·", "Key"] return ["Dit", "Key"]
} }
Reset() { Reset() {
@ -248,15 +188,6 @@ class BugKeyer extends StraightKeyer {
this.keyPressed = [false, false] this.keyPressed = [false, false]
} }
/**
* Set the duration of dit.
*
* @param {Duration} d New dit duration
*/
SetDitDuration(d) {
this.ditDuration = d
}
Key(key, pressed) { Key(key, pressed) {
this.keyPressed[key] = pressed this.keyPressed[key] = pressed
if (key == 0) { if (key == 0) {
@ -302,7 +233,7 @@ class BugKeyer extends StraightKeyer {
*/ */
class ElBugKeyer extends BugKeyer { class ElBugKeyer extends BugKeyer {
KeyNames() { KeyNames() {
return ["· ", ""] return ["Dit", "Dah"]
} }
Reset() { Reset() {
@ -499,281 +430,47 @@ class IambicBKeyer extends IambicKeyer {
} }
} }
/** class KeyaheadKeyer extends ElBugKeyer {
* Keyer class. This handles iambic and straight key input. Reset() {
* super.Reset()
* 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
this.queue = [] this.queue = []
this.pulseTimer = null
} }
pulse() { Key(key, pressed) {
if (this.queue.length == 0) { if (pressed) {
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
}
}
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)
}
}
this.pulseTimer = setTimeout(() => this.pulse(), next * this.intervalDuration)
}
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
}
/**
* 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) this.queue.push(key)
if (key > 0) {
this.queue.push(PAUSE)
} }
this.maybePulse() super.Key(key, pressed)
} }
nextTx() {
let next = this.queue.shift()
if (next != null) {
return next
}
return super.nextTx()
}
}
/** /**
* Enqueue a morse code string (eg "... --- ...") * A dictionary of all available keyers
*
* @param {string} ms String to enqueue
*/ */
EnqueueMorseString(ms) { const Keyers = {
for (let mc of ms) { straight: StraightKeyer,
switch (mc) { cootie: CootieKeyer,
case ".": bug: BugKeyer,
this.Enqueue(DIT) elbug: ElBugKeyer,
break singledot: SingleDotKeyer,
case "-": ultimatic: UltimaticKeyer,
this.Enqueue(DAH) iambic: IambicKeyer,
break iambica: IambicAKeyer,
case " ": iambicb: IambicBKeyer,
this.Enqueue(PAUSE_LETTER) keyahead: KeyaheadKeyer,
break
}
}
}
/** robo: RoboKeyer.Keyer,
* 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)
}
}
}
} }
export { export {
StraightKeyer, Keyers,
CootieKeyer, BugKeyer, ElBugKeyer,
SingleDotKeyer, UltimaticKeyer,
IambicKeyer, IambicAKeyer, IambicBKeyer,
} }

View File

@ -124,11 +124,10 @@ export class Vail {
export class Null { export class Null {
constructor(rx) { constructor(rx) {
this.rx = rx this.rx = rx
this.interval = setInterval(() => this.pulse(), 1 * Second) this.interval = setInterval(() => this.pulse(), 3 * Second)
} }
pulse() { pulse() {
console.log("pulse")
this.rx(0, 0, {note: "local"}) 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,50 +1,54 @@
.navbar-item img {
margin-right: 1em;
}
.key { .key {
width: 100%; width: 95%;
height: 6em; height: 6em;
} }
.wide {
width: 100%;
}
.mashable-area { .mashable-area {
user-select: none; user-select: none;
-webkit-user-select: none; /* 2022-04-26 Safari still needs this */ -webkit-user-select: none; /* 2022-04-26 Safari still needs this */
} }
.center { #recv.rx {
text-align: center; background-color: orange;
} }
.maximize { input[type=range] {
position: absolute; height: 25px;
bottom: 0; margin: 10px 0;
right: 0;
}
.maximized .key {
height: 90vh;
}
.maximized {
width: 90vw;
}
.wide {
width: 100%; width: 100%;
accent-color: #00d1b2;
} }
.hidden { .bottom {
visibility: none; position: fixed;
bottom: 0;
width: 100vw;
} }
#errors { #errors {
color: rgba(127, 0, 0, .54); max-height: 10em;
max-height: 5em;
overflow-y: scroll; overflow-y: scroll;
} }
@keyframes yellow-fade { @keyframes yellow-fade {
0% {background: yellow;} 0% {background-color: orange;}
100% {background: none;} 100% {background-color: default;}
} }
#errors p { #errors p {
margin: 0; background-color: #444;
animation: yellow-fade 2s ease-in 1; color: white;
margin: 0.5em;
padding: 0.2em;
border-radius: 4px;
text-align: center;
animation: yellow-fade 0.3s ease-in 1;
} }
kbd { kbd {
@ -72,78 +76,6 @@ code {
padding: 0.1em; 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 { #charts {
line-height: 0; 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 Buzzer from "./buzzer.mjs"
import * as Inputs from "./inputs.mjs" import * as Inputs from "./inputs.mjs"
import * as Repeaters from "./repeaters.mjs" import * as Repeaters from "./repeaters.mjs"
@ -7,22 +7,20 @@ import * as Chart from "./chart.mjs"
const DefaultRepeater = "General" const DefaultRepeater = "General"
const Millisecond = 1 const Millisecond = 1
const Second = 1000 * Millisecond 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 * @param {string} msg Message to display
*/ */
function toast(msg) { function toast(msg, timeout=4*Second) {
console.info(msg) console.info(msg)
let el = document.querySelector("#snackbar")
if (!el || !el.MaterialSnackbar) { let errors = document.querySelector("#errors")
return let p = errors.appendChild(document.createElement("p"))
} p.textContent = msg
el.MaterialSnackbar.showSnackbar({ setTimeout(() => p.remove(), timeout)
message: msg,
timeout: 2000
})
} }
// iOS kludge // iOS kludge
@ -40,21 +38,16 @@ class VailClient {
this.beginTxTime = null // Time when we began transmitting this.beginTxTime = null // Time when we began transmitting
// Make helpers // Make helpers
this.lamp = new Buzzer.Lamp() this.lamp = new Buzzer.Lamp(document.querySelector("#recv"))
this.buzzer = new Buzzer.ToneBuzzer() this.buzzer = new Buzzer.ToneBuzzer()
this.straightKeyer = new Keyer.StraightKeyer(() => this.beginTx(), () => this.endTx()) this.straightKeyer = new Keyers.straight(() => this.beginTx(), () => this.endTx())
this.keyer = new Keyer.SingleDotKeyer(() => this.beginTx(), () => this.endTx()) this.keyer = new Keyers.straight(() => this.beginTx(), () => this.endTx())
this.roboKeyer = new Keyer.ElBugKeyer(() => this.Buzz(), () => this.Silence()) this.roboKeyer = new Keyers.robo(() => this.Buzz(), () => this.Silence())
// Set up various input methods // Set up various input methods
// Send this as the keyer so we can intercept dit and dah events for charts // Send this as the keyer so we can intercept dit and dah events for charts
this.inputs = Inputs.SetupAll(this) 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 // Maximize button
for (let e of document.querySelectorAll("button.maximize")) { for (let e of document.querySelectorAll("button.maximize")) {
e.addEventListener("click", e => this.maximize(e)) e.addEventListener("click", e => this.maximize(e))
@ -67,39 +60,36 @@ class VailClient {
} }
// Set up inputs // Set up inputs
this.inputInit("#iambic-duration", e => { this.inputInit("#keyer-mode", e => this.setKeyer(e.target.value))
this.keyer.SetDitDuration(e.target.value) this.inputInit("#keyer-rate", e => {
this.roboKeyer.SetDitDuration(e.target.value) 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)) { for (let i of Object.values(this.inputs)) {
i.SetDitDuration(e.target.value) i.SetDitDuration(ditDuration)
} }
}) })
this.inputInit("#rx-delay", e => { this.inputInit("#rx-delay", e => {
this.rxDelay = Number(e.target.value) 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.inputInit("#telegraph-buzzer", e => {
this.setTelegraphBuzzer(e.target.checked) this.setTelegraphBuzzer(e.target.checked)
}) })
this.inputInit("#timing-chart", e => { this.inputInit("#notes")
this.setTimingCharts(e.target.checked)
})
// Fill in the name of our repeater // Fill in the name of our repeater
document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim())) document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim()))
window.addEventListener("hashchange", () => this.hashchange()) window.addEventListener("hashchange", () => this.hashchange())
this.hashchange() this.hashchange()
this.setTimingCharts(true)
// Turn off the "muted" symbol when we can start making noise // Turn off the "muted" symbol when we can start making noise
Buzzer.Ready() Buzzer.Ready()
.then(() => { .then(() => {
console.log("Audio context ready") 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) { Straight(down) {
this.straightKeyer.Key(0, 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) { Key(key, down) {
this.keyer.Key(0, down) this.keyer.Key(key, down)
if (this.ditChart) this.ditChart.Set(down?1:0) if (this.keyCharts) this.keyCharts[key].Set(down?1:0)
} }
/** setKeyer(keyerName) {
* Dah key change (keyer shim) let newKeyerClass = Keyers[keyerName]
* if (!newKeyerClass) {
* @param down If the key has been depressed console.error("Keyer not found", keyerName)
*/ return
Dah(down) { }
this.keyer.Key(1, down) let newKeyer = new newKeyerClass(() => this.beginTx(), () => this.endTx())
if (this.dahChart) this.dahChart.Set(down?1:0) 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() { Buzz() {
@ -196,14 +195,15 @@ class VailClient {
let chartsContainer = document.querySelector("#charts") let chartsContainer = document.querySelector("#charts")
if (enable) { if (enable) {
chartsContainer.classList.remove("hidden") chartsContainer.classList.remove("hidden")
this.ditChart = Chart.FromSelector("#ditChart") this.keyCharts = [
this.dahChart = Chart.FromSelector("#dahChart") Chart.FromSelector("#key0Chart"),
Chart.FromSelector("#key1Chart")
]
this.txChart = Chart.FromSelector("#txChart") this.txChart = Chart.FromSelector("#txChart")
this.rxChart = Chart.FromSelector("#rxChart") this.rxChart = Chart.FromSelector("#rxChart")
} else { } else {
chartsContainer.classList.add("hidden") chartsContainer.classList.add("hidden")
this.ditChart = null this.keyCharts = []
this.dahChart = null
this.txChart = null this.txChart = null
this.rxChart = null this.rxChart = null
} }
@ -232,19 +232,6 @@ class VailClient {
this.setRepeater(decodeURIComponent(hashParts[1] || "")) 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. * Connect to a repeater by name.
* *
@ -326,8 +313,8 @@ class VailClient {
element.value = storedValue element.value = storedValue
element.checked = (storedValue == "on") element.checked = (storedValue == "on")
} }
let outputElement = document.querySelector(selector + "-value") let id = element.id
let outputWpmElement = document.querySelector(selector + "-wpm") let outputElement = document.querySelector(`[for="${id}"]`)
element.addEventListener("input", e => { element.addEventListener("input", e => {
let value = element.value let value = element.value
@ -339,9 +326,6 @@ class VailClient {
if (outputElement) { if (outputElement) {
outputElement.value = value outputElement.value = value
} }
if (outputWpmElement) {
outputWpmElement.value = (1200 / value).toFixed(1)
}
if (callback) { if (callback) {
callback(e) callback(e)
} }
@ -388,7 +372,7 @@ class VailClient {
let longestRxDuration = this.rxDurations.reduce((a,b) => Math.max(a,b)) let longestRxDuration = this.rxDurations.reduce((a,b) => Math.max(a,b))
let suggestedDelay = ((averageLag + longestRxDuration) * 1.2).toFixed(0) 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("#lag-value", averageLag)
this.updateReading("#longest-rx-value", longestRxDuration) this.updateReading("#longest-rx-value", longestRxDuration)
this.updateReading("#suggested-delay-value", suggestedDelay) this.updateReading("#suggested-delay-value", suggestedDelay)
@ -457,7 +441,7 @@ class VailClient {
} }
} }
function vailInit() { function init() {
if (navigator.serviceWorker) { if (navigator.serviceWorker) {
navigator.serviceWorker.register("sw.js") navigator.serviceWorker.register("sw.js")
} }
@ -471,9 +455,9 @@ function vailInit() {
if (document.readyState === "loading") { if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", vailInit) document.addEventListener("DOMContentLoaded", init)
} else { } else {
vailInit() init()
} }
// vim: noet sw=2 ts=2 // vim: noet sw=2 ts=2