[feat](trx-frontend-http): streamline main control layout
Refine main control interactions and presentation in the HTTP frontend.\n\n- remove frequency and mode Set buttons\n- apply mode changes immediately on picker change\n- place Mode/Tune/Transmit-Power controls in one horizontal row\n- align control labels vertically across that row\n- move and enlarge MHz/kHz/Hz selector beside frequency input\n- keep Enter-to-set frequency behavior\n- switch signal measurement to elapsed-time averaging\n- enlarge header logo 2x\n\nCo-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -19,7 +19,6 @@ const vfoPicker = document.getElementById("vfo-picker");
|
|||||||
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");
|
||||||
const modeBtn = document.getElementById("mode-apply");
|
|
||||||
const txLimitInput = document.getElementById("tx-limit");
|
const txLimitInput = document.getElementById("tx-limit");
|
||||||
const txLimitBtn = document.getElementById("tx-limit-btn");
|
const txLimitBtn = document.getElementById("tx-limit-btn");
|
||||||
const txLimitRow = document.getElementById("tx-limit-row");
|
const txLimitRow = document.getElementById("tx-limit-row");
|
||||||
@@ -41,7 +40,12 @@ let lastRendered = null;
|
|||||||
let rigName = "Rig";
|
let rigName = "Rig";
|
||||||
let hintTimer = null;
|
let hintTimer = null;
|
||||||
let sigMeasuring = false;
|
let sigMeasuring = false;
|
||||||
let sigSamples = [];
|
let sigLastSUnits = null;
|
||||||
|
let sigMeasureTimer = null;
|
||||||
|
let sigMeasureLastTickMs = 0;
|
||||||
|
let sigMeasureAccumMs = 0;
|
||||||
|
let sigMeasureWeighted = 0;
|
||||||
|
let sigMeasurePeak = null;
|
||||||
let lastFreqHz = null;
|
let lastFreqHz = null;
|
||||||
let jogStep = loadSetting("jogStep", 1000);
|
let jogStep = loadSetting("jogStep", 1000);
|
||||||
let minFreqStepHz = 1;
|
let minFreqStepHz = 1;
|
||||||
@@ -69,7 +73,6 @@ function showHint(msg, duration) {
|
|||||||
let supportedModes = [];
|
let supportedModes = [];
|
||||||
let supportedBands = [];
|
let supportedBands = [];
|
||||||
let freqDirty = false;
|
let freqDirty = false;
|
||||||
let modeDirty = false;
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let lastEventAt = Date.now();
|
let lastEventAt = Date.now();
|
||||||
let es;
|
let es;
|
||||||
@@ -228,7 +231,7 @@ function formatSignal(sUnits) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setDisabled(disabled) {
|
function setDisabled(disabled) {
|
||||||
[freqEl, modeEl, modeBtn, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => {
|
[freqEl, modeEl, pttBtn, powerBtn, txLimitInput, txLimitBtn, lockBtn].forEach((el) => {
|
||||||
if (el) el.disabled = disabled;
|
if (el) el.disabled = disabled;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -317,7 +320,7 @@ function render(update) {
|
|||||||
window.updateFt8RfDisplay();
|
window.updateFt8RfDisplay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!modeDirty && update.status && update.status.mode) {
|
if (update.status && update.status.mode) {
|
||||||
const mode = normalizeMode(update.status.mode);
|
const mode = normalizeMode(update.status.mode);
|
||||||
modeEl.value = mode ? mode.toUpperCase() : "";
|
modeEl.value = mode ? mode.toUpperCase() : "";
|
||||||
}
|
}
|
||||||
@@ -424,14 +427,12 @@ function render(update) {
|
|||||||
}
|
}
|
||||||
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 sUnits = dbmToSUnits(update.status.rx.sig);
|
const sUnits = dbmToSUnits(update.status.rx.sig);
|
||||||
|
sigLastSUnits = sUnits;
|
||||||
const pct = sUnits <= 9 ? Math.max(0, Math.min(100, (sUnits / 9) * 100)) : 100;
|
const pct = sUnits <= 9 ? Math.max(0, Math.min(100, (sUnits / 9) * 100)) : 100;
|
||||||
signalBar.style.width = `${pct}%`;
|
signalBar.style.width = `${pct}%`;
|
||||||
signalValue.textContent = formatSignal(sUnits);
|
signalValue.textContent = formatSignal(sUnits);
|
||||||
if (sigMeasuring) {
|
|
||||||
sigSamples.push(sUnits);
|
|
||||||
sigMeasureBtn.textContent = `Stop (${sigSamples.length})`;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
sigLastSUnits = null;
|
||||||
signalBar.style.width = "0%";
|
signalBar.style.width = "0%";
|
||||||
signalValue.textContent = "--";
|
signalValue.textContent = "--";
|
||||||
}
|
}
|
||||||
@@ -756,14 +757,13 @@ jogStepEl.addEventListener("click", (e) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modeBtn.addEventListener("click", async () => {
|
async function applyModeFromPicker() {
|
||||||
const mode = modeEl.value || "";
|
const mode = modeEl.value || "";
|
||||||
if (!mode) {
|
if (!mode) {
|
||||||
showHint("Mode missing", 1500);
|
showHint("Mode missing", 1500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
modeDirty = false;
|
modeEl.disabled = true;
|
||||||
modeBtn.disabled = true;
|
|
||||||
showHint("Setting mode…");
|
showHint("Setting mode…");
|
||||||
try {
|
try {
|
||||||
await postPath(`/set_mode?mode=${encodeURIComponent(mode)}`);
|
await postPath(`/set_mode?mode=${encodeURIComponent(mode)}`);
|
||||||
@@ -772,13 +772,11 @@ modeBtn.addEventListener("click", async () => {
|
|||||||
showHint("Set mode failed", 2000);
|
showHint("Set mode failed", 2000);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
modeBtn.disabled = false;
|
modeEl.disabled = false;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
modeEl.addEventListener("input", () => {
|
modeEl.addEventListener("change", applyModeFromPicker);
|
||||||
modeDirty = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
txLimitInput.addEventListener("keydown", (e) => {
|
txLimitInput.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
@@ -997,27 +995,67 @@ const sigMeasureBtn = document.getElementById("sig-measure-btn");
|
|||||||
const sigClearBtn = document.getElementById("sig-clear-btn");
|
const sigClearBtn = document.getElementById("sig-clear-btn");
|
||||||
const sigResult = document.getElementById("sig-result");
|
const sigResult = document.getElementById("sig-result");
|
||||||
|
|
||||||
|
function resetSignalMeasurementState() {
|
||||||
|
sigMeasureLastTickMs = 0;
|
||||||
|
sigMeasureAccumMs = 0;
|
||||||
|
sigMeasureWeighted = 0;
|
||||||
|
sigMeasurePeak = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSignalMeasurement(nowMs) {
|
||||||
|
if (!sigMeasuring) return;
|
||||||
|
if (sigMeasureLastTickMs === 0) {
|
||||||
|
sigMeasureLastTickMs = nowMs;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dt = Math.max(0, nowMs - sigMeasureLastTickMs);
|
||||||
|
sigMeasureLastTickMs = nowMs;
|
||||||
|
if (!Number.isFinite(sigLastSUnits)) return;
|
||||||
|
|
||||||
|
sigMeasureAccumMs += dt;
|
||||||
|
sigMeasureWeighted += sigLastSUnits * dt;
|
||||||
|
if (sigMeasurePeak === null || sigLastSUnits > sigMeasurePeak) {
|
||||||
|
sigMeasurePeak = sigLastSUnits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSignalMeasurement() {
|
||||||
|
if (sigMeasureTimer) {
|
||||||
|
clearInterval(sigMeasureTimer);
|
||||||
|
sigMeasureTimer = null;
|
||||||
|
}
|
||||||
|
sigMeasuring = false;
|
||||||
|
sigMeasureBtn.textContent = "Measure";
|
||||||
|
sigMeasureBtn.style.borderColor = "";
|
||||||
|
sigMeasureBtn.style.color = "";
|
||||||
|
}
|
||||||
|
|
||||||
sigMeasureBtn.addEventListener("click", () => {
|
sigMeasureBtn.addEventListener("click", () => {
|
||||||
if (!sigMeasuring) {
|
if (!sigMeasuring) {
|
||||||
sigSamples = [];
|
resetSignalMeasurementState();
|
||||||
sigMeasuring = true;
|
sigMeasuring = true;
|
||||||
sigMeasureBtn.textContent = "Stop (0)";
|
sigMeasureBtn.textContent = "Stop (0.0s)";
|
||||||
sigMeasureBtn.style.borderColor = "#00d17f";
|
sigMeasureBtn.style.borderColor = "#00d17f";
|
||||||
sigMeasureBtn.style.color = "#00d17f";
|
sigMeasureBtn.style.color = "#00d17f";
|
||||||
|
sigMeasureTimer = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
updateSignalMeasurement(now);
|
||||||
|
sigMeasureBtn.textContent = `Stop (${(sigMeasureAccumMs / 1000).toFixed(1)}s)`;
|
||||||
|
}, 200);
|
||||||
} else {
|
} else {
|
||||||
sigMeasuring = false;
|
updateSignalMeasurement(Date.now());
|
||||||
sigMeasureBtn.textContent = "Measure";
|
stopSignalMeasurement();
|
||||||
sigMeasureBtn.style.borderColor = "";
|
if (sigMeasureAccumMs > 0) {
|
||||||
sigMeasureBtn.style.color = "";
|
const avg = sigMeasureWeighted / sigMeasureAccumMs;
|
||||||
if (sigSamples.length > 0) {
|
const peak = sigMeasurePeak;
|
||||||
const avg = sigSamples.reduce((a, b) => a + b, 0) / sigSamples.length;
|
sigResult.textContent = `Avg ${formatSignal(avg)} / Peak ${formatSignal(peak)} (${(sigMeasureAccumMs / 1000).toFixed(1)}s)`;
|
||||||
const peak = Math.max(...sigSamples);
|
|
||||||
sigResult.textContent = `Avg ${formatSignal(avg)} / Peak ${formatSignal(peak)} (${sigSamples.length} samples)`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
sigClearBtn.addEventListener("click", () => {
|
sigClearBtn.addEventListener("click", () => {
|
||||||
|
stopSignalMeasurement();
|
||||||
|
resetSignalMeasurementState();
|
||||||
sigResult.textContent = "";
|
sigResult.textContent = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -41,25 +41,27 @@
|
|||||||
<button type="button" data-step="1">Hz</button>
|
<button type="button" data-step="1">Hz</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="jog-container">
|
|
||||||
<button id="jog-down" type="button" class="jog-btn">−</button>
|
|
||||||
<div class="jog-wheel" id="jog-wheel">
|
|
||||||
<div class="jog-indicator" id="jog-indicator"></div>
|
|
||||||
</div>
|
|
||||||
<button id="jog-up" type="button" class="jog-btn">+</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="controls-row full-row">
|
<div class="controls-row full-row">
|
||||||
<div>
|
<div class="controls-col">
|
||||||
<div class="label">Mode</div>
|
<div class="label">Mode</div>
|
||||||
<div class="inline">
|
<div class="inline">
|
||||||
<select class="status-input" id="mode">
|
<select class="status-input" id="mode">
|
||||||
<option value="">--</option>
|
<option value="">--</option>
|
||||||
</select>
|
</select>
|
||||||
<button id="mode-apply" type="button">Set</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="controls-col controls-col-center">
|
||||||
|
<div class="label">Tune</div>
|
||||||
|
<div class="jog-container">
|
||||||
|
<button id="jog-down" type="button" class="jog-btn">−</button>
|
||||||
|
<div class="jog-wheel" id="jog-wheel">
|
||||||
|
<div class="jog-indicator" id="jog-indicator"></div>
|
||||||
|
</div>
|
||||||
|
<button id="jog-up" type="button" class="jog-btn">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls-col">
|
||||||
<div class="label">Transmit / Power</div>
|
<div class="label">Transmit / Power</div>
|
||||||
<div class="btn-grid">
|
<div class="btn-grid">
|
||||||
<button id="ptt-btn" type="button">Toggle PTT</button>
|
<button id="ptt-btn" type="button">Toggle PTT</button>
|
||||||
|
|||||||
@@ -22,10 +22,18 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
|||||||
#freq { font-family: 'DSEG14 Classic', monospace; font-size: 2rem; padding: 0.5rem 0.6rem; letter-spacing: 0.05em; text-align: center; }
|
#freq { font-family: 'DSEG14 Classic', monospace; font-size: 2rem; padding: 0.5rem 0.6rem; letter-spacing: 0.05em; text-align: center; }
|
||||||
.controls-row {
|
.controls-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
.controls-col { min-width: 0; }
|
||||||
|
.controls-col-center { justify-self: center; width: auto; }
|
||||||
|
.controls-row .label {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
min-height: 1.2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.btn-grid {
|
.btn-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
@@ -37,7 +45,6 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.6rem;
|
|
||||||
}
|
}
|
||||||
.jog-wheel {
|
.jog-wheel {
|
||||||
width: 52px;
|
width: 52px;
|
||||||
@@ -137,7 +144,7 @@ button:disabled { opacity: 0.6; cursor: not-allowed; }
|
|||||||
small { color: var(--text-muted); }
|
small { color: var(--text-muted); }
|
||||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem; }
|
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem; }
|
||||||
.title { font-size: 1.4rem; font-weight: 700; display: inline-flex; align-items: center; gap: 0.35rem; }
|
.title { font-size: 1.4rem; font-weight: 700; display: inline-flex; align-items: center; gap: 0.35rem; }
|
||||||
.header-logo { height: 5em; width: auto; flex-shrink: 0; filter: drop-shadow(0 4px 12px rgba(0,0,0,0.35)); }
|
.header-logo { height: 10em; width: auto; flex-shrink: 0; filter: drop-shadow(0 4px 12px rgba(0,0,0,0.35)); }
|
||||||
.subtitle { color: var(--text-muted); font-size: 0.95rem; }
|
.subtitle { color: var(--text-muted); font-size: 0.95rem; }
|
||||||
.subtitle a { color: var(--accent-green); text-decoration: none; }
|
.subtitle a { color: var(--accent-green); text-decoration: none; }
|
||||||
.subtitle a:hover { text-decoration: underline; }
|
.subtitle a:hover { text-decoration: underline; }
|
||||||
|
|||||||
Reference in New Issue
Block a user