From 906db0aee2ec3410d269a2398abd8b0c9f390a8e Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Tue, 3 Mar 2026 01:22:09 +0100 Subject: [PATCH] [fix](trx-rs): handle VDES audio messages and offset sweet spot Recognize VDES decode frames in the audio client and keep sweet-spot scans from centering directly on the tuned frequency. Co-authored-by: Stan Grams Signed-off-by: Stan Grams --- src/trx-client/src/audio_client.rs | 5 +- .../trx-frontend-http/assets/web/app.js | 93 +++++++++++++++++-- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/src/trx-client/src/audio_client.rs b/src/trx-client/src/audio_client.rs index 6c1d4f1..580091b 100644 --- a/src/trx-client/src/audio_client.rs +++ b/src/trx-client/src/audio_client.rs @@ -20,7 +20,7 @@ use trx_frontend::RemoteRigEntry; use trx_core::audio::{ read_audio_msg, write_audio_msg, AudioStreamInfo, AUDIO_MSG_AIS_DECODE, AUDIO_MSG_APRS_DECODE, AUDIO_MSG_CW_DECODE, AUDIO_MSG_FT8_DECODE, AUDIO_MSG_RX_FRAME, - AUDIO_MSG_STREAM_INFO, AUDIO_MSG_TX_FRAME, AUDIO_MSG_WSPR_DECODE, + AUDIO_MSG_STREAM_INFO, AUDIO_MSG_TX_FRAME, AUDIO_MSG_VDES_DECODE, AUDIO_MSG_WSPR_DECODE, }; use trx_core::decode::DecodedMessage; @@ -148,7 +148,8 @@ async fn handle_audio_connection( let _ = rx_tx.send(Bytes::from(payload)); } Ok(( - AUDIO_MSG_AIS_DECODE + AUDIO_MSG_VDES_DECODE + | AUDIO_MSG_AIS_DECODE | AUDIO_MSG_APRS_DECODE | AUDIO_MSG_CW_DECODE | AUDIO_MSG_FT8_DECODE diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index 7687c3c..2a2b578 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -1273,6 +1273,54 @@ function effectiveSpectrumCoverageSpanHz(sampleRateHz) { return sampleRate * Math.max(0.01, Math.min(1.0, ratio)); } +function sweetSpotMinimumOffsetHz(bandwidthHz) { + if (!Number.isFinite(bandwidthHz) || bandwidthHz <= 0) return 0; + return bandwidthHz / 2; +} + +function sweetSpotCenterHasRequiredOffset(centerHz, freqHz, bandwidthHz) { + if (!Number.isFinite(centerHz) || !Number.isFinite(freqHz)) return false; + const minOffsetHz = sweetSpotMinimumOffsetHz(bandwidthHz); + if (!Number.isFinite(minOffsetHz) || minOffsetHz <= 0) return true; + return Math.abs(centerHz - freqHz) >= minOffsetHz - 1; +} + +function chooseSweetSpotCenterOutsideOffsetRange(freqHz, bandwidthHz, minCenterHz, maxCenterHz, preferredCenterHz = null) { + if (!Number.isFinite(freqHz) || !Number.isFinite(minCenterHz) || !Number.isFinite(maxCenterHz) || minCenterHz > maxCenterHz) { + return null; + } + + const minOffsetHz = sweetSpotMinimumOffsetHz(bandwidthHz); + if (!Number.isFinite(minOffsetHz) || minOffsetHz <= 0) { + const fallbackCenterHz = Number.isFinite(preferredCenterHz) ? preferredCenterHz : freqHz; + return alignFreqToRigStep(Math.round(Math.max(minCenterHz, Math.min(maxCenterHz, fallbackCenterHz)))); + } + + const targetCentersHz = []; + const lowerTargetHz = alignFreqToRigStep(Math.round(freqHz - minOffsetHz)); + const upperTargetHz = alignFreqToRigStep(Math.round(freqHz + minOffsetHz)); + if (lowerTargetHz >= minCenterHz && lowerTargetHz <= maxCenterHz) targetCentersHz.push(lowerTargetHz); + if (upperTargetHz >= minCenterHz && upperTargetHz <= maxCenterHz && !targetCentersHz.some((value) => Math.abs(value - upperTargetHz) < 1)) { + targetCentersHz.push(upperTargetHz); + } + if (!targetCentersHz.length) return null; + + if (Number.isFinite(preferredCenterHz)) { + let bestCenterHz = targetCentersHz[0]; + let bestDistance = Math.abs(bestCenterHz - preferredCenterHz); + for (const targetCenterHz of targetCentersHz.slice(1)) { + const distance = Math.abs(targetCenterHz - preferredCenterHz); + if (distance < bestDistance) { + bestDistance = distance; + bestCenterHz = targetCenterHz; + } + } + return bestCenterHz; + } + + return targetCentersHz[0]; +} + function requiredCenterFreqForCoverageInFrame(data, freqHz, bandwidthHz = coverageGuardBandwidthHz()) { if (!data || !Number.isFinite(freqHz)) return null; const sampleRate = effectiveSpectrumCoverageSpanHz(data.sample_rate); @@ -1370,7 +1418,13 @@ function sweetSpotCandidateForFrame(data, freqHz, bandwidthHz) { const requiredLoHz = span.loHz - spectrumCoverageMarginHz; const requiredHiHz = span.hiHz + spectrumCoverageMarginHz; if (requiredHiHz - requiredLoHz >= usableSpanHz) { - const fallbackCenterHz = requiredCenterFreqForCoverageInFrame(data, freqHz, bandwidthHz); + const fallbackCenterHz = chooseSweetSpotCenterOutsideOffsetRange( + freqHz, + bandwidthHz, + currentCenterHz - halfUsableSpanHz, + currentCenterHz + halfUsableSpanHz, + requiredCenterFreqForCoverageInFrame(data, freqHz, bandwidthHz), + ); if (!Number.isFinite(fallbackCenterHz)) return null; return { centerHz: fallbackCenterHz, score: Number.POSITIVE_INFINITY }; } @@ -1383,7 +1437,13 @@ function sweetSpotCandidateForFrame(data, freqHz, bandwidthHz) { const minCenterHz = Math.max(evalMinCenterHz, fitMinCenterHz); const maxCenterHz = Math.min(evalMaxCenterHz, fitMaxCenterHz); if (!Number.isFinite(minCenterHz) || !Number.isFinite(maxCenterHz) || minCenterHz > maxCenterHz) { - const fallbackCenterHz = requiredCenterFreqForCoverageInFrame(data, freqHz, bandwidthHz); + const fallbackCenterHz = chooseSweetSpotCenterOutsideOffsetRange( + freqHz, + bandwidthHz, + evalMinCenterHz, + evalMaxCenterHz, + requiredCenterFreqForCoverageInFrame(data, freqHz, bandwidthHz), + ); if (!Number.isFinite(fallbackCenterHz)) return null; return { centerHz: fallbackCenterHz, score: Number.POSITIVE_INFINITY }; } @@ -1409,6 +1469,9 @@ function sweetSpotCandidateForFrame(data, freqHz, bandwidthHz) { const endIdx = Math.min(maxIdx, startIdx + usableBins); const windowLoHz = fullLoHz + (startIdx / maxIdx) * sampleRate; const candidateCenterHz = windowLoHz + halfUsableSpanHz; + if (!sweetSpotCenterHasRequiredOffset(candidateCenterHz, freqHz, bandwidthHz)) { + continue; + } const signalLoIdx = Math.max(startIdx, Math.min(endIdx, spectrumBinIndexForHz(data, signalLoHz))); const signalHiIdx = Math.max(startIdx, Math.min(endIdx, spectrumBinIndexForHz(data, signalHiHz))); @@ -1429,7 +1492,13 @@ function sweetSpotCandidateForFrame(data, freqHz, bandwidthHz) { } if (!Number.isFinite(bestScore) || bestStartIdx == null) { - const fallbackCenterHz = requiredCenterFreqForCoverageInFrame(data, freqHz, bandwidthHz); + const fallbackCenterHz = chooseSweetSpotCenterOutsideOffsetRange( + freqHz, + bandwidthHz, + minCenterHz, + maxCenterHz, + requiredCenterFreqForCoverageInFrame(data, freqHz, bandwidthHz), + ); if (!Number.isFinite(fallbackCenterHz)) return null; return { centerHz: fallbackCenterHz, score: Number.POSITIVE_INFINITY }; } @@ -1462,7 +1531,16 @@ function sweetSpotProbeCenters(data, freqHz, bandwidthHz) { const requiredLoHz = span.loHz - spectrumCoverageMarginHz; const requiredHiHz = span.hiHz + spectrumCoverageMarginHz; if (requiredHiHz - requiredLoHz >= usableSpanHz) { - return [alignFreqToRigStep(Math.round(freqHz))]; + const probeCenters = []; + const minOffsetHz = sweetSpotMinimumOffsetHz(bandwidthHz); + for (const centerHz of [freqHz - minOffsetHz, freqHz + minOffsetHz]) { + const alignedHz = alignFreqToRigStep(Math.round(centerHz)); + if (sweetSpotCenterHasRequiredOffset(alignedHz, freqHz, bandwidthHz) + && !probeCenters.some((value) => Math.abs(value - alignedHz) < 1)) { + probeCenters.push(alignedHz); + } + } + return probeCenters; } const minCenterHz = requiredHiHz - halfUsableSpanHz; @@ -1476,13 +1554,16 @@ function sweetSpotProbeCenters(data, freqHz, bandwidthHz) { for (let i = 0; i < points; i++) { const frac = points === 1 ? 0.5 : i / (points - 1); const centerHz = alignFreqToRigStep(Math.round(minCenterHz + (maxCenterHz - minCenterHz) * frac)); - if (!centers.some((value) => Math.abs(value - centerHz) < 1)) { + if (sweetSpotCenterHasRequiredOffset(centerHz, freqHz, bandwidthHz) + && !centers.some((value) => Math.abs(value - centerHz) < 1)) { centers.push(centerHz); } } const currentCenterHz = alignFreqToRigStep(Math.round(Number(data.center_hz))); - if (Number.isFinite(currentCenterHz) && !centers.some((value) => Math.abs(value - currentCenterHz) < 1)) { + if (Number.isFinite(currentCenterHz) + && sweetSpotCenterHasRequiredOffset(currentCenterHz, freqHz, bandwidthHz) + && !centers.some((value) => Math.abs(value - currentCenterHz) < 1)) { centers.push(currentCenterHz); centers.sort((a, b) => a - b); }