[feat](trx-frontend-http): replace signal graph with measurement mode

Replace the rolling canvas signal graph with a practical signal
measurement feature. The operator can start/stop measurement to
collect signal samples, then view averaged and peak S-unit results.

Also fix signal display to correctly convert dBm wire format to
S-units using standard S-meter scale (S1=-121dBm, S9=-73dBm,
6dB per S-unit).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-08 12:14:30 +01:00
parent 822a19588f
commit c67e8feb9c
3 changed files with 61 additions and 70 deletions
@@ -4,7 +4,6 @@ const bandLabel = document.getElementById("band-label");
const powerBtn = document.getElementById("power-btn"); const powerBtn = document.getElementById("power-btn");
const powerHint = document.getElementById("power-hint"); const powerHint = document.getElementById("power-hint");
const vfoPicker = document.getElementById("vfo-picker"); const vfoPicker = document.getElementById("vfo-picker");
const signalGraph = document.getElementById("signal-graph");
const signalBar = document.getElementById("signal-bar"); const signalBar = document.getElementById("signal-bar");
const signalValue = document.getElementById("signal-value"); const signalValue = document.getElementById("signal-value");
const pttBtn = document.getElementById("ptt-btn"); const pttBtn = document.getElementById("ptt-btn");
@@ -30,8 +29,8 @@ let lastTxEn = null;
let lastRendered = null; let lastRendered = null;
let rigName = "Rig"; let rigName = "Rig";
let hintTimer = null; let hintTimer = null;
const signalHistory = []; let sigMeasuring = false;
const SIGNAL_HISTORY_MAX = 120; let sigSamples = [];
let lastFreqHz = null; let lastFreqHz = null;
let jogStep = 1000; // default 1 kHz let jogStep = 1000; // default 1 kHz
let jogAngle = 0; let jogAngle = 0;
@@ -124,6 +123,20 @@ function freqAllowed(hz) {
return supportedBands.some((b) => hz >= b.low && hz <= b.high); return supportedBands.some((b) => hz >= b.low && hz <= b.high);
} }
// Convert dBm (wire format) to S-units (S1=-121dBm, S9=-73dBm, 6dB/S-unit).
// Above S9, returns 9 + (overshoot in S-unit-equivalent, i.e. dB/10).
function dbmToSUnits(dbm) {
if (dbm <= -121) return 0;
if (dbm >= -73) return 9 + (dbm + 73) / 10;
return (dbm + 121) / 6;
}
function formatSignal(sUnits) {
if (sUnits <= 9) return `S${sUnits.toFixed(1)}`;
const overDb = (sUnits - 9) * 10;
return `S9 + ${overDb.toFixed(0)}dB`;
}
function setDisabled(disabled) { function setDisabled(disabled) {
[freqEl, modeEl, freqBtn, modeBtn, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => { [freqEl, modeEl, freqBtn, modeBtn, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => {
if (el) el.disabled = disabled; if (el) el.disabled = disabled;
@@ -244,28 +257,18 @@ function render(update) {
vfoPicker.innerHTML = "<button type=\"button\" class=\"active\">--</button>"; vfoPicker.innerHTML = "<button type=\"button\" class=\"active\">--</button>";
} }
if (update.status && update.status.rx && typeof update.status.rx.sig === "number") { if (update.status && update.status.rx && typeof update.status.rx.sig === "number") {
const raw = Math.max(0, update.status.rx.sig); const sUnits = dbmToSUnits(update.status.rx.sig);
let pct; const pct = sUnits <= 9 ? Math.max(0, Math.min(100, (sUnits / 9) * 100)) : 100;
let label;
if (raw <= 9) {
pct = Math.max(0, Math.min(100, (raw / 9) * 100));
label = `S${raw.toFixed(1)}`;
} else {
const overDb = (raw - 9) * 10;
pct = 100;
label = `S9 + ${overDb.toFixed(0)}dB`;
}
signalBar.style.width = `${pct}%`; signalBar.style.width = `${pct}%`;
signalValue.textContent = label; signalValue.textContent = formatSignal(sUnits);
signalHistory.push(raw); if (sigMeasuring) {
if (signalHistory.length > SIGNAL_HISTORY_MAX) signalHistory.shift(); sigSamples.push(sUnits);
sigMeasureBtn.textContent = `Stop (${sigSamples.length})`;
}
} else { } else {
signalBar.style.width = "0%"; signalBar.style.width = "0%";
signalValue.textContent = "--"; signalValue.textContent = "--";
signalHistory.push(0);
if (signalHistory.length > SIGNAL_HISTORY_MAX) signalHistory.shift();
} }
drawSignalGraph();
bandLabel.textContent = typeof update.band === "string" ? update.band : "--"; bandLabel.textContent = typeof update.band === "string" ? update.band : "--";
if (typeof update.enabled === "boolean") { if (typeof update.enabled === "boolean") {
powerBtn.disabled = false; powerBtn.disabled = false;
@@ -311,49 +314,6 @@ function render(update) {
} }
} }
function drawSignalGraph() {
if (!signalGraph) return;
const ctx = signalGraph.getContext("2d");
const w = signalGraph.width;
const h = signalGraph.height;
ctx.clearRect(0, 0, w, h);
if (signalHistory.length < 2) return;
const maxVal = 12; // S9+30dB in S-units
const len = signalHistory.length;
const step = w / (SIGNAL_HISTORY_MAX - 1);
const offsetX = (SIGNAL_HISTORY_MAX - len) * step;
ctx.beginPath();
ctx.moveTo(offsetX, h);
for (let i = 0; i < len; i++) {
const val = Math.min(signalHistory[i], maxVal);
const x = offsetX + i * step;
const y = h - (val / maxVal) * h;
ctx.lineTo(x, y);
}
ctx.lineTo(offsetX + (len - 1) * step, h);
ctx.closePath();
const grad = ctx.createLinearGradient(0, h, 0, 0);
grad.addColorStop(0, "rgba(0,209,127,0.25)");
grad.addColorStop(0.6, "rgba(240,173,78,0.35)");
grad.addColorStop(1, "rgba(229,83,83,0.45)");
ctx.fillStyle = grad;
ctx.fill();
ctx.beginPath();
for (let i = 0; i < len; i++) {
const val = Math.min(signalHistory[i], maxVal);
const x = offsetX + i * step;
const y = h - (val / maxVal) * h;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.strokeStyle = "rgba(0,209,127,0.8)";
ctx.lineWidth = 1.5;
ctx.stroke();
}
function connect() { function connect() {
if (es) { if (es) {
es.close(); es.close();
@@ -610,6 +570,35 @@ lockBtn.addEventListener("click", async () => {
connect(); connect();
// --- Signal measurement ---
const sigMeasureBtn = document.getElementById("sig-measure-btn");
const sigClearBtn = document.getElementById("sig-clear-btn");
const sigResult = document.getElementById("sig-result");
sigMeasureBtn.addEventListener("click", () => {
if (!sigMeasuring) {
sigSamples = [];
sigMeasuring = true;
sigMeasureBtn.textContent = "Stop (0)";
sigMeasureBtn.style.borderColor = "#00d17f";
sigMeasureBtn.style.color = "#00d17f";
} else {
sigMeasuring = false;
sigMeasureBtn.textContent = "Measure";
sigMeasureBtn.style.borderColor = "";
sigMeasureBtn.style.color = "";
if (sigSamples.length > 0) {
const avg = sigSamples.reduce((a, b) => a + b, 0) / sigSamples.length;
const peak = Math.max(...sigSamples);
sigResult.textContent = `Avg ${formatSignal(avg)} / Peak ${formatSignal(peak)} (${sigSamples.length} samples)`;
}
}
});
sigClearBtn.addEventListener("click", () => {
sigResult.textContent = "";
});
// --- Audio streaming --- // --- Audio streaming ---
const rxAudioBtn = document.getElementById("rx-audio-btn"); const rxAudioBtn = document.getElementById("rx-audio-btn");
const txAudioBtn = document.getElementById("tx-audio-btn"); const txAudioBtn = document.getElementById("tx-audio-btn");
@@ -71,7 +71,11 @@
<div class="signal-bar"><div class="signal-bar-fill" id="signal-bar"></div></div> <div class="signal-bar"><div class="signal-bar-fill" id="signal-bar"></div></div>
<div class="signal-value" id="signal-value">--</div> <div class="signal-value" id="signal-value">--</div>
</div> </div>
<canvas id="signal-graph" width="600" height="60"></canvas> <div class="signal-measure">
<button id="sig-measure-btn" type="button">Measure</button>
<button id="sig-clear-btn" type="button">Clear</button>
<span id="sig-result"></span>
</div>
</div> </div>
<div class="full-row" id="tx-meters" style="display:none;"> <div class="full-row" id="tx-meters" style="display:none;">
<div class="label">TX Meters</div> <div class="label">TX Meters</div>
@@ -122,13 +122,11 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
color: var(--accent-green); color: var(--accent-green);
font-weight: 600; font-weight: 600;
} }
#signal-graph { .signal-measure {
width: 100%; display: inline-flex;
height: 60px; gap: 0.5rem;
align-items: center;
margin-top: 0.4rem; margin-top: 0.4rem;
border-radius: 6px;
background: var(--input-bg);
border: 1px solid var(--border-light);
} }
button { padding: 0.5rem 0.9rem; border-radius: 6px; border: 1px solid var(--btn-border); background: var(--btn-bg); color: var(--text); cursor: pointer; height: 2.6rem; } button { padding: 0.5rem 0.9rem; border-radius: 6px; border: 1px solid var(--btn-border); background: var(--btn-bg); color: var(--text); cursor: pointer; height: 2.6rem; }
button:disabled { opacity: 0.6; cursor: not-allowed; } button:disabled { opacity: 0.6; cursor: not-allowed; }