From 67c7b5d1d30b00398dff2cff95efc3f253eaa0f2 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Tue, 3 Mar 2026 01:27:33 +0100 Subject: [PATCH] [feat](trx-rs): map VDES positions and restore burst gating Publish decoded VDES positions into the map and revert the VDES burst detector to its original gating thresholds. Co-authored-by: Stan Grams Signed-off-by: Stan Grams --- src/decoders/trx-vdes/src/lib.rs | 25 +--- .../trx-frontend-http/assets/web/app.js | 115 +++++++++++++++++- .../trx-frontend-http/assets/web/index.html | 1 + .../assets/web/plugins/vdes.js | 5 + 4 files changed, 126 insertions(+), 20 deletions(-) diff --git a/src/decoders/trx-vdes/src/lib.rs b/src/decoders/trx-vdes/src/lib.rs index aed2038..76a6251 100644 --- a/src/decoders/trx-vdes/src/lib.rs +++ b/src/decoders/trx-vdes/src/lib.rs @@ -20,9 +20,8 @@ use num_complex::Complex; use trx_core::decode::VdesMessage; const VDES_SYMBOL_RATE: f32 = 76_800.0; -const MIN_BURST_MS: f32 = 1.5; -const BURST_END_MS: f32 = 0.35; -const MAX_BURST_MS: f32 = 45.0; +const MIN_BURST_MS: f32 = 2.0; +const BURST_END_MS: f32 = 0.4; const MIN_BURST_SYMBOLS: usize = 64; const TER_MCS1_100_BURST_SYMBOLS: usize = 1_984; const TER_MCS1_100_RAMP_SYMBOLS: usize = 32; @@ -36,10 +35,10 @@ const TER_MCS1_100_SYNC_BITS: &[u8; TER_MCS1_100_SYNC_SYMBOLS] = b"1111110011010 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 = 3.0; -const BURST_TRIGGER_FLOOR: f32 = 1.0e-10; -const BURST_SUSTAIN_NOISE_MULT: f32 = 1.15; -const BURST_SUSTAIN_FLOOR: f32 = 1.0e-11; +const BURST_TRIGGER_NOISE_MULT: f32 = 8.0; +const BURST_TRIGGER_FLOOR: f32 = 2.0e-4; +const BURST_SUSTAIN_NOISE_MULT: f32 = 3.0; +const BURST_SUSTAIN_FLOOR: f32 = 1.2e-4; #[derive(Debug, Clone)] pub struct VdesDecoder { @@ -74,8 +73,6 @@ impl VdesDecoder { ((self.sample_rate * (MIN_BURST_MS / 1000.0)).round() as usize).max(16); let quiet_limit = ((self.sample_rate * (BURST_END_MS / 1000.0)).round() as u32).max(4); - let max_burst_samples = - ((self.sample_rate * (MAX_BURST_MS / 1000.0)).round() as usize).max(min_burst_samples); for &sample in samples { let power = sample.norm_sqr(); @@ -99,16 +96,6 @@ impl VdesDecoder { self.quiet_run = 0; } - if self.burst_samples.len() >= max_burst_samples { - if let Some(msg) = self.finalize_burst(channel) { - out.push(msg); - } - self.in_burst = false; - self.quiet_run = 0; - self.burst_samples.clear(); - continue; - } - if self.quiet_run >= quiet_limit { if self.burst_samples.len() >= min_burst_samples { if let Some(msg) = self.finalize_burst(channel) { 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 2a2b578..36c5c40 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 @@ -3139,10 +3139,11 @@ let aprsRadioPath = null; const stationMarkers = new Map(); const locatorMarkers = new Map(); const mapMarkers = new Set(); -const mapFilter = { ais: true, aprs: true, ft8: true, wspr: true }; +const mapFilter = { ais: true, vdes: true, aprs: true, ft8: true, wspr: true }; const APRS_TRACK_MAX_POINTS = 64; const AIS_TRACK_MAX_POINTS = 64; const aisMarkers = new Map(); +const vdesMarkers = new Map(); let selectedAisTrackMmsi = null; function syncAprsReceiverMarker() { @@ -3205,6 +3206,17 @@ window.clearMapMarkersByType = function(type) { return; } + if (type === "vdes") { + vdesMarkers.forEach((entry) => { + if (entry && entry.marker) { + if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap); + mapMarkers.delete(entry.marker); + } + }); + vdesMarkers.clear(); + return; + } + if (type === "ft8" || type === "wspr") { const prefix = `${type}:`; for (const [key, entry] of locatorMarkers.entries()) { @@ -3311,6 +3323,19 @@ function initAprsMap() { { className: "aprs-radio-path", weight: 2, interactive: false } ).addTo(aprsMap); } + return; + } + + if (marker._vdesKey) { + const entry = vdesMarkers.get(String(marker._vdesKey)); + if (!entry || !entry.msg) return; + e.popup.setContent(buildVdesPopupHtml(entry.msg)); + if (serverLat != null && serverLon != null) { + aprsRadioPath = L.polyline( + [[serverLat, serverLon], [ll.lat, ll.lng]], + { className: "aprs-radio-path", weight: 2, interactive: false } + ).addTo(aprsMap); + } } }); @@ -3334,6 +3359,7 @@ function initAprsMap() { applyMapFilter(); const aisFilter = document.getElementById("map-filter-ais"); + const vdesFilter = document.getElementById("map-filter-vdes"); const aprsFilter = document.getElementById("map-filter-aprs"); const ft8Filter = document.getElementById("map-filter-ft8"); const wsprFilter = document.getElementById("map-filter-wspr"); @@ -3349,6 +3375,12 @@ function initAprsMap() { } }); } + if (vdesFilter) { + vdesFilter.addEventListener("change", () => { + mapFilter.vdes = vdesFilter.checked; + applyMapFilter(); + }); + } if (aprsFilter) { aprsFilter.addEventListener("change", () => { mapFilter.aprs = aprsFilter.checked; @@ -3528,6 +3560,43 @@ function buildAisPopupHtml(msg) { ``; } +function buildVdesPopupHtml(msg) { + const age = formatTimeAgo(msg?.ts_ms); + const distKm = (serverLat != null && serverLon != null && msg?.lat != null && msg?.lon != null) + ? haversineKm(serverLat, serverLon, msg.lat, msg.lon) + : null; + const distStr = distKm != null + ? (distKm < 1 ? `${Math.round(distKm * 1000)} m` : `${distKm.toFixed(1)} km`) + : null; + const meta = [ + age, + distStr, + msg?.message_label ? escapeMapHtml(msg.message_label) : null, + Number.isFinite(msg?.link_id) ? `LID ${Number(msg.link_id)}` : null, + ].filter(Boolean).join(" · "); + let rows = ""; + if (distStr) rows += `Range${distStr} from TRX`; + rows += `Type${escapeMapHtml(String(msg?.message_type ?? "--"))}`; + if (Number.isFinite(msg?.source_id)) rows += `Source${escapeMapHtml(String(msg.source_id))}`; + if (Number.isFinite(msg?.destination_id)) rows += `Dest${escapeMapHtml(String(msg.destination_id))}`; + if (msg?.lat != null && msg?.lon != null) rows += `Pos${msg.lat.toFixed(5)}, ${msg.lon.toFixed(5)}`; + if (Number.isFinite(msg?.sync_score)) rows += `Sync${(Number(msg.sync_score) * 100).toFixed(0)}%`; + if (msg?.fec_state) rows += `FEC${escapeMapHtml(String(msg.fec_state))}`; + const info = [ + msg?.vessel_name, + msg?.callsign, + msg?.destination, + msg?.payload_preview, + ].filter(Boolean).map(escapeMapHtml).join(" ยท "); + const title = escapeMapHtml(msg?.vessel_name || msg?.callsign || "VDES Position"); + return `
` + + `
${title}
` + + (meta ? `
${meta}
` : "") + + (rows ? `${rows}
` : "") + + (info ? `
${info}
` : "") + + `
`; +} + function aprsPositionsEqual(a, b) { if (!a || !b) return false; return Math.abs(a[0] - b[0]) < 0.000001 && Math.abs(a[1] - b[1]) < 0.000001; @@ -3538,6 +3607,15 @@ function aisPositionsEqual(a, b) { return Math.abs(a[0] - b[0]) < 0.000001 && Math.abs(a[1] - b[1]) < 0.000001; } +function vdesMarkerKey(msg) { + if (Number.isFinite(msg?.source_id)) return `src:${Number(msg.source_id)}`; + if (Number.isFinite(msg?.mmsi) && Number(msg.mmsi) > 0) return `mmsi:${Number(msg.mmsi)}`; + if (msg?.lat != null && msg?.lon != null) { + return `pos:${Number(msg.lat).toFixed(4)}:${Number(msg.lon).toFixed(4)}:${Number(msg?.message_type ?? 0)}`; + } + return null; +} + function ensureAprsTrack(call, entry) { if (!aprsMap || !entry || !Array.isArray(entry.trackPoints) || entry.trackPoints.length < 2) return; if (entry.track) { @@ -3719,6 +3797,40 @@ window.aisMapAddVessel = function(msg) { applyMapFilter(); }; +window.vdesMapAddPoint = function(msg) { + if (msg == null || msg.lat == null || msg.lon == null) return; + const key = vdesMarkerKey(msg); + if (!key) return; + if (!aprsMap) initAprsMap(); + const popupHtml = buildVdesPopupHtml(msg); + const existing = vdesMarkers.get(key); + if (existing) { + existing.msg = msg; + if (existing.marker) { + existing.marker.setLatLng([msg.lat, msg.lon]); + existing.marker.setPopupContent(popupHtml); + } + return; + } + const entry = { + marker: null, + msg, + }; + vdesMarkers.set(key, entry); + if (!aprsMap) return; + const marker = L.circleMarker([msg.lat, msg.lon], { + radius: 5, + color: "#5c394f", + fillColor: "#c46392", + fillOpacity: 0.82, + }).addTo(aprsMap).bindPopup(popupHtml); + marker.__trxType = "vdes"; + marker._vdesKey = key; + entry.marker = marker; + mapMarkers.add(marker); + applyMapFilter(); +}; + function maidenheadToBounds(grid) { if (!grid || grid.length < 4) return null; const g = grid.toUpperCase(); @@ -3754,6 +3866,7 @@ function applyMapFilter() { const type = marker.__trxType; const visible = (type === "ais" && mapFilter.ais) || + (type === "vdes" && mapFilter.vdes) || (type === "aprs" && mapFilter.aprs) || (type === "ft8" && mapFilter.ft8) || (type === "wspr" && mapFilter.wspr); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index d67e20b..0105dca 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -560,6 +560,7 @@