[feat](trx-frontend-http): add background decode settings
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -3606,7 +3606,7 @@ async function initializeApp() {
|
||||
updateAuthUI();
|
||||
connect();
|
||||
connectDecode();
|
||||
initSchedulerUI();
|
||||
initSettingsUI();
|
||||
resizeHeaderSignalCanvas();
|
||||
startHeaderSignalSampling();
|
||||
return;
|
||||
@@ -3620,7 +3620,7 @@ async function initializeApp() {
|
||||
applyAuthRestrictions();
|
||||
connect();
|
||||
connectDecode();
|
||||
initSchedulerUI();
|
||||
initSettingsUI();
|
||||
resizeHeaderSignalCanvas();
|
||||
startHeaderSignalSampling();
|
||||
} else {
|
||||
@@ -3631,11 +3631,15 @@ async function initializeApp() {
|
||||
}
|
||||
}
|
||||
|
||||
function initSchedulerUI() {
|
||||
function initSettingsUI() {
|
||||
if (typeof initScheduler === "function") {
|
||||
initScheduler(lastActiveRigId, authRole);
|
||||
wireSchedulerEvents();
|
||||
}
|
||||
if (typeof initBackgroundDecode === "function") {
|
||||
initBackgroundDecode(lastActiveRigId, authRole);
|
||||
wireBackgroundDecodeEvents();
|
||||
}
|
||||
}
|
||||
|
||||
// Setup auth form
|
||||
@@ -3655,7 +3659,7 @@ document.getElementById("auth-form").addEventListener("submit", async (e) => {
|
||||
applyAuthRestrictions();
|
||||
connect();
|
||||
connectDecode();
|
||||
initSchedulerUI();
|
||||
initSettingsUI();
|
||||
resizeHeaderSignalCanvas();
|
||||
startHeaderSignalSampling();
|
||||
} catch (err) {
|
||||
@@ -3678,7 +3682,7 @@ if (guestBtn) {
|
||||
applyAuthRestrictions();
|
||||
connect();
|
||||
connectDecode();
|
||||
initSchedulerUI();
|
||||
initSettingsUI();
|
||||
resizeHeaderSignalCanvas();
|
||||
startHeaderSignalSampling();
|
||||
});
|
||||
|
||||
@@ -674,6 +674,7 @@
|
||||
<div id="tab-settings" class="tab-panel" style="display:none;">
|
||||
<div class="sub-tab-bar">
|
||||
<button class="sub-tab active" data-subtab="settings-scheduler">Scheduler</button>
|
||||
<button class="sub-tab" data-subtab="settings-background-decode">Background Decode</button>
|
||||
</div>
|
||||
<div id="subtab-settings-scheduler" class="sub-tab-panel">
|
||||
<div id="scheduler-panel" class="sch-panel">
|
||||
@@ -779,6 +780,44 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="subtab-settings-background-decode" class="sub-tab-panel" style="display:none;">
|
||||
<div id="background-decode-panel" class="sch-panel">
|
||||
<div class="sch-toast" id="background-decode-toast" style="display:none;"></div>
|
||||
<div class="sch-section">
|
||||
<div class="sch-section-title">Configuration</div>
|
||||
<div class="sch-row">
|
||||
<label class="sch-label">Rig
|
||||
<select id="background-decode-rig-select" class="status-input sch-rig-select" aria-label="Select rig"></select>
|
||||
</label>
|
||||
<label class="sch-label bgd-toggle-wrap">Background decode
|
||||
<span class="bgd-toggle-row">
|
||||
<input type="checkbox" id="background-decode-enabled" />
|
||||
<span>Enable hidden background decoder channels</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="sch-row">
|
||||
<label class="sch-label bgd-bookmark-pick">Bookmark
|
||||
<div class="bgd-add-row">
|
||||
<select id="background-decode-bookmark-pick" class="status-input" aria-label="Background decode bookmark"></select>
|
||||
<button id="background-decode-bookmark-add" type="button" class="sch-write">+ Add</button>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div id="background-decode-bookmark-list" class="bgd-bookmark-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="sch-actions">
|
||||
<button id="background-decode-save-btn" class="sch-write sch-save-btn" type="button" style="display:none;">Save</button>
|
||||
<button id="background-decode-reset-btn" class="sch-write sch-reset-btn" type="button" style="display:none;">Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="sch-section">
|
||||
<div class="sch-section-title">Runtime Status</div>
|
||||
<div id="background-decode-status-card" class="sch-status-card">No background decode bookmarks configured.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-about" class="tab-panel" style="display:none;">
|
||||
<div id="auth-badge" style="display:none; margin-bottom: 1rem; padding: 0.5rem; background: var(--bg-secondary); border-radius: 0.25rem; color: var(--text-muted); font-size: 0.85rem;">Authenticated as: <strong id="auth-role-badge">--</strong></div>
|
||||
@@ -819,6 +858,7 @@
|
||||
<script src="/cw.js"></script>
|
||||
<script src="/bookmarks.js"></script>
|
||||
<script src="/scheduler.js"></script>
|
||||
<script src="/background-decode.js"></script>
|
||||
<script src="/vchan.js"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="/leaflet-ais-tracksymbol.js"></script>
|
||||
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const SUPPORTED_DECODERS = ["ft8", "wspr", "hf-aprs"];
|
||||
|
||||
let backgroundDecodeRole = null;
|
||||
let currentRigId = null;
|
||||
let currentConfig = null;
|
||||
let bookmarkList = [];
|
||||
let statusInterval = null;
|
||||
|
||||
function initBackgroundDecode(rigId, role) {
|
||||
backgroundDecodeRole = role;
|
||||
currentRigId = rigId || null;
|
||||
renderRigSelect();
|
||||
loadBackgroundDecode();
|
||||
startStatusPolling();
|
||||
}
|
||||
|
||||
function renderRigSelect() {
|
||||
const sel = document.getElementById("background-decode-rig-select");
|
||||
if (!sel) return;
|
||||
const rigs = typeof getAvailableRigIds === "function" ? getAvailableRigIds() : [];
|
||||
if (!rigs.length) return;
|
||||
sel.innerHTML = "";
|
||||
rigs.forEach(function (rigId) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = rigId;
|
||||
opt.textContent = rigId;
|
||||
if (rigId === currentRigId) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
if (!currentRigId || !rigs.includes(currentRigId)) {
|
||||
currentRigId = rigs[0];
|
||||
sel.value = currentRigId;
|
||||
}
|
||||
}
|
||||
|
||||
function apiGetConfig(rigId) {
|
||||
return fetch("/background-decode/" + encodeURIComponent(rigId)).then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
return r.json();
|
||||
});
|
||||
}
|
||||
|
||||
function apiPutConfig(rigId, config) {
|
||||
return fetch("/background-decode/" + encodeURIComponent(rigId), {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(config),
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
return r.json();
|
||||
});
|
||||
}
|
||||
|
||||
function apiResetConfig(rigId) {
|
||||
return fetch("/background-decode/" + encodeURIComponent(rigId), {
|
||||
method: "DELETE",
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
return r.json();
|
||||
});
|
||||
}
|
||||
|
||||
function apiGetStatus(rigId) {
|
||||
return fetch("/background-decode/" + encodeURIComponent(rigId) + "/status").then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
return r.json();
|
||||
});
|
||||
}
|
||||
|
||||
function apiGetBookmarks() {
|
||||
return fetch("/bookmarks").then(function (r) {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
return r.json();
|
||||
});
|
||||
}
|
||||
|
||||
function loadBackgroundDecode() {
|
||||
const rigId = currentRigId;
|
||||
if (!rigId) return;
|
||||
Promise.all([apiGetConfig(rigId), apiGetBookmarks()])
|
||||
.then(function ([config, bookmarks]) {
|
||||
currentConfig = config || { rig_id: rigId, enabled: false, bookmark_ids: [] };
|
||||
bookmarkList = Array.isArray(bookmarks) ? bookmarks : [];
|
||||
renderBookmarkPick();
|
||||
renderBackgroundDecode();
|
||||
pollBackgroundDecodeStatus();
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("background decode load failed", err);
|
||||
});
|
||||
}
|
||||
|
||||
function supportedBookmarks() {
|
||||
return bookmarkList.filter(function (bookmark) {
|
||||
return bookmarkDecoderKinds(bookmark).length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function bookmarkDecoderKinds(bookmark) {
|
||||
const decoders = Array.isArray(bookmark && bookmark.decoders) ? bookmark.decoders : [];
|
||||
return decoders
|
||||
.map(function (item) { return String(item || "").trim().toLowerCase(); })
|
||||
.filter(function (item, index, arr) {
|
||||
return SUPPORTED_DECODERS.includes(item) && arr.indexOf(item) === index;
|
||||
});
|
||||
}
|
||||
|
||||
function renderBookmarkPick() {
|
||||
const sel = document.getElementById("background-decode-bookmark-pick");
|
||||
if (!sel) return;
|
||||
const selectedIds = new Set(currentConfig && Array.isArray(currentConfig.bookmark_ids) ? currentConfig.bookmark_ids : []);
|
||||
sel.innerHTML = '<option value="">- select bookmark -</option>';
|
||||
supportedBookmarks().forEach(function (bookmark) {
|
||||
if (selectedIds.has(bookmark.id)) return;
|
||||
const opt = document.createElement("option");
|
||||
opt.value = bookmark.id;
|
||||
opt.textContent = bookmark.name + " (" + formatFreq(bookmark.freq_hz) + " " + bookmark.mode + ")";
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function renderBackgroundDecode() {
|
||||
if (!currentConfig) {
|
||||
currentConfig = { rig_id: currentRigId, enabled: false, bookmark_ids: [] };
|
||||
}
|
||||
setCheckbox("background-decode-enabled", !!currentConfig.enabled);
|
||||
renderBookmarkList();
|
||||
|
||||
const isControl = backgroundDecodeRole === "control" || (typeof authEnabled !== "undefined" && !authEnabled);
|
||||
const panel = document.getElementById("background-decode-panel");
|
||||
if (panel) {
|
||||
panel.querySelectorAll("input, select, button.sch-write").forEach(function (el) {
|
||||
el.disabled = !isControl;
|
||||
});
|
||||
}
|
||||
const saveBtn = document.getElementById("background-decode-save-btn");
|
||||
const resetBtn = document.getElementById("background-decode-reset-btn");
|
||||
if (saveBtn) saveBtn.style.display = isControl ? "" : "none";
|
||||
if (resetBtn) resetBtn.style.display = isControl ? "" : "none";
|
||||
}
|
||||
|
||||
function renderBookmarkList() {
|
||||
const container = document.getElementById("background-decode-bookmark-list");
|
||||
if (!container) return;
|
||||
container.innerHTML = "";
|
||||
const ids = currentConfig && Array.isArray(currentConfig.bookmark_ids) ? currentConfig.bookmark_ids : [];
|
||||
if (!ids.length) {
|
||||
container.textContent = "No background decode bookmarks selected.";
|
||||
return;
|
||||
}
|
||||
ids.forEach(function (id) {
|
||||
const bookmark = bookmarkList.find(function (item) { return item.id === id; });
|
||||
const chip = document.createElement("div");
|
||||
chip.className = "bgd-bookmark-chip";
|
||||
const decoders = bookmarkDecoderKinds(bookmark);
|
||||
chip.innerHTML =
|
||||
'<span>' + escHtml(bookmark ? bookmark.name : id) + '</span>' +
|
||||
'<span class="bgd-bookmark-chip-meta">' + escHtml(bookmark ? (formatFreq(bookmark.freq_hz) + " " + bookmark.mode + " · " + decoders.join("/").toUpperCase()) : "Missing bookmark") + '</span>';
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "bgd-bookmark-chip-remove sch-write";
|
||||
btn.textContent = "×";
|
||||
btn.addEventListener("click", function () {
|
||||
removeBookmark(id);
|
||||
});
|
||||
chip.appendChild(btn);
|
||||
container.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
||||
function removeBookmark(id) {
|
||||
if (!currentConfig || !Array.isArray(currentConfig.bookmark_ids)) return;
|
||||
currentConfig.bookmark_ids = currentConfig.bookmark_ids.filter(function (item) { return item !== id; });
|
||||
renderBookmarkPick();
|
||||
renderBackgroundDecode();
|
||||
}
|
||||
|
||||
function addBookmark() {
|
||||
const sel = document.getElementById("background-decode-bookmark-pick");
|
||||
if (!sel || !sel.value) return;
|
||||
if (!currentConfig) {
|
||||
currentConfig = { rig_id: currentRigId, enabled: false, bookmark_ids: [] };
|
||||
}
|
||||
if (!Array.isArray(currentConfig.bookmark_ids)) currentConfig.bookmark_ids = [];
|
||||
if (!currentConfig.bookmark_ids.includes(sel.value)) currentConfig.bookmark_ids.push(sel.value);
|
||||
sel.value = "";
|
||||
renderBookmarkPick();
|
||||
renderBackgroundDecode();
|
||||
}
|
||||
|
||||
function saveBackgroundDecode() {
|
||||
const rigId = currentRigId;
|
||||
if (!rigId) return;
|
||||
const payload = {
|
||||
rig_id: rigId,
|
||||
enabled: !!document.getElementById("background-decode-enabled").checked,
|
||||
bookmark_ids: Array.isArray(currentConfig && currentConfig.bookmark_ids) ? currentConfig.bookmark_ids.slice() : [],
|
||||
};
|
||||
const btn = document.getElementById("background-decode-save-btn");
|
||||
if (btn) btn.disabled = true;
|
||||
apiPutConfig(rigId, payload)
|
||||
.then(function (saved) {
|
||||
currentConfig = saved;
|
||||
renderBookmarkPick();
|
||||
renderBackgroundDecode();
|
||||
pollBackgroundDecodeStatus();
|
||||
showToast("Background decode saved.");
|
||||
})
|
||||
.catch(function (err) {
|
||||
showToast("Save failed: " + err.message, true);
|
||||
})
|
||||
.finally(function () {
|
||||
if (btn) btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function resetBackgroundDecode() {
|
||||
const rigId = currentRigId;
|
||||
if (!rigId) return;
|
||||
apiResetConfig(rigId)
|
||||
.then(function (saved) {
|
||||
currentConfig = saved;
|
||||
renderBookmarkPick();
|
||||
renderBackgroundDecode();
|
||||
pollBackgroundDecodeStatus();
|
||||
showToast("Background decode reset.");
|
||||
})
|
||||
.catch(function (err) {
|
||||
showToast("Reset failed: " + err.message, true);
|
||||
});
|
||||
}
|
||||
|
||||
function startStatusPolling() {
|
||||
if (statusInterval) clearInterval(statusInterval);
|
||||
statusInterval = setInterval(pollBackgroundDecodeStatus, 15000);
|
||||
}
|
||||
|
||||
function pollBackgroundDecodeStatus() {
|
||||
const rigId = currentRigId;
|
||||
if (!rigId) return;
|
||||
apiGetStatus(rigId)
|
||||
.then(renderStatus)
|
||||
.catch(function () {});
|
||||
}
|
||||
|
||||
function renderStatus(status) {
|
||||
const card = document.getElementById("background-decode-status-card");
|
||||
if (!card) return;
|
||||
const entries = Array.isArray(status && status.entries) ? status.entries : [];
|
||||
if (!entries.length) {
|
||||
card.textContent = "No background decode bookmarks configured.";
|
||||
return;
|
||||
}
|
||||
const summary = [];
|
||||
if (status.active_rig) {
|
||||
if (Number.isFinite(status.center_hz)) summary.push("Center " + formatFreq(status.center_hz));
|
||||
if (Number.isFinite(status.sample_rate) && status.sample_rate > 0) summary.push("Span ±" + formatFreq(status.sample_rate / 2));
|
||||
} else {
|
||||
summary.push("This rig is not currently selected for audio.");
|
||||
}
|
||||
let html = summary.length ? '<div style="margin-bottom:0.8rem;color:var(--text-muted);">' + escHtml(summary.join(" · ")) + "</div>" : "";
|
||||
html += '<div class="bgd-status-list">';
|
||||
entries.forEach(function (entry) {
|
||||
const name = entry.bookmark_name || entry.bookmark_id || "Unknown bookmark";
|
||||
const parts = [];
|
||||
if (Number.isFinite(entry.freq_hz)) parts.push(formatFreq(entry.freq_hz));
|
||||
if (entry.mode) parts.push(entry.mode);
|
||||
if (Array.isArray(entry.decoder_kinds) && entry.decoder_kinds.length) {
|
||||
parts.push(entry.decoder_kinds.join("/").toUpperCase());
|
||||
}
|
||||
html +=
|
||||
'<div class="bgd-status-row">' +
|
||||
'<div>' +
|
||||
'<div class="bgd-status-name">' + escHtml(name) + '</div>' +
|
||||
'<div class="bgd-status-meta">' + escHtml(parts.join(" · ")) + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="bgd-status-state" data-state="' + escHtml(entry.state || "inactive") + '">' + escHtml(prettyState(entry.state)) + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
html += "</div>";
|
||||
card.innerHTML = html;
|
||||
}
|
||||
|
||||
function prettyState(state) {
|
||||
switch (state) {
|
||||
case "active": return "Active";
|
||||
case "out_of_span": return "Out of span";
|
||||
case "waiting_for_spectrum": return "Waiting";
|
||||
case "missing_bookmark": return "Missing";
|
||||
case "no_supported_decoders": return "Unsupported";
|
||||
case "disabled": return "Disabled";
|
||||
default: return "Inactive";
|
||||
}
|
||||
}
|
||||
|
||||
function setCheckbox(id, value) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.checked = !!value;
|
||||
}
|
||||
|
||||
function formatFreq(hz) {
|
||||
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
||||
if (hz >= 1e6) return (hz / 1e6).toFixed(3).replace(/\.?0+$/, "") + " MHz";
|
||||
if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
|
||||
return hz + " Hz";
|
||||
}
|
||||
|
||||
function escHtml(value) {
|
||||
return String(value == null ? "" : value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function showToast(msg, isError) {
|
||||
const el = document.getElementById("background-decode-toast");
|
||||
if (!el) return;
|
||||
el.textContent = msg;
|
||||
el.style.background = isError ? "var(--color-error, #c00)" : "var(--accent-green)";
|
||||
el.style.display = "block";
|
||||
setTimeout(function () {
|
||||
el.style.display = "none";
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function wireBackgroundDecodeEvents() {
|
||||
const rigSel = document.getElementById("background-decode-rig-select");
|
||||
if (rigSel) {
|
||||
rigSel.addEventListener("change", function () {
|
||||
currentRigId = rigSel.value;
|
||||
loadBackgroundDecode();
|
||||
});
|
||||
}
|
||||
|
||||
const addBtn = document.getElementById("background-decode-bookmark-add");
|
||||
if (addBtn) addBtn.addEventListener("click", addBookmark);
|
||||
|
||||
const saveBtn = document.getElementById("background-decode-save-btn");
|
||||
if (saveBtn) saveBtn.addEventListener("click", saveBackgroundDecode);
|
||||
|
||||
const resetBtn = document.getElementById("background-decode-reset-btn");
|
||||
if (resetBtn) resetBtn.addEventListener("click", resetBackgroundDecode);
|
||||
}
|
||||
|
||||
window.initBackgroundDecode = initBackgroundDecode;
|
||||
window.wireBackgroundDecodeEvents = wireBackgroundDecodeEvents;
|
||||
})();
|
||||
@@ -3491,6 +3491,94 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
line-height: 1;
|
||||
}
|
||||
.sch-extra-bm-rm:hover { opacity: 1; }
|
||||
.bgd-toggle-wrap {
|
||||
min-width: 18rem;
|
||||
}
|
||||
.bgd-toggle-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
min-height: var(--control-height);
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
.bgd-add-row {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
align-items: center;
|
||||
}
|
||||
.bgd-bookmark-pick {
|
||||
min-width: min(34rem, 100%);
|
||||
}
|
||||
.bgd-bookmark-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
.bgd-bookmark-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.35rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-light);
|
||||
background: var(--btn-bg);
|
||||
color: var(--text);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.bgd-bookmark-chip-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.bgd-bookmark-chip-remove {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
}
|
||||
.bgd-bookmark-chip-remove:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
.bgd-status-list {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
.bgd-status-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 0.55rem;
|
||||
background: color-mix(in srgb, var(--card-bg) 74%, transparent);
|
||||
}
|
||||
.bgd-status-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.bgd-status-meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.bgd-status-state {
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent-green);
|
||||
}
|
||||
.bgd-status-state[data-state="out_of_span"],
|
||||
.bgd-status-state[data-state="waiting_for_spectrum"],
|
||||
.bgd-status-state[data-state="inactive"] {
|
||||
color: var(--accent-yellow);
|
||||
}
|
||||
.bgd-status-state[data-state="missing_bookmark"],
|
||||
.bgd-status-state[data-state="no_supported_decoders"] {
|
||||
color: var(--accent-red);
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.sch-row {
|
||||
flex-direction: column;
|
||||
@@ -3498,4 +3586,9 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
.sch-label {
|
||||
min-width: 100%;
|
||||
}
|
||||
.bgd-add-row,
|
||||
.bgd-status-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1300,6 +1300,10 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(crate::server::scheduler::put_scheduler)
|
||||
.service(crate::server::scheduler::delete_scheduler)
|
||||
.service(crate::server::scheduler::get_scheduler_status)
|
||||
.service(crate::server::background_decode::get_background_decode)
|
||||
.service(crate::server::background_decode::put_background_decode)
|
||||
.service(crate::server::background_decode::delete_background_decode)
|
||||
.service(crate::server::background_decode::get_background_decode_status)
|
||||
.service(crate::server::audio::audio_ws)
|
||||
.service(favicon)
|
||||
.service(favicon_png)
|
||||
@@ -1317,6 +1321,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(cw_js)
|
||||
.service(bookmarks_js)
|
||||
.service(scheduler_js)
|
||||
.service(background_decode_js)
|
||||
.service(vchan_js)
|
||||
// Virtual channels
|
||||
.service(list_channels)
|
||||
@@ -1487,6 +1492,16 @@ async fn scheduler_js() -> impl Responder {
|
||||
.body(status::SCHEDULER_JS)
|
||||
}
|
||||
|
||||
#[get("/background-decode.js")]
|
||||
async fn background_decode_js() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
.insert_header((
|
||||
header::CONTENT_TYPE,
|
||||
"application/javascript; charset=utf-8",
|
||||
))
|
||||
.body(status::BACKGROUND_DECODE_JS)
|
||||
}
|
||||
|
||||
#[get("/vchan.js")]
|
||||
async fn vchan_js() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stanislaw Grams <stanislawgrams@gmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use actix_web::{delete, get, put, web, HttpResponse, Responder};
|
||||
use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::time;
|
||||
use tracing::warn;
|
||||
use trx_frontend::{FrontendRuntimeContext, SharedSpectrum, VChanAudioCmd};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::server::bookmarks::{Bookmark, BookmarkStore};
|
||||
|
||||
const SUPPORTED_DECODER_KINDS: &[&str] = &["ft8", "wspr", "hf-aprs"];
|
||||
const CHANNEL_KIND_NAME: &str = "VirtualBackgroundDecodeChannel";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BackgroundDecodeConfig {
|
||||
pub rig_id: String,
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub bookmark_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
pub struct BackgroundDecodeBookmarkStatus {
|
||||
pub bookmark_id: String,
|
||||
pub bookmark_name: Option<String>,
|
||||
pub freq_hz: Option<u64>,
|
||||
pub mode: Option<String>,
|
||||
#[serde(default)]
|
||||
pub decoder_kinds: Vec<String>,
|
||||
pub state: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub channel_kind: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
pub struct BackgroundDecodeStatus {
|
||||
pub rig_id: String,
|
||||
pub enabled: bool,
|
||||
pub active_rig: bool,
|
||||
pub center_hz: Option<u64>,
|
||||
pub sample_rate: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub entries: Vec<BackgroundDecodeBookmarkStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct VirtualBackgroundDecodeChannel {
|
||||
uuid: Uuid,
|
||||
rig_id: String,
|
||||
bookmark_id: String,
|
||||
freq_hz: u64,
|
||||
mode: String,
|
||||
bandwidth_hz: u32,
|
||||
decoder_kinds: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BackgroundRuntimeState {
|
||||
current_rig_id: Option<String>,
|
||||
active_channels: HashMap<String, VirtualBackgroundDecodeChannel>,
|
||||
}
|
||||
|
||||
pub struct BackgroundDecodeStore {
|
||||
db: Arc<RwLock<PickleDb>>,
|
||||
}
|
||||
|
||||
impl BackgroundDecodeStore {
|
||||
pub fn open(path: &Path) -> Self {
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let db = if path.exists() {
|
||||
PickleDb::load(path, PickleDbDumpPolicy::AutoDump, SerializationMethod::Json)
|
||||
.unwrap_or_else(|_| {
|
||||
PickleDb::new(path, PickleDbDumpPolicy::AutoDump, SerializationMethod::Json)
|
||||
})
|
||||
} else {
|
||||
PickleDb::new(path, PickleDbDumpPolicy::AutoDump, SerializationMethod::Json)
|
||||
};
|
||||
Self {
|
||||
db: Arc::new(RwLock::new(db)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_path() -> PathBuf {
|
||||
dirs::config_dir()
|
||||
.map(|p| p.join("trx-rs").join("background_decode.db"))
|
||||
.unwrap_or_else(|| PathBuf::from("background_decode.db"))
|
||||
}
|
||||
|
||||
pub fn get(&self, rig_id: &str) -> Option<BackgroundDecodeConfig> {
|
||||
let db = self.db.read().unwrap_or_else(|e| e.into_inner());
|
||||
db.get::<BackgroundDecodeConfig>(&format!("bgd:{rig_id}"))
|
||||
}
|
||||
|
||||
pub fn upsert(&self, config: &BackgroundDecodeConfig) -> bool {
|
||||
let mut db = self.db.write().unwrap_or_else(|e| e.into_inner());
|
||||
db.set(&format!("bgd:{}", config.rig_id), config).is_ok()
|
||||
}
|
||||
|
||||
pub fn remove(&self, rig_id: &str) -> bool {
|
||||
let mut db = self.db.write().unwrap_or_else(|e| e.into_inner());
|
||||
db.rem(&format!("bgd:{rig_id}")).unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BackgroundDecodeManager {
|
||||
store: Arc<BackgroundDecodeStore>,
|
||||
bookmarks: Arc<BookmarkStore>,
|
||||
context: Arc<FrontendRuntimeContext>,
|
||||
status: Arc<RwLock<HashMap<String, BackgroundDecodeStatus>>>,
|
||||
notify_tx: broadcast::Sender<()>,
|
||||
}
|
||||
|
||||
impl BackgroundDecodeManager {
|
||||
pub fn new(
|
||||
store: Arc<BackgroundDecodeStore>,
|
||||
bookmarks: Arc<BookmarkStore>,
|
||||
context: Arc<FrontendRuntimeContext>,
|
||||
) -> Arc<Self> {
|
||||
let (notify_tx, _) = broadcast::channel(16);
|
||||
Arc::new(Self {
|
||||
store,
|
||||
bookmarks,
|
||||
context,
|
||||
status: Arc::new(RwLock::new(HashMap::new())),
|
||||
notify_tx,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn spawn(self: &Arc<Self>) {
|
||||
let manager = self.clone();
|
||||
tokio::spawn(async move {
|
||||
manager.run().await;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_config(&self, rig_id: &str) -> BackgroundDecodeConfig {
|
||||
self.store.get(rig_id).unwrap_or_else(|| BackgroundDecodeConfig {
|
||||
rig_id: rig_id.to_string(),
|
||||
enabled: false,
|
||||
bookmark_ids: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn put_config(&self, mut config: BackgroundDecodeConfig) -> Option<BackgroundDecodeConfig> {
|
||||
config.bookmark_ids = dedup_ids(&config.bookmark_ids);
|
||||
if self.store.upsert(&config) {
|
||||
self.trigger();
|
||||
Some(config)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_config(&self, rig_id: &str) -> bool {
|
||||
let removed = self.store.remove(rig_id);
|
||||
self.trigger();
|
||||
removed
|
||||
}
|
||||
|
||||
pub fn status(&self, rig_id: &str) -> BackgroundDecodeStatus {
|
||||
if let Ok(status) = self.status.read() {
|
||||
if let Some(entry) = status.get(rig_id) {
|
||||
return entry.clone();
|
||||
}
|
||||
}
|
||||
let cfg = self.get_config(rig_id);
|
||||
let bookmarks: HashMap<String, Bookmark> = self
|
||||
.bookmarks
|
||||
.list()
|
||||
.into_iter()
|
||||
.map(|bookmark| (bookmark.id.clone(), bookmark))
|
||||
.collect();
|
||||
BackgroundDecodeStatus {
|
||||
rig_id: rig_id.to_string(),
|
||||
enabled: cfg.enabled,
|
||||
active_rig: self.active_rig_id().as_deref() == Some(rig_id),
|
||||
center_hz: None,
|
||||
sample_rate: None,
|
||||
entries: cfg
|
||||
.bookmark_ids
|
||||
.into_iter()
|
||||
.map(|bookmark_id| {
|
||||
let bookmark = bookmarks.get(&bookmark_id);
|
||||
BackgroundDecodeBookmarkStatus {
|
||||
bookmark_id,
|
||||
bookmark_name: bookmark.map(|item| item.name.clone()),
|
||||
freq_hz: bookmark.map(|item| item.freq_hz),
|
||||
mode: bookmark.map(|item| item.mode.clone()),
|
||||
decoder_kinds: bookmark
|
||||
.map(|item| supported_decoder_kinds(&item.decoders))
|
||||
.unwrap_or_default(),
|
||||
state: "inactive".to_string(),
|
||||
channel_kind: None,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trigger(&self) {
|
||||
let _ = self.notify_tx.send(());
|
||||
}
|
||||
|
||||
fn active_rig_id(&self) -> Option<String> {
|
||||
self.context
|
||||
.remote_active_rig_id
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|guard| guard.clone())
|
||||
}
|
||||
|
||||
fn send_audio_cmd(&self, cmd: VChanAudioCmd) {
|
||||
if let Ok(guard) = self.context.vchan_audio_cmd.lock() {
|
||||
if let Some(tx) = guard.as_ref() {
|
||||
let _ = tx.try_send(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_channel(&self, channel: &VirtualBackgroundDecodeChannel) {
|
||||
self.send_audio_cmd(VChanAudioCmd::Remove(channel.uuid));
|
||||
}
|
||||
|
||||
fn clear_runtime_channels(&self, runtime: &mut BackgroundRuntimeState) {
|
||||
let channels: Vec<VirtualBackgroundDecodeChannel> =
|
||||
runtime.active_channels.drain().map(|(_, ch)| ch).collect();
|
||||
for channel in channels {
|
||||
self.remove_channel(&channel);
|
||||
}
|
||||
runtime.current_rig_id = None;
|
||||
}
|
||||
|
||||
fn desired_channel(
|
||||
&self,
|
||||
rig_id: &str,
|
||||
bookmark: &Bookmark,
|
||||
decoder_kinds: Vec<String>,
|
||||
) -> VirtualBackgroundDecodeChannel {
|
||||
VirtualBackgroundDecodeChannel {
|
||||
uuid: Uuid::new_v4(),
|
||||
rig_id: rig_id.to_string(),
|
||||
bookmark_id: bookmark.id.clone(),
|
||||
freq_hz: bookmark.freq_hz,
|
||||
mode: bookmark.mode.clone(),
|
||||
bandwidth_hz: bookmark
|
||||
.bandwidth_hz
|
||||
.unwrap_or(0)
|
||||
.min(u32::MAX as u64) as u32,
|
||||
decoder_kinds,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_matches(
|
||||
channel: &VirtualBackgroundDecodeChannel,
|
||||
desired: &VirtualBackgroundDecodeChannel,
|
||||
) -> bool {
|
||||
channel.rig_id == desired.rig_id
|
||||
&& channel.bookmark_id == desired.bookmark_id
|
||||
&& channel.freq_hz == desired.freq_hz
|
||||
&& channel.mode == desired.mode
|
||||
&& channel.bandwidth_hz == desired.bandwidth_hz
|
||||
&& channel.decoder_kinds == desired.decoder_kinds
|
||||
}
|
||||
|
||||
fn reconcile(&self, runtime: &mut BackgroundRuntimeState, spectrum: &SharedSpectrum) {
|
||||
let active_rig_id = self.active_rig_id();
|
||||
|
||||
if runtime.current_rig_id != active_rig_id {
|
||||
if let Some(prev_rig_id) = runtime.current_rig_id.clone() {
|
||||
if let Ok(mut guard) = self.status.write() {
|
||||
if let Some(prev_status) = guard.get_mut(&prev_rig_id) {
|
||||
prev_status.active_rig = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.clear_runtime_channels(runtime);
|
||||
}
|
||||
|
||||
let Some(rig_id) = active_rig_id else {
|
||||
return;
|
||||
};
|
||||
runtime.current_rig_id = Some(rig_id.clone());
|
||||
|
||||
let config = self.get_config(&rig_id);
|
||||
let selected = dedup_ids(&config.bookmark_ids);
|
||||
let selected_bookmarks: HashMap<String, Bookmark> = self
|
||||
.bookmarks
|
||||
.list()
|
||||
.into_iter()
|
||||
.filter(|bookmark| selected.iter().any(|id| id == &bookmark.id))
|
||||
.map(|bookmark| (bookmark.id.clone(), bookmark))
|
||||
.collect();
|
||||
|
||||
let frame = spectrum.frame.as_ref().map(Arc::as_ref);
|
||||
let center_hz = frame.map(|frame| frame.center_hz);
|
||||
let sample_rate = frame.map(|frame| frame.sample_rate);
|
||||
let half_span_hz = frame.map(|frame| i64::from(frame.sample_rate) / 2);
|
||||
|
||||
let mut statuses = Vec::new();
|
||||
let mut desired_channels = HashMap::new();
|
||||
|
||||
for bookmark_id in selected {
|
||||
let Some(bookmark) = selected_bookmarks.get(&bookmark_id) else {
|
||||
statuses.push(BackgroundDecodeBookmarkStatus {
|
||||
bookmark_id,
|
||||
state: "missing_bookmark".to_string(),
|
||||
..BackgroundDecodeBookmarkStatus::default()
|
||||
});
|
||||
continue;
|
||||
};
|
||||
|
||||
let decoder_kinds = supported_decoder_kinds(&bookmark.decoders);
|
||||
let mut status = BackgroundDecodeBookmarkStatus {
|
||||
bookmark_id: bookmark.id.clone(),
|
||||
bookmark_name: Some(bookmark.name.clone()),
|
||||
freq_hz: Some(bookmark.freq_hz),
|
||||
mode: Some(bookmark.mode.clone()),
|
||||
decoder_kinds: decoder_kinds.clone(),
|
||||
state: "disabled".to_string(),
|
||||
channel_kind: None,
|
||||
};
|
||||
|
||||
if decoder_kinds.is_empty() {
|
||||
status.state = "no_supported_decoders".to_string();
|
||||
statuses.push(status);
|
||||
continue;
|
||||
}
|
||||
|
||||
if !config.enabled {
|
||||
statuses.push(status);
|
||||
continue;
|
||||
}
|
||||
|
||||
let (Some(center_hz), Some(half_span_hz)) = (center_hz, half_span_hz) else {
|
||||
status.state = "waiting_for_spectrum".to_string();
|
||||
statuses.push(status);
|
||||
continue;
|
||||
};
|
||||
|
||||
let offset_hz = bookmark.freq_hz as i64 - center_hz as i64;
|
||||
if offset_hz.abs() > half_span_hz {
|
||||
status.state = "out_of_span".to_string();
|
||||
statuses.push(status);
|
||||
continue;
|
||||
}
|
||||
|
||||
status.state = "active".to_string();
|
||||
status.channel_kind = Some(CHANNEL_KIND_NAME.to_string());
|
||||
let desired = self.desired_channel(&rig_id, bookmark, decoder_kinds);
|
||||
desired_channels.insert(bookmark.id.clone(), desired);
|
||||
statuses.push(status);
|
||||
}
|
||||
|
||||
let mut to_remove = Vec::new();
|
||||
for (bookmark_id, channel) in &runtime.active_channels {
|
||||
if let Some(desired) = desired_channels.get(bookmark_id) {
|
||||
if !Self::channel_matches(channel, desired) {
|
||||
to_remove.push(bookmark_id.clone());
|
||||
}
|
||||
} else {
|
||||
to_remove.push(bookmark_id.clone());
|
||||
}
|
||||
}
|
||||
for bookmark_id in to_remove {
|
||||
if let Some(channel) = runtime.active_channels.remove(&bookmark_id) {
|
||||
self.remove_channel(&channel);
|
||||
}
|
||||
}
|
||||
|
||||
for (bookmark_id, desired) in desired_channels {
|
||||
if runtime.active_channels.contains_key(&bookmark_id) {
|
||||
continue;
|
||||
}
|
||||
self.send_audio_cmd(VChanAudioCmd::SubscribeBackground {
|
||||
uuid: desired.uuid,
|
||||
freq_hz: desired.freq_hz,
|
||||
mode: desired.mode.clone(),
|
||||
bandwidth_hz: desired.bandwidth_hz,
|
||||
decoder_kinds: desired.decoder_kinds.clone(),
|
||||
});
|
||||
runtime.active_channels.insert(bookmark_id, desired);
|
||||
}
|
||||
|
||||
if let Ok(mut guard) = self.status.write() {
|
||||
guard.insert(
|
||||
rig_id.clone(),
|
||||
BackgroundDecodeStatus {
|
||||
rig_id,
|
||||
enabled: config.enabled,
|
||||
active_rig: true,
|
||||
center_hz,
|
||||
sample_rate,
|
||||
entries: statuses,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(self: Arc<Self>) {
|
||||
let mut runtime = BackgroundRuntimeState::default();
|
||||
let mut notify_rx = self.notify_tx.subscribe();
|
||||
let mut spectrum_rx = self.context.spectrum.subscribe();
|
||||
let mut interval = time::interval(Duration::from_secs(2));
|
||||
|
||||
loop {
|
||||
self.reconcile(&mut runtime, &spectrum_rx.borrow().clone());
|
||||
tokio::select! {
|
||||
changed = spectrum_rx.changed() => {
|
||||
if changed.is_err() {
|
||||
warn!("background decode: spectrum watch closed");
|
||||
self.clear_runtime_channels(&mut runtime);
|
||||
break;
|
||||
}
|
||||
}
|
||||
recv = notify_rx.recv() => {
|
||||
match recv {
|
||||
Ok(()) => {}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
_ = interval.tick() => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dedup_ids(ids: &[String]) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for id in ids {
|
||||
if !out.iter().any(|existing| existing == id) {
|
||||
out.push(id.clone());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn supported_decoder_kinds(decoders: &[String]) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
for decoder in decoders {
|
||||
let decoder = decoder.trim().to_ascii_lowercase();
|
||||
if SUPPORTED_DECODER_KINDS.contains(&decoder.as_str())
|
||||
&& !out.iter().any(|existing| existing == &decoder)
|
||||
{
|
||||
out.push(decoder);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[get("/background-decode/{rig_id}")]
|
||||
pub async fn get_background_decode(
|
||||
path: web::Path<String>,
|
||||
manager: web::Data<Arc<BackgroundDecodeManager>>,
|
||||
) -> impl Responder {
|
||||
HttpResponse::Ok().json(manager.get_config(&path.into_inner()))
|
||||
}
|
||||
|
||||
#[put("/background-decode/{rig_id}")]
|
||||
pub async fn put_background_decode(
|
||||
path: web::Path<String>,
|
||||
body: web::Json<BackgroundDecodeConfig>,
|
||||
manager: web::Data<Arc<BackgroundDecodeManager>>,
|
||||
) -> impl Responder {
|
||||
let rig_id = path.into_inner();
|
||||
let mut config = body.into_inner();
|
||||
config.rig_id = rig_id;
|
||||
match manager.put_config(config) {
|
||||
Some(saved) => HttpResponse::Ok().json(saved),
|
||||
None => HttpResponse::InternalServerError().body("failed to save background decode config"),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/background-decode/{rig_id}")]
|
||||
pub async fn delete_background_decode(
|
||||
path: web::Path<String>,
|
||||
manager: web::Data<Arc<BackgroundDecodeManager>>,
|
||||
) -> impl Responder {
|
||||
let rig_id = path.into_inner();
|
||||
manager.reset_config(&rig_id);
|
||||
HttpResponse::Ok().json(BackgroundDecodeConfig {
|
||||
rig_id,
|
||||
enabled: false,
|
||||
bookmark_ids: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[get("/background-decode/{rig_id}/status")]
|
||||
pub async fn get_background_decode_status(
|
||||
path: web::Path<String>,
|
||||
manager: web::Data<Arc<BackgroundDecodeManager>>,
|
||||
) -> impl Responder {
|
||||
HttpResponse::Ok().json(manager.status(&path.into_inner()))
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
mod api;
|
||||
#[path = "audio.rs"]
|
||||
pub mod audio;
|
||||
#[path = "background_decode.rs"]
|
||||
pub mod background_decode;
|
||||
#[path = "auth.rs"]
|
||||
pub mod auth;
|
||||
#[path = "bookmarks.rs"]
|
||||
@@ -37,6 +39,7 @@ use trx_core::RigState;
|
||||
use trx_frontend::{FrontendRuntimeContext, FrontendSpawner};
|
||||
|
||||
use auth::{AuthConfig, AuthState, SameSite};
|
||||
use background_decode::{BackgroundDecodeManager, BackgroundDecodeStore};
|
||||
use scheduler::{SchedulerStatusMap, SchedulerStore};
|
||||
use vchan::ClientChannelManager;
|
||||
|
||||
@@ -71,17 +74,27 @@ async fn serve(
|
||||
let scheduler_path = SchedulerStore::default_path();
|
||||
let scheduler_store = Arc::new(SchedulerStore::open(&scheduler_path));
|
||||
let bookmark_path = bookmarks::BookmarkStore::default_path();
|
||||
let bookmark_store_for_scheduler = Arc::new(bookmarks::BookmarkStore::open(&bookmark_path));
|
||||
let bookmark_store = Arc::new(bookmarks::BookmarkStore::open(&bookmark_path));
|
||||
let scheduler_status: SchedulerStatusMap = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
scheduler::spawn_scheduler_task(
|
||||
context.clone(),
|
||||
rig_tx.clone(),
|
||||
scheduler_store.clone(),
|
||||
bookmark_store_for_scheduler,
|
||||
bookmark_store.clone(),
|
||||
scheduler_status.clone(),
|
||||
);
|
||||
|
||||
let background_decode_path = BackgroundDecodeStore::default_path();
|
||||
let background_decode_store =
|
||||
Arc::new(BackgroundDecodeStore::open(&background_decode_path));
|
||||
let background_decode_mgr = BackgroundDecodeManager::new(
|
||||
background_decode_store,
|
||||
bookmark_store.clone(),
|
||||
context.clone(),
|
||||
);
|
||||
background_decode_mgr.spawn();
|
||||
|
||||
let vchan_mgr = Arc::new(ClientChannelManager::new(4));
|
||||
|
||||
// Wire the audio-command sender so allocate/delete/freq/mode operations on
|
||||
@@ -110,7 +123,18 @@ async fn serve(
|
||||
});
|
||||
}
|
||||
|
||||
let server = build_server(addr, state_rx, rig_tx, callsign, context, scheduler_store, scheduler_status, vchan_mgr)?;
|
||||
let server = build_server(
|
||||
addr,
|
||||
state_rx,
|
||||
rig_tx,
|
||||
callsign,
|
||||
context,
|
||||
bookmark_store,
|
||||
scheduler_store,
|
||||
scheduler_status,
|
||||
vchan_mgr,
|
||||
background_decode_mgr,
|
||||
)?;
|
||||
let handle = server.handle();
|
||||
tokio::spawn(async move {
|
||||
let _ = signal::ctrl_c().await;
|
||||
@@ -129,9 +153,11 @@ fn build_server(
|
||||
rig_tx: mpsc::Sender<RigRequest>,
|
||||
_callsign: Option<String>,
|
||||
context: Arc<FrontendRuntimeContext>,
|
||||
bookmark_store: Arc<bookmarks::BookmarkStore>,
|
||||
scheduler_store: Arc<SchedulerStore>,
|
||||
scheduler_status: SchedulerStatusMap,
|
||||
vchan_mgr: Arc<ClientChannelManager>,
|
||||
background_decode_mgr: Arc<BackgroundDecodeManager>,
|
||||
) -> Result<Server, actix_web::Error> {
|
||||
let state_data = web::Data::new(state_rx);
|
||||
let rig_tx = web::Data::new(rig_tx);
|
||||
@@ -139,12 +165,12 @@ fn build_server(
|
||||
// scheduler task can observe the connected-client count.
|
||||
let clients = web::Data::new(context.sse_clients.clone());
|
||||
|
||||
let bookmark_path = bookmarks::BookmarkStore::default_path();
|
||||
let bookmark_store = web::Data::new(Arc::new(bookmarks::BookmarkStore::open(&bookmark_path)));
|
||||
let bookmark_store = web::Data::new(bookmark_store);
|
||||
|
||||
let scheduler_store = web::Data::new(scheduler_store);
|
||||
let scheduler_status = web::Data::new(scheduler_status);
|
||||
let vchan_mgr = web::Data::new(vchan_mgr);
|
||||
let background_decode_mgr = web::Data::new(background_decode_mgr);
|
||||
|
||||
// Extract auth config values before moving context
|
||||
let same_site = match context.http_auth_cookie_same_site.as_str() {
|
||||
@@ -188,6 +214,7 @@ fn build_server(
|
||||
.app_data(scheduler_store.clone())
|
||||
.app_data(scheduler_status.clone())
|
||||
.app_data(vchan_mgr.clone())
|
||||
.app_data(background_decode_mgr.clone())
|
||||
.wrap(Compress::default())
|
||||
.wrap(
|
||||
DefaultHeaders::new()
|
||||
|
||||
@@ -21,6 +21,8 @@ pub const WSPR_JS: &str = include_str!("../assets/web/plugins/wspr.js");
|
||||
pub const CW_JS: &str = include_str!("../assets/web/plugins/cw.js");
|
||||
pub const BOOKMARKS_JS: &str = include_str!("../assets/web/plugins/bookmarks.js");
|
||||
pub const SCHEDULER_JS: &str = include_str!("../assets/web/plugins/scheduler.js");
|
||||
pub const BACKGROUND_DECODE_JS: &str =
|
||||
include_str!("../assets/web/plugins/background-decode.js");
|
||||
pub const VCHAN_JS: &str = include_str!("../assets/web/plugins/vchan.js");
|
||||
|
||||
pub fn index_html() -> String {
|
||||
|
||||
Reference in New Issue
Block a user