[fix](trx-rs): improve marine decode paths
Finish the pending MARINE frontend and decoder activation wiring, and lower the VDES detector power floors so weak signals are eligible for burst detection in the same power domain used by the IQ path. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -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_CANDIDATE_SCORE: f32 = 0.20;
|
||||||
const MIN_SYNC_PARSE_SCORE: f32 = 0.50;
|
const MIN_SYNC_PARSE_SCORE: f32 = 0.50;
|
||||||
const BURST_TRIGGER_NOISE_MULT: f32 = 1.8;
|
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_NOISE_MULT: f32 = 1.02;
|
||||||
const BURST_SUSTAIN_FLOOR: f32 = 5.0e-7;
|
const BURST_SUSTAIN_FLOOR: f32 = 1.0e-13;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct VdesDecoder {
|
pub struct VdesDecoder {
|
||||||
@@ -54,7 +54,7 @@ impl VdesDecoder {
|
|||||||
pub fn new(sample_rate: u32) -> Self {
|
pub fn new(sample_rate: u32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
sample_rate: sample_rate.max(1) as f32,
|
sample_rate: sample_rate.max(1) as f32,
|
||||||
noise_floor: 1.0e-4,
|
noise_floor: 1.0e-12,
|
||||||
in_burst: false,
|
in_burst: false,
|
||||||
quiet_run: 0,
|
quiet_run: 0,
|
||||||
burst_samples: Vec::new(),
|
burst_samples: Vec::new(),
|
||||||
@@ -62,7 +62,7 @@ impl VdesDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.noise_floor = 1.0e-4;
|
self.noise_floor = 1.0e-12;
|
||||||
self.in_burst = false;
|
self.in_burst = false;
|
||||||
self.quiet_run = 0;
|
self.quiet_run = 0;
|
||||||
self.burst_samples.clear();
|
self.burst_samples.clear();
|
||||||
|
|||||||
@@ -847,10 +847,10 @@ function drawSignalOverlay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lastFreqHz != null && currentBandwidthHz > 0) {
|
if (lastFreqHz != null && currentBandwidthHz > 0) {
|
||||||
const halfBw = currentBandwidthHz / 2;
|
for (const spec of visibleBandwidthSpecs(lastFreqHz)) {
|
||||||
for (const centerHz of visibleBandwidthCenters(lastFreqHz)) {
|
const halfBw = spec.widthHz / 2;
|
||||||
const xL = hzToX(centerHz - halfBw);
|
const xL = hzToX(spec.centerHz - halfBw);
|
||||||
const xR = hzToX(centerHz + halfBw);
|
const xR = hzToX(spec.centerHz + halfBw);
|
||||||
const stripW = xR - xL;
|
const stripW = xR - xL;
|
||||||
if (stripW <= 1) continue;
|
if (stripW <= 1) continue;
|
||||||
const grd = ctx.createLinearGradient(xL, 0, xR, 0);
|
const grd = ctx.createLinearGradient(xL, 0, xR, 0);
|
||||||
@@ -1216,25 +1216,53 @@ function isVdesMode(mode = modeEl ? modeEl.value : "") {
|
|||||||
return String(mode || "").toUpperCase() === "VDES";
|
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 : "") {
|
function coverageSpanForMode(freqHz, bandwidthHz = coverageGuardBandwidthHz(), mode = modeEl ? modeEl.value : "") {
|
||||||
if (!Number.isFinite(freqHz)) return null;
|
if (!Number.isFinite(freqHz)) return null;
|
||||||
const safeBw = Math.max(0, Number.isFinite(bandwidthHz) ? bandwidthHz : 0);
|
const specs = visibleBandwidthSpecs(freqHz, mode).map((spec) => {
|
||||||
let loHz = freqHz - safeBw / 2;
|
const widthHz = Math.max(
|
||||||
let hiHz = freqHz + safeBw / 2;
|
0,
|
||||||
if (isAisMode(mode)) {
|
Number.isFinite(spec.widthHz) ? spec.widthHz : Math.max(0, Number.isFinite(bandwidthHz) ? bandwidthHz : 0),
|
||||||
const aisBFreqHz = freqHz + 50_000;
|
);
|
||||||
loHz = Math.min(loHz, aisBFreqHz - safeBw / 2);
|
return {
|
||||||
hiHz = Math.max(hiHz, aisBFreqHz + safeBw / 2);
|
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 };
|
return { loHz, hiHz };
|
||||||
}
|
}
|
||||||
|
|
||||||
function visibleBandwidthCenters(freqHz = lastFreqHz, mode = modeEl ? modeEl.value : "") {
|
function visibleBandwidthCenters(freqHz = lastFreqHz, mode = modeEl ? modeEl.value : "") {
|
||||||
if (!Number.isFinite(freqHz)) return [];
|
return visibleBandwidthSpecs(freqHz, mode).map((spec) => spec.centerHz);
|
||||||
if (isAisMode(mode)) {
|
|
||||||
return [freqHz, freqHz + 50_000];
|
|
||||||
}
|
|
||||||
return [freqHz];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function effectiveSpectrumCoverageSpanHz(sampleRateHz) {
|
function effectiveSpectrumCoverageSpanHz(sampleRateHz) {
|
||||||
@@ -2043,14 +2071,14 @@ function render(update) {
|
|||||||
const wsprStatus = document.getElementById("wspr-status");
|
const wsprStatus = document.getElementById("wspr-status");
|
||||||
setModeBoundDecodeStatus(
|
setModeBoundDecodeStatus(
|
||||||
aisStatus,
|
aisStatus,
|
||||||
["AIS"],
|
["AIS", "MARINE"],
|
||||||
"Select AIS mode to decode",
|
"Select AIS mode to decode",
|
||||||
"Connected, listening for packets",
|
"Connected, listening for packets",
|
||||||
);
|
);
|
||||||
if (window.updateAisBar) window.updateAisBar();
|
if (window.updateAisBar) window.updateAisBar();
|
||||||
setModeBoundDecodeStatus(
|
setModeBoundDecodeStatus(
|
||||||
vdesStatus,
|
vdesStatus,
|
||||||
["VDES"],
|
["VDES", "MARINE"],
|
||||||
"Select VDES mode to decode",
|
"Select VDES mode to decode",
|
||||||
"Connected, listening for bursts",
|
"Connected, listening for bursts",
|
||||||
);
|
);
|
||||||
@@ -2748,6 +2776,7 @@ const MODE_BW_DEFAULTS = {
|
|||||||
FM: [12_500, 2_500, 25_000, 500],
|
FM: [12_500, 2_500, 25_000, 500],
|
||||||
AIS: [25_000, 12_500, 50_000, 500],
|
AIS: [25_000, 12_500, 50_000, 500],
|
||||||
VDES: [100_000, 25_000, 200_000, 1_000],
|
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],
|
WFM: [180_000, 50_000,300_000,5_000],
|
||||||
DIG: [3_000, 300, 6_000, 100],
|
DIG: [3_000, 300, 6_000, 100],
|
||||||
PKT: [25_000, 300, 50_000, 500],
|
PKT: [25_000, 300, 50_000, 500],
|
||||||
@@ -4345,9 +4374,9 @@ function updateDecodeStatus(text) {
|
|||||||
const aprs = document.getElementById("aprs-status");
|
const aprs = document.getElementById("aprs-status");
|
||||||
const cw = document.getElementById("cw-status");
|
const cw = document.getElementById("cw-status");
|
||||||
const ft8 = document.getElementById("ft8-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;
|
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);
|
setModeBoundDecodeStatus(aprs, ["PKT"], "Select PKT mode to decode", text);
|
||||||
if (cw && cw.textContent !== "Receiving") cw.textContent = text;
|
if (cw && cw.textContent !== "Receiving") cw.textContent = text;
|
||||||
if (ft8 && ft8.textContent !== "Receiving") ft8.textContent = text;
|
if (ft8 && ft8.textContent !== "Receiving") ft8.textContent = text;
|
||||||
@@ -5148,8 +5177,8 @@ function drawSpectrum(data) {
|
|||||||
if (_bwDragEdge) {
|
if (_bwDragEdge) {
|
||||||
// Bottom bookmark tab centered on each visible channel, shown while resizing BW
|
// Bottom bookmark tab centered on each visible channel, shown while resizing BW
|
||||||
const bwText = formatBwLabel(currentBandwidthHz);
|
const bwText = formatBwLabel(currentBandwidthHz);
|
||||||
for (const centerHz of visibleBandwidthCenters(lastFreqHz)) {
|
for (const spec of visibleBandwidthSpecs(lastFreqHz)) {
|
||||||
const xMid = hzToX(centerHz);
|
const xMid = hzToX(spec.centerHz);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.font = `bold ${Math.round(10 * dpr)}px sans-serif`;
|
ctx.font = `bold ${Math.round(10 * dpr)}px sans-serif`;
|
||||||
const tw = ctx.measureText(bwText).width;
|
const tw = ctx.measureText(bwText).width;
|
||||||
@@ -5539,13 +5568,14 @@ if (overviewCanvas) {
|
|||||||
// ── BW strip edge hit-test (CSS pixels) ──────────────────────────────────────
|
// ── BW strip edge hit-test (CSS pixels) ──────────────────────────────────────
|
||||||
function getBwEdgeHit(cssX, cssW, range) {
|
function getBwEdgeHit(cssX, cssW, range) {
|
||||||
if (!lastFreqHz || !currentBandwidthHz || !lastSpectrumData) return null;
|
if (!lastFreqHz || !currentBandwidthHz || !lastSpectrumData) return null;
|
||||||
const halfBw = currentBandwidthHz / 2;
|
if (isMarineMode()) return null;
|
||||||
const HIT = 8;
|
const HIT = 8;
|
||||||
let bestEdge = null;
|
let bestEdge = null;
|
||||||
let bestDist = Number.POSITIVE_INFINITY;
|
let bestDist = Number.POSITIVE_INFINITY;
|
||||||
for (const centerHz of visibleBandwidthCenters(lastFreqHz)) {
|
for (const spec of visibleBandwidthSpecs(lastFreqHz)) {
|
||||||
const xL = ((centerHz - halfBw - range.visLoHz) / range.visSpanHz) * cssW;
|
const halfBw = spec.widthHz / 2;
|
||||||
const xR = ((centerHz + halfBw - range.visLoHz) / range.visSpanHz) * cssW;
|
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 distL = Math.abs(cssX - xL);
|
||||||
const distR = Math.abs(cssX - xR);
|
const distR = Math.abs(cssX - xR);
|
||||||
if (distL < HIT && distL < bestDist) {
|
if (distL < HIT && distL < bestDist) {
|
||||||
|
|||||||
@@ -891,7 +891,7 @@ fn downmix_if_needed(frame: Vec<f32>, channels: u16) -> Vec<f32> {
|
|||||||
mono
|
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(
|
pub async fn run_ais_decoder(
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
channels: u16,
|
channels: u16,
|
||||||
@@ -905,14 +905,14 @@ pub async fn run_ais_decoder(
|
|||||||
let mut decoder_a = AisDecoder::new(sample_rate);
|
let mut decoder_a = AisDecoder::new(sample_rate);
|
||||||
let mut decoder_b = AisDecoder::new(sample_rate);
|
let mut decoder_b = AisDecoder::new(sample_rate);
|
||||||
let mut was_active = false;
|
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 {
|
loop {
|
||||||
if !active {
|
if !active {
|
||||||
match state_rx.changed().await {
|
match state_rx.changed().await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let state = state_rx.borrow();
|
let state = state_rx.borrow();
|
||||||
active = matches!(state.status.mode, RigMode::AIS);
|
active = matches!(state.status.mode, RigMode::AIS | RigMode::MARINE);
|
||||||
if active {
|
if active {
|
||||||
pcm_a_rx = pcm_a_rx.resubscribe();
|
pcm_a_rx = pcm_a_rx.resubscribe();
|
||||||
pcm_b_rx = pcm_b_rx.resubscribe();
|
pcm_b_rx = pcm_b_rx.resubscribe();
|
||||||
@@ -958,7 +958,7 @@ pub async fn run_ais_decoder(
|
|||||||
match changed {
|
match changed {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let state = state_rx.borrow();
|
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 {
|
if !active && was_active {
|
||||||
decoder_a.reset();
|
decoder_a.reset();
|
||||||
decoder_b.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(
|
pub async fn run_vdes_decoder(
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
mut iq_rx: broadcast::Receiver<Vec<Complex<f32>>>,
|
mut iq_rx: broadcast::Receiver<Vec<Complex<f32>>>,
|
||||||
@@ -987,14 +987,14 @@ pub async fn run_vdes_decoder(
|
|||||||
info!("VDES decoder started ({}Hz complex baseband)", sample_rate);
|
info!("VDES decoder started ({}Hz complex baseband)", sample_rate);
|
||||||
let mut decoder = VdesDecoder::new(sample_rate);
|
let mut decoder = VdesDecoder::new(sample_rate);
|
||||||
let mut was_active = false;
|
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 {
|
loop {
|
||||||
if !active {
|
if !active {
|
||||||
match state_rx.changed().await {
|
match state_rx.changed().await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let state = state_rx.borrow();
|
let state = state_rx.borrow();
|
||||||
active = matches!(state.status.mode, RigMode::VDES);
|
active = matches!(state.status.mode, RigMode::VDES | RigMode::MARINE);
|
||||||
if active {
|
if active {
|
||||||
iq_rx = iq_rx.resubscribe();
|
iq_rx = iq_rx.resubscribe();
|
||||||
}
|
}
|
||||||
@@ -1024,7 +1024,7 @@ pub async fn run_vdes_decoder(
|
|||||||
match changed {
|
match changed {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let state = state_rx.borrow();
|
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 {
|
if !active && was_active {
|
||||||
decoder.reset();
|
decoder.reset();
|
||||||
was_active = false;
|
was_active = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user