[feat](trx-client): support virtual channel bandwidth control

Add client-side command plumbing, HTTP endpoint handling, and frontend interception so bandwidth changes are applied per active virtual channel and survive reconnects.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-11 21:41:32 +01:00
parent 717228a635
commit daa0631b35
7 changed files with 150 additions and 12 deletions
@@ -3278,6 +3278,7 @@ async function applyBandwidthFromInput() {
syncBandwidthInput(clamped);
if (lastSpectrumData) scheduleSpectrumDraw();
try {
if (typeof vchanInterceptBandwidth === "function" && await vchanInterceptBandwidth(clamped)) return;
await postPath(`/set_bandwidth?hz=${clamped}`);
if (Number.isFinite(lastFreqHz)) {
await ensureTunedBandwidthCoverage(lastFreqHz);
@@ -3352,6 +3353,7 @@ async function applyAutoBandwidth() {
syncBandwidthInput(estimated);
if (lastSpectrumData) scheduleSpectrumDraw();
try {
if (typeof vchanInterceptBandwidth === "function" && await vchanInterceptBandwidth(estimated)) return;
await postPath(`/set_bandwidth?hz=${estimated}`);
if (Number.isFinite(lastFreqHz)) {
await ensureTunedBandwidthCoverage(lastFreqHz);
@@ -7774,9 +7776,12 @@ if (spectrumCanvas || overviewCanvas) {
window.addEventListener("mouseup", async () => {
if (_bwDragEdge) {
try {
await postPath(`/set_bandwidth?hz=${Math.round(currentBandwidthHz)}`);
if (Number.isFinite(lastFreqHz)) {
await ensureTunedBandwidthCoverage(lastFreqHz, currentBandwidthHz);
const bwHz = Math.round(currentBandwidthHz);
if (!(typeof vchanInterceptBandwidth === "function" && await vchanInterceptBandwidth(bwHz))) {
await postPath(`/set_bandwidth?hz=${bwHz}`);
if (Number.isFinite(lastFreqHz)) {
await ensureTunedBandwidthCoverage(lastFreqHz, currentBandwidthHz);
}
}
} catch (_) {}
_bwDragEdge = null;
@@ -286,10 +286,17 @@ async function bmApply(bm) {
modeEl.value = String(bm.mode || "").toUpperCase();
}
if (bm.bandwidth_hz) {
await postPath("/set_bandwidth?hz=" + bm.bandwidth_hz);
const bwHandledByVchan = typeof vchanInterceptBandwidth === "function"
&& await vchanInterceptBandwidth(bm.bandwidth_hz);
if (!bwHandledByVchan) {
await postPath("/set_bandwidth?hz=" + bm.bandwidth_hz);
}
if (typeof currentBandwidthHz !== "undefined") {
currentBandwidthHz = bm.bandwidth_hz;
}
if (typeof window !== "undefined") {
window.currentBandwidthHz = bm.bandwidth_hz;
}
if (typeof syncBandwidthInput === "function") {
syncBandwidthInput(bm.bandwidth_hz);
}
@@ -236,6 +236,29 @@ function vchanSyncModeDisplay() {
// When on primary channel, app.js rig-state updates handle the picker.
}
// Sync the BW input to the active virtual channel's bandwidth.
function vchanSyncBwDisplay() {
if (!vchanIsOnVirtual()) return;
const ch = vchanActiveChannel();
if (!ch) return;
const bwEl = document.getElementById("spectrum-bw-input");
if (!bwEl) return;
// bandwidth_hz == 0 means mode-default; derive it from the channel mode.
let bwHz = ch.bandwidth_hz || 0;
if (bwHz === 0 && typeof mwDefaultsForMode === "function") {
bwHz = mwDefaultsForMode(ch.mode)[0] || 0;
}
if (bwHz > 0) {
bwEl.value = (bwHz / 1000).toFixed(3).replace(/\.?0+$/, "");
if (typeof currentBandwidthHz !== "undefined") {
currentBandwidthHz = bwHz;
window.currentBandwidthHz = bwHz;
} else {
window.currentBandwidthHz = bwHz;
}
}
}
// Add / remove the vchan accent class from the freq and BW inputs.
function vchanSyncAccentUI() {
const onVirtual = vchanIsOnVirtual();
@@ -246,6 +269,7 @@ function vchanSyncAccentUI() {
if (onVirtual) {
vchanUpdateFreqDisplay();
vchanSyncModeDisplay();
vchanSyncBwDisplay();
} else if (typeof _origRefreshFreqDisplay === "function") {
_origRefreshFreqDisplay();
}
@@ -286,6 +310,23 @@ async function vchanSetChannelFreq(freqHz) {
}
}
async function vchanSetChannelBandwidth(bwHz) {
if (!vchanRigId || !vchanActiveId) return;
try {
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 {
@@ -312,6 +353,14 @@ window.vchanInterceptMode = async function(mode) {
return true;
};
// Called by app.js bandwidth setters before sending /set_bandwidth to the
// server. Returns true if the change was handled by the virtual channel.
window.vchanInterceptBandwidth = async function(bwHz) {
if (!vchanIsOnVirtual()) return false;
await vchanSetChannelBandwidth(bwHz);
return true;
};
// Wrap setRigFrequency (defined in app.js, loaded before this file) so that
// frequency changes are redirected to the active virtual channel instead of
// the server when on a non-primary channel.