vail/static/scripts/vail.mjs

529 lines
13 KiB
JavaScript
Raw Normal View History

2022-05-22 21:37:36 -06:00
import * as Keyers from "./keyers.mjs"
import * as Outputs from "./outputs.mjs"
2021-04-27 17:30:16 -06:00
import * as Inputs from "./inputs.mjs"
import * as Repeaters from "./repeaters.mjs"
import * as Chart from "./chart.mjs"
2022-06-06 21:32:04 -06:00
import * as I18n from "./i18n.mjs"
2023-01-17 12:25:20 -07:00
import * as time from "./time.mjs"
2023-01-16 17:29:40 -07:00
import * as Music from "./music.mjs"
import * as Icon from "./icon.mjs"
2023-01-29 16:00:59 -07:00
import * as Noise from "./noise.mjs"
2020-04-09 23:09:33 -06:00
const DefaultRepeater = "General"
2021-04-27 12:42:06 -06:00
2023-01-16 17:29:40 -07:00
console.warn("Chrome will now complain about an AudioContext not being allowed to start. This is normal, and there is no way to make Chrome stop complaining about this.")
const globalAudioContext = new AudioContext({
latencyHint: "interactive",
})
2023-03-18 12:50:34 -06:00
function initLog(message) {
for (let modal of document.querySelectorAll(".modal.init")) {
if (!message) {
modal.remove()
} else {
let ul = modal.querySelector("ul")
while (ul.childNodes.length > 5) {
ul.firstChild.remove()
}
let li = ul.appendChild(document.createElement("li"))
li.textContent = message
}
}
}
2021-04-27 17:30:16 -06:00
/**
2022-05-22 21:37:36 -06:00
* Pop up a message, using an notification.
2021-04-27 17:30:16 -06:00
*
* @param {string} msg Message to display
*/
2023-01-17 12:25:20 -07:00
function toast(msg, timeout=4*time.Second) {
console.info(msg)
2022-05-14 18:51:05 -06:00
let errors = document.querySelector("#errors")
let p = errors.appendChild(document.createElement("p"))
p.textContent = msg
setTimeout(() => p.remove(), timeout)
2021-04-27 17:30:16 -06:00
}
2022-05-08 11:33:25 -06:00
// iOS kludge
if (!window.AudioContext) {
window.AudioContext = window.webkitAudioContext
}
2021-04-27 17:30:16 -06:00
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
2023-01-17 12:25:20 -07:00
this.rxDelay = 0 * time.Millisecond // Time to add to incoming timestamps
2020-05-01 15:07:09 -06:00
this.beginTxTime = null // Time when we began transmitting
2023-03-18 12:50:34 -06:00
initLog("Initializing outputs")
2023-01-16 17:29:40 -07:00
this.outputs = new Outputs.Collection(globalAudioContext)
this.outputs.connect(globalAudioContext.destination)
2023-01-29 16:00:59 -07:00
2023-03-18 12:50:34 -06:00
initLog("Starting up noise")
2023-01-29 16:00:59 -07:00
this.noise = new Noise.Noise(globalAudioContext)
this.noise.connect(globalAudioContext.destination)
2023-03-18 12:50:34 -06:00
initLog("Setting app icon name")
this.icon = new Icon.Icon()
2022-05-22 21:37:36 -06:00
2023-03-18 12:50:34 -06:00
initLog("Initializing keyers")
2022-05-22 21:37:36 -06:00
this.straightKeyer = new Keyers.Keyers.straight(this)
this.keyer = new Keyers.Keyers.straight(this)
this.roboKeyer = new Keyers.Keyers.robo(() => this.Buzz(), () => this.Silence())
2021-04-27 17:30:16 -06:00
// Send this as the keyer so we can intercept dit and dah events for charts
2023-03-18 12:50:34 -06:00
initLog("Setting up input methods")
2022-05-22 21:37:36 -06:00
this.inputs = new Inputs.Collection(this)
2021-01-18 14:32:48 -07:00
2023-03-18 12:50:34 -06:00
initLog("Listening on AudioContext")
2023-01-16 17:29:40 -07:00
document.body.addEventListener(
"click",
e => globalAudioContext.resume(),
true,
)
2023-03-18 12:50:34 -06:00
initLog('Setting up maximize button')
2021-04-27 17:30:16 -06:00
for (let e of document.querySelectorAll("button.maximize")) {
e.addEventListener("click", e => this.maximize(e))
}
for (let e of document.querySelectorAll("#reset")) {
e.addEventListener("click", e => this.reset())
}
2021-04-27 17:30:16 -06:00
2023-03-18 12:50:34 -06:00
initLog("Initializing knobs")
2022-05-14 18:51:05 -06:00
this.inputInit("#keyer-mode", e => this.setKeyer(e.target.value))
this.inputInit("#keyer-rate", e => {
let rate = e.target.value
2023-01-17 12:25:20 -07:00
this.ditDuration = Math.round(time.Minute / rate / 50)
2022-05-14 21:17:44 -06:00
for (let e of document.querySelectorAll("[data-fill='keyer-ms']")) {
e.textContent = this.ditDuration
2022-05-14 21:17:44 -06:00
}
this.keyer.SetDitDuration(this.ditDuration)
this.roboKeyer.SetDitDuration(this.ditDuration)
2022-05-22 21:37:36 -06:00
this.inputs.SetDitDuration(this.ditDuration)
2021-04-27 17:30:16 -06:00
})
this.inputInit("#rx-delay", e => {
2023-01-17 12:25:20 -07:00
this.rxDelay = e.target.value * time.Second
2021-04-27 17:30:16 -06:00
})
2023-01-16 17:29:40 -07:00
this.inputInit("#masterGain", e => {
this.outputs.SetGain(e.target.value / 100)
})
2023-01-29 16:00:59 -07:00
this.inputInit("#noiseGain", e => {
this.noise.SetGain(e.target.value / 100)
})
2023-01-16 18:42:07 -07:00
let toneTransform = {
note: Music.MIDINoteName,
freq: Music.MIDINoteFrequency,
}
2023-01-16 17:29:40 -07:00
this.inputInit(
"#rx-tone",
2023-02-25 18:12:17 -07:00
e => {
this.noise.SetNoiseFrequency(1, Music.MIDINoteFrequency(e.target.value))
this.outputs.SetMIDINote(false, e.target.value)
},
2023-01-16 18:42:07 -07:00
toneTransform,
2023-01-16 17:29:40 -07:00
)
this.inputInit(
"#tx-tone",
e => this.outputs.SetMIDINote(true, e.target.value),
2023-01-16 18:42:07 -07:00
toneTransform,
2023-01-16 17:29:40 -07:00
)
2022-04-21 18:31:33 -06:00
this.inputInit("#telegraph-buzzer", e => {
this.setTelegraphBuzzer(e.target.checked)
})
2022-05-14 18:51:05 -06:00
this.inputInit("#notes")
2023-03-18 12:50:34 -06:00
initLog("Filling in repeater name")
2022-04-21 18:31:33 -06:00
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()
2023-03-18 12:50:34 -06:00
initLog("Starting timing charts")
2022-05-14 18:51:05 -06:00
this.setTimingCharts(true)
2023-03-18 12:50:34 -06:00
initLog("Setting up mute icon")
2023-01-16 17:29:40 -07:00
globalAudioContext.resume()
.then(() => {
2023-01-16 17:29:40 -07:00
for (let e of document.querySelectorAll(".muted")) {
e.classList.add("is-hidden")
}
})
2021-04-28 14:28:59 -06:00
}
/**
* Straight key change (keyer shim)
*
* @param down If key has been depressed
*/
Straight(down) {
2022-05-08 11:33:25 -06:00
this.straightKeyer.Key(0, down)
}
/**
2022-05-14 18:51:05 -06:00
* Key/paddle change
*
2022-05-14 18:51:05 -06:00
* @param {Number} key Key which was pressed
* @param {Boolean} down True if key was pressed
*/
2022-05-14 18:51:05 -06:00
Key(key, down) {
this.keyer.Key(key, down)
if (this.keyCharts) this.keyCharts[key].Set(down?1:0)
}
2022-05-14 18:51:05 -06:00
setKeyer(keyerName) {
2022-05-22 21:37:36 -06:00
let newKeyerClass = Keyers.Keyers[keyerName]
let newKeyerNumber = Keyers.Numbers[keyerName]
2022-05-14 18:51:05 -06:00
if (!newKeyerClass) {
console.error("Keyer not found", keyerName)
return
}
2022-05-22 21:37:36 -06:00
let newKeyer = new newKeyerClass(this)
2022-05-14 18:51:05 -06:00
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
2022-05-22 21:37:36 -06:00
this.inputs.SetKeyerMode(newKeyerNumber)
2022-05-14 18:51:05 -06:00
document.querySelector("#keyer-rate").dispatchEvent(new Event("input"))
}
Buzz() {
2022-05-22 21:37:36 -06:00
this.outputs.Buzz(false)
this.icon.Set("rx")
if (this.rxChart) this.rxChart.Set(1)
}
Silence() {
2022-05-22 21:37:36 -06:00
this.outputs.Silence()
if (this.rxChart) this.rxChart.Set(0)
}
BuzzDuration(tx, when, duration) {
2022-05-22 21:37:36 -06:00
this.outputs.BuzzDuration(tx, when, duration)
let chart
if (tx) {
chart = this.txChart
} else {
chart = this.rxChart
this.icon.SetAt("rx", when)
}
if (chart) {
chart.SetAt(1, when)
2022-04-24 19:42:57 -06:00
chart.SetAt(0, when+duration)
}
}
/**
* Start the side tone buzzer.
*
* Called from the keyer.
*/
2022-05-22 21:37:36 -06:00
BeginTx() {
this.beginTxTime = Date.now()
2022-05-22 21:37:36 -06:00
this.outputs.Buzz(true)
if (this.txChart) this.txChart.Set(1)
2022-05-22 21:37:36 -06:00
}
/**
* Stop the side tone buzzer, and send out how long it was active.
*
* Called from the keyer
*/
2022-05-22 21:37:36 -06:00
EndTx() {
if (!this.beginTxTime) {
return
}
let endTxTime = Date.now()
let duration = endTxTime - this.beginTxTime
2022-05-22 21:37:36 -06:00
this.outputs.Silence(true)
this.repeater.Transmit(this.beginTxTime, duration)
this.beginTxTime = null
if (this.txChart) this.txChart.Set(0)
}
2021-04-28 14:28:59 -06:00
2022-04-24 17:13:56 -06:00
/**
* Toggle timing charts.
*
* @param enable True to enable charts
*/
setTimingCharts(enable) {
// XXX: UI code shouldn't be in the Keyer class.
// Actually, the charts calls should be in vail
let chartsContainer = document.querySelector("#charts")
2022-05-14 21:17:44 -06:00
if (!chartsContainer) {
return
}
2022-04-24 17:13:56 -06:00
if (enable) {
chartsContainer.classList.remove("hidden")
2022-05-14 18:51:05 -06:00
this.keyCharts = [
Chart.FromSelector("#key0Chart"),
Chart.FromSelector("#key1Chart")
]
this.txChart = Chart.FromSelector("#txChart")
this.rxChart = Chart.FromSelector("#rxChart")
2022-04-24 17:13:56 -06:00
} else {
chartsContainer.classList.add("hidden")
2022-05-14 18:51:05 -06:00
this.keyCharts = []
this.txChart = null
this.rxChart = null
2022-04-24 17:13:56 -06:00
}
}
2022-04-21 18:31:33 -06:00
/**
* Toggle the clicktastic buzzer, instead of the beeptastic one.
*
* @param {bool} enable true to enable clicky buzzer
*/
setTelegraphBuzzer(enable) {
if (enable) {
2022-05-22 21:37:36 -06:00
this.outputs.SetAudioType("telegraph")
2022-04-23 21:22:38 -06:00
toast("Telegraphs only make sound when receiving!")
2022-04-21 18:31:33 -06:00
} else {
2022-05-22 21:37:36 -06:00
this.outputs.SetAudioType()
2022-04-21 18:31:33 -06:00
}
}
2021-04-28 14:28:59 -06:00
/**
* 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
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
this.Silence()
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])
}
2022-06-06 16:52:22 -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")) {
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
}
2020-05-01 15:07:09 -06:00
}
2021-04-27 17:30:16 -06:00
/**
* Set up an HTML input element.
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
2023-01-16 18:42:07 -07:00
* @param {Object.<string, function>} transform Transform functions
2021-04-27 17:30:16 -06:00
*/
2023-01-16 18:42:07 -07:00
inputInit(selector, callback, transform={}) {
2020-05-01 15:07:09 -06:00
let element = document.querySelector(selector)
2021-04-27 17:30:16 -06:00
if (!element) {
2022-04-24 17:13:56 -06:00
console.warn("Unable to find an input to init", selector)
2021-04-27 17:30:16 -06:00
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
2022-04-21 18:31:33 -06:00
element.checked = (storedValue == "on")
2020-05-01 15:07:09 -06:00
}
2022-05-14 18:51:05 -06:00
let id = element.id
2023-01-16 18:42:07 -07:00
let outputElements = document.querySelectorAll(`[for="${id}"]`)
2020-05-01 15:07:09 -06:00
element.addEventListener("input", e => {
let value = element.value
if (element.type == "checkbox") {
value = element.checked?"on":"off"
}
localStorage[element.id] = value
2023-01-16 18:42:07 -07:00
for (let e of outputElements) {
if (e.dataset.transform) {
let tf = transform[e.dataset.transform]
e.value = tf(value)
} else {
e.value = value
}
2020-05-01 15:07:09 -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)
2022-05-22 21:37:36 -06:00
this.outputs.Error()
2020-05-01 15:07:09 -06:00
}
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) {
2022-04-24 19:58:24 -06:00
console.warn("Too old", when, duration)
2021-04-27 17:30:16 -06:00
this.error("Packet requested playback " + (now - when) + "ms in the past. Increase receive delay!")
return
2020-05-01 15:07:09 -06:00
}
this.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
}
2022-06-06 16:52:22 -06:00
if (stats.notice) {
toast(stats.notice)
}
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)
2022-06-06 10:55:11 -06:00
if (stats.connected !== undefined) {
this.outputs.SetConnected(stats.connected)
}
2022-06-06 13:49:52 -06:00
this.updateReading("#note", stats.note || stats.clients || "😎")
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
}
/** Reset to factory defaults */
reset() {
localStorage.clear()
location.reload()
}
2020-04-10 08:59:15 -06:00
}
2023-03-18 12:50:34 -06:00
async function init() {
initLog("Starting service worker")
2020-05-26 20:52:48 -06:00
if (navigator.serviceWorker) {
2023-01-17 12:25:20 -07:00
navigator.serviceWorker.register("scripts/sw.js")
2020-05-26 20:52:48 -06:00
}
2023-03-18 12:50:34 -06:00
initLog("Setting up internationalization")
await I18n.Setup()
initLog("Creating client")
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)
}
2023-03-18 12:50:34 -06:00
initLog(false)
2020-04-09 23:09:33 -06:00
}
if (document.readyState === "loading") {
2022-05-14 18:51:05 -06:00
document.addEventListener("DOMContentLoaded", init)
2020-04-09 23:09:33 -06:00
} else {
2022-05-14 18:51:05 -06:00
init()
2020-04-09 23:09:33 -06:00
}
// vim: noet sw=2 ts=2