build: enable frontend module code splitting
This commit is contained in:
@@ -1,459 +1,456 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// src/plugins/vchan.ts
|
||||
var vchanWindow = window;
|
||||
var vchanSessionId = null;
|
||||
var vchanRigId = null;
|
||||
var vchanChannels = [];
|
||||
var vchanActiveId = null;
|
||||
var schedulerReleaseState = null;
|
||||
var schedulerReleasePollTimer = null;
|
||||
function vchanFmtFreq(hz) {
|
||||
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
||||
if (hz >= 1e9) return (hz / 1e9).toFixed(4).replace(/\.?0+$/, "") + " GHz";
|
||||
if (hz >= 1e6) return (hz / 1e6).toFixed(4).replace(/\.?0+$/, "") + " MHz";
|
||||
if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
|
||||
return `${String(hz)} Hz`;
|
||||
// src/plugins/vchan.ts
|
||||
var vchanWindow = window;
|
||||
var vchanSessionId = null;
|
||||
var vchanRigId = null;
|
||||
var vchanChannels = [];
|
||||
var vchanActiveId = null;
|
||||
var schedulerReleaseState = null;
|
||||
var schedulerReleasePollTimer = null;
|
||||
function vchanFmtFreq(hz) {
|
||||
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
||||
if (hz >= 1e9) return (hz / 1e9).toFixed(4).replace(/\.?0+$/, "") + " GHz";
|
||||
if (hz >= 1e6) return (hz / 1e6).toFixed(4).replace(/\.?0+$/, "") + " MHz";
|
||||
if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
|
||||
return `${String(hz)} Hz`;
|
||||
}
|
||||
function schedulerReleaseSummaryText(state) {
|
||||
if (!state) return "Scheduler is controlling the rig.";
|
||||
const connected = Number(state.connected_sessions) || 0;
|
||||
const released = Number(state.released_sessions) || 0;
|
||||
if (connected === 0) return "Scheduler can control the rig.";
|
||||
if (state.all_released) {
|
||||
return connected === 1 ? "Scheduler is controlling the rig." : `Scheduler is controlling the rig for all ${connected} users.`;
|
||||
}
|
||||
function schedulerReleaseSummaryText(state) {
|
||||
if (!state) return "Scheduler is controlling the rig.";
|
||||
const connected = Number(state.connected_sessions) || 0;
|
||||
const released = Number(state.released_sessions) || 0;
|
||||
if (connected === 0) return "Scheduler can control the rig.";
|
||||
if (state.all_released) {
|
||||
return connected === 1 ? "Scheduler is controlling the rig." : `Scheduler is controlling the rig for all ${connected} users.`;
|
||||
if (!state.current_session_released) {
|
||||
const othersReleased = Math.max(released, 0);
|
||||
return othersReleased > 0 ? `You are holding control. ${othersReleased} other user${othersReleased === 1 ? "" : "s"} already released it.` : "You are holding control. Release it to return control to the scheduler.";
|
||||
}
|
||||
const blocking = Math.max(connected - released, 0);
|
||||
return blocking > 0 ? `Scheduler is waiting for ${blocking} user${blocking === 1 ? "" : "s"} to stop manual tuning.` : "Scheduler can control the rig.";
|
||||
}
|
||||
function vchanRenderSchedulerRelease() {
|
||||
const btn = document.getElementById("scheduler-release-btn");
|
||||
const status = document.getElementById("scheduler-release-status");
|
||||
if (!btn || !status) return;
|
||||
const currentReleased = !!(schedulerReleaseState && schedulerReleaseState.current_session_released);
|
||||
btn.disabled = !vchanSessionId || currentReleased;
|
||||
btn.classList.toggle("active", !currentReleased);
|
||||
btn.textContent = "Release to Scheduler";
|
||||
status.textContent = schedulerReleaseSummaryText(schedulerReleaseState);
|
||||
}
|
||||
async function vchanPollSchedulerRelease() {
|
||||
if (!vchanSessionId) {
|
||||
schedulerReleaseState = null;
|
||||
vchanRenderSchedulerRelease();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`/scheduler-control?session_id=${encodeURIComponent(vchanSessionId)}`);
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
schedulerReleaseState = await resp.json();
|
||||
vchanRenderSchedulerRelease();
|
||||
} catch (e) {
|
||||
console.error("scheduler release status failed", e);
|
||||
}
|
||||
}
|
||||
function vchanStartSchedulerReleasePolling() {
|
||||
if (schedulerReleasePollTimer) {
|
||||
clearInterval(schedulerReleasePollTimer);
|
||||
}
|
||||
schedulerReleasePollTimer = setInterval(() => {
|
||||
void vchanPollSchedulerRelease();
|
||||
}, 1e4);
|
||||
}
|
||||
async function vchanToggleSchedulerRelease() {
|
||||
if (!vchanSessionId) return;
|
||||
const rigId = vchanRigId || vchanWindow.lastActiveRigId || null;
|
||||
try {
|
||||
const resp = await fetch("/scheduler-control", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_id: vchanSessionId, released: true, remote: rigId })
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
schedulerReleaseState = await resp.json();
|
||||
vchanRenderSchedulerRelease();
|
||||
} catch (e) {
|
||||
console.error("scheduler release toggle failed", e);
|
||||
}
|
||||
}
|
||||
async function vchanTakeSchedulerControl() {
|
||||
if (!vchanSessionId) return;
|
||||
if (schedulerReleaseState && !schedulerReleaseState.current_session_released) return;
|
||||
try {
|
||||
const resp = await fetch("/scheduler-control", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_id: vchanSessionId, released: false })
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
schedulerReleaseState = await resp.json();
|
||||
vchanRenderSchedulerRelease();
|
||||
} catch (e) {
|
||||
console.error("scheduler control takeover failed", e);
|
||||
}
|
||||
}
|
||||
vchanWindow.vchanTakeSchedulerControl = vchanTakeSchedulerControl;
|
||||
function vchanHandleSession(data) {
|
||||
try {
|
||||
const d = JSON.parse(data);
|
||||
vchanSessionId = d.session_id || null;
|
||||
void vchanPollSchedulerRelease();
|
||||
} catch (e) {
|
||||
console.warn("vchan: bad session event", e);
|
||||
}
|
||||
}
|
||||
function vchanHandleChannels(data) {
|
||||
try {
|
||||
const d = JSON.parse(data);
|
||||
vchanRigId = d.remote || null;
|
||||
vchanChannels = d.channels || [];
|
||||
const ids = new Set(vchanChannels.map((c) => c.id));
|
||||
const primaryChannel = vchanChannels[0];
|
||||
if (!vchanActiveId && primaryChannel && vchanSessionId) {
|
||||
void vchanAutoJoinPrimary(primaryChannel.id);
|
||||
} else if (vchanActiveId && !ids.has(vchanActiveId)) {
|
||||
vchanActiveId = vchanChannels[0]?.id ?? null;
|
||||
vchanReconnectAudio();
|
||||
}
|
||||
if (!state.current_session_released) {
|
||||
const othersReleased = Math.max(released, 0);
|
||||
return othersReleased > 0 ? `You are holding control. ${othersReleased} other user${othersReleased === 1 ? "" : "s"} already released it.` : "You are holding control. Release it to return control to the scheduler.";
|
||||
vchanRender();
|
||||
vchanRenderSchedulerRelease();
|
||||
vchanWindow.renderRdsOverlays?.();
|
||||
} catch (e) {
|
||||
console.warn("vchan: bad channels event", e);
|
||||
}
|
||||
}
|
||||
vchanWindow.vchanHandleSession = vchanHandleSession;
|
||||
vchanWindow.vchanHandleChannels = vchanHandleChannels;
|
||||
function vchanRender() {
|
||||
const picker = document.getElementById("vchan-picker");
|
||||
if (!picker) return;
|
||||
picker.innerHTML = "";
|
||||
vchanChannels.forEach((ch) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.title = `Ch ${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode} · ${ch.subscribers} subscriber${ch.subscribers !== 1 ? "s" : ""}`;
|
||||
if (ch.id === vchanActiveId) btn.classList.add("active");
|
||||
const label = document.createElement("span");
|
||||
label.className = "vchan-label";
|
||||
label.textContent = `${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode}`;
|
||||
btn.appendChild(label);
|
||||
if (!ch.permanent) {
|
||||
const del = document.createElement("span");
|
||||
del.className = "vchan-del";
|
||||
del.textContent = "×";
|
||||
del.title = "Delete channel";
|
||||
del.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
void vchanDelete(ch.id);
|
||||
});
|
||||
btn.appendChild(del);
|
||||
}
|
||||
const blocking = Math.max(connected - released, 0);
|
||||
return blocking > 0 ? `Scheduler is waiting for ${blocking} user${blocking === 1 ? "" : "s"} to stop manual tuning.` : "Scheduler can control the rig.";
|
||||
btn.addEventListener("click", () => {
|
||||
if (ch.id !== vchanActiveId) void vchanSubscribe(ch.id);
|
||||
});
|
||||
picker.appendChild(btn);
|
||||
});
|
||||
const addBtn = document.createElement("button");
|
||||
addBtn.type = "button";
|
||||
addBtn.className = "vchan-add";
|
||||
addBtn.textContent = "+";
|
||||
addBtn.title = "Allocate new virtual channel at current frequency";
|
||||
addBtn.addEventListener("click", () => {
|
||||
void vchanAllocate();
|
||||
});
|
||||
picker.appendChild(addBtn);
|
||||
vchanSyncAccentUI();
|
||||
if (vchanWindow.updateDocumentTitle && vchanWindow.activeChannelRds) {
|
||||
vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
|
||||
}
|
||||
function vchanRenderSchedulerRelease() {
|
||||
const btn = document.getElementById("scheduler-release-btn");
|
||||
const status = document.getElementById("scheduler-release-status");
|
||||
if (!btn || !status) return;
|
||||
const currentReleased = !!(schedulerReleaseState && schedulerReleaseState.current_session_released);
|
||||
btn.disabled = !vchanSessionId || currentReleased;
|
||||
btn.classList.toggle("active", !currentReleased);
|
||||
btn.textContent = "Release to Scheduler";
|
||||
status.textContent = schedulerReleaseSummaryText(schedulerReleaseState);
|
||||
}
|
||||
async function vchanPollSchedulerRelease() {
|
||||
if (!vchanSessionId) {
|
||||
schedulerReleaseState = null;
|
||||
vchanRenderSchedulerRelease();
|
||||
vchanRenderSchedulerRelease();
|
||||
}
|
||||
async function vchanAllocate() {
|
||||
if (!vchanSessionId || !vchanRigId) return;
|
||||
const freqHz = typeof vchanWindow.lastFreqHz === "number" && vchanWindow.lastFreqHz > 0 ? vchanWindow.lastFreqHz : 0;
|
||||
const modeEl = document.getElementById("mode");
|
||||
const mode = modeEl ? modeEl.value || "USB" : "USB";
|
||||
try {
|
||||
const resp = await fetch(`/channels/${encodeURIComponent(vchanRigId)}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_id: vchanSessionId, freq_hz: freqHz, mode })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const msg = await resp.text().catch(() => String(resp.status));
|
||||
console.warn("vchan: allocate failed —", msg);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`/scheduler-control?session_id=${encodeURIComponent(vchanSessionId)}`);
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
schedulerReleaseState = await resp.json();
|
||||
vchanRenderSchedulerRelease();
|
||||
} catch (e) {
|
||||
console.error("scheduler release status failed", e);
|
||||
}
|
||||
const ch = await resp.json();
|
||||
vchanActiveId = ch.id;
|
||||
vchanRender();
|
||||
vchanReconnectAudio();
|
||||
} catch (e) {
|
||||
console.error("vchan: allocate error", e);
|
||||
}
|
||||
function vchanStartSchedulerReleasePolling() {
|
||||
if (schedulerReleasePollTimer) {
|
||||
clearInterval(schedulerReleasePollTimer);
|
||||
}
|
||||
async function vchanDelete(channelId) {
|
||||
if (!vchanRigId) return;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}`,
|
||||
{ method: "DELETE" }
|
||||
);
|
||||
if (!resp.ok) {
|
||||
console.warn("vchan: delete failed", resp.status);
|
||||
}
|
||||
schedulerReleasePollTimer = setInterval(() => {
|
||||
void vchanPollSchedulerRelease();
|
||||
}, 1e4);
|
||||
} catch (e) {
|
||||
console.error("vchan: delete error", e);
|
||||
}
|
||||
async function vchanToggleSchedulerRelease() {
|
||||
if (!vchanSessionId) return;
|
||||
const rigId = vchanRigId || vchanWindow.lastActiveRigId || null;
|
||||
try {
|
||||
const resp = await fetch("/scheduler-control", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_id: vchanSessionId, released: true, remote: rigId })
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
schedulerReleaseState = await resp.json();
|
||||
vchanRenderSchedulerRelease();
|
||||
} catch (e) {
|
||||
console.error("scheduler release toggle failed", e);
|
||||
}
|
||||
}
|
||||
async function vchanTakeSchedulerControl() {
|
||||
if (!vchanSessionId) return;
|
||||
if (schedulerReleaseState && !schedulerReleaseState.current_session_released) return;
|
||||
try {
|
||||
const resp = await fetch("/scheduler-control", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_id: vchanSessionId, released: false })
|
||||
});
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
schedulerReleaseState = await resp.json();
|
||||
vchanRenderSchedulerRelease();
|
||||
} catch (e) {
|
||||
console.error("scheduler control takeover failed", e);
|
||||
}
|
||||
}
|
||||
vchanWindow.vchanTakeSchedulerControl = vchanTakeSchedulerControl;
|
||||
function vchanHandleSession(data) {
|
||||
try {
|
||||
const d = JSON.parse(data);
|
||||
vchanSessionId = d.session_id || null;
|
||||
void vchanPollSchedulerRelease();
|
||||
} catch (e) {
|
||||
console.warn("vchan: bad session event", e);
|
||||
}
|
||||
}
|
||||
function vchanHandleChannels(data) {
|
||||
try {
|
||||
const d = JSON.parse(data);
|
||||
vchanRigId = d.remote || null;
|
||||
vchanChannels = d.channels || [];
|
||||
const ids = new Set(vchanChannels.map((c) => c.id));
|
||||
const primaryChannel = vchanChannels[0];
|
||||
if (!vchanActiveId && primaryChannel && vchanSessionId) {
|
||||
void vchanAutoJoinPrimary(primaryChannel.id);
|
||||
} else if (vchanActiveId && !ids.has(vchanActiveId)) {
|
||||
vchanActiveId = vchanChannels[0]?.id ?? null;
|
||||
vchanReconnectAudio();
|
||||
}
|
||||
vchanRender();
|
||||
vchanRenderSchedulerRelease();
|
||||
vchanWindow.renderRdsOverlays?.();
|
||||
} catch (e) {
|
||||
console.warn("vchan: bad channels event", e);
|
||||
}
|
||||
}
|
||||
vchanWindow.vchanHandleSession = vchanHandleSession;
|
||||
vchanWindow.vchanHandleChannels = vchanHandleChannels;
|
||||
function vchanRender() {
|
||||
const picker = document.getElementById("vchan-picker");
|
||||
if (!picker) return;
|
||||
picker.innerHTML = "";
|
||||
vchanChannels.forEach((ch) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.title = `Ch ${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode} · ${ch.subscribers} subscriber${ch.subscribers !== 1 ? "s" : ""}`;
|
||||
if (ch.id === vchanActiveId) btn.classList.add("active");
|
||||
const label = document.createElement("span");
|
||||
label.className = "vchan-label";
|
||||
label.textContent = `${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode}`;
|
||||
btn.appendChild(label);
|
||||
if (!ch.permanent) {
|
||||
const del = document.createElement("span");
|
||||
del.className = "vchan-del";
|
||||
del.textContent = "×";
|
||||
del.title = "Delete channel";
|
||||
del.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
void vchanDelete(ch.id);
|
||||
});
|
||||
btn.appendChild(del);
|
||||
}
|
||||
btn.addEventListener("click", () => {
|
||||
if (ch.id !== vchanActiveId) void vchanSubscribe(ch.id);
|
||||
});
|
||||
picker.appendChild(btn);
|
||||
});
|
||||
const addBtn = document.createElement("button");
|
||||
addBtn.type = "button";
|
||||
addBtn.className = "vchan-add";
|
||||
addBtn.textContent = "+";
|
||||
addBtn.title = "Allocate new virtual channel at current frequency";
|
||||
addBtn.addEventListener("click", () => {
|
||||
void vchanAllocate();
|
||||
});
|
||||
picker.appendChild(addBtn);
|
||||
vchanSyncAccentUI();
|
||||
if (vchanWindow.updateDocumentTitle && vchanWindow.activeChannelRds) {
|
||||
vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
|
||||
}
|
||||
vchanRenderSchedulerRelease();
|
||||
}
|
||||
async function vchanAllocate() {
|
||||
if (!vchanSessionId || !vchanRigId) return;
|
||||
const freqHz = typeof vchanWindow.lastFreqHz === "number" && vchanWindow.lastFreqHz > 0 ? vchanWindow.lastFreqHz : 0;
|
||||
const modeEl = document.getElementById("mode");
|
||||
const mode = modeEl ? modeEl.value || "USB" : "USB";
|
||||
try {
|
||||
const resp = await fetch(`/channels/${encodeURIComponent(vchanRigId)}`, {
|
||||
}
|
||||
async function vchanAutoJoinPrimary(channelId) {
|
||||
if (!vchanSessionId || !vchanRigId) return;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}/subscribe`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_id: vchanSessionId, freq_hz: freqHz, mode })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const msg = await resp.text().catch(() => String(resp.status));
|
||||
console.warn("vchan: allocate failed —", msg);
|
||||
return;
|
||||
body: JSON.stringify({ session_id: vchanSessionId })
|
||||
}
|
||||
const ch = await resp.json();
|
||||
vchanActiveId = ch.id;
|
||||
vchanRender();
|
||||
vchanReconnectAudio();
|
||||
} catch (e) {
|
||||
console.error("vchan: allocate error", e);
|
||||
);
|
||||
if (!resp.ok) {
|
||||
console.warn("vchan: auto-join primary failed", resp.status);
|
||||
return;
|
||||
}
|
||||
vchanActiveId = channelId;
|
||||
vchanRender();
|
||||
} catch (e) {
|
||||
console.error("vchan: auto-join error", e);
|
||||
}
|
||||
async function vchanDelete(channelId) {
|
||||
if (!vchanRigId) return;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}`,
|
||||
{ method: "DELETE" }
|
||||
);
|
||||
if (!resp.ok) {
|
||||
console.warn("vchan: delete failed", resp.status);
|
||||
}
|
||||
async function vchanSubscribe(channelId) {
|
||||
if (!vchanSessionId || !vchanRigId) return;
|
||||
try {
|
||||
await vchanTakeSchedulerControl();
|
||||
const resp = await fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}/subscribe`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_id: vchanSessionId })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("vchan: delete error", e);
|
||||
);
|
||||
if (!resp.ok) {
|
||||
console.warn("vchan: subscribe failed", resp.status);
|
||||
return;
|
||||
}
|
||||
vchanActiveId = channelId;
|
||||
vchanRender();
|
||||
vchanSyncModeDisplay();
|
||||
vchanReconnectAudio();
|
||||
} catch (e) {
|
||||
console.error("vchan: subscribe error", e);
|
||||
}
|
||||
async function vchanAutoJoinPrimary(channelId) {
|
||||
if (!vchanSessionId || !vchanRigId) return;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}/subscribe`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_id: vchanSessionId })
|
||||
}
|
||||
);
|
||||
if (!resp.ok) {
|
||||
console.warn("vchan: auto-join primary failed", resp.status);
|
||||
return;
|
||||
}
|
||||
vchanActiveId = channelId;
|
||||
vchanRender();
|
||||
} catch (e) {
|
||||
console.error("vchan: auto-join error", e);
|
||||
}
|
||||
}
|
||||
function vchanReconnectAudio() {
|
||||
const ch = vchanIsOnVirtual() ? vchanActiveChannel() : null;
|
||||
vchanWindow._audioChannelOverride = ch?.id ?? null;
|
||||
if (!vchanWindow.rxActive) return;
|
||||
vchanWindow.stopRxAudio?.();
|
||||
setTimeout(() => {
|
||||
vchanWindow.startRxAudio?.();
|
||||
}, 300);
|
||||
}
|
||||
function vchanApplyCapabilities(caps) {
|
||||
const picker = document.getElementById("vchan-picker");
|
||||
if (!picker) return;
|
||||
picker.style.display = caps && caps.filter_controls ? "" : "none";
|
||||
vchanRenderSchedulerRelease();
|
||||
}
|
||||
vchanWindow.vchanApplyCapabilities = vchanApplyCapabilities;
|
||||
function vchanIsOnVirtual() {
|
||||
if (!vchanActiveId || vchanChannels.length === 0) return false;
|
||||
return vchanActiveId !== vchanChannels[0]?.id;
|
||||
}
|
||||
vchanWindow.vchanIsOnVirtual = vchanIsOnVirtual;
|
||||
function vchanActiveChannel() {
|
||||
return vchanChannels.find((c) => c.id === vchanActiveId) || null;
|
||||
}
|
||||
function vchanUpdateFreqDisplay() {
|
||||
const ch = vchanActiveChannel();
|
||||
if (!ch) return;
|
||||
const el = document.getElementById("freq");
|
||||
if (!el) return;
|
||||
if (vchanWindow.formatFreqForStep && typeof vchanWindow.jogUnit === "number") {
|
||||
el.value = vchanWindow.formatFreqForStep(ch.freq_hz, vchanWindow.jogUnit);
|
||||
} else {
|
||||
el.value = (ch.freq_hz / 1e6).toFixed(6).replace(/\.?0+$/, "");
|
||||
}
|
||||
async function vchanSubscribe(channelId) {
|
||||
if (!vchanSessionId || !vchanRigId) return;
|
||||
try {
|
||||
await vchanTakeSchedulerControl();
|
||||
const resp = await fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}/subscribe`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ session_id: vchanSessionId })
|
||||
}
|
||||
);
|
||||
if (!resp.ok) {
|
||||
console.warn("vchan: subscribe failed", resp.status);
|
||||
return;
|
||||
}
|
||||
vchanActiveId = channelId;
|
||||
vchanRender();
|
||||
vchanSyncModeDisplay();
|
||||
vchanReconnectAudio();
|
||||
} catch (e) {
|
||||
console.error("vchan: subscribe error", e);
|
||||
}
|
||||
}
|
||||
function vchanReconnectAudio() {
|
||||
const ch = vchanIsOnVirtual() ? vchanActiveChannel() : null;
|
||||
vchanWindow._audioChannelOverride = ch?.id ?? null;
|
||||
if (!vchanWindow.rxActive) return;
|
||||
vchanWindow.stopRxAudio?.();
|
||||
setTimeout(() => {
|
||||
vchanWindow.startRxAudio?.();
|
||||
}, 300);
|
||||
}
|
||||
function vchanApplyCapabilities(caps) {
|
||||
const picker = document.getElementById("vchan-picker");
|
||||
if (!picker) return;
|
||||
picker.style.display = caps && caps.filter_controls ? "" : "none";
|
||||
vchanRenderSchedulerRelease();
|
||||
}
|
||||
vchanWindow.vchanApplyCapabilities = vchanApplyCapabilities;
|
||||
function vchanIsOnVirtual() {
|
||||
if (!vchanActiveId || vchanChannels.length === 0) return false;
|
||||
return vchanActiveId !== vchanChannels[0]?.id;
|
||||
}
|
||||
vchanWindow.vchanIsOnVirtual = vchanIsOnVirtual;
|
||||
function vchanActiveChannel() {
|
||||
return vchanChannels.find((c) => c.id === vchanActiveId) || null;
|
||||
}
|
||||
function vchanUpdateFreqDisplay() {
|
||||
}
|
||||
function vchanSyncModeDisplay() {
|
||||
const modeEl = document.getElementById("mode");
|
||||
if (!modeEl) return;
|
||||
if (vchanIsOnVirtual()) {
|
||||
const ch = vchanActiveChannel();
|
||||
if (!ch) return;
|
||||
const el = document.getElementById("freq");
|
||||
if (!el) return;
|
||||
if (vchanWindow.formatFreqForStep && typeof vchanWindow.jogUnit === "number") {
|
||||
el.value = vchanWindow.formatFreqForStep(ch.freq_hz, vchanWindow.jogUnit);
|
||||
} else {
|
||||
el.value = (ch.freq_hz / 1e6).toFixed(6).replace(/\.?0+$/, "");
|
||||
}
|
||||
if (ch && ch.mode) modeEl.value = ch.mode.toUpperCase();
|
||||
}
|
||||
function vchanSyncModeDisplay() {
|
||||
const modeEl = document.getElementById("mode");
|
||||
if (!modeEl) return;
|
||||
if (vchanIsOnVirtual()) {
|
||||
const ch = vchanActiveChannel();
|
||||
if (ch && ch.mode) modeEl.value = ch.mode.toUpperCase();
|
||||
const modeUpper = (modeEl.value || "").toUpperCase();
|
||||
if (typeof vchanWindow.lastModeName === "string") {
|
||||
if (modeUpper === "WFM" && vchanWindow.lastModeName !== "WFM") {
|
||||
vchanWindow.setJogDivisor?.(10);
|
||||
vchanWindow.resetRdsDisplay?.();
|
||||
} else if (modeUpper !== "WFM" && vchanWindow.lastModeName === "WFM") {
|
||||
vchanWindow.resetRdsDisplay?.();
|
||||
}
|
||||
const modeUpper = (modeEl.value || "").toUpperCase();
|
||||
if (typeof vchanWindow.lastModeName === "string") {
|
||||
if (modeUpper === "WFM" && vchanWindow.lastModeName !== "WFM") {
|
||||
vchanWindow.setJogDivisor?.(10);
|
||||
vchanWindow.resetRdsDisplay?.();
|
||||
} else if (modeUpper !== "WFM" && vchanWindow.lastModeName === "WFM") {
|
||||
vchanWindow.resetRdsDisplay?.();
|
||||
vchanWindow.lastModeName = modeUpper;
|
||||
}
|
||||
vchanWindow.updateWfmControls?.();
|
||||
vchanWindow.updateSdrSquelchControlVisibility?.();
|
||||
if (vchanWindow.refreshRdsUi) {
|
||||
vchanWindow.refreshRdsUi();
|
||||
} else {
|
||||
vchanWindow.positionRdsPsOverlay?.();
|
||||
}
|
||||
}
|
||||
function vchanSyncBwDisplay() {
|
||||
if (!vchanIsOnVirtual()) return;
|
||||
const ch = vchanActiveChannel();
|
||||
if (!ch) return;
|
||||
const bwEl = document.getElementById("spectrum-bw-input");
|
||||
if (!bwEl) return;
|
||||
let bwHz = ch.bandwidth_hz || 0;
|
||||
if (bwHz === 0 && vchanWindow.mwDefaultsForMode) {
|
||||
bwHz = vchanWindow.mwDefaultsForMode(ch.mode)[0] || 0;
|
||||
}
|
||||
if (bwHz > 0) {
|
||||
bwEl.value = (bwHz / 1e3).toFixed(3).replace(/\.?0+$/, "");
|
||||
vchanWindow.currentBandwidthHz = bwHz;
|
||||
}
|
||||
}
|
||||
function vchanSyncAccentUI() {
|
||||
const onVirtual = vchanIsOnVirtual();
|
||||
const freqEl = document.getElementById("freq");
|
||||
const bwEl = document.getElementById("spectrum-bw-input");
|
||||
if (freqEl) freqEl.classList.toggle("vchan-ch-active", onVirtual);
|
||||
if (bwEl) bwEl.classList.toggle("vchan-ch-active", onVirtual);
|
||||
if (onVirtual) {
|
||||
vchanUpdateFreqDisplay();
|
||||
vchanSyncModeDisplay();
|
||||
vchanSyncBwDisplay();
|
||||
} else {
|
||||
origRefreshFreqDisplay?.();
|
||||
}
|
||||
if (vchanWindow.updateDocumentTitle && vchanWindow.activeChannelRds) {
|
||||
vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
|
||||
}
|
||||
}
|
||||
var origRefreshFreqDisplay = null;
|
||||
function vchanSetChannelFreq(freqHz) {
|
||||
if (!vchanRigId || !vchanActiveId) return;
|
||||
if (vchanWindow.lastSpectrumData && vchanWindow.lastSpectrumData.sample_rate > 0) {
|
||||
const halfSpan = vchanWindow.lastSpectrumData.sample_rate / 2;
|
||||
const center = vchanWindow.lastSpectrumData.center_hz;
|
||||
if (Math.abs(freqHz - center) > halfSpan) {
|
||||
if (vchanWindow.showHint) {
|
||||
vchanWindow.showHint(
|
||||
`Out of SDR bandwidth (center ${(center / 1e6).toFixed(3)} MHz ±${(halfSpan / 1e3).toFixed(0)} kHz)`,
|
||||
3e3
|
||||
);
|
||||
}
|
||||
vchanWindow.lastModeName = modeUpper;
|
||||
}
|
||||
vchanWindow.updateWfmControls?.();
|
||||
vchanWindow.updateSdrSquelchControlVisibility?.();
|
||||
if (vchanWindow.refreshRdsUi) {
|
||||
vchanWindow.refreshRdsUi();
|
||||
} else {
|
||||
vchanWindow.positionRdsPsOverlay?.();
|
||||
return;
|
||||
}
|
||||
}
|
||||
function vchanSyncBwDisplay() {
|
||||
if (!vchanIsOnVirtual()) return;
|
||||
const ch = vchanActiveChannel();
|
||||
if (!ch) return;
|
||||
const bwEl = document.getElementById("spectrum-bw-input");
|
||||
if (!bwEl) return;
|
||||
let bwHz = ch.bandwidth_hz || 0;
|
||||
if (bwHz === 0 && vchanWindow.mwDefaultsForMode) {
|
||||
bwHz = vchanWindow.mwDefaultsForMode(ch.mode)[0] || 0;
|
||||
void vchanTakeSchedulerControl();
|
||||
void fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/freq`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ freq_hz: Math.round(freqHz) })
|
||||
}
|
||||
if (bwHz > 0) {
|
||||
bwEl.value = (bwHz / 1e3).toFixed(3).replace(/\.?0+$/, "");
|
||||
vchanWindow.currentBandwidthHz = bwHz;
|
||||
}
|
||||
}
|
||||
function vchanSyncAccentUI() {
|
||||
const onVirtual = vchanIsOnVirtual();
|
||||
const freqEl = document.getElementById("freq");
|
||||
const bwEl = document.getElementById("spectrum-bw-input");
|
||||
if (freqEl) freqEl.classList.toggle("vchan-ch-active", onVirtual);
|
||||
if (bwEl) bwEl.classList.toggle("vchan-ch-active", onVirtual);
|
||||
if (onVirtual) {
|
||||
vchanUpdateFreqDisplay();
|
||||
vchanSyncModeDisplay();
|
||||
vchanSyncBwDisplay();
|
||||
} else {
|
||||
origRefreshFreqDisplay?.();
|
||||
}
|
||||
if (vchanWindow.updateDocumentTitle && vchanWindow.activeChannelRds) {
|
||||
vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
|
||||
}
|
||||
}
|
||||
var origRefreshFreqDisplay = null;
|
||||
function vchanSetChannelFreq(freqHz) {
|
||||
if (!vchanRigId || !vchanActiveId) return;
|
||||
if (vchanWindow.lastSpectrumData && vchanWindow.lastSpectrumData.sample_rate > 0) {
|
||||
const halfSpan = vchanWindow.lastSpectrumData.sample_rate / 2;
|
||||
const center = vchanWindow.lastSpectrumData.center_hz;
|
||||
if (Math.abs(freqHz - center) > halfSpan) {
|
||||
if (vchanWindow.showHint) {
|
||||
vchanWindow.showHint(
|
||||
`Out of SDR bandwidth (center ${(center / 1e6).toFixed(3)} MHz ±${(halfSpan / 1e3).toFixed(0)} kHz)`,
|
||||
3e3
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
void vchanTakeSchedulerControl();
|
||||
void fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/freq`,
|
||||
).catch((error) => {
|
||||
console.error("vchan: set freq error", error);
|
||||
});
|
||||
}
|
||||
async function vchanSetChannelBandwidth(bwHz) {
|
||||
if (!vchanRigId || !vchanActiveId) return;
|
||||
try {
|
||||
await vchanTakeSchedulerControl();
|
||||
const resp = await fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/bw`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ freq_hz: Math.round(freqHz) })
|
||||
body: JSON.stringify({ bandwidth_hz: Math.round(bwHz) })
|
||||
}
|
||||
).catch((error) => {
|
||||
console.error("vchan: set freq error", error);
|
||||
);
|
||||
if (!resp.ok) console.warn("vchan: set bw failed", resp.status);
|
||||
} catch (e) {
|
||||
console.error("vchan: set bw error", e);
|
||||
}
|
||||
}
|
||||
async function vchanSetChannelMode(mode) {
|
||||
if (!vchanRigId || !vchanActiveId) return;
|
||||
try {
|
||||
await vchanTakeSchedulerControl();
|
||||
const resp = await fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/mode`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ mode })
|
||||
}
|
||||
);
|
||||
if (!resp.ok) console.warn("vchan: set mode failed", resp.status);
|
||||
} catch (e) {
|
||||
console.error("vchan: set mode error", e);
|
||||
}
|
||||
}
|
||||
vchanWindow.vchanInterceptMode = async function(mode) {
|
||||
if (!vchanIsOnVirtual()) return false;
|
||||
await vchanSetChannelMode(mode);
|
||||
return true;
|
||||
};
|
||||
vchanWindow.vchanInterceptBandwidth = async function(bwHz) {
|
||||
if (!vchanIsOnVirtual()) return false;
|
||||
await vchanSetChannelBandwidth(bwHz);
|
||||
return true;
|
||||
};
|
||||
(function() {
|
||||
const original = vchanWindow.setRigFrequency;
|
||||
vchanWindow.setRigFrequency = function(freqHz) {
|
||||
if (vchanIsOnVirtual()) {
|
||||
if (vchanWindow.applyLocalTunedFrequency) {
|
||||
if (typeof vchanWindow._freqOptimisticSeq === "number") {
|
||||
vchanWindow._freqOptimisticSeq += 1;
|
||||
vchanWindow._freqOptimisticHz = Math.round(freqHz);
|
||||
}
|
||||
vchanWindow.applyLocalTunedFrequency(Math.round(freqHz));
|
||||
}
|
||||
vchanSetChannelFreq(freqHz);
|
||||
return;
|
||||
}
|
||||
void vchanTakeSchedulerControl();
|
||||
original?.(freqHz);
|
||||
};
|
||||
})();
|
||||
(function initSchedulerReleaseControl() {
|
||||
const btn = document.getElementById("scheduler-release-btn");
|
||||
if (btn) {
|
||||
btn.addEventListener("click", () => {
|
||||
void vchanToggleSchedulerRelease();
|
||||
});
|
||||
}
|
||||
async function vchanSetChannelBandwidth(bwHz) {
|
||||
if (!vchanRigId || !vchanActiveId) return;
|
||||
try {
|
||||
await vchanTakeSchedulerControl();
|
||||
const resp = await fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/bw`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ bandwidth_hz: Math.round(bwHz) })
|
||||
}
|
||||
);
|
||||
if (!resp.ok) console.warn("vchan: set bw failed", resp.status);
|
||||
} catch (e) {
|
||||
console.error("vchan: set bw error", e);
|
||||
vchanStartSchedulerReleasePolling();
|
||||
vchanRenderSchedulerRelease();
|
||||
})();
|
||||
(function() {
|
||||
origRefreshFreqDisplay = vchanWindow.refreshFreqDisplay ?? null;
|
||||
vchanWindow.refreshFreqDisplay = function() {
|
||||
if (vchanIsOnVirtual()) {
|
||||
vchanUpdateFreqDisplay();
|
||||
return;
|
||||
}
|
||||
}
|
||||
async function vchanSetChannelMode(mode) {
|
||||
if (!vchanRigId || !vchanActiveId) return;
|
||||
try {
|
||||
await vchanTakeSchedulerControl();
|
||||
const resp = await fetch(
|
||||
`/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/mode`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ mode })
|
||||
}
|
||||
);
|
||||
if (!resp.ok) console.warn("vchan: set mode failed", resp.status);
|
||||
} catch (e) {
|
||||
console.error("vchan: set mode error", e);
|
||||
}
|
||||
}
|
||||
vchanWindow.vchanInterceptMode = async function(mode) {
|
||||
if (!vchanIsOnVirtual()) return false;
|
||||
await vchanSetChannelMode(mode);
|
||||
return true;
|
||||
};
|
||||
vchanWindow.vchanInterceptBandwidth = async function(bwHz) {
|
||||
if (!vchanIsOnVirtual()) return false;
|
||||
await vchanSetChannelBandwidth(bwHz);
|
||||
return true;
|
||||
origRefreshFreqDisplay?.();
|
||||
};
|
||||
(function() {
|
||||
const original = vchanWindow.setRigFrequency;
|
||||
vchanWindow.setRigFrequency = function(freqHz) {
|
||||
if (vchanIsOnVirtual()) {
|
||||
if (vchanWindow.applyLocalTunedFrequency) {
|
||||
if (typeof vchanWindow._freqOptimisticSeq === "number") {
|
||||
vchanWindow._freqOptimisticSeq += 1;
|
||||
vchanWindow._freqOptimisticHz = Math.round(freqHz);
|
||||
}
|
||||
vchanWindow.applyLocalTunedFrequency(Math.round(freqHz));
|
||||
}
|
||||
vchanSetChannelFreq(freqHz);
|
||||
return;
|
||||
}
|
||||
void vchanTakeSchedulerControl();
|
||||
original?.(freqHz);
|
||||
};
|
||||
})();
|
||||
(function initSchedulerReleaseControl() {
|
||||
const btn = document.getElementById("scheduler-release-btn");
|
||||
if (btn) {
|
||||
btn.addEventListener("click", () => {
|
||||
void vchanToggleSchedulerRelease();
|
||||
});
|
||||
}
|
||||
vchanStartSchedulerReleasePolling();
|
||||
vchanRenderSchedulerRelease();
|
||||
})();
|
||||
(function() {
|
||||
origRefreshFreqDisplay = vchanWindow.refreshFreqDisplay ?? null;
|
||||
vchanWindow.refreshFreqDisplay = function() {
|
||||
if (vchanIsOnVirtual()) {
|
||||
vchanUpdateFreqDisplay();
|
||||
return;
|
||||
}
|
||||
origRefreshFreqDisplay?.();
|
||||
};
|
||||
})();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user