Files
trx-rs/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vchan.js
T

471 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.`;
}
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);
}
}
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);
}
}
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)}`, {
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;
}
const ch = await resp.json();
vchanActiveId = ch.id;
vchanRender();
vchanReconnectAudio();
} catch (e) {
console.error("vchan: allocate 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);
}
} catch (e) {
console.error("vchan: delete 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);
}
}
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();
}
function vchanIsOnVirtual() {
if (!vchanActiveId || vchanChannels.length === 0) return false;
return vchanActiveId !== vchanChannels[0]?.id;
}
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+$/, "");
}
}
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?.();
}
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
);
}
return;
}
}
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) })
}
).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({ 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);
}
}
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);
}
}
async function vchanInterceptMode(mode) {
if (!vchanIsOnVirtual()) return false;
await vchanSetChannelMode(mode);
return true;
}
async function vchanInterceptBandwidth(bwHz) {
if (!vchanIsOnVirtual()) return false;
await vchanSetChannelBandwidth(bwHz);
return true;
}
vchanWindow.trx ??= {};
vchanWindow.trx.modules ??= {};
vchanWindow.trx.modules.vchan = {
get channels() {
return vchanChannels;
},
get activeId() {
return vchanActiveId;
},
activeChannel: vchanActiveChannel,
applyCapabilities: vchanApplyCapabilities,
handleSession: vchanHandleSession,
handleChannels: vchanHandleChannels,
isOnVirtual: vchanIsOnVirtual,
interceptMode: vchanInterceptMode,
interceptBandwidth: vchanInterceptBandwidth,
takeSchedulerControl: vchanTakeSchedulerControl,
releaseToScheduler: vchanToggleSchedulerRelease
};
(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?.();
};
})();