[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 version_b = ((block_b >> 11) & 0x1) != 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;
|
||||
if 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);
|
||||
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)]
|
||||
|
||||
@@ -390,6 +390,20 @@ function currentTheme() {
|
||||
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) {
|
||||
const next = theme === "light" ? "light" : "dark";
|
||||
document.documentElement.setAttribute("data-theme", next);
|
||||
@@ -957,6 +971,7 @@ function applyLocalTunedFrequency(hz, forceDisplay = false) {
|
||||
resetRdsDisplay();
|
||||
}
|
||||
lastFreqHz = hz;
|
||||
updateDocumentTitle(lastSpectrumData?.rds ?? null);
|
||||
refreshWavelengthDisplay(lastFreqHz);
|
||||
if (forceDisplay) {
|
||||
freqDirty = false;
|
||||
@@ -1136,8 +1151,10 @@ function updateFooterBuildInfo() {
|
||||
|
||||
function updateTitle() {
|
||||
const titleEl = document.getElementById("rig-title");
|
||||
if (!titleEl) return;
|
||||
titleEl.textContent = serverVersion ? `trx-rs v${serverVersion}` : "trx-rs";
|
||||
if (titleEl) {
|
||||
titleEl.textContent = serverVersion ? `trx-rs v${serverVersion}` : "trx-rs";
|
||||
}
|
||||
updateDocumentTitle(lastSpectrumData?.rds ?? null);
|
||||
}
|
||||
|
||||
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() {
|
||||
const rds = lastSpectrumData?.rds;
|
||||
const ps = rds?.program_service;
|
||||
@@ -3328,8 +3381,19 @@ const rdsRawCopyBtn = document.getElementById("rds-raw-copy-btn");
|
||||
if (rdsRawCopyBtn) {
|
||||
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) {
|
||||
updateDocumentTitle(rds);
|
||||
// Overview strip overlay
|
||||
if (rdsPsOverlay) {
|
||||
const ps = rds?.program_service;
|
||||
@@ -3377,6 +3441,7 @@ function updateRdsPsOverlay(rds) {
|
||||
const compEl = document.getElementById("rds-compressed");
|
||||
const headEl = document.getElementById("rds-artificial-head");
|
||||
const dynPtyEl = document.getElementById("rds-dynamic-pty");
|
||||
const afEl = document.getElementById("rds-af-list");
|
||||
const rtEl = document.getElementById("rds-radio-text");
|
||||
const rawEl = document.getElementById("rds-raw");
|
||||
if (!statusEl) return;
|
||||
@@ -3401,6 +3466,7 @@ function updateRdsPsOverlay(rds) {
|
||||
if (compEl) compEl.textContent = "--";
|
||||
if (headEl) headEl.textContent = "--";
|
||||
if (dynPtyEl) dynPtyEl.textContent = "--";
|
||||
if (afEl) afEl.textContent = "--";
|
||||
if (rtEl) rtEl.textContent = "--";
|
||||
if (rawEl && lastSpectrumData) {
|
||||
const { bins: _b, ...rest } = lastSpectrumData;
|
||||
@@ -3427,6 +3493,7 @@ function updateRdsPsOverlay(rds) {
|
||||
if (compEl) compEl.textContent = formatRdsFlag(rds.compressed);
|
||||
if (headEl) headEl.textContent = formatRdsFlag(rds.artificial_head);
|
||||
if (dynPtyEl) dynPtyEl.textContent = formatRdsFlag(rds.dynamic_pty);
|
||||
renderRdsAlternativeFrequencies(rds.alternative_frequencies_hz);
|
||||
if (rtEl) rtEl.textContent = rds.radio_text ?? "--";
|
||||
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">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">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>
|
||||
<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-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-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-decoding { color: var(--accent-green); }
|
||||
.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>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user