[feat](trx-rds,trx-frontend-http): add af tuning and dynamic page title
Co-authored-by: Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -305,6 +305,12 @@ impl Candidate {
|
|||||||
let group_type = ((block_b >> 12) & 0x0f) as u8;
|
let group_type = ((block_b >> 12) & 0x0f) as u8;
|
||||||
let version_b = ((block_b >> 11) & 0x1) != 0;
|
let version_b = ((block_b >> 11) & 0x1) != 0;
|
||||||
if group_type == 0 {
|
if group_type == 0 {
|
||||||
|
if !version_b && block_c_kind == BlockKind::C {
|
||||||
|
let [af0, af1] = block_c.to_be_bytes();
|
||||||
|
if self.process_af_pair(af0, af1) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
let ta = ((block_b >> 4) & 0x1) != 0;
|
let ta = ((block_b >> 4) & 0x1) != 0;
|
||||||
if self.state.traffic_announcement != Some(ta) {
|
if self.state.traffic_announcement != Some(ta) {
|
||||||
self.state.traffic_announcement = Some(ta);
|
self.state.traffic_announcement = Some(ta);
|
||||||
@@ -421,6 +427,45 @@ impl Candidate {
|
|||||||
self.score = self.score.saturating_add(1);
|
self.score = self.score.saturating_add(1);
|
||||||
changed.then(|| self.state.clone())
|
changed.then(|| self.state.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn process_af_pair(&mut self, af0: u8, af1: u8) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
if !is_af_count_code(af0) {
|
||||||
|
changed |= self.record_af_code(af0);
|
||||||
|
}
|
||||||
|
if !is_af_count_code(af1) {
|
||||||
|
changed |= self.record_af_code(af1);
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_af_code(&mut self, code: u8) -> bool {
|
||||||
|
let Some(hz) = af_code_to_hz(code) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let afs = self
|
||||||
|
.state
|
||||||
|
.alternative_frequencies_hz
|
||||||
|
.get_or_insert_with(Vec::new);
|
||||||
|
if afs.contains(&hz) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
afs.push(hz);
|
||||||
|
afs.sort_unstable();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_af_count_code(code: u8) -> bool {
|
||||||
|
(224..=249).contains(&code)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn af_code_to_hz(code: u8) -> Option<u32> {
|
||||||
|
if (1..=204).contains(&code) {
|
||||||
|
Some(87_500_000 + u32::from(code) * 100_000)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -390,6 +390,20 @@ function currentTheme() {
|
|||||||
return document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark";
|
return document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateDocumentTitle(rds = null) {
|
||||||
|
if (!Number.isFinite(lastFreqHz)) {
|
||||||
|
document.title = originalTitle;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parts = [formatFreq(lastFreqHz)];
|
||||||
|
const ps = rds?.program_service;
|
||||||
|
if (ps && ps.length > 0) {
|
||||||
|
parts.push(ps);
|
||||||
|
}
|
||||||
|
parts.push(originalTitle);
|
||||||
|
document.title = parts.join(" - ");
|
||||||
|
}
|
||||||
|
|
||||||
function setTheme(theme) {
|
function setTheme(theme) {
|
||||||
const next = theme === "light" ? "light" : "dark";
|
const next = theme === "light" ? "light" : "dark";
|
||||||
document.documentElement.setAttribute("data-theme", next);
|
document.documentElement.setAttribute("data-theme", next);
|
||||||
@@ -957,6 +971,7 @@ function applyLocalTunedFrequency(hz, forceDisplay = false) {
|
|||||||
resetRdsDisplay();
|
resetRdsDisplay();
|
||||||
}
|
}
|
||||||
lastFreqHz = hz;
|
lastFreqHz = hz;
|
||||||
|
updateDocumentTitle(lastSpectrumData?.rds ?? null);
|
||||||
refreshWavelengthDisplay(lastFreqHz);
|
refreshWavelengthDisplay(lastFreqHz);
|
||||||
if (forceDisplay) {
|
if (forceDisplay) {
|
||||||
freqDirty = false;
|
freqDirty = false;
|
||||||
@@ -1136,8 +1151,10 @@ function updateFooterBuildInfo() {
|
|||||||
|
|
||||||
function updateTitle() {
|
function updateTitle() {
|
||||||
const titleEl = document.getElementById("rig-title");
|
const titleEl = document.getElementById("rig-title");
|
||||||
if (!titleEl) return;
|
if (titleEl) {
|
||||||
titleEl.textContent = serverVersion ? `trx-rs v${serverVersion}` : "trx-rs";
|
titleEl.textContent = serverVersion ? `trx-rs v${serverVersion}` : "trx-rs";
|
||||||
|
}
|
||||||
|
updateDocumentTitle(lastSpectrumData?.rds ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(update) {
|
function render(update) {
|
||||||
@@ -3281,6 +3298,42 @@ function buildRdsRawPayload(rds) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatRdsAfMHz(hz) {
|
||||||
|
return `${(hz / 1_000_000).toFixed(1)} MHz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tuneRdsAlternativeFrequency(hz) {
|
||||||
|
if (!Number.isFinite(hz) || hz <= 0) return;
|
||||||
|
const targetHz = Math.round(hz);
|
||||||
|
try {
|
||||||
|
await postPath(`/set_freq?hz=${targetHz}`);
|
||||||
|
applyLocalTunedFrequency(targetHz);
|
||||||
|
showHint(`Tuned ${formatRdsAfMHz(targetHz)}`, 1200);
|
||||||
|
} catch (_) {
|
||||||
|
showHint("Set freq failed", 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRdsAlternativeFrequencies(list) {
|
||||||
|
const afEl = document.getElementById("rds-af-list");
|
||||||
|
if (!afEl) return;
|
||||||
|
if (!Array.isArray(list) || list.length === 0) {
|
||||||
|
afEl.textContent = "--";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
afEl.innerHTML = "";
|
||||||
|
for (const hz of list) {
|
||||||
|
if (!Number.isFinite(hz) || hz <= 0) continue;
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "rds-af-btn";
|
||||||
|
btn.dataset.hz = String(Math.round(hz));
|
||||||
|
btn.textContent = formatRdsAfMHz(hz);
|
||||||
|
afEl.appendChild(btn);
|
||||||
|
}
|
||||||
|
if (!afEl.childElementCount) afEl.textContent = "--";
|
||||||
|
}
|
||||||
|
|
||||||
async function copyRdsPsToClipboard() {
|
async function copyRdsPsToClipboard() {
|
||||||
const rds = lastSpectrumData?.rds;
|
const rds = lastSpectrumData?.rds;
|
||||||
const ps = rds?.program_service;
|
const ps = rds?.program_service;
|
||||||
@@ -3328,8 +3381,19 @@ const rdsRawCopyBtn = document.getElementById("rds-raw-copy-btn");
|
|||||||
if (rdsRawCopyBtn) {
|
if (rdsRawCopyBtn) {
|
||||||
rdsRawCopyBtn.addEventListener("click", () => { copyRdsRawToClipboard(); });
|
rdsRawCopyBtn.addEventListener("click", () => { copyRdsRawToClipboard(); });
|
||||||
}
|
}
|
||||||
|
const rdsAfListEl = document.getElementById("rds-af-list");
|
||||||
|
if (rdsAfListEl) {
|
||||||
|
rdsAfListEl.addEventListener("click", (event) => {
|
||||||
|
const btn = event.target instanceof HTMLElement ? event.target.closest(".rds-af-btn") : null;
|
||||||
|
const hz = Number(btn?.dataset?.hz);
|
||||||
|
if (btn && Number.isFinite(hz)) {
|
||||||
|
tuneRdsAlternativeFrequency(hz);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateRdsPsOverlay(rds) {
|
function updateRdsPsOverlay(rds) {
|
||||||
|
updateDocumentTitle(rds);
|
||||||
// Overview strip overlay
|
// Overview strip overlay
|
||||||
if (rdsPsOverlay) {
|
if (rdsPsOverlay) {
|
||||||
const ps = rds?.program_service;
|
const ps = rds?.program_service;
|
||||||
@@ -3377,6 +3441,7 @@ function updateRdsPsOverlay(rds) {
|
|||||||
const compEl = document.getElementById("rds-compressed");
|
const compEl = document.getElementById("rds-compressed");
|
||||||
const headEl = document.getElementById("rds-artificial-head");
|
const headEl = document.getElementById("rds-artificial-head");
|
||||||
const dynPtyEl = document.getElementById("rds-dynamic-pty");
|
const dynPtyEl = document.getElementById("rds-dynamic-pty");
|
||||||
|
const afEl = document.getElementById("rds-af-list");
|
||||||
const rtEl = document.getElementById("rds-radio-text");
|
const rtEl = document.getElementById("rds-radio-text");
|
||||||
const rawEl = document.getElementById("rds-raw");
|
const rawEl = document.getElementById("rds-raw");
|
||||||
if (!statusEl) return;
|
if (!statusEl) return;
|
||||||
@@ -3401,6 +3466,7 @@ function updateRdsPsOverlay(rds) {
|
|||||||
if (compEl) compEl.textContent = "--";
|
if (compEl) compEl.textContent = "--";
|
||||||
if (headEl) headEl.textContent = "--";
|
if (headEl) headEl.textContent = "--";
|
||||||
if (dynPtyEl) dynPtyEl.textContent = "--";
|
if (dynPtyEl) dynPtyEl.textContent = "--";
|
||||||
|
if (afEl) afEl.textContent = "--";
|
||||||
if (rtEl) rtEl.textContent = "--";
|
if (rtEl) rtEl.textContent = "--";
|
||||||
if (rawEl && lastSpectrumData) {
|
if (rawEl && lastSpectrumData) {
|
||||||
const { bins: _b, ...rest } = lastSpectrumData;
|
const { bins: _b, ...rest } = lastSpectrumData;
|
||||||
@@ -3427,6 +3493,7 @@ function updateRdsPsOverlay(rds) {
|
|||||||
if (compEl) compEl.textContent = formatRdsFlag(rds.compressed);
|
if (compEl) compEl.textContent = formatRdsFlag(rds.compressed);
|
||||||
if (headEl) headEl.textContent = formatRdsFlag(rds.artificial_head);
|
if (headEl) headEl.textContent = formatRdsFlag(rds.artificial_head);
|
||||||
if (dynPtyEl) dynPtyEl.textContent = formatRdsFlag(rds.dynamic_pty);
|
if (dynPtyEl) dynPtyEl.textContent = formatRdsFlag(rds.dynamic_pty);
|
||||||
|
renderRdsAlternativeFrequencies(rds.alternative_frequencies_hz);
|
||||||
if (rtEl) rtEl.textContent = rds.radio_text ?? "--";
|
if (rtEl) rtEl.textContent = rds.radio_text ?? "--";
|
||||||
rawEl.textContent = JSON.stringify(buildRdsRawPayload(rds), null, 2);
|
rawEl.textContent = JSON.stringify(buildRdsRawPayload(rds), null, 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,6 +287,7 @@
|
|||||||
<div class="rds-field"><span class="rds-label">Compressed</span><span id="rds-compressed" class="rds-value">--</span></div>
|
<div class="rds-field"><span class="rds-label">Compressed</span><span id="rds-compressed" class="rds-value">--</span></div>
|
||||||
<div class="rds-field"><span class="rds-label">Artificial Head</span><span id="rds-artificial-head" class="rds-value">--</span></div>
|
<div class="rds-field"><span class="rds-label">Artificial Head</span><span id="rds-artificial-head" class="rds-value">--</span></div>
|
||||||
<div class="rds-field"><span class="rds-label">Dynamic PTY</span><span id="rds-dynamic-pty" class="rds-value">--</span></div>
|
<div class="rds-field"><span class="rds-label">Dynamic PTY</span><span id="rds-dynamic-pty" class="rds-value">--</span></div>
|
||||||
|
<div class="rds-field"><span class="rds-label">AF</span><span id="rds-af-list" class="rds-value rds-af-list">--</span></div>
|
||||||
<div class="rds-field"><span class="rds-label">RadioText</span><span id="rds-radio-text" class="rds-value rds-text">--</span></div>
|
<div class="rds-field"><span class="rds-label">RadioText</span><span id="rds-radio-text" class="rds-value rds-text">--</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rds-raw-header">
|
<div class="rds-raw-header">
|
||||||
|
|||||||
@@ -812,6 +812,21 @@ small { color: var(--text-muted); }
|
|||||||
.rds-value { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.95rem; color: var(--text); }
|
.rds-value { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.95rem; color: var(--text); }
|
||||||
.rds-ps { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.08em; color: var(--accent-green); }
|
.rds-ps { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.08em; color: var(--accent-green); }
|
||||||
.rds-text { white-space: normal; overflow-wrap: anywhere; line-height: 1.35; }
|
.rds-text { white-space: normal; overflow-wrap: anywhere; line-height: 1.35; }
|
||||||
|
.rds-af-list { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
|
||||||
|
.rds-af-btn {
|
||||||
|
height: 1.7rem;
|
||||||
|
padding: 0 0.55rem;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.rds-af-btn:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
.rds-no-signal { color: var(--text-muted); }
|
.rds-no-signal { color: var(--text-muted); }
|
||||||
.rds-decoding { color: var(--accent-green); }
|
.rds-decoding { color: var(--accent-green); }
|
||||||
.rds-raw-header { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; margin-bottom: 0.3rem; }
|
.rds-raw-header { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; margin-bottom: 0.3rem; }
|
||||||
|
|||||||
@@ -320,6 +320,8 @@ pub struct RdsData {
|
|||||||
pub compressed: Option<bool>,
|
pub compressed: Option<bool>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub dynamic_pty: Option<bool>,
|
pub dynamic_pty: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub alternative_frequencies_hz: Option<Vec<u32>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read-only projection of state shared with clients.
|
/// Read-only projection of state shared with clients.
|
||||||
|
|||||||
Reference in New Issue
Block a user