diff --git a/src/decoders/trx-vdes/src/lib.rs b/src/decoders/trx-vdes/src/lib.rs index 7d6425f..3d8d41a 100644 --- a/src/decoders/trx-vdes/src/lib.rs +++ b/src/decoders/trx-vdes/src/lib.rs @@ -37,9 +37,9 @@ const PI4_QPSK_DIBITS: [u8; 4] = [0b00, 0b01, 0b11, 0b10]; const MIN_SYNC_CANDIDATE_SCORE: f32 = 0.20; const MIN_SYNC_PARSE_SCORE: f32 = 0.50; const BURST_TRIGGER_NOISE_MULT: f32 = 1.8; -const BURST_TRIGGER_FLOOR: f32 = 1.0e-6; +const BURST_TRIGGER_FLOOR: f32 = 1.0e-12; const BURST_SUSTAIN_NOISE_MULT: f32 = 1.02; -const BURST_SUSTAIN_FLOOR: f32 = 5.0e-7; +const BURST_SUSTAIN_FLOOR: f32 = 1.0e-13; #[derive(Debug, Clone)] pub struct VdesDecoder { @@ -54,7 +54,7 @@ impl VdesDecoder { pub fn new(sample_rate: u32) -> Self { Self { sample_rate: sample_rate.max(1) as f32, - noise_floor: 1.0e-4, + noise_floor: 1.0e-12, in_burst: false, quiet_run: 0, burst_samples: Vec::new(), @@ -62,7 +62,7 @@ impl VdesDecoder { } pub fn reset(&mut self) { - self.noise_floor = 1.0e-4; + self.noise_floor = 1.0e-12; self.in_burst = false; self.quiet_run = 0; self.burst_samples.clear(); 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 a7738b1..7687c3c 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 @@ -847,10 +847,10 @@ function drawSignalOverlay() { } if (lastFreqHz != null && currentBandwidthHz > 0) { - const halfBw = currentBandwidthHz / 2; - for (const centerHz of visibleBandwidthCenters(lastFreqHz)) { - const xL = hzToX(centerHz - halfBw); - const xR = hzToX(centerHz + halfBw); + for (const spec of visibleBandwidthSpecs(lastFreqHz)) { + const halfBw = spec.widthHz / 2; + const xL = hzToX(spec.centerHz - halfBw); + const xR = hzToX(spec.centerHz + halfBw); const stripW = xR - xL; if (stripW <= 1) continue; const grd = ctx.createLinearGradient(xL, 0, xR, 0); @@ -1216,25 +1216,53 @@ function isVdesMode(mode = modeEl ? modeEl.value : "") { return String(mode || "").toUpperCase() === "VDES"; } +function isMarineMode(mode = modeEl ? modeEl.value : "") { + return String(mode || "").toUpperCase() === "MARINE"; +} + +function visibleBandwidthSpecs(freqHz = lastFreqHz, mode = modeEl ? modeEl.value : "") { + if (!Number.isFinite(freqHz)) return []; + const modeUpper = String(mode || "").toUpperCase(); + if (modeUpper === "MARINE") { + return [ + { centerHz: freqHz - 137_500, widthHz: 100_000 }, + { centerHz: freqHz, widthHz: 12_500 }, + { centerHz: freqHz + 50_000, widthHz: 12_500 }, + ]; + } + if (modeUpper === "AIS") { + return [ + { centerHz: freqHz, widthHz: currentBandwidthHz }, + { centerHz: freqHz + 50_000, widthHz: currentBandwidthHz }, + ]; + } + return [{ centerHz: freqHz, widthHz: currentBandwidthHz }]; +} + function coverageSpanForMode(freqHz, bandwidthHz = coverageGuardBandwidthHz(), mode = modeEl ? modeEl.value : "") { if (!Number.isFinite(freqHz)) return null; - const safeBw = Math.max(0, Number.isFinite(bandwidthHz) ? bandwidthHz : 0); - let loHz = freqHz - safeBw / 2; - let hiHz = freqHz + safeBw / 2; - if (isAisMode(mode)) { - const aisBFreqHz = freqHz + 50_000; - loHz = Math.min(loHz, aisBFreqHz - safeBw / 2); - hiHz = Math.max(hiHz, aisBFreqHz + safeBw / 2); + const specs = visibleBandwidthSpecs(freqHz, mode).map((spec) => { + const widthHz = Math.max( + 0, + Number.isFinite(spec.widthHz) ? spec.widthHz : Math.max(0, Number.isFinite(bandwidthHz) ? bandwidthHz : 0), + ); + return { + loHz: spec.centerHz - widthHz / 2, + hiHz: spec.centerHz + widthHz / 2, + }; + }); + if (specs.length === 0) return null; + let loHz = specs[0].loHz; + let hiHz = specs[0].hiHz; + for (const spec of specs.slice(1)) { + loHz = Math.min(loHz, spec.loHz); + hiHz = Math.max(hiHz, spec.hiHz); } return { loHz, hiHz }; } function visibleBandwidthCenters(freqHz = lastFreqHz, mode = modeEl ? modeEl.value : "") { - if (!Number.isFinite(freqHz)) return []; - if (isAisMode(mode)) { - return [freqHz, freqHz + 50_000]; - } - return [freqHz]; + return visibleBandwidthSpecs(freqHz, mode).map((spec) => spec.centerHz); } function effectiveSpectrumCoverageSpanHz(sampleRateHz) { @@ -2043,14 +2071,14 @@ function render(update) { const wsprStatus = document.getElementById("wspr-status"); setModeBoundDecodeStatus( aisStatus, - ["AIS"], + ["AIS", "MARINE"], "Select AIS mode to decode", "Connected, listening for packets", ); if (window.updateAisBar) window.updateAisBar(); setModeBoundDecodeStatus( vdesStatus, - ["VDES"], + ["VDES", "MARINE"], "Select VDES mode to decode", "Connected, listening for bursts", ); @@ -2748,6 +2776,7 @@ const MODE_BW_DEFAULTS = { FM: [12_500, 2_500, 25_000, 500], AIS: [25_000, 12_500, 50_000, 500], VDES: [100_000, 25_000, 200_000, 1_000], + MARINE: [100_000, 12_500, 100_000, 500], WFM: [180_000, 50_000,300_000,5_000], DIG: [3_000, 300, 6_000, 100], PKT: [25_000, 300, 50_000, 500], @@ -4345,9 +4374,9 @@ function updateDecodeStatus(text) { const aprs = document.getElementById("aprs-status"); const cw = document.getElementById("cw-status"); const ft8 = document.getElementById("ft8-status"); - setModeBoundDecodeStatus(ais, ["AIS"], "Select AIS mode to decode", text); + setModeBoundDecodeStatus(ais, ["AIS", "MARINE"], "Select AIS mode to decode", text); const vdesText = text === "Connected, listening for packets" ? "Connected, listening for bursts" : text; - setModeBoundDecodeStatus(vdes, ["VDES"], "Select VDES mode to decode", vdesText); + setModeBoundDecodeStatus(vdes, ["VDES", "MARINE"], "Select VDES mode to decode", vdesText); setModeBoundDecodeStatus(aprs, ["PKT"], "Select PKT mode to decode", text); if (cw && cw.textContent !== "Receiving") cw.textContent = text; if (ft8 && ft8.textContent !== "Receiving") ft8.textContent = text; @@ -5148,8 +5177,8 @@ function drawSpectrum(data) { if (_bwDragEdge) { // Bottom bookmark tab centered on each visible channel, shown while resizing BW const bwText = formatBwLabel(currentBandwidthHz); - for (const centerHz of visibleBandwidthCenters(lastFreqHz)) { - const xMid = hzToX(centerHz); + for (const spec of visibleBandwidthSpecs(lastFreqHz)) { + const xMid = hzToX(spec.centerHz); ctx.save(); ctx.font = `bold ${Math.round(10 * dpr)}px sans-serif`; const tw = ctx.measureText(bwText).width; @@ -5539,13 +5568,14 @@ if (overviewCanvas) { // ── BW strip edge hit-test (CSS pixels) ────────────────────────────────────── function getBwEdgeHit(cssX, cssW, range) { if (!lastFreqHz || !currentBandwidthHz || !lastSpectrumData) return null; - const halfBw = currentBandwidthHz / 2; + if (isMarineMode()) return null; const HIT = 8; let bestEdge = null; let bestDist = Number.POSITIVE_INFINITY; - for (const centerHz of visibleBandwidthCenters(lastFreqHz)) { - const xL = ((centerHz - halfBw - range.visLoHz) / range.visSpanHz) * cssW; - const xR = ((centerHz + halfBw - range.visLoHz) / range.visSpanHz) * cssW; + for (const spec of visibleBandwidthSpecs(lastFreqHz)) { + const halfBw = spec.widthHz / 2; + const xL = ((spec.centerHz - halfBw - range.visLoHz) / range.visSpanHz) * cssW; + const xR = ((spec.centerHz + halfBw - range.visLoHz) / range.visSpanHz) * cssW; const distL = Math.abs(cssX - xL); const distR = Math.abs(cssX - xR); if (distL < HIT && distL < bestDist) { diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index b9c7cfe..f59db18 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -891,7 +891,7 @@ fn downmix_if_needed(frame: Vec, channels: u16) -> Vec { mono } -/// Run the AIS decoder task. Only processes PCM when rig mode is AIS. +/// Run the AIS decoder task. Only processes PCM when rig mode is AIS or MARINE. pub async fn run_ais_decoder( sample_rate: u32, channels: u16, @@ -905,14 +905,14 @@ pub async fn run_ais_decoder( let mut decoder_a = AisDecoder::new(sample_rate); let mut decoder_b = AisDecoder::new(sample_rate); let mut was_active = false; - let mut active = matches!(state_rx.borrow().status.mode, RigMode::AIS); + let mut active = matches!(state_rx.borrow().status.mode, RigMode::AIS | RigMode::MARINE); loop { if !active { match state_rx.changed().await { Ok(()) => { let state = state_rx.borrow(); - active = matches!(state.status.mode, RigMode::AIS); + active = matches!(state.status.mode, RigMode::AIS | RigMode::MARINE); if active { pcm_a_rx = pcm_a_rx.resubscribe(); pcm_b_rx = pcm_b_rx.resubscribe(); @@ -958,7 +958,7 @@ pub async fn run_ais_decoder( match changed { Ok(()) => { let state = state_rx.borrow(); - active = matches!(state.status.mode, RigMode::AIS); + active = matches!(state.status.mode, RigMode::AIS | RigMode::MARINE); if !active && was_active { decoder_a.reset(); decoder_b.reset(); @@ -976,7 +976,7 @@ pub async fn run_ais_decoder( } } -/// Run the VDES decoder task. Only processes PCM when rig mode is VDES. +/// Run the VDES decoder task. Only processes PCM when rig mode is VDES or MARINE. pub async fn run_vdes_decoder( sample_rate: u32, mut iq_rx: broadcast::Receiver>>, @@ -987,14 +987,14 @@ pub async fn run_vdes_decoder( info!("VDES decoder started ({}Hz complex baseband)", sample_rate); let mut decoder = VdesDecoder::new(sample_rate); let mut was_active = false; - let mut active = matches!(state_rx.borrow().status.mode, RigMode::VDES); + let mut active = matches!(state_rx.borrow().status.mode, RigMode::VDES | RigMode::MARINE); loop { if !active { match state_rx.changed().await { Ok(()) => { let state = state_rx.borrow(); - active = matches!(state.status.mode, RigMode::VDES); + active = matches!(state.status.mode, RigMode::VDES | RigMode::MARINE); if active { iq_rx = iq_rx.resubscribe(); } @@ -1024,7 +1024,7 @@ pub async fn run_vdes_decoder( match changed { Ok(()) => { let state = state_rx.borrow(); - active = matches!(state.status.mode, RigMode::VDES); + active = matches!(state.status.mode, RigMode::VDES | RigMode::MARINE); if !active && was_active { decoder.reset(); was_active = false;