refactor: extract typed frontend formatters
This commit is contained in:
@@ -191,6 +191,80 @@
|
|||||||
if (response.status !== 404 && !response.ok) throw new Error("Logout failed");
|
if (response.status !== 404 && !response.ok) throw new Error("Logout failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/core/format.ts
|
||||||
|
function formatDuration(milliseconds) {
|
||||||
|
const seconds = Math.floor(milliseconds / 1e3);
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor(seconds % 86400 / 3600);
|
||||||
|
const minutes = Math.floor(seconds % 3600 / 60);
|
||||||
|
const remainder = seconds % 60;
|
||||||
|
const parts = [];
|
||||||
|
if (days > 0) parts.push(`${days}d`);
|
||||||
|
if (hours > 0 || days > 0) parts.push(`${hours}h`);
|
||||||
|
parts.push(`${minutes}m`, `${remainder}s`);
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
|
function formatFrequency(frequencyHz) {
|
||||||
|
if (!Number.isFinite(frequencyHz)) return "--";
|
||||||
|
if (frequencyHz >= 1e9) return `${(frequencyHz / 1e9).toFixed(3)} GHz`;
|
||||||
|
if (frequencyHz >= 1e7) return `${(frequencyHz / 1e6).toFixed(3)} MHz`;
|
||||||
|
return `${(frequencyHz / 1e3).toFixed(1)} kHz`;
|
||||||
|
}
|
||||||
|
function formatFrequencyForStep(frequencyHz, stepHz) {
|
||||||
|
if (!Number.isFinite(frequencyHz)) return "--";
|
||||||
|
if (stepHz >= 1e6) return (frequencyHz / 1e6).toFixed(6);
|
||||||
|
if (stepHz >= 1e3) return (frequencyHz / 1e3).toFixed(3);
|
||||||
|
if (stepHz >= 1) return String(Math.round(frequencyHz));
|
||||||
|
return formatFrequency(frequencyHz);
|
||||||
|
}
|
||||||
|
function formatFrequencyForHumans(frequencyHz) {
|
||||||
|
if (!Number.isFinite(frequencyHz)) return "--";
|
||||||
|
if (frequencyHz >= 1e9) return `${(frequencyHz / 1e9).toFixed(3)} GHz`;
|
||||||
|
if (frequencyHz >= 1e6) return `${(frequencyHz / 1e6).toFixed(3)} MHz`;
|
||||||
|
if (frequencyHz >= 1e3) return `${(frequencyHz / 1e3).toFixed(3)} kHz`;
|
||||||
|
return `${Math.round(frequencyHz)} Hz`;
|
||||||
|
}
|
||||||
|
function formatWavelength(frequencyHz) {
|
||||||
|
if (!Number.isFinite(frequencyHz) || frequencyHz <= 0) return "--";
|
||||||
|
const meters = 299792458 / frequencyHz;
|
||||||
|
return meters >= 1 ? `${Math.round(meters)} m` : `${Math.round(meters * 100)} cm`;
|
||||||
|
}
|
||||||
|
function parseFrequencyInput(value, defaultStepHz, mode) {
|
||||||
|
if (!value) return null;
|
||||||
|
const match = /^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/.exec(value.trim().toLowerCase());
|
||||||
|
if (!match?.[1]) return null;
|
||||||
|
const rawNumber = match[1];
|
||||||
|
let frequency = Number.parseFloat(rawNumber.replace(",", "."));
|
||||||
|
const unit = match[2] ?? "";
|
||||||
|
if (Number.isNaN(frequency)) return null;
|
||||||
|
if (unit.startsWith("gh") || unit === "g") frequency *= 1e9;
|
||||||
|
else if (unit.startsWith("mh") || unit === "m") frequency *= 1e6;
|
||||||
|
else if (unit.startsWith("kh") || unit === "k") frequency *= 1e3;
|
||||||
|
else if (!unit) {
|
||||||
|
const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(",");
|
||||||
|
if (mode.toUpperCase() === "WFM") {
|
||||||
|
if (hasDecimalSeparator && frequency >= 50 && frequency < 200) {
|
||||||
|
return Math.round(frequency * 1e6);
|
||||||
|
}
|
||||||
|
if (!hasDecimalSeparator && frequency >= 875 && frequency <= 1080) {
|
||||||
|
return Math.round(frequency / 10 * 1e6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (defaultStepHz >= 1e6) frequency *= 1e6;
|
||||||
|
else if (defaultStepHz >= 1e3) frequency *= 1e3;
|
||||||
|
else if (defaultStepHz < 1) {
|
||||||
|
if (frequency < 1e3) frequency *= 1e6;
|
||||||
|
else if (frequency < 1e6) frequency *= 1e3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Math.round(frequency);
|
||||||
|
}
|
||||||
|
function formatByteSize(bytes) {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / 1048576).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
// src/app.js
|
// src/app.js
|
||||||
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
||||||
var authRole = null;
|
var authRole = null;
|
||||||
@@ -872,7 +946,7 @@
|
|||||||
document.title = originalTitle;
|
document.title = originalTitle;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const parts = [formatFreq(freqHz)];
|
const parts = [formatFrequency(freqHz)];
|
||||||
const ps = rds?.program_service;
|
const ps = rds?.program_service;
|
||||||
if (ps && ps.length > 0) {
|
if (ps && ps.length > 0) {
|
||||||
parts.push(ps);
|
parts.push(ps);
|
||||||
@@ -1416,23 +1490,10 @@
|
|||||||
var aboutUptimeStart = null;
|
var aboutUptimeStart = null;
|
||||||
var es;
|
var es;
|
||||||
var esHeartbeat;
|
var esHeartbeat;
|
||||||
function formatUptime(ms) {
|
|
||||||
const s = Math.floor(ms / 1e3);
|
|
||||||
const d = Math.floor(s / 86400);
|
|
||||||
const h = Math.floor(s % 86400 / 3600);
|
|
||||||
const m = Math.floor(s % 3600 / 60);
|
|
||||||
const sec = s % 60;
|
|
||||||
const parts = [];
|
|
||||||
if (d > 0) parts.push(`${d}d`);
|
|
||||||
if (h > 0 || d > 0) parts.push(`${h}h`);
|
|
||||||
parts.push(`${m}m`);
|
|
||||||
parts.push(`${sec}s`);
|
|
||||||
return parts.join(" ");
|
|
||||||
}
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (!aboutUptimeStart) return;
|
if (!aboutUptimeStart) return;
|
||||||
const el = document.getElementById("about-uptime");
|
const el = document.getElementById("about-uptime");
|
||||||
if (el) el.textContent = formatUptime(Date.now() - aboutUptimeStart);
|
if (el) el.textContent = formatDuration(Date.now() - aboutUptimeStart);
|
||||||
}, 1e3);
|
}, 1e3);
|
||||||
var reconnectTimer = null;
|
var reconnectTimer = null;
|
||||||
var overviewSignalSamples = [];
|
var overviewSignalSamples = [];
|
||||||
@@ -1857,36 +1918,13 @@
|
|||||||
texData[offset + 2] = _wfLut[p + 2];
|
texData[offset + 2] = _wfLut[p + 2];
|
||||||
texData[offset + 3] = _wfLut[p + 3];
|
texData[offset + 3] = _wfLut[p + 3];
|
||||||
}
|
}
|
||||||
function formatFreq(hz) {
|
|
||||||
if (!Number.isFinite(hz)) return "--";
|
|
||||||
if (hz >= 1e9) {
|
|
||||||
return `${(hz / 1e9).toFixed(3)} GHz`;
|
|
||||||
}
|
|
||||||
if (hz >= 1e7) {
|
|
||||||
return `${(hz / 1e6).toFixed(3)} MHz`;
|
|
||||||
}
|
|
||||||
return `${(hz / 1e3).toFixed(1)} kHz`;
|
|
||||||
}
|
|
||||||
function formatFreqForStep(hz, step) {
|
|
||||||
if (!Number.isFinite(hz)) return "--";
|
|
||||||
if (step >= 1e6) return (hz / 1e6).toFixed(6);
|
|
||||||
if (step >= 1e3) return (hz / 1e3).toFixed(3);
|
|
||||||
if (step >= 1) return String(Math.round(hz));
|
|
||||||
return formatFreq(hz);
|
|
||||||
}
|
|
||||||
function formatWavelength(hz) {
|
|
||||||
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
|
||||||
const meters = 299792458 / hz;
|
|
||||||
if (meters >= 1) return `${Math.round(meters)} m`;
|
|
||||||
return `${Math.round(meters * 100)} cm`;
|
|
||||||
}
|
|
||||||
function refreshWavelengthDisplay(hz) {
|
function refreshWavelengthDisplay(hz) {
|
||||||
if (!wavelengthEl) return;
|
if (!wavelengthEl) return;
|
||||||
wavelengthEl.textContent = formatWavelength(hz);
|
wavelengthEl.textContent = formatWavelength(hz);
|
||||||
}
|
}
|
||||||
function refreshFreqDisplay() {
|
function refreshFreqDisplay() {
|
||||||
if (lastFreqHz == null || freqDirty) return;
|
if (lastFreqHz == null || freqDirty) return;
|
||||||
freqEl.value = formatFreqForStep(lastFreqHz, jogUnit);
|
freqEl.value = formatFrequencyForStep(lastFreqHz, jogUnit);
|
||||||
refreshWavelengthDisplay(lastFreqHz);
|
refreshWavelengthDisplay(lastFreqHz);
|
||||||
}
|
}
|
||||||
function activeRdsChannelId() {
|
function activeRdsChannelId() {
|
||||||
@@ -2296,7 +2334,7 @@
|
|||||||
if (lastSpectrumData && Math.abs(nextCenterHz - Number(lastSpectrumData.center_hz)) < 1) return;
|
if (lastSpectrumData && Math.abs(nextCenterHz - Number(lastSpectrumData.center_hz)) < 1) return;
|
||||||
await postPath(`/set_center_freq?hz=${nextCenterHz}`);
|
await postPath(`/set_center_freq?hz=${nextCenterHz}`);
|
||||||
if (centerFreqEl && !centerFreqDirty) {
|
if (centerFreqEl && !centerFreqDirty) {
|
||||||
centerFreqEl.value = formatFreqForStep(nextCenterHz, jogUnit);
|
centerFreqEl.value = formatFrequencyForStep(nextCenterHz, jogUnit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var _freqOptimisticHz = null;
|
var _freqOptimisticHz = null;
|
||||||
@@ -2535,7 +2573,7 @@
|
|||||||
await postPath(`/set_center_freq?hz=${targetCenterHz}`);
|
await postPath(`/set_center_freq?hz=${targetCenterHz}`);
|
||||||
}
|
}
|
||||||
if (centerFreqEl && !centerFreqDirty) {
|
if (centerFreqEl && !centerFreqDirty) {
|
||||||
centerFreqEl.value = formatFreqForStep(targetCenterHz, jogUnit);
|
centerFreqEl.value = formatFrequencyForStep(targetCenterHz, jogUnit);
|
||||||
}
|
}
|
||||||
if (Number.isFinite(originalCenterHz) && Math.abs(targetCenterHz - originalCenterHz) < 1) {
|
if (Number.isFinite(originalCenterHz) && Math.abs(targetCenterHz - originalCenterHz) < 1) {
|
||||||
showHint("Already at sweet spot", 900);
|
showHint("Already at sweet spot", 900);
|
||||||
@@ -2578,7 +2616,7 @@
|
|||||||
showHint("Shifting spectrum…", 900);
|
showHint("Shifting spectrum…", 900);
|
||||||
await postPath(`/set_center_freq?hz=${nextCenterHz}`);
|
await postPath(`/set_center_freq?hz=${nextCenterHz}`);
|
||||||
if (centerFreqEl && !centerFreqDirty) {
|
if (centerFreqEl && !centerFreqDirty) {
|
||||||
centerFreqEl.value = formatFreqForStep(nextCenterHz, jogUnit);
|
centerFreqEl.value = formatFrequencyForStep(nextCenterHz, jogUnit);
|
||||||
}
|
}
|
||||||
const nextFreqHz = tunedFrequencyForCenterCoverage(nextCenterHz);
|
const nextFreqHz = tunedFrequencyForCenterCoverage(nextCenterHz);
|
||||||
if (Number.isFinite(nextFreqHz) && Math.abs(nextFreqHz - Number(lastFreqHz)) >= 1) {
|
if (Number.isFinite(nextFreqHz) && Math.abs(nextFreqHz - Number(lastFreqHz)) >= 1) {
|
||||||
@@ -2588,51 +2626,7 @@
|
|||||||
}
|
}
|
||||||
function refreshCenterFreqDisplay() {
|
function refreshCenterFreqDisplay() {
|
||||||
if (!centerFreqEl || !lastSpectrumData || centerFreqDirty) return;
|
if (!centerFreqEl || !lastSpectrumData || centerFreqDirty) return;
|
||||||
centerFreqEl.value = formatFreqForStep(lastSpectrumData.center_hz, jogUnit);
|
centerFreqEl.value = formatFrequencyForStep(lastSpectrumData.center_hz, jogUnit);
|
||||||
}
|
|
||||||
function parseFreqInput(val, defaultStep) {
|
|
||||||
if (!val) return null;
|
|
||||||
const trimmed = val.trim().toLowerCase();
|
|
||||||
const match = trimmed.match(/^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/);
|
|
||||||
if (!match) return null;
|
|
||||||
const rawNumber = match[1];
|
|
||||||
let num = parseFloat(rawNumber.replace(",", "."));
|
|
||||||
const unit = match[2] || "";
|
|
||||||
if (Number.isNaN(num)) return null;
|
|
||||||
if (unit.startsWith("gh") || unit === "g") {
|
|
||||||
num *= 1e9;
|
|
||||||
} else if (unit.startsWith("mh") || unit === "m") {
|
|
||||||
num *= 1e6;
|
|
||||||
} else if (unit.startsWith("kh") || unit === "k") {
|
|
||||||
num *= 1e3;
|
|
||||||
} else if (!unit) {
|
|
||||||
const mode = (modeEl?.value || "").toUpperCase();
|
|
||||||
const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(",");
|
|
||||||
if (mode === "WFM") {
|
|
||||||
if (hasDecimalSeparator && num >= 50 && num < 200) {
|
|
||||||
num *= 1e6;
|
|
||||||
return Math.round(num);
|
|
||||||
}
|
|
||||||
if (!hasDecimalSeparator && num >= 875 && num <= 1080) {
|
|
||||||
num = num / 10 * 1e6;
|
|
||||||
return Math.round(num);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (defaultStep >= 1e6) {
|
|
||||||
num *= 1e6;
|
|
||||||
} else if (defaultStep >= 1e3) {
|
|
||||||
num *= 1e3;
|
|
||||||
} else if (defaultStep >= 1) {
|
|
||||||
} else {
|
|
||||||
if (num >= 1e6) {
|
|
||||||
} else if (num >= 1e3) {
|
|
||||||
num *= 1e3;
|
|
||||||
} else {
|
|
||||||
num *= 1e6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Math.round(num);
|
|
||||||
}
|
}
|
||||||
function normalizeMinFreqStep(cap) {
|
function normalizeMinFreqStep(cap) {
|
||||||
const val = Number(cap && cap.min_freq_step_hz);
|
const val = Number(cap && cap.min_freq_step_hz);
|
||||||
@@ -2698,18 +2692,11 @@
|
|||||||
}
|
}
|
||||||
function unsupportedBandSummary() {
|
function unsupportedBandSummary() {
|
||||||
if (supportedBands.length === 0) return "No supported frequency ranges were reported by the rig.";
|
if (supportedBands.length === 0) return "No supported frequency ranges were reported by the rig.";
|
||||||
const ranges = supportedBands.slice().sort((a, b) => a.low - b.low).map((b) => `${formatFreqForHumans(b.low)} to ${formatFreqForHumans(b.high)}`);
|
const ranges = supportedBands.slice().sort((a, b) => a.low - b.low).map((b) => `${formatFrequencyForHumans(b.low)} to ${formatFrequencyForHumans(b.high)}`);
|
||||||
return `Supported ranges: ${ranges.join(", ")}`;
|
return `Supported ranges: ${ranges.join(", ")}`;
|
||||||
}
|
}
|
||||||
function formatFreqForHumans(hz) {
|
|
||||||
if (!Number.isFinite(hz)) return "--";
|
|
||||||
if (hz >= 1e9) return `${(hz / 1e9).toFixed(3)} GHz`;
|
|
||||||
if (hz >= 1e6) return `${(hz / 1e6).toFixed(3)} MHz`;
|
|
||||||
if (hz >= 1e3) return `${(hz / 1e3).toFixed(3)} kHz`;
|
|
||||||
return `${Math.round(hz)} Hz`;
|
|
||||||
}
|
|
||||||
function showUnsupportedFreqPopup(hz) {
|
function showUnsupportedFreqPopup(hz) {
|
||||||
const message = `Unsupported frequency: ${formatFreqForHumans(hz)}.
|
const message = `Unsupported frequency: ${formatFrequencyForHumans(hz)}.
|
||||||
|
|
||||||
${unsupportedBandSummary()}`;
|
${unsupportedBandSummary()}`;
|
||||||
showHint("Out of supported range", 1800);
|
showHint("Out of supported range", 1800);
|
||||||
@@ -3288,7 +3275,7 @@ ${unsupportedBandSummary()}`;
|
|||||||
if (hz === null) return;
|
if (hz === null) return;
|
||||||
const mode = entry.mode ? normalizeMode(entry.mode) : "";
|
const mode = entry.mode ? normalizeMode(entry.mode) : "";
|
||||||
const modeText = mode ? ` [${mode}]` : "";
|
const modeText = mode ? ` [${mode}]` : "";
|
||||||
const label = `${entry.name || String.fromCharCode(65 + idx)}: ${formatFreq(hz)}${modeText}`;
|
const label = `${entry.name || String.fromCharCode(65 + idx)}: ${formatFrequency(hz)}${modeText}`;
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.type = "button";
|
btn.type = "button";
|
||||||
btn.textContent = label;
|
btn.textContent = label;
|
||||||
@@ -3738,7 +3725,7 @@ ${unsupportedBandSummary()}`;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
function applyFreqFromInput() {
|
function applyFreqFromInput() {
|
||||||
const parsedRaw = parseFreqInput(freqEl.value, jogUnit);
|
const parsedRaw = parseFrequencyInput(freqEl.value, jogUnit, modeEl?.value || "");
|
||||||
const parsed = alignFreqToRigStep(parsedRaw);
|
const parsed = alignFreqToRigStep(parsedRaw);
|
||||||
if (parsed === null) {
|
if (parsed === null) {
|
||||||
showHint("Freq missing", 1500);
|
showHint("Freq missing", 1500);
|
||||||
@@ -3753,7 +3740,7 @@ ${unsupportedBandSummary()}`;
|
|||||||
}
|
}
|
||||||
async function applyCenterFreqFromInput() {
|
async function applyCenterFreqFromInput() {
|
||||||
if (!centerFreqEl) return;
|
if (!centerFreqEl) return;
|
||||||
const parsedRaw = parseFreqInput(centerFreqEl.value, jogUnit);
|
const parsedRaw = parseFrequencyInput(centerFreqEl.value, jogUnit, modeEl?.value || "");
|
||||||
const parsed = alignFreqToRigStep(parsedRaw);
|
const parsed = alignFreqToRigStep(parsedRaw);
|
||||||
if (parsed === null) {
|
if (parsed === null) {
|
||||||
showHint("Central freq missing", 1500);
|
showHint("Central freq missing", 1500);
|
||||||
@@ -4594,11 +4581,11 @@ ${unsupportedBandSummary()}`;
|
|||||||
loadSetting,
|
loadSetting,
|
||||||
showHint,
|
showHint,
|
||||||
escapeMapHtml: escapeHtml,
|
escapeMapHtml: escapeHtml,
|
||||||
formatFreq,
|
formatFreq: formatFrequency,
|
||||||
formatFreqForHumans,
|
formatFreqForHumans: formatFrequencyForHumans,
|
||||||
formatWavelength,
|
formatWavelength,
|
||||||
formatBwLabel,
|
formatBwLabel,
|
||||||
formatUptime,
|
formatUptime: formatDuration,
|
||||||
formatSigStrength,
|
formatSigStrength,
|
||||||
formatSignal,
|
formatSignal,
|
||||||
postPath,
|
postPath,
|
||||||
@@ -5640,11 +5627,6 @@ ${unsupportedBandSummary()}`;
|
|||||||
html += "</tbody></table>";
|
html += "</tbody></table>";
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
}
|
}
|
||||||
function recorderFormatSize(bytes) {
|
|
||||||
if (bytes < 1024) return bytes + " B";
|
|
||||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
|
|
||||||
return (bytes / 1048576).toFixed(1) + " MB";
|
|
||||||
}
|
|
||||||
function recFilterAndSort() {
|
function recFilterAndSort() {
|
||||||
const filterEl = document.getElementById("recorder-filter");
|
const filterEl = document.getElementById("recorder-filter");
|
||||||
const sortEl = document.getElementById("recorder-sort");
|
const sortEl = document.getElementById("recorder-sort");
|
||||||
@@ -5711,7 +5693,7 @@ ${unsupportedBandSummary()}`;
|
|||||||
for (const f of page) {
|
for (const f of page) {
|
||||||
const safeName = escapeHtml(f.name);
|
const safeName = escapeHtml(f.name);
|
||||||
const encodedName = encodeURIComponent(f.name);
|
const encodedName = encodeURIComponent(f.name);
|
||||||
html += '<tr data-name="' + safeName + '"><td>' + safeName + "</td><td>" + recorderFormatSize(f.size) + '</td><td><div class="rec-file-actions"><button class="rec-file-btn rec-play-btn" data-name="' + safeName + '" data-url="/api/recorder/download/' + encodedName + '" type="button" aria-expanded="false">Play</button><a class="rec-file-btn" href="/api/recorder/download/' + encodedName + '" download="' + safeName + '">Download</a><button class="rec-file-btn rec-delete-btn" data-name="' + safeName + '" type="button">Remove</button></div></td></tr>';
|
html += '<tr data-name="' + safeName + '"><td>' + safeName + "</td><td>" + formatByteSize(f.size) + '</td><td><div class="rec-file-actions"><button class="rec-file-btn rec-play-btn" data-name="' + safeName + '" data-url="/api/recorder/download/' + encodedName + '" type="button" aria-expanded="false">Play</button><a class="rec-file-btn" href="/api/recorder/download/' + encodedName + '" download="' + safeName + '">Download</a><button class="rec-file-btn rec-delete-btn" data-name="' + safeName + '" type="button">Remove</button></div></td></tr>';
|
||||||
}
|
}
|
||||||
html += "</tbody></table>";
|
html += "</tbody></table>";
|
||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
@@ -6513,7 +6495,7 @@ ${unsupportedBandSummary()}`;
|
|||||||
let peakIndex = 0;
|
let peakIndex = 0;
|
||||||
for (let i = 1; i < bins.length; i += 1) if (bins[i] > bins[peakIndex]) peakIndex = i;
|
for (let i = 1; i < bins.length; i += 1) if (bins[i] > bins[peakIndex]) peakIndex = i;
|
||||||
const peakHz = centerHz - sampleRate / 2 + peakIndex / Math.max(1, bins.length - 1) * sampleRate;
|
const peakHz = centerHz - sampleRate / 2 + peakIndex / Math.max(1, bins.length - 1) * sampleRate;
|
||||||
spectrumSummary.textContent = `Spectrum centered at ${formatFreqForHumans(centerHz)}, spanning ${formatFreqForHumans(sampleRate)}. Strongest visible bin near ${formatFreqForHumans(peakHz)} at ${bins[peakIndex]} dB.`;
|
spectrumSummary.textContent = `Spectrum centered at ${formatFrequencyForHumans(centerHz)}, spanning ${formatFrequencyForHumans(sampleRate)}. Strongest visible bin near ${formatFrequencyForHumans(peakHz)} at ${bins[peakIndex]} dB.`;
|
||||||
}
|
}
|
||||||
if (spectrumCenterPendingHz !== null && Math.abs(centerHz - spectrumCenterPendingHz) < 1e3) {
|
if (spectrumCenterPendingHz !== null && Math.abs(centerHz - spectrumCenterPendingHz) < 1e3) {
|
||||||
spectrumCenterPendingHz = null;
|
spectrumCenterPendingHz = null;
|
||||||
@@ -7458,7 +7440,7 @@ ${unsupportedBandSummary()}`;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRigFrequency(rounded);
|
setRigFrequency(rounded);
|
||||||
showHint(`Rounded → ${formatFreq(rounded)}`, 1200);
|
showHint(`Rounded → ${formatFrequency(rounded)}`, 1200);
|
||||||
} else {
|
} else {
|
||||||
showHint("Already on step", 1200);
|
showHint("Already on step", 1200);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ import {
|
|||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
} from "./api/auth.js";
|
} from "./api/auth.js";
|
||||||
|
import {
|
||||||
|
formatByteSize as recorderFormatSize,
|
||||||
|
formatDuration as formatUptime,
|
||||||
|
formatFrequency as formatFreq,
|
||||||
|
formatFrequencyForHumans as formatFreqForHumans,
|
||||||
|
formatFrequencyForStep as formatFreqForStep,
|
||||||
|
formatWavelength,
|
||||||
|
parseFrequencyInput,
|
||||||
|
} from "./core/format.js";
|
||||||
|
|
||||||
// --- Decoder registry (fetched from /decoders on load) ---
|
// --- Decoder registry (fetched from /decoders on load) ---
|
||||||
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
|
||||||
@@ -1246,19 +1255,6 @@ let aboutUptimeStart = null;
|
|||||||
let es;
|
let es;
|
||||||
let esHeartbeat;
|
let esHeartbeat;
|
||||||
|
|
||||||
function formatUptime(ms) {
|
|
||||||
const s = Math.floor(ms / 1000);
|
|
||||||
const d = Math.floor(s / 86400);
|
|
||||||
const h = Math.floor((s % 86400) / 3600);
|
|
||||||
const m = Math.floor((s % 3600) / 60);
|
|
||||||
const sec = s % 60;
|
|
||||||
const parts = [];
|
|
||||||
if (d > 0) parts.push(`${d}d`);
|
|
||||||
if (h > 0 || d > 0) parts.push(`${h}h`);
|
|
||||||
parts.push(`${m}m`);
|
|
||||||
parts.push(`${sec}s`);
|
|
||||||
return parts.join(" ");
|
|
||||||
}
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (!aboutUptimeStart) return;
|
if (!aboutUptimeStart) return;
|
||||||
const el = document.getElementById("about-uptime");
|
const el = document.getElementById("about-uptime");
|
||||||
@@ -1738,32 +1734,6 @@ function waterfallLutWrite(texData, offset, db) {
|
|||||||
texData[offset + 3] = _wfLut[p + 3];
|
texData[offset + 3] = _wfLut[p + 3];
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFreq(hz) {
|
|
||||||
if (!Number.isFinite(hz)) return "--";
|
|
||||||
if (hz >= 1_000_000_000) {
|
|
||||||
return `${(hz / 1_000_000_000).toFixed(3)} GHz`;
|
|
||||||
}
|
|
||||||
if (hz >= 10_000_000) {
|
|
||||||
return `${(hz / 1_000_000).toFixed(3)} MHz`;
|
|
||||||
}
|
|
||||||
return `${(hz / 1_000).toFixed(1)} kHz`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatFreqForStep(hz, step) {
|
|
||||||
if (!Number.isFinite(hz)) return "--";
|
|
||||||
if (step >= 1_000_000) return (hz / 1_000_000).toFixed(6);
|
|
||||||
if (step >= 1_000) return (hz / 1_000).toFixed(3);
|
|
||||||
if (step >= 1) return String(Math.round(hz));
|
|
||||||
return formatFreq(hz);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatWavelength(hz) {
|
|
||||||
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
|
||||||
const meters = 299_792_458 / hz;
|
|
||||||
if (meters >= 1) return `${Math.round(meters)} m`;
|
|
||||||
return `${Math.round(meters * 100)} cm`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshWavelengthDisplay(hz) {
|
function refreshWavelengthDisplay(hz) {
|
||||||
if (!wavelengthEl) return;
|
if (!wavelengthEl) return;
|
||||||
wavelengthEl.textContent = formatWavelength(hz);
|
wavelengthEl.textContent = formatWavelength(hz);
|
||||||
@@ -2602,55 +2572,6 @@ function refreshCenterFreqDisplay() {
|
|||||||
centerFreqEl.value = formatFreqForStep(lastSpectrumData.center_hz, jogUnit);
|
centerFreqEl.value = formatFreqForStep(lastSpectrumData.center_hz, jogUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFreqInput(val, defaultStep) {
|
|
||||||
if (!val) return null;
|
|
||||||
const trimmed = val.trim().toLowerCase();
|
|
||||||
const match = trimmed.match(/^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/);
|
|
||||||
if (!match) return null;
|
|
||||||
const rawNumber = match[1];
|
|
||||||
let num = parseFloat(rawNumber.replace(",", "."));
|
|
||||||
const unit = match[2] || "";
|
|
||||||
if (Number.isNaN(num)) return null;
|
|
||||||
if (unit.startsWith("gh") || unit === "g") {
|
|
||||||
num *= 1_000_000_000;
|
|
||||||
} else if (unit.startsWith("mh") || unit === "m") {
|
|
||||||
num *= 1_000_000;
|
|
||||||
} else if (unit.startsWith("kh") || unit === "k") {
|
|
||||||
num *= 1_000;
|
|
||||||
} else if (!unit) {
|
|
||||||
const mode = (modeEl?.value || "").toUpperCase();
|
|
||||||
const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(",");
|
|
||||||
if (mode === "WFM") {
|
|
||||||
if (hasDecimalSeparator && num >= 50 && num < 200) {
|
|
||||||
num *= 1_000_000;
|
|
||||||
return Math.round(num);
|
|
||||||
}
|
|
||||||
if (!hasDecimalSeparator && num >= 875 && num <= 1080) {
|
|
||||||
num = (num / 10) * 1_000_000;
|
|
||||||
return Math.round(num);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Use currently selected input unit when user omits suffix.
|
|
||||||
if (defaultStep >= 1_000_000) {
|
|
||||||
num *= 1_000_000;
|
|
||||||
} else if (defaultStep >= 1_000) {
|
|
||||||
num *= 1_000;
|
|
||||||
} else if (defaultStep >= 1) {
|
|
||||||
// already Hz
|
|
||||||
} else {
|
|
||||||
// Fallback heuristic.
|
|
||||||
if (num >= 1_000_000) {
|
|
||||||
// Assume already Hz.
|
|
||||||
} else if (num >= 1_000) {
|
|
||||||
num *= 1_000;
|
|
||||||
} else {
|
|
||||||
num *= 1_000_000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Math.round(num);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeMinFreqStep(cap) {
|
function normalizeMinFreqStep(cap) {
|
||||||
const val = Number(cap && cap.min_freq_step_hz);
|
const val = Number(cap && cap.min_freq_step_hz);
|
||||||
if (!Number.isFinite(val) || val < 1) return 1;
|
if (!Number.isFinite(val) || val < 1) return 1;
|
||||||
@@ -2740,14 +2661,6 @@ function unsupportedBandSummary() {
|
|||||||
return `Supported ranges: ${ranges.join(", ")}`;
|
return `Supported ranges: ${ranges.join(", ")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFreqForHumans(hz) {
|
|
||||||
if (!Number.isFinite(hz)) return "--";
|
|
||||||
if (hz >= 1_000_000_000) return `${(hz / 1_000_000_000).toFixed(3)} GHz`;
|
|
||||||
if (hz >= 1_000_000) return `${(hz / 1_000_000).toFixed(3)} MHz`;
|
|
||||||
if (hz >= 1_000) return `${(hz / 1_000).toFixed(3)} kHz`;
|
|
||||||
return `${Math.round(hz)} Hz`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showUnsupportedFreqPopup(hz) {
|
function showUnsupportedFreqPopup(hz) {
|
||||||
const message = `Unsupported frequency: ${formatFreqForHumans(hz)}.\n\n${unsupportedBandSummary()}`;
|
const message = `Unsupported frequency: ${formatFreqForHumans(hz)}.\n\n${unsupportedBandSummary()}`;
|
||||||
showHint("Out of supported range", 1800);
|
showHint("Out of supported range", 1800);
|
||||||
@@ -3913,7 +3826,7 @@ pttBtn.addEventListener("click", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function applyFreqFromInput() {
|
function applyFreqFromInput() {
|
||||||
const parsedRaw = parseFreqInput(freqEl.value, jogUnit);
|
const parsedRaw = parseFrequencyInput(freqEl.value, jogUnit, modeEl?.value || "");
|
||||||
const parsed = alignFreqToRigStep(parsedRaw);
|
const parsed = alignFreqToRigStep(parsedRaw);
|
||||||
if (parsed === null) {
|
if (parsed === null) {
|
||||||
showHint("Freq missing", 1500);
|
showHint("Freq missing", 1500);
|
||||||
@@ -3930,7 +3843,7 @@ function applyFreqFromInput() {
|
|||||||
|
|
||||||
async function applyCenterFreqFromInput() {
|
async function applyCenterFreqFromInput() {
|
||||||
if (!centerFreqEl) return;
|
if (!centerFreqEl) return;
|
||||||
const parsedRaw = parseFreqInput(centerFreqEl.value, jogUnit);
|
const parsedRaw = parseFrequencyInput(centerFreqEl.value, jogUnit, modeEl?.value || "");
|
||||||
const parsed = alignFreqToRigStep(parsedRaw);
|
const parsed = alignFreqToRigStep(parsedRaw);
|
||||||
if (parsed === null) {
|
if (parsed === null) {
|
||||||
showHint("Central freq missing", 1500);
|
showHint("Central freq missing", 1500);
|
||||||
@@ -5839,12 +5752,6 @@ function renderRecorderActive(list) {
|
|||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function recorderFormatSize(bytes) {
|
|
||||||
if (bytes < 1024) return bytes + " B";
|
|
||||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
|
|
||||||
return (bytes / 1048576).toFixed(1) + " MB";
|
|
||||||
}
|
|
||||||
|
|
||||||
function recFilterAndSort() {
|
function recFilterAndSort() {
|
||||||
const filterEl = document.getElementById("recorder-filter");
|
const filterEl = document.getElementById("recorder-filter");
|
||||||
const sortEl = document.getElementById("recorder-sort");
|
const sortEl = document.getElementById("recorder-sort");
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
export function formatDuration(milliseconds: number): string {
|
||||||
|
const seconds = Math.floor(milliseconds / 1000);
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
const remainder = seconds % 60;
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (days > 0) parts.push(`${days}d`);
|
||||||
|
if (hours > 0 || days > 0) parts.push(`${hours}h`);
|
||||||
|
parts.push(`${minutes}m`, `${remainder}s`);
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFrequency(frequencyHz: number): string {
|
||||||
|
if (!Number.isFinite(frequencyHz)) return "--";
|
||||||
|
if (frequencyHz >= 1_000_000_000) return `${(frequencyHz / 1_000_000_000).toFixed(3)} GHz`;
|
||||||
|
if (frequencyHz >= 10_000_000) return `${(frequencyHz / 1_000_000).toFixed(3)} MHz`;
|
||||||
|
return `${(frequencyHz / 1_000).toFixed(1)} kHz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFrequencyForStep(frequencyHz: number, stepHz: number): string {
|
||||||
|
if (!Number.isFinite(frequencyHz)) return "--";
|
||||||
|
if (stepHz >= 1_000_000) return (frequencyHz / 1_000_000).toFixed(6);
|
||||||
|
if (stepHz >= 1_000) return (frequencyHz / 1_000).toFixed(3);
|
||||||
|
if (stepHz >= 1) return String(Math.round(frequencyHz));
|
||||||
|
return formatFrequency(frequencyHz);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFrequencyForHumans(frequencyHz: number): string {
|
||||||
|
if (!Number.isFinite(frequencyHz)) return "--";
|
||||||
|
if (frequencyHz >= 1_000_000_000) return `${(frequencyHz / 1_000_000_000).toFixed(3)} GHz`;
|
||||||
|
if (frequencyHz >= 1_000_000) return `${(frequencyHz / 1_000_000).toFixed(3)} MHz`;
|
||||||
|
if (frequencyHz >= 1_000) return `${(frequencyHz / 1_000).toFixed(3)} kHz`;
|
||||||
|
return `${Math.round(frequencyHz)} Hz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWavelength(frequencyHz: number): string {
|
||||||
|
if (!Number.isFinite(frequencyHz) || frequencyHz <= 0) return "--";
|
||||||
|
const meters = 299_792_458 / frequencyHz;
|
||||||
|
return meters >= 1 ? `${Math.round(meters)} m` : `${Math.round(meters * 100)} cm`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFrequencyInput(
|
||||||
|
value: string,
|
||||||
|
defaultStepHz: number,
|
||||||
|
mode: string,
|
||||||
|
): number | null {
|
||||||
|
if (!value) return null;
|
||||||
|
const match = /^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/
|
||||||
|
.exec(value.trim().toLowerCase());
|
||||||
|
if (!match?.[1]) return null;
|
||||||
|
const rawNumber = match[1];
|
||||||
|
let frequency = Number.parseFloat(rawNumber.replace(",", "."));
|
||||||
|
const unit = match[2] ?? "";
|
||||||
|
if (Number.isNaN(frequency)) return null;
|
||||||
|
|
||||||
|
if (unit.startsWith("gh") || unit === "g") frequency *= 1_000_000_000;
|
||||||
|
else if (unit.startsWith("mh") || unit === "m") frequency *= 1_000_000;
|
||||||
|
else if (unit.startsWith("kh") || unit === "k") frequency *= 1_000;
|
||||||
|
else if (!unit) {
|
||||||
|
const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(",");
|
||||||
|
if (mode.toUpperCase() === "WFM") {
|
||||||
|
if (hasDecimalSeparator && frequency >= 50 && frequency < 200) {
|
||||||
|
return Math.round(frequency * 1_000_000);
|
||||||
|
}
|
||||||
|
if (!hasDecimalSeparator && frequency >= 875 && frequency <= 1080) {
|
||||||
|
return Math.round((frequency / 10) * 1_000_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (defaultStepHz >= 1_000_000) frequency *= 1_000_000;
|
||||||
|
else if (defaultStepHz >= 1_000) frequency *= 1_000;
|
||||||
|
else if (defaultStepHz < 1) {
|
||||||
|
if (frequency < 1_000) frequency *= 1_000_000;
|
||||||
|
else if (frequency < 1_000_000) frequency *= 1_000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Math.round(frequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatByteSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1_048_576) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / 1_048_576).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import vm from "node:vm";
|
||||||
|
import { build } from "esbuild";
|
||||||
|
|
||||||
|
async function loadFormats() {
|
||||||
|
const result = await build({
|
||||||
|
entryPoints: [new URL("../src/core/format.ts", import.meta.url).pathname],
|
||||||
|
bundle: true,
|
||||||
|
format: "cjs",
|
||||||
|
platform: "browser",
|
||||||
|
target: "es2022",
|
||||||
|
write: false,
|
||||||
|
});
|
||||||
|
const module = { exports: {} };
|
||||||
|
vm.runInNewContext(result.outputFiles[0].text, { module, exports: module.exports });
|
||||||
|
return module.exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("frequency parsing respects explicit units and WFM shorthand", async () => {
|
||||||
|
const { parseFrequencyInput } = await loadFormats();
|
||||||
|
assert.equal(parseFrequencyInput("145.500 MHz", 1000, "FM"), 145_500_000);
|
||||||
|
assert.equal(parseFrequencyInput("99.5", 100_000, "WFM"), 99_500_000);
|
||||||
|
assert.equal(parseFrequencyInput("995", 100_000, "WFM"), 99_500_000);
|
||||||
|
assert.equal(parseFrequencyInput("not a frequency", 1000, "USB"), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("frequency and byte formatters handle display boundaries", async () => {
|
||||||
|
const { formatByteSize, formatFrequencyForHumans, formatWavelength } = await loadFormats();
|
||||||
|
assert.equal(formatFrequencyForHumans(145_500_000), "145.500 MHz");
|
||||||
|
assert.equal(formatByteSize(1536), "1.5 KB");
|
||||||
|
assert.equal(formatWavelength(299_792_458), "1 m");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user