Add per-channel RDS overlays for WFM vchans
This commit is contained in:
@@ -146,7 +146,7 @@ async fn run_spectrum_connection(
|
|||||||
warn!("Spectrum connection dropped: {}", e);
|
warn!("Spectrum connection dropped: {}", e);
|
||||||
}
|
}
|
||||||
// Mark spectrum unavailable while reconnecting.
|
// Mark spectrum unavailable while reconnecting.
|
||||||
config.spectrum.send_modify(|s| s.set(None));
|
config.spectrum.send_modify(|s| s.set(None, None));
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => warn!("Spectrum connect failed: {}", e),
|
Ok(Err(e)) => warn!("Spectrum connect failed: {}", e),
|
||||||
Err(_) => warn!("Spectrum connect timed out"),
|
Err(_) => warn!("Spectrum connect timed out"),
|
||||||
@@ -183,18 +183,20 @@ async fn handle_spectrum_connection(
|
|||||||
}
|
}
|
||||||
_ = interval.tick() => {
|
_ = interval.tick() => {
|
||||||
if !should_poll_spectrum(config) {
|
if !should_poll_spectrum(config) {
|
||||||
config.spectrum.send_modify(|s| s.set(None));
|
config.spectrum.send_modify(|s| s.set(None, None));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match send_command_no_state_update(
|
match send_command_no_state_update(
|
||||||
config, &mut writer, &mut reader,
|
config, &mut writer, &mut reader,
|
||||||
ClientCommand::GetSpectrum,
|
ClientCommand::GetSpectrum,
|
||||||
).await {
|
).await {
|
||||||
Ok(snapshot) => config.spectrum.send_modify(|s| s.set(snapshot.spectrum)),
|
Ok(snapshot) => config
|
||||||
|
.spectrum
|
||||||
|
.send_modify(|s| s.set(snapshot.spectrum, snapshot.vchan_rds)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// A spectrum timeout desynchronises the TCP framing;
|
// A spectrum timeout desynchronises the TCP framing;
|
||||||
// return so the caller reconnects and restores sync.
|
// return so the caller reconnects and restores sync.
|
||||||
config.spectrum.send_modify(|s| s.set(None));
|
config.spectrum.send_modify(|s| s.set(None, None));
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -775,6 +777,7 @@ mod tests {
|
|||||||
cw_tone_hz: 700,
|
cw_tone_hz: 700,
|
||||||
filter: None,
|
filter: None,
|
||||||
spectrum: None,
|
spectrum: None,
|
||||||
|
vchan_rds: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,15 +73,20 @@ pub struct SharedSpectrum {
|
|||||||
/// RDS JSON pre-serialised at ingestion so SSE clients don't repeat the
|
/// RDS JSON pre-serialised at ingestion so SSE clients don't repeat the
|
||||||
/// work on every tick.
|
/// work on every tick.
|
||||||
pub rds_json: Option<String>,
|
pub rds_json: Option<String>,
|
||||||
|
/// Virtual-channel RDS JSON pre-serialised at ingestion.
|
||||||
|
pub vchan_rds_json: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SharedSpectrum {
|
impl SharedSpectrum {
|
||||||
/// Replace the stored frame, pre-serialising RDS in one pass.
|
/// Replace the stored frame, pre-serialising RDS in one pass.
|
||||||
pub fn set(&mut self, frame: Option<SpectrumData>) {
|
pub fn set(&mut self, frame: Option<SpectrumData>, vchan_rds: Option<Vec<trx_core::rig::state::VchanRdsEntry>>) {
|
||||||
self.rds_json = frame
|
self.rds_json = frame
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|f| f.rds.as_ref())
|
.and_then(|f| f.rds.as_ref())
|
||||||
.and_then(|r| serde_json::to_string(r).ok());
|
.and_then(|r| serde_json::to_string(r).ok());
|
||||||
|
self.vchan_rds_json = vchan_rds
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|list| serde_json::to_string(list).ok());
|
||||||
self.frame = frame.map(Arc::new);
|
self.frame = frame.map(Arc::new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ mod tests {
|
|||||||
cw_tone_hz: 700,
|
cw_tone_hz: 700,
|
||||||
filter: None,
|
filter: None,
|
||||||
spectrum: None,
|
spectrum: None,
|
||||||
|
vchan_rds: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -351,6 +351,9 @@ const headerRigSwitchSelect = document.getElementById("header-rig-switch-select"
|
|||||||
const headerStylePickSelect = document.getElementById("header-style-pick-select");
|
const headerStylePickSelect = document.getElementById("header-style-pick-select");
|
||||||
const rdsPsOverlay = document.getElementById("rds-ps-overlay");
|
const rdsPsOverlay = document.getElementById("rds-ps-overlay");
|
||||||
let overviewPeakHoldMs = Number(loadSetting("overviewPeakHoldMs", 2000));
|
let overviewPeakHoldMs = Number(loadSetting("overviewPeakHoldMs", 2000));
|
||||||
|
let primaryRds = null;
|
||||||
|
let vchanRdsById = new Map();
|
||||||
|
let rdsOverlayEntries = [];
|
||||||
|
|
||||||
function syncTopBarAccess() {
|
function syncTopBarAccess() {
|
||||||
const loggedOut = authEnabled && !authRole;
|
const loggedOut = authEnabled && !authRole;
|
||||||
@@ -1260,23 +1263,151 @@ function refreshFreqDisplay() {
|
|||||||
refreshWavelengthDisplay(lastFreqHz);
|
refreshWavelengthDisplay(lastFreqHz);
|
||||||
}
|
}
|
||||||
|
|
||||||
function positionRdsPsOverlay() {
|
function activeRdsChannelId() {
|
||||||
if (!rdsPsOverlay || !lastSpectrumData || lastFreqHz == null || !overviewCanvas) return;
|
if (typeof vchanActiveId !== "undefined" && vchanActiveId) return vchanActiveId;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeChannelRds() {
|
||||||
|
if (!activeChannelIsWfm()) return null;
|
||||||
|
const activeId = activeRdsChannelId();
|
||||||
|
if (activeId) {
|
||||||
|
const rds = vchanRdsById.get(activeId);
|
||||||
|
if (rds) return rds;
|
||||||
|
if (typeof vchanChannels !== "undefined" && Array.isArray(vchanChannels) && vchanChannels.length > 0) {
|
||||||
|
if (vchanChannels[0].id === activeId) return primaryRds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return primaryRds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeChannelIsWfm() {
|
||||||
|
if (typeof vchanChannels !== "undefined" && Array.isArray(vchanChannels) && vchanChannels.length > 0) {
|
||||||
|
const activeId = activeRdsChannelId();
|
||||||
|
const active = vchanChannels.find((ch) => ch.id === activeId) || vchanChannels[0];
|
||||||
|
return String(active?.mode || "").toUpperCase() === "WFM";
|
||||||
|
}
|
||||||
|
return lastModeName === "WFM";
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeChannelFreqHz() {
|
||||||
|
if (typeof vchanActiveChannel === "function") {
|
||||||
|
const ch = vchanActiveChannel();
|
||||||
|
if (Number.isFinite(ch?.freq_hz)) return ch.freq_hz;
|
||||||
|
}
|
||||||
|
return lastFreqHz;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRdsOverlayHtml(rds) {
|
||||||
|
const ps = rds?.program_service;
|
||||||
|
const hasPs = !!(ps && ps.length > 0);
|
||||||
|
const hasPi = rds?.pi != null;
|
||||||
|
if (!hasPs && !hasPi) return "";
|
||||||
|
const mainText = hasPs ? formatOverlayPs(ps) : formatOverlayPi(rds?.pi);
|
||||||
|
const mainClass = hasPs ? "rds-ps-main" : "rds-ps-fallback";
|
||||||
|
const metaText = hasPs
|
||||||
|
? `${formatOverlayPi(rds?.pi)} · ${formatOverlayPty(rds?.pty, rds?.pty_name)}`
|
||||||
|
: (rds?.pty_name ?? (rds?.pty != null ? String(rds.pty) : ""));
|
||||||
|
const trafficFlags =
|
||||||
|
`<span class="rds-ps-flags">` +
|
||||||
|
`${overlayTrafficFlagHtml("TP", rds?.traffic_program)}` +
|
||||||
|
`${overlayTrafficFlagHtml("TA", rds?.traffic_announcement)}` +
|
||||||
|
`</span>`;
|
||||||
|
return (
|
||||||
|
`<span class="${mainClass}">${hasPs ? formatPsHtml(ps) : escapeMapHtml(mainText)}</span>` +
|
||||||
|
`<span class="rds-ps-meta">` +
|
||||||
|
`<span class="rds-ps-meta-text">${escapeMapHtml(metaText)}</span>` +
|
||||||
|
`${trafficFlags}` +
|
||||||
|
`</span>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRdsOverlayEntries() {
|
||||||
|
const entries = [];
|
||||||
|
if (typeof vchanChannels !== "undefined" && Array.isArray(vchanChannels) && vchanChannels.length > 0) {
|
||||||
|
for (const ch of vchanChannels) {
|
||||||
|
if (String(ch?.mode || "").toUpperCase() !== "WFM") continue;
|
||||||
|
if (!Number.isFinite(ch?.freq_hz)) continue;
|
||||||
|
const rds = vchanRdsById.get(ch.id) || (vchanChannels[0].id === ch.id ? primaryRds : null);
|
||||||
|
if (!rds) continue;
|
||||||
|
entries.push({ id: ch.id, freq_hz: ch.freq_hz, rds });
|
||||||
|
}
|
||||||
|
} else if (lastModeName === "WFM" && primaryRds && Number.isFinite(lastFreqHz)) {
|
||||||
|
entries.push({ id: "primary", freq_hz: lastFreqHz, rds: primaryRds });
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRdsOverlays() {
|
||||||
|
if (!rdsPsOverlay) return;
|
||||||
|
if (!lastSpectrumData || !overviewCanvas) {
|
||||||
|
rdsOverlayEntries = [];
|
||||||
|
rdsPsOverlay.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entries = collectRdsOverlayEntries();
|
||||||
|
rdsOverlayEntries = [];
|
||||||
|
rdsPsOverlay.innerHTML = "";
|
||||||
|
if (entries.length === 0) {
|
||||||
|
rdsPsOverlay.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entries.forEach((entry, idx) => {
|
||||||
|
const html = buildRdsOverlayHtml(entry.rds);
|
||||||
|
if (!html) return;
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "rds-ps-overlay-item";
|
||||||
|
el.dataset.freqHz = String(entry.freq_hz);
|
||||||
|
el.dataset.stackIdx = String(idx);
|
||||||
|
el.innerHTML = html;
|
||||||
|
el.addEventListener("click", (evt) => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
copyRdsPsToClipboard(entry.rds, entry.freq_hz);
|
||||||
|
});
|
||||||
|
rdsPsOverlay.appendChild(el);
|
||||||
|
rdsOverlayEntries.push({ ...entry, el, stackIdx: idx });
|
||||||
|
});
|
||||||
|
if (rdsOverlayEntries.length === 0) {
|
||||||
|
rdsPsOverlay.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rdsPsOverlay.style.display = "block";
|
||||||
|
positionRdsOverlays();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.renderRdsOverlays = renderRdsOverlays;
|
||||||
|
|
||||||
|
function positionRdsOverlays() {
|
||||||
|
if (!rdsPsOverlay || !lastSpectrumData || !overviewCanvas || rdsOverlayEntries.length === 0) return;
|
||||||
const width = overviewCanvas.clientWidth || overviewCanvas.width || 0;
|
const width = overviewCanvas.clientWidth || overviewCanvas.width || 0;
|
||||||
if (width <= 0) {
|
if (width <= 0) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const range = spectrumVisibleRange(lastSpectrumData);
|
const range = spectrumVisibleRange(lastSpectrumData);
|
||||||
if (!Number.isFinite(range.visLoHz) || !Number.isFinite(range.visSpanHz) || range.visSpanHz <= 0) {
|
if (!Number.isFinite(range.visLoHz) || !Number.isFinite(range.visSpanHz) || range.visSpanHz <= 0) return;
|
||||||
return;
|
const count = rdsOverlayEntries.length;
|
||||||
}
|
const mid = (count - 1) / 2;
|
||||||
const rel = (lastFreqHz - range.visLoHz) / range.visSpanHz;
|
const stackStepPx = 26;
|
||||||
const clamped = Math.max(0.06, Math.min(0.94, rel));
|
rdsOverlayEntries.forEach((entry, idx) => {
|
||||||
rdsPsOverlay.style.left = `${clamped * width}px`;
|
const el = entry.el;
|
||||||
|
if (!el) return;
|
||||||
|
if (!Number.isFinite(entry.freq_hz)) {
|
||||||
|
el.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.style.display = "";
|
||||||
|
const rel = (entry.freq_hz - range.visLoHz) / range.visSpanHz;
|
||||||
|
const clamped = Math.max(0.06, Math.min(0.94, rel));
|
||||||
|
const offsetPx = Math.round((idx - mid) * stackStepPx);
|
||||||
|
el.style.left = `${clamped * width}px`;
|
||||||
|
el.style.top = `calc(50% + ${offsetPx}px)`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionRdsPsOverlay() {
|
||||||
|
positionRdsOverlays();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetRdsDisplay() {
|
function resetRdsDisplay() {
|
||||||
updateRdsPsOverlay(null);
|
updateRdsPsOverlay(primaryRds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetWfmStereoIndicator() {
|
function resetWfmStereoIndicator() {
|
||||||
@@ -1290,12 +1421,13 @@ function applyLocalTunedFrequency(hz, forceDisplay = false) {
|
|||||||
if (!Number.isFinite(hz)) return;
|
if (!Number.isFinite(hz)) return;
|
||||||
const freqChanged = lastFreqHz !== hz;
|
const freqChanged = lastFreqHz !== hz;
|
||||||
if (freqChanged) {
|
if (freqChanged) {
|
||||||
|
primaryRds = null;
|
||||||
resetRdsDisplay();
|
resetRdsDisplay();
|
||||||
resetWfmStereoIndicator();
|
resetWfmStereoIndicator();
|
||||||
}
|
}
|
||||||
lastFreqHz = hz;
|
lastFreqHz = hz;
|
||||||
window.lastFreqHz = lastFreqHz;
|
window.lastFreqHz = lastFreqHz;
|
||||||
updateDocumentTitle(lastSpectrumData?.rds ?? null);
|
updateDocumentTitle(activeChannelRds());
|
||||||
refreshWavelengthDisplay(lastFreqHz);
|
refreshWavelengthDisplay(lastFreqHz);
|
||||||
if (forceDisplay) {
|
if (forceDisplay) {
|
||||||
freqDirty = false;
|
freqDirty = false;
|
||||||
@@ -2243,7 +2375,7 @@ function updateTitle() {
|
|||||||
titleEl.textContent = serverVersion ? `trx-rs v${serverVersion}` : "trx-rs";
|
titleEl.textContent = serverVersion ? `trx-rs v${serverVersion}` : "trx-rs";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateDocumentTitle(lastSpectrumData?.rds ?? null);
|
updateDocumentTitle(activeChannelRds());
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayLabelFromUrl(url) {
|
function displayLabelFromUrl(url) {
|
||||||
@@ -2434,29 +2566,30 @@ function render(update) {
|
|||||||
if (update.status && update.status.mode) {
|
if (update.status && update.status.mode) {
|
||||||
const mode = normalizeMode(update.status.mode);
|
const mode = normalizeMode(update.status.mode);
|
||||||
const modeUpper = mode ? mode.toUpperCase() : "";
|
const modeUpper = mode ? mode.toUpperCase() : "";
|
||||||
|
const onVirtual = typeof vchanIsOnVirtual === "function" && vchanIsOnVirtual();
|
||||||
// When subscribed to a virtual channel the mode picker must reflect
|
// When subscribed to a virtual channel the mode picker must reflect
|
||||||
// that channel's mode, not the primary rig mode. Skip the update here;
|
// that channel's mode, not the primary rig mode. Skip the update here;
|
||||||
// vchan.js will apply the correct mode via vchanSyncModeDisplay().
|
// vchan.js will apply the correct mode via vchanSyncModeDisplay().
|
||||||
if (typeof vchanIsOnVirtual !== "function" || !vchanIsOnVirtual()) {
|
if (!onVirtual) {
|
||||||
modeEl.value = modeUpper;
|
modeEl.value = modeUpper;
|
||||||
|
if (modeUpper === "WFM" && lastModeName !== "WFM") {
|
||||||
|
setJogDivisor(10);
|
||||||
|
resetRdsDisplay();
|
||||||
|
} else if (modeUpper !== "WFM" && lastModeName === "WFM") {
|
||||||
|
resetRdsDisplay();
|
||||||
|
}
|
||||||
|
lastModeName = modeUpper;
|
||||||
|
// When filter panel is active (SDR backend), update the BW slider range
|
||||||
|
// to match the new mode — but only if the server hasn't already sent a
|
||||||
|
// filter state that overrides it.
|
||||||
|
// When SDR backend is active (spectrum visible), apply BW default for new
|
||||||
|
// mode — but only if the server hasn't already pushed a filter_state.
|
||||||
|
if (lastSpectrumData && !update.filter) {
|
||||||
|
applyBwDefaultForMode(mode, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (modeUpper === "WFM" && lastModeName !== "WFM") {
|
|
||||||
setJogDivisor(10);
|
|
||||||
resetRdsDisplay();
|
|
||||||
} else if (modeUpper !== "WFM" && lastModeName === "WFM") {
|
|
||||||
resetRdsDisplay();
|
|
||||||
}
|
|
||||||
lastModeName = modeUpper;
|
|
||||||
updateWfmControls();
|
updateWfmControls();
|
||||||
updateSdrSquelchControlVisibility();
|
updateSdrSquelchControlVisibility();
|
||||||
// When filter panel is active (SDR backend), update the BW slider range
|
|
||||||
// to match the new mode — but only if the server hasn't already sent a
|
|
||||||
// filter state that overrides it.
|
|
||||||
// When SDR backend is active (spectrum visible), apply BW default for new
|
|
||||||
// mode — but only if the server hasn't already pushed a filter_state.
|
|
||||||
if (lastSpectrumData && !update.filter) {
|
|
||||||
applyBwDefaultForMode(mode, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const modeUpper = update.status && update.status.mode ? normalizeMode(update.status.mode).toUpperCase() : "";
|
const modeUpper = update.status && update.status.mode ? normalizeMode(update.status.mode).toUpperCase() : "";
|
||||||
const aisStatus = document.getElementById("ais-status");
|
const aisStatus = document.getElementById("ais-status");
|
||||||
@@ -6658,6 +6791,7 @@ function startSpectrumStreaming() {
|
|||||||
const centerHz = Number(evt.data.slice(0, commaA));
|
const centerHz = Number(evt.data.slice(0, commaA));
|
||||||
const sampleRate = Number(evt.data.slice(commaA + 1, commaB));
|
const sampleRate = Number(evt.data.slice(commaA + 1, commaB));
|
||||||
const b64 = evt.data.slice(commaB + 1);
|
const b64 = evt.data.slice(commaB + 1);
|
||||||
|
const hadSpectrum = !!lastSpectrumData;
|
||||||
const raw = atob(b64);
|
const raw = atob(b64);
|
||||||
const bins = new Array(raw.length);
|
const bins = new Array(raw.length);
|
||||||
for (let i = 0; i < raw.length; i++) bins[i] = (raw.charCodeAt(i) << 24 >> 24);
|
for (let i = 0; i < raw.length; i++) bins[i] = (raw.charCodeAt(i) << 24 >> 24);
|
||||||
@@ -6676,7 +6810,11 @@ function startSpectrumStreaming() {
|
|||||||
refreshCenterFreqDisplay();
|
refreshCenterFreqDisplay();
|
||||||
if (window.refreshCwTonePicker) window.refreshCwTonePicker();
|
if (window.refreshCwTonePicker) window.refreshCwTonePicker();
|
||||||
scheduleSpectrumDraw();
|
scheduleSpectrumDraw();
|
||||||
if (lastModeName === "WFM") updateRdsPsOverlay(lastSpectrumData.rds);
|
if (!hadSpectrum) {
|
||||||
|
updateRdsPsOverlay(lastSpectrumData.rds);
|
||||||
|
} else {
|
||||||
|
positionRdsPsOverlay();
|
||||||
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
// Named "rds" event = RDS metadata changed (emitted only when it changes).
|
// Named "rds" event = RDS metadata changed (emitted only when it changes).
|
||||||
@@ -6684,8 +6822,20 @@ function startSpectrumStreaming() {
|
|||||||
try {
|
try {
|
||||||
const rds = evt.data === "null" ? undefined : JSON.parse(evt.data);
|
const rds = evt.data === "null" ? undefined : JSON.parse(evt.data);
|
||||||
if (lastSpectrumData) lastSpectrumData.rds = rds;
|
if (lastSpectrumData) lastSpectrumData.rds = rds;
|
||||||
if (lastModeName === "WFM") updateRdsPsOverlay(rds ?? null);
|
updateRdsPsOverlay(rds ?? null);
|
||||||
updateDocumentTitle(rds ?? null);
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
spectrumSource.addEventListener("rds_vchan", (evt) => {
|
||||||
|
try {
|
||||||
|
const payload = evt.data === "null" ? [] : JSON.parse(evt.data);
|
||||||
|
const next = new Map();
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
payload.forEach((entry) => {
|
||||||
|
if (entry && entry.id) next.set(entry.id, entry.rds ?? null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
vchanRdsById = next;
|
||||||
|
updateRdsPsOverlay(primaryRds);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
});
|
});
|
||||||
spectrumSource.onerror = () => {
|
spectrumSource.onerror = () => {
|
||||||
@@ -6790,9 +6940,10 @@ function formatMinuteTimestamp(date = new Date()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildRdsRawPayload(rds) {
|
function buildRdsRawPayload(rds) {
|
||||||
|
const freqHz = activeChannelFreqHz();
|
||||||
return {
|
return {
|
||||||
time: formatMinuteTimestamp(),
|
time: formatMinuteTimestamp(),
|
||||||
freq_hz: Number.isFinite(lastFreqHz) ? Math.round(lastFreqHz) : null,
|
freq_hz: Number.isFinite(freqHz) ? Math.round(freqHz) : null,
|
||||||
...rds,
|
...rds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -6841,14 +6992,15 @@ function renderRdsAlternativeFrequencies(list) {
|
|||||||
if (!afEl.childElementCount) afEl.textContent = "--";
|
if (!afEl.childElementCount) afEl.textContent = "--";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyRdsPsToClipboard() {
|
async function copyRdsPsToClipboard(rdsOverride = null, freqOverrideHz = null) {
|
||||||
const rds = lastSpectrumData?.rds;
|
const rds = rdsOverride || activeChannelRds();
|
||||||
const ps = rds?.program_service;
|
const ps = rds?.program_service;
|
||||||
if (!rds || !ps || ps.length === 0) {
|
if (!rds || !ps || ps.length === 0) {
|
||||||
showHint("No RDS PS", 1200);
|
showHint("No RDS PS", 1200);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const freqMhz = Number.isFinite(lastFreqHz) ? (Math.round((lastFreqHz / 100_000)) / 10).toFixed(1) : "--.-";
|
const freqHz = Number.isFinite(freqOverrideHz) ? freqOverrideHz : activeChannelFreqHz();
|
||||||
|
const freqMhz = Number.isFinite(freqHz) ? (Math.round((freqHz / 100_000)) / 10).toFixed(1) : "--.-";
|
||||||
const piHex = rds.pi != null
|
const piHex = rds.pi != null
|
||||||
? `0x${rds.pi.toString(16).toUpperCase().padStart(4, "0")}`
|
? `0x${rds.pi.toString(16).toUpperCase().padStart(4, "0")}`
|
||||||
: "--";
|
: "--";
|
||||||
@@ -6877,9 +7029,6 @@ async function copyRdsRawToClipboard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rdsPsOverlay) {
|
|
||||||
rdsPsOverlay.addEventListener("click", () => { copyRdsPsToClipboard(); });
|
|
||||||
}
|
|
||||||
const rdsPsValueEl = document.getElementById("rds-ps");
|
const rdsPsValueEl = document.getElementById("rds-ps");
|
||||||
if (rdsPsValueEl) {
|
if (rdsPsValueEl) {
|
||||||
rdsPsValueEl.addEventListener("click", () => { copyRdsPsToClipboard(); });
|
rdsPsValueEl.addEventListener("click", () => { copyRdsPsToClipboard(); });
|
||||||
@@ -6900,38 +7049,10 @@ if (rdsAfListEl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateRdsPsOverlay(rds) {
|
function updateRdsPsOverlay(rds) {
|
||||||
updateDocumentTitle(rds);
|
primaryRds = rds || null;
|
||||||
// Overview strip overlay
|
const activeRds = activeChannelRds();
|
||||||
if (rdsPsOverlay) {
|
updateDocumentTitle(activeRds);
|
||||||
const ps = rds?.program_service;
|
renderRdsOverlays();
|
||||||
const hasPs = !!(ps && ps.length > 0);
|
|
||||||
const hasPi = rds?.pi != null;
|
|
||||||
if (hasPs || hasPi) {
|
|
||||||
const mainText = hasPs
|
|
||||||
? formatOverlayPs(ps)
|
|
||||||
: formatOverlayPi(rds?.pi);
|
|
||||||
const mainClass = hasPs ? "rds-ps-main" : "rds-ps-fallback";
|
|
||||||
const metaText = hasPs
|
|
||||||
? `${formatOverlayPi(rds?.pi)} · ${formatOverlayPty(rds?.pty, rds?.pty_name)}`
|
|
||||||
: (rds?.pty_name ?? (rds?.pty != null ? String(rds.pty) : ""));
|
|
||||||
const trafficFlags =
|
|
||||||
`<span class="rds-ps-flags">` +
|
|
||||||
`${overlayTrafficFlagHtml("TP", rds?.traffic_program)}` +
|
|
||||||
`${overlayTrafficFlagHtml("TA", rds?.traffic_announcement)}` +
|
|
||||||
`</span>`;
|
|
||||||
rdsPsOverlay.innerHTML =
|
|
||||||
`<span class="${mainClass}">${hasPs ? formatPsHtml(ps) : escapeMapHtml(mainText)}</span>` +
|
|
||||||
`<span class="rds-ps-meta">` +
|
|
||||||
`<span class="rds-ps-meta-text">${escapeMapHtml(metaText)}</span>` +
|
|
||||||
`${trafficFlags}` +
|
|
||||||
`</span>`;
|
|
||||||
positionRdsPsOverlay();
|
|
||||||
rdsPsOverlay.style.display = "flex";
|
|
||||||
} else {
|
|
||||||
rdsPsOverlay.innerHTML = "";
|
|
||||||
rdsPsOverlay.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RDS debug panel
|
// RDS debug panel
|
||||||
const statusEl = document.getElementById("rds-status");
|
const statusEl = document.getElementById("rds-status");
|
||||||
@@ -6956,7 +7077,7 @@ function updateRdsPsOverlay(rds) {
|
|||||||
// Always show the current mode, frame counter, and a sanitised spectrum snapshot
|
// Always show the current mode, frame counter, and a sanitised spectrum snapshot
|
||||||
if (modeEl) modeEl.textContent = document.getElementById("mode")?.value || "--";
|
if (modeEl) modeEl.textContent = document.getElementById("mode")?.value || "--";
|
||||||
|
|
||||||
if (!rds) {
|
if (!activeRds) {
|
||||||
statusEl.textContent = "No signal";
|
statusEl.textContent = "No signal";
|
||||||
statusEl.className = "rds-value rds-no-signal";
|
statusEl.className = "rds-value rds-no-signal";
|
||||||
piEl.textContent = "--";
|
piEl.textContent = "--";
|
||||||
@@ -6975,9 +7096,10 @@ function updateRdsPsOverlay(rds) {
|
|||||||
if (rtEl) rtEl.textContent = "--";
|
if (rtEl) rtEl.textContent = "--";
|
||||||
if (rawEl && lastSpectrumData) {
|
if (rawEl && lastSpectrumData) {
|
||||||
const { bins: _b, ...rest } = lastSpectrumData;
|
const { bins: _b, ...rest } = lastSpectrumData;
|
||||||
|
const freqHz = activeChannelFreqHz();
|
||||||
rawEl.textContent = JSON.stringify({
|
rawEl.textContent = JSON.stringify({
|
||||||
time: formatMinuteTimestamp(),
|
time: formatMinuteTimestamp(),
|
||||||
freq_hz: Number.isFinite(lastFreqHz) ? Math.round(lastFreqHz) : null,
|
freq_hz: Number.isFinite(freqHz) ? Math.round(freqHz) : null,
|
||||||
...rest,
|
...rest,
|
||||||
}, null, 2);
|
}, null, 2);
|
||||||
}
|
}
|
||||||
@@ -6986,29 +7108,31 @@ function updateRdsPsOverlay(rds) {
|
|||||||
|
|
||||||
statusEl.textContent = "Decoding";
|
statusEl.textContent = "Decoding";
|
||||||
statusEl.className = "rds-value rds-decoding";
|
statusEl.className = "rds-value rds-decoding";
|
||||||
piEl.textContent = rds.pi != null ? `0x${rds.pi.toString(16).toUpperCase().padStart(4, "0")}` : "--";
|
piEl.textContent = activeRds.pi != null ? `0x${activeRds.pi.toString(16).toUpperCase().padStart(4, "0")}` : "--";
|
||||||
if (psEl) {
|
if (psEl) {
|
||||||
if (rds.program_service) {
|
if (activeRds.program_service) {
|
||||||
psEl.innerHTML = formatPsHtml(rds.program_service);
|
psEl.innerHTML = formatPsHtml(activeRds.program_service);
|
||||||
} else {
|
} else {
|
||||||
psEl.textContent = "--";
|
psEl.textContent = "--";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ptyEl.textContent = rds.pty_name ?? (rds.pty != null ? String(rds.pty) : "--");
|
ptyEl.textContent = activeRds.pty_name ?? (activeRds.pty != null ? String(activeRds.pty) : "--");
|
||||||
ptyNameEl.textContent = rds.pty != null ? String(rds.pty) : "--";
|
ptyNameEl.textContent = activeRds.pty != null ? String(activeRds.pty) : "--";
|
||||||
if (ptynEl) ptynEl.textContent = rds.program_type_name_long ?? "--";
|
if (ptynEl) ptynEl.textContent = activeRds.program_type_name_long ?? "--";
|
||||||
if (tpEl) tpEl.textContent = formatRdsFlag(rds.traffic_program);
|
if (tpEl) tpEl.textContent = formatRdsFlag(activeRds.traffic_program);
|
||||||
if (taEl) taEl.textContent = formatRdsFlag(rds.traffic_announcement);
|
if (taEl) taEl.textContent = formatRdsFlag(activeRds.traffic_announcement);
|
||||||
if (musicEl) musicEl.textContent = formatRdsAudio(rds.music);
|
if (musicEl) musicEl.textContent = formatRdsAudio(activeRds.music);
|
||||||
if (stereoEl) stereoEl.textContent = formatRdsFlag(rds.stereo);
|
if (stereoEl) stereoEl.textContent = formatRdsFlag(activeRds.stereo);
|
||||||
if (compEl) compEl.textContent = formatRdsFlag(rds.compressed);
|
if (compEl) compEl.textContent = formatRdsFlag(activeRds.compressed);
|
||||||
if (headEl) headEl.textContent = formatRdsFlag(rds.artificial_head);
|
if (headEl) headEl.textContent = formatRdsFlag(activeRds.artificial_head);
|
||||||
if (dynPtyEl) dynPtyEl.textContent = formatRdsFlag(rds.dynamic_pty);
|
if (dynPtyEl) dynPtyEl.textContent = formatRdsFlag(activeRds.dynamic_pty);
|
||||||
renderRdsAlternativeFrequencies(rds.alternative_frequencies_hz);
|
renderRdsAlternativeFrequencies(activeRds.alternative_frequencies_hz);
|
||||||
if (rtEl) rtEl.textContent = rds.radio_text ?? "--";
|
if (rtEl) rtEl.textContent = activeRds.radio_text ?? "--";
|
||||||
rawEl.textContent = JSON.stringify(buildRdsRawPayload(rds), null, 2);
|
rawEl.textContent = JSON.stringify(buildRdsRawPayload(activeRds), null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.refreshRdsUi = () => updateRdsPsOverlay(primaryRds);
|
||||||
|
|
||||||
function scheduleSpectrumDraw() {
|
function scheduleSpectrumDraw() {
|
||||||
if (spectrumDrawPending) return;
|
if (spectrumDrawPending) return;
|
||||||
spectrumDrawPending = true;
|
spectrumDrawPending = true;
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ function vchanHandleChannels(data) {
|
|||||||
vchanReconnectAudio();
|
vchanReconnectAudio();
|
||||||
}
|
}
|
||||||
vchanRender();
|
vchanRender();
|
||||||
|
if (typeof renderRdsOverlays === "function") renderRdsOverlays();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("vchan: bad channels event", e);
|
console.warn("vchan: bad channels event", e);
|
||||||
}
|
}
|
||||||
@@ -234,6 +235,25 @@ function vchanSyncModeDisplay() {
|
|||||||
if (ch && ch.mode) modeEl.value = ch.mode.toUpperCase();
|
if (ch && ch.mode) modeEl.value = ch.mode.toUpperCase();
|
||||||
}
|
}
|
||||||
// When on primary channel, app.js rig-state updates handle the picker.
|
// When on primary channel, app.js rig-state updates handle the picker.
|
||||||
|
const modeUpper = (modeEl.value || "").toUpperCase();
|
||||||
|
if (typeof lastModeName !== "undefined") {
|
||||||
|
if (modeUpper === "WFM" && lastModeName !== "WFM") {
|
||||||
|
if (typeof setJogDivisor === "function") setJogDivisor(10);
|
||||||
|
if (typeof resetRdsDisplay === "function") resetRdsDisplay();
|
||||||
|
} else if (modeUpper !== "WFM" && lastModeName === "WFM") {
|
||||||
|
if (typeof resetRdsDisplay === "function") resetRdsDisplay();
|
||||||
|
}
|
||||||
|
lastModeName = modeUpper;
|
||||||
|
}
|
||||||
|
if (typeof updateWfmControls === "function") updateWfmControls();
|
||||||
|
if (typeof updateSdrSquelchControlVisibility === "function") {
|
||||||
|
updateSdrSquelchControlVisibility();
|
||||||
|
}
|
||||||
|
if (typeof refreshRdsUi === "function") {
|
||||||
|
refreshRdsUi();
|
||||||
|
} else if (typeof positionRdsPsOverlay === "function") {
|
||||||
|
positionRdsPsOverlay();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync the BW input to the active virtual channel's bandwidth.
|
// Sync the BW input to the active virtual channel's bandwidth.
|
||||||
|
|||||||
@@ -620,10 +620,13 @@ small { color: var(--text-muted); }
|
|||||||
#rds-ps-overlay {
|
#rds-ps-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
inset: 0;
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.rds-ps-overlay-item {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-heading);
|
color: var(--text-heading);
|
||||||
|
|||||||
@@ -501,6 +501,7 @@ pub async fn spectrum(
|
|||||||
// woken exactly when new spectrum data is pushed (no 40 ms polling needed).
|
// woken exactly when new spectrum data is pushed (no 40 ms polling needed).
|
||||||
let rx = context.spectrum.subscribe();
|
let rx = context.spectrum.subscribe();
|
||||||
let mut last_rds_json: Option<String> = None;
|
let mut last_rds_json: Option<String> = None;
|
||||||
|
let mut last_vchan_rds_json: Option<String> = None;
|
||||||
let mut last_had_frame = false;
|
let mut last_had_frame = false;
|
||||||
let updates = WatchStream::new(rx).filter_map(move |snapshot| {
|
let updates = WatchStream::new(rx).filter_map(move |snapshot| {
|
||||||
let sse_chunk: Option<String> = if let Some(ref frame) = snapshot.frame {
|
let sse_chunk: Option<String> = if let Some(ref frame) = snapshot.frame {
|
||||||
@@ -513,6 +514,11 @@ pub async fn spectrum(
|
|||||||
chunk.push_str(&format!("event: rds\ndata: {data}\n\n"));
|
chunk.push_str(&format!("event: rds\ndata: {data}\n\n"));
|
||||||
last_rds_json = snapshot.rds_json;
|
last_rds_json = snapshot.rds_json;
|
||||||
}
|
}
|
||||||
|
if snapshot.vchan_rds_json != last_vchan_rds_json {
|
||||||
|
let data = snapshot.vchan_rds_json.as_deref().unwrap_or("null");
|
||||||
|
chunk.push_str(&format!("event: rds_vchan\ndata: {data}\n\n"));
|
||||||
|
last_vchan_rds_json = snapshot.vchan_rds_json;
|
||||||
|
}
|
||||||
Some(chunk)
|
Some(chunk)
|
||||||
} else if last_had_frame {
|
} else if last_had_frame {
|
||||||
last_had_frame = false;
|
last_had_frame = false;
|
||||||
@@ -1573,6 +1579,7 @@ async fn wait_for_view(mut rx: watch::Receiver<RigState>) -> Result<RigSnapshot,
|
|||||||
wspr_decode_enabled: state.wspr_decode_enabled,
|
wspr_decode_enabled: state.wspr_decode_enabled,
|
||||||
filter: state.filter.clone(),
|
filter: state.filter.clone(),
|
||||||
spectrum: None,
|
spectrum: None,
|
||||||
|
vchan_rds: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -665,6 +665,7 @@ mod tests {
|
|||||||
cw_tone_hz: 0,
|
cw_tone_hz: 0,
|
||||||
filter: None,
|
filter: None,
|
||||||
spectrum: None,
|
spectrum: None,
|
||||||
|
vchan_rds: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,6 +230,11 @@ pub trait RigCat: Rig + Send {
|
|||||||
fn get_spectrum(&self) -> Option<state::SpectrumData> {
|
fn get_spectrum(&self) -> Option<state::SpectrumData> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the latest per-virtual-channel RDS data if supported.
|
||||||
|
fn get_vchan_rds(&self) -> Option<Vec<state::VchanRdsEntry>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot of a rig's status that every backend can expose.
|
/// Snapshot of a rig's status that every backend can expose.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// SPDX-License-Identifier: BSD-2-Clause
|
// SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::radio::freq::Freq;
|
use crate::radio::freq::Freq;
|
||||||
use crate::rig::{RigControl, RigInfo, RigRxStatus, RigStatus, RigStatusProvider, RigTxStatus};
|
use crate::rig::{RigControl, RigInfo, RigRxStatus, RigStatus, RigStatusProvider, RigTxStatus};
|
||||||
@@ -52,6 +53,10 @@ pub struct RigState {
|
|||||||
/// Skipped in serde (not part of persistent state); flows into RigSnapshot on demand.
|
/// Skipped in serde (not part of persistent state); flows into RigSnapshot on demand.
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub spectrum: Option<SpectrumData>,
|
pub spectrum: Option<SpectrumData>,
|
||||||
|
/// Latest virtual-channel RDS data from SDR backends.
|
||||||
|
/// Skipped in serde (not part of persistent state); flows into RigSnapshot on demand.
|
||||||
|
#[serde(skip)]
|
||||||
|
pub vchan_rds: Option<Vec<VchanRdsEntry>>,
|
||||||
#[serde(default, skip_serializing)]
|
#[serde(default, skip_serializing)]
|
||||||
pub aprs_decode_reset_seq: u64,
|
pub aprs_decode_reset_seq: u64,
|
||||||
#[serde(default, skip_serializing)]
|
#[serde(default, skip_serializing)]
|
||||||
@@ -144,6 +149,7 @@ impl RigState {
|
|||||||
cw_tone_hz: 700,
|
cw_tone_hz: 700,
|
||||||
filter: None,
|
filter: None,
|
||||||
spectrum: None,
|
spectrum: None,
|
||||||
|
vchan_rds: None,
|
||||||
aprs_decode_reset_seq: 0,
|
aprs_decode_reset_seq: 0,
|
||||||
hf_aprs_decode_reset_seq: 0,
|
hf_aprs_decode_reset_seq: 0,
|
||||||
cw_decode_reset_seq: 0,
|
cw_decode_reset_seq: 0,
|
||||||
@@ -207,6 +213,7 @@ impl RigState {
|
|||||||
wspr_decode_enabled: snapshot.wspr_decode_enabled,
|
wspr_decode_enabled: snapshot.wspr_decode_enabled,
|
||||||
filter: snapshot.filter,
|
filter: snapshot.filter,
|
||||||
spectrum: None, // spectrum flows through /api/spectrum, not persistent state
|
spectrum: None, // spectrum flows through /api/spectrum, not persistent state
|
||||||
|
vchan_rds: None, // vchan RDS flows through /api/spectrum, not persistent state
|
||||||
aprs_decode_reset_seq: 0,
|
aprs_decode_reset_seq: 0,
|
||||||
hf_aprs_decode_reset_seq: 0,
|
hf_aprs_decode_reset_seq: 0,
|
||||||
cw_decode_reset_seq: 0,
|
cw_decode_reset_seq: 0,
|
||||||
@@ -248,6 +255,7 @@ impl RigState {
|
|||||||
wspr_decode_enabled: self.wspr_decode_enabled,
|
wspr_decode_enabled: self.wspr_decode_enabled,
|
||||||
filter: self.filter.clone(),
|
filter: self.filter.clone(),
|
||||||
spectrum: self.spectrum.clone(),
|
spectrum: self.spectrum.clone(),
|
||||||
|
vchan_rds: self.vchan_rds.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,6 +377,16 @@ pub struct RdsData {
|
|||||||
pub alternative_frequencies_hz: Option<Vec<u32>>,
|
pub alternative_frequencies_hz: Option<Vec<u32>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// RDS metadata snapshot for a virtual channel.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct VchanRdsEntry {
|
||||||
|
/// Virtual channel UUID.
|
||||||
|
pub id: Uuid,
|
||||||
|
/// Latest RDS data, if decoded.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub rds: Option<RdsData>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Read-only projection of state shared with clients.
|
/// Read-only projection of state shared with clients.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct RigSnapshot {
|
pub struct RigSnapshot {
|
||||||
@@ -409,4 +427,7 @@ pub struct RigSnapshot {
|
|||||||
pub filter: Option<RigFilterState>,
|
pub filter: Option<RigFilterState>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub spectrum: Option<SpectrumData>,
|
pub spectrum: Option<SpectrumData>,
|
||||||
|
/// Per-virtual-channel RDS snapshots, when available.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub vchan_rds: Option<Vec<VchanRdsEntry>>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ mod tests {
|
|||||||
cw_tone_hz: 0,
|
cw_tone_hz: 0,
|
||||||
filter: None,
|
filter: None,
|
||||||
spectrum: None,
|
spectrum: None,
|
||||||
|
vchan_rds: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -544,8 +544,10 @@ async fn process_command(
|
|||||||
RigCommand::GetSpectrum => {
|
RigCommand::GetSpectrum => {
|
||||||
// Fetch current spectrum and embed it in a one-shot snapshot.
|
// Fetch current spectrum and embed it in a one-shot snapshot.
|
||||||
ctx.state.spectrum = ctx.rig.get_spectrum();
|
ctx.state.spectrum = ctx.rig.get_spectrum();
|
||||||
|
ctx.state.vchan_rds = ctx.rig.get_vchan_rds();
|
||||||
let result = snapshot_from(ctx.state);
|
let result = snapshot_from(ctx.state);
|
||||||
ctx.state.spectrum = None; // don't persist in ongoing state
|
ctx.state.spectrum = None; // don't persist in ongoing state
|
||||||
|
ctx.state.vchan_rds = None; // don't persist in ongoing state
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
_ => {} // fall through to normal rig handler
|
_ => {} // fall through to normal rig handler
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
|
|
||||||
use trx_core::radio::freq::{Band, Freq};
|
use trx_core::radio::freq::{Band, Freq};
|
||||||
use trx_core::rig::response::RigError;
|
use trx_core::rig::response::RigError;
|
||||||
use trx_core::rig::state::{RigFilterState, SpectrumData, WfmDenoiseLevel};
|
use trx_core::rig::state::{RigFilterState, SpectrumData, VchanRdsEntry, WfmDenoiseLevel};
|
||||||
use trx_core::rig::{
|
use trx_core::rig::{
|
||||||
AudioSource, Rig, RigAccessMethod, RigCapabilities, RigCat, RigInfo, RigStatusFuture,
|
AudioSource, Rig, RigAccessMethod, RigCapabilities, RigCat, RigInfo, RigStatusFuture,
|
||||||
};
|
};
|
||||||
@@ -820,6 +820,10 @@ impl RigCat for SoapySdrRig {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_vchan_rds(&self) -> Option<Vec<VchanRdsEntry>> {
|
||||||
|
Some(self.channel_manager.rds_snapshots())
|
||||||
|
}
|
||||||
|
|
||||||
/// Override: this backend provides demodulated PCM audio.
|
/// Override: this backend provides demodulated PCM audio.
|
||||||
fn as_audio_source(&self) -> Option<&dyn AudioSource> {
|
fn as_audio_source(&self) -> Option<&dyn AudioSource> {
|
||||||
Some(self)
|
Some(self)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use std::sync::{Arc, RwLock};
|
|||||||
|
|
||||||
use num_complex::Complex;
|
use num_complex::Complex;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use trx_core::rig::state::RigMode;
|
use trx_core::rig::state::{RigMode, VchanRdsEntry};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::dsp::SdrPipeline;
|
use crate::dsp::SdrPipeline;
|
||||||
@@ -184,6 +184,22 @@ impl SdrVirtualChannelManager {
|
|||||||
ch.mode = mode.clone();
|
ch.mode = mode.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Snapshot RDS data for each WFM virtual channel (including primary).
|
||||||
|
pub fn rds_snapshots(&self) -> Vec<VchanRdsEntry> {
|
||||||
|
let channels = self.channels.read().unwrap();
|
||||||
|
let dsps = self.pipeline.channel_dsps.read().unwrap();
|
||||||
|
channels
|
||||||
|
.iter()
|
||||||
|
.filter(|ch| matches!(ch.mode, RigMode::WFM))
|
||||||
|
.map(|ch| {
|
||||||
|
let rds = dsps
|
||||||
|
.get(ch.pipeline_slot)
|
||||||
|
.and_then(|dsp| dsp.lock().ok().and_then(|d| d.rds_data()));
|
||||||
|
VchanRdsEntry { id: ch.id, rds }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VirtualChannelManager for SdrVirtualChannelManager {
|
impl VirtualChannelManager for SdrVirtualChannelManager {
|
||||||
|
|||||||
Reference in New Issue
Block a user