vail/static/vail.mjs

353 lines
8.9 KiB
JavaScript
Raw Normal View History

2021-04-26 16:09:03 -06:00
import * as Morse from "./morse.mjs"
2021-04-27 17:30:16 -06:00
import * as Inputs from "./inputs.mjs"
import * as Repeaters from "./repeaters.mjs"
2020-04-09 23:09:33 -06:00
2021-04-27 12:42:06 -06:00
const DefaultRepeater = "General Chaos"
2021-04-28 14:28:59 -06:00
const Millisecond = 1
const Second = 1000 * Millisecond
const Minute = 60 * Second
2021-04-27 12:42:06 -06:00
2021-04-27 17:30:16 -06:00
/**
* Pop up a message, using an MDL snackbar.
*
* @param {string} msg Message to display
*/
function toast(msg) {
let el = document.querySelector("#snackbar")
if (!el || !el.MaterialSnackbar) {
console.warn(msg)
return
}
el.MaterialSnackbar.showSnackbar({
message: msg,
timeout: 2000
})
}
class VailClient {
2020-05-01 15:07:09 -06:00
constructor() {
this.sent = []
this.lagTimes = [0]
this.rxDurations = [0]
2021-04-28 10:17:23 -06:00
this.clockOffset = null // How badly our clock is off of the server's
2020-05-01 15:07:09 -06:00
this.rxDelay = 0 // Milliseconds to add to incoming timestamps
this.beginTxTime = null // Time when we began transmitting
2020-06-29 19:28:51 -06:00
this.debug = localStorage.debug
2020-05-01 15:07:09 -06:00
2021-04-27 12:42:06 -06:00
// Redirect old URLs
if (window.location.search) {
let me = new URL(location)
let repeater = me.searchParams.get("repeater")
me.search = ""
2021-04-27 18:37:25 -06:00
me.hash = decodeURIComponent(repeater)
2021-04-27 12:42:06 -06:00
window.location = me
}
2021-04-27 17:30:16 -06:00
// Make helpers
this.buzzer = new Morse.Buzzer()
this.keyer = new Morse.Keyer(() => this.beginTx(), () => this.endTx())
2021-04-27 18:37:25 -06:00
this.roboKeyer = new Morse.Keyer(() => this.buzzer.Buzz(), () => this.buzzer.Silence())
2021-04-27 17:30:16 -06:00
// Set up various input methods
this.inputs = Inputs.SetupAll(this.keyer)
2021-01-18 14:32:48 -07:00
// 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))
}
2021-04-27 17:30:16 -06:00
// Maximize button
for (let e of document.querySelectorAll("button.maximize")) {
e.addEventListener("click", e => this.maximize(e))
}
2021-04-27 17:30:16 -06:00
for (let e of document.querySelectorAll("#ck")) {
e.addEventListener("click", e => this.test())
}
// Set up inputs
this.inputInit("#iambic-duration", e => {
2021-04-27 17:30:16 -06:00
this.keyer.SetIntervalDuration(e.target.value)
2021-04-27 18:37:25 -06:00
this.roboKeyer.SetIntervalDuration(e.target.value)
2021-04-27 17:30:16 -06:00
})
this.inputInit("#rx-delay", e => {
2021-04-27 17:30:16 -06:00
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)
})
2021-04-28 14:28:59 -06:00
2021-04-27 17:30:16 -06:00
// Fill in the name of our repeater
let repeaterElement = document.querySelector("#repeater").addEventListener("change", e => this.setRepeater(e.target.value.trim()))
2021-04-28 14:28:59 -06:00
window.addEventListener("hashchange", () => this.hashchange())
this.hashchange()
}
/**
* Called when the hash part of the URL has changed.
*/
hashchange() {
let hashParts = window.location.hash.split("#")
this.setRepeater(decodeURIComponent(hashParts[1] || ""))
2020-05-05 20:10:16 -06:00
}
2021-01-18 14:32:48 -07:00
/**
* VBand: Called when something happens to change what a single key does
*
* @param {Event} event What caused this
*/
singlekeyChange(event) {
2022-04-21 11:17:36 -06:00
for (let e of event.composedPath()) {
if (e.dataset && e.dataset.singlekey) {
this.inputs.Keyboard.iambic = (e.dataset.singlekey == "iambic")
}
}
}
2021-04-27 17:30:16 -06:00
/**
* Connect to a repeater by name.
*
2021-04-28 14:28:59 -06:00
* This does some switching logic to provide multiple types of repeaters,
* like the Fortunes repeaters.
2021-04-27 17:30:16 -06:00
*
* @param {string} name Repeater name
*/
2021-04-27 12:42:06 -06:00
setRepeater(name) {
2021-04-27 13:01:46 -06:00
if (!name || (name == "")) {
2021-04-27 18:37:25 -06:00
name = DefaultRepeater
2021-04-27 13:01:46 -06:00
}
2021-04-27 12:42:06 -06:00
this.repeaterName = name
2021-04-27 13:20:24 -06:00
// Set value of repeater element
let repeaterElement = document.querySelector("#repeater")
let paps = repeaterElement.parentElement
if (paps.MaterialTextfield) {
paps.MaterialTextfield.change(name)
} else {
repeaterElement.value = name
}
2021-04-27 12:42:06 -06:00
// Set window URL
2021-04-28 14:28:59 -06:00
let prevHash = window.location.hash
window.location.hash = (name == DefaultRepeater) ? "" : name
if (window.location.hash != prevHash) {
// We're going to get a hashchange event, which will re-run this method
return
2021-04-27 12:42:06 -06:00
}
2021-04-27 13:20:24 -06:00
2021-04-27 17:30:16 -06:00
if (this.repeater) {
this.repeater.Close()
}
2021-04-27 18:37:25 -06:00
let rx = (w,d,s) => this.receive(w,d,s)
2021-04-28 14:28:59 -06:00
// If there's a number in the name, store that for potential later use
let numberMatch = name.match(/[0-9]+/)
let number = 0
if (numberMatch) {
number = Number(numberMatch[0])
}
2021-04-27 18:37:25 -06:00
if (name.startsWith("Fortunes")) {
2021-04-28 14:28:59 -06:00
this.roboKeyer.SetPauseMultiplier(number || 1)
2021-04-27 18:37:25 -06:00
this.repeater = new Repeaters.Fortune(rx, this.roboKeyer)
2021-04-28 14:28:59 -06:00
} else if (name.startsWith("Echo")) {
let delayElement = document.querySelector("#rx-delay")
delayElement.value = (number || 2) * Second
delayElement.dispatchEvent(new Event("input"))
this.repeater = new Repeaters.Echo(rx)
} else if (name == "Null") {
this.repeater = new Repeaters.Null(rx)
2021-04-27 18:37:25 -06:00
} else {
2021-04-28 14:28:59 -06:00
this.repeater = new Repeaters.Vail(rx, name)
2021-04-27 18:37:25 -06:00
}
2021-04-27 12:42:06 -06:00
2021-04-27 17:30:16 -06:00
toast(`Now using repeater: ${name}`)
2020-05-01 15:07:09 -06:00
}
2021-04-27 17:30:16 -06:00
/**
* Set up an input.
2021-04-27 17:30:16 -06:00
*
* This reads any previously saved value and sets the input value to that.
* When the input is updated, it saves the value it's updated to,
2021-04-27 17:30:16 -06:00
* and calls the provided callback with the new value.
*
* @param {string} selector CSS path to the element
* @param {function} callback Callback to call with any new value that is set
*/
inputInit(selector, callback) {
2020-05-01 15:07:09 -06:00
let element = document.querySelector(selector)
2021-04-27 17:30:16 -06:00
if (!element) {
return
}
2020-05-01 15:07:09 -06:00
let storedValue = localStorage[element.id]
if (storedValue != null) {
2020-05-01 15:07:09 -06:00
element.value = storedValue
element.checked = JSON.parse(storedValue)
2020-05-01 15:07:09 -06:00
}
let outputElement = document.querySelector(selector + "-value")
2022-04-18 15:37:17 -06:00
let outputWpmElement = document.querySelector(selector + "-wpm")
2020-05-01 15:07:09 -06:00
element.addEventListener("input", e => {
let value = element.value
if (element.hasAttribute("checked")) {
value = element.checked
}
localStorage[element.id] = value
2020-05-01 15:07:09 -06:00
if (outputElement) {
outputElement.value = value
2020-05-01 15:07:09 -06:00
}
2022-04-18 15:37:17 -06:00
if (outputWpmElement) {
outputWpmElement.value = (1200 / value).toFixed(1)
2022-04-18 15:37:17 -06:00
}
2021-04-27 17:30:16 -06:00
if (callback) {
callback(e)
}
2020-05-01 15:07:09 -06:00
})
element.dispatchEvent(new Event("input"))
}
2021-01-18 14:32:48 -07:00
2021-04-27 17:30:16 -06:00
/**
* Make an error sound and pop up a message
*
* @param {string} msg The message to pop up
*/
2020-05-01 15:07:09 -06:00
error(msg) {
2021-04-27 17:30:16 -06:00
toast(msg)
2020-05-01 15:07:09 -06:00
this.buzzer.ErrorTone()
}
2021-04-27 17:30:16 -06:00
/**
* Start the side tone buzzer.
*/
2020-05-01 15:07:09 -06:00
beginTx() {
this.beginTxTime = Date.now()
this.buzzer.Buzz(true)
}
2021-04-27 17:30:16 -06:00
/**
* Stop the side tone buzzer, and send out how long it was active.
*/
2020-05-01 15:07:09 -06:00
endTx() {
let endTxTime = Date.now()
let duration = endTxTime - this.beginTxTime
this.buzzer.Silence(true)
2021-04-27 17:30:16 -06:00
this.repeater.Transmit(this.beginTxTime, duration)
2020-05-01 15:07:09 -06:00
this.beginTxTime = null
}
2021-04-27 17:30:16 -06:00
/**
* Called by a repeater class when there's something received.
*
* @param {number} when When to play the tone
* @param {number} duration How long to play the tone
* @param {dict} stats Stuff the repeater class would like us to know about
*/
receive(when, duration, stats) {
2021-04-28 10:17:23 -06:00
this.clockOffset = stats.clockOffset || "?"
2020-05-01 15:07:09 -06:00
let now = Date.now()
2021-04-27 17:30:16 -06:00
when += this.rxDelay
2020-05-01 15:07:09 -06:00
2021-04-27 17:30:16 -06:00
if (duration > 0) {
if (when < now) {
this.error("Packet requested playback " + (now - when) + "ms in the past. Increase receive delay!")
return
2020-05-01 15:07:09 -06:00
}
2021-04-27 17:30:16 -06:00
this.buzzer.BuzzDuration(false, when, duration)
2021-01-18 14:32:48 -07:00
2021-04-27 17:30:16 -06:00
this.rxDurations.unshift(duration)
this.rxDurations.splice(20, 2)
2020-05-01 15:07:09 -06:00
}
2021-04-27 17:30:16 -06:00
let averageLag = (stats.averageLag || 0).toFixed(2)
let longestRxDuration = this.rxDurations.reduce((a,b) => Math.max(a,b))
let suggestedDelay = ((averageLag + longestRxDuration) * 1.2).toFixed(0)
2021-04-28 10:17:23 -06:00
this.updateReading("#note", stats.note || "")
2021-04-27 17:30:16 -06:00
this.updateReading("#lag-value", averageLag)
this.updateReading("#longest-rx-value", longestRxDuration)
this.updateReading("#suggested-delay-value", suggestedDelay)
this.updateReading("#clock-off-value", this.clockOffset)
2020-05-01 15:07:09 -06:00
}
2021-04-27 17:30:16 -06:00
/**
* Update an element with a value, if that element exists
*
* @param {string} selector CSS path to the element
* @param value Value to set
*/
updateReading(selector, value) {
let e = document.querySelector(selector)
if (e) {
e.value = value
2020-05-19 08:21:33 -06:00
}
}
2021-01-18 14:32:48 -07:00
2021-04-27 17:30:16 -06:00
/**
* Maximize/minimize a card
*
* @param e Event
*/
maximize(e) {
let element = e.target
while (!element.classList.contains("mdl-card")) {
element = element.parentElement
if (!element) {
console.log("Maximize button: couldn't find parent card")
return
2020-05-21 20:32:23 -06:00
}
2020-05-19 08:21:33 -06:00
}
2021-04-27 17:30:16 -06:00
element.classList.toggle("maximized")
console.log(element)
2020-05-19 08:21:33 -06:00
}
2020-05-01 15:07:09 -06:00
/**
2021-04-27 17:30:16 -06:00
* Send "CK" to server, and don't squelch the echo
2020-05-01 15:07:09 -06:00
*/
2021-04-27 17:30:16 -06:00
test() {
let when = Date.now()
2020-05-01 15:07:09 -06:00
let dit = Number(document.querySelector("#iambic-duration-value").value)
let dah = dit * 3
let s = dit
2021-04-27 17:30:16 -06:00
let message = [
2020-05-01 15:07:09 -06:00
dah, s, dit, s, dah, s, dit,
s * 3,
dah, s, dit, s, dah
]
2021-04-27 17:30:16 -06:00
this.repeater.Transmit(when, 0) // Get round-trip time
for (let i in message) {
let duration = message[i]
if (i % 2 == 0) {
this.repeater.Transmit(when, duration, false)
2020-05-19 08:21:33 -06:00
}
2021-04-27 17:30:16 -06:00
when += duration
2020-05-19 08:21:33 -06:00
}
}
2020-04-10 08:59:15 -06:00
}
function vailInit() {
2020-05-26 20:52:48 -06:00
if (navigator.serviceWorker) {
navigator.serviceWorker.register("sw.js")
}
try {
2021-04-27 17:30:16 -06:00
window.app = new VailClient()
} catch (err) {
console.log(err)
2021-04-27 17:30:16 -06:00
toast(err)
}
2020-04-09 23:09:33 -06:00
}
if (document.readyState === "loading") {
2020-05-01 15:07:09 -06:00
document.addEventListener("DOMContentLoaded", vailInit)
2020-04-09 23:09:33 -06:00
} else {
2020-05-01 15:07:09 -06:00
vailInit()
2020-04-09 23:09:33 -06:00
}
// vim: noet sw=2 ts=2