From c8d81ed3534f1884a6176a2ed1eff243621c5fc7 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Tue, 3 Mar 2026 00:54:31 +0100 Subject: [PATCH] [feat](trx-rs): add marine mode scaffolding and VDES fallback Keep weak VDES bursts visible by emitting unsynced diagnostic frames instead of dropping them, remove receiver badges from FT8 and WSPR history rows, and carry the current MARINE composite-mode scaffolding through the shared mode enums and backend mappings. Co-authored-by: OpenAI Codex Signed-off-by: Stanislaw Grams --- src/decoders/trx-vdes/src/lib.rs | 75 +++++++++++++++++-- .../assets/web/plugins/ft8.js | 5 +- .../assets/web/plugins/wspr.js | 5 +- src/trx-core/src/rig/state.rs | 1 + src/trx-protocol/src/codec.rs | 2 + src/trx-server/src/main.rs | 3 +- src/trx-server/src/rig_task.rs | 4 +- .../trx-backend/trx-backend-ft450d/src/lib.rs | 2 +- .../trx-backend/trx-backend-ft817/src/lib.rs | 2 +- .../trx-backend-soapysdr/src/demod.rs | 2 +- .../trx-backend-soapysdr/src/dsp/channel.rs | 2 +- .../trx-backend-soapysdr/src/lib.rs | 2 +- 12 files changed, 85 insertions(+), 20 deletions(-) diff --git a/src/decoders/trx-vdes/src/lib.rs b/src/decoders/trx-vdes/src/lib.rs index ed14666..19b9918 100644 --- a/src/decoders/trx-vdes/src/lib.rs +++ b/src/decoders/trx-vdes/src/lib.rs @@ -33,6 +33,8 @@ const TER_MCS1_100_FEC_OUTPUT_BITS: usize = 1_872; const TER_MCS1_100_FEC_TAIL_BITS: usize = 10; const TER_MCS1_100_SYNC_BITS: &[u8; TER_MCS1_100_SYNC_SYMBOLS] = b"111111001101010000011001010"; 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; #[derive(Debug, Clone)] pub struct VdesDecoder { @@ -117,14 +119,32 @@ impl VdesDecoder { } let framed = extract_candidate_frame(&symbols)?; - let link_id = decode_link_id_from_symbols(&framed.symbols); + let rms = burst_rms(&samples); + let mode = classify_vdes_burst(framed.symbols.len()); let payload_symbols = framed.payload_symbols(); let deinterleaved = deinterleave_100khz_frame(payload_symbols); + if framed.sync_score < MIN_SYNC_PARSE_SCORE { + return Some(build_unsynced_message( + channel, + &framed, + &mode, + rms, + &deinterleaved, + )); + } + + let link_id = decode_link_id_from_symbols(&framed.symbols); let (fec_input_symbols, fec_tail_symbols) = split_fec_frame(&deinterleaved); let coded_bits = dibits_to_bits(fec_input_symbols); let decoded_bits = viterbi_decode_rate_half(&coded_bits); if decoded_bits.is_empty() { - return None; + return Some(build_unsynced_message( + channel, + &framed, + &mode, + rms, + &deinterleaved, + )); } let parsed = parse_vdes_payload(&decoded_bits); let payload_bits = if parsed.payload_bits.is_empty() { @@ -133,8 +153,6 @@ impl VdesDecoder { parsed.payload_bits.as_slice() }; let raw_bytes = pack_bits_msb(payload_bits); - let rms = burst_rms(&samples); - let mode = classify_vdes_burst(framed.symbols.len()); let link_text = link_id .map(|value| format!("LID {}", value)) .unwrap_or_else(|| "LID ?".to_string()); @@ -308,7 +326,7 @@ fn extract_candidate_frame(symbols: &[u8]) -> Option { } } } - if best_score <= 0.5 { + if best_score <= MIN_SYNC_CANDIDATE_SCORE { return None; } @@ -327,6 +345,53 @@ fn extract_candidate_frame(symbols: &[u8]) -> Option { }) } +fn build_unsynced_message( + channel: &str, + framed: &FrameSlice, + mode: &BurstMode<'_>, + rms: f32, + deinterleaved: &[u8], +) -> VdesMessage { + let raw_bytes = pack_dibits_msb(deinterleaved); + let sync_pct = framed.sync_score * 100.0; + VdesMessage { + ts_ms: None, + channel: channel.to_string(), + message_type: mode.message_type, + repeat: 0, + mmsi: 0, + crc_ok: false, + bit_len: deinterleaved.len() * 2, + raw_bytes, + lat: None, + lon: None, + sog_knots: None, + cog_deg: None, + heading_deg: None, + nav_status: None, + vessel_name: Some(format!("Unsynced {} sym", framed.symbols.len())), + callsign: Some(format!("{} raw @{}", mode.label, framed.start_offset)), + destination: Some(format!( + "Weak sync {:.0}% ({}) · RMS {:.2} · raw symbol dump", + sync_pct, framed.sync_errors, rms + )), + message_label: Some("Unsynced".to_string()), + session_id: None, + source_id: None, + destination_id: None, + data_count: None, + asm_identifier: None, + ack_nack_mask: None, + channel_quality: None, + payload_preview: None, + link_id: None, + sync_score: Some(framed.sync_score), + sync_errors: Some(framed.sync_errors), + phase_rotation: Some(framed.phase_rotation), + fec_state: Some("Sync below parse threshold".to_string()), + } +} + fn syncword_score(symbols: &[u8], rotation: u8) -> (f32, u8) { if symbols.len() < TER_MCS1_100_SYNC_SYMBOLS { return (0.0, u8::MAX); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js index f721966..64dc848 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js @@ -34,10 +34,7 @@ function renderFt8Row(msg) { const rfHz = Number.isFinite(msg.freq_hz) && Number.isFinite(baseHz) ? (baseHz + msg.freq_hz) : null; const freq = Number.isFinite(rfHz) ? rfHz.toFixed(0) : "--"; const renderedMessage = renderFt8Message(rawMessage); - const receiverHtml = msg.receiver - ? `${msg.receiver.label}` - : ""; - row.innerHTML = `${fmtTime(msg.ts_ms)}${receiverHtml}${snr}${dt}${freq}${renderedMessage}`; + row.innerHTML = `${fmtTime(msg.ts_ms)}${snr}${dt}${freq}${renderedMessage}`; applyFt8FilterToRow(row); return row; } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js index c25750b..2bbea28 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js @@ -34,10 +34,7 @@ function renderWsprRow(msg) { const freq = Number.isFinite(rfHz) ? rfHz.toFixed(0) : "--"; const message = (msg.message || "").toString(); row.dataset.message = message.toUpperCase(); - const receiverHtml = msg.receiver - ? `${msg.receiver.label}` - : ""; - row.innerHTML = `${fmtWsprTime(msg.ts_ms)}${receiverHtml}${snr}${dt}${freq}${escapeWsprHtml(message)}`; + row.innerHTML = `${fmtWsprTime(msg.ts_ms)}${snr}${dt}${freq}${escapeWsprHtml(message)}`; applyWsprFilterToRow(row); return row; } diff --git a/src/trx-core/src/rig/state.rs b/src/trx-core/src/rig/state.rs index 5f0ddbc..c311fd4 100644 --- a/src/trx-core/src/rig/state.rs +++ b/src/trx-core/src/rig/state.rs @@ -72,6 +72,7 @@ pub enum RigMode { FM, AIS, VDES, + MARINE, DIG, PKT, Other(String), diff --git a/src/trx-protocol/src/codec.rs b/src/trx-protocol/src/codec.rs index 30cf1ad..72c1c15 100644 --- a/src/trx-protocol/src/codec.rs +++ b/src/trx-protocol/src/codec.rs @@ -24,6 +24,7 @@ pub fn parse_mode(s: &str) -> RigMode { "WFM" => RigMode::WFM, "AIS" => RigMode::AIS, "VDES" => RigMode::VDES, + "MARINE" => RigMode::MARINE, "DIG" | "DIGI" => RigMode::DIG, "PKT" | "PACKET" => RigMode::PKT, other => RigMode::Other(other.to_string()), @@ -45,6 +46,7 @@ pub fn mode_to_string(mode: &RigMode) -> String { RigMode::WFM => "WFM".to_string(), RigMode::AIS => "AIS".to_string(), RigMode::VDES => "VDES".to_string(), + RigMode::MARINE => "MARINE".to_string(), RigMode::DIG => "DIG".to_string(), RigMode::PKT => "PKT".to_string(), RigMode::Other(s) => s.clone(), diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index 01bd41a..94b8d2e 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -245,7 +245,7 @@ fn default_audio_bandwidth_for_mode(mode: &trx_core::rig::state::RigMode) -> u32 RigMode::FM => 12_500, RigMode::WFM => 180_000, RigMode::AIS => 25_000, - RigMode::VDES => 100_000, + RigMode::VDES | RigMode::MARINE => 100_000, RigMode::Other(_) => 3_000, } } @@ -268,6 +268,7 @@ fn parse_rig_mode( "FM" => RigMode::FM, "AIS" => RigMode::AIS, "VDES" => RigMode::VDES, + "MARINE" => RigMode::MARINE, "DIG" => RigMode::DIG, "PKT" => RigMode::PKT, _ => initial_mode.clone(), diff --git a/src/trx-server/src/rig_task.rs b/src/trx-server/src/rig_task.rs index 339b6ab..7931993 100644 --- a/src/trx-server/src/rig_task.rs +++ b/src/trx-server/src/rig_task.rs @@ -773,7 +773,9 @@ fn map_signal_strength(mode: &RigMode, raw: u8) -> i32 { // FT-817 returns 0-15 for signal strength // Map to approximate dBm / S-units match mode { - RigMode::FM | RigMode::WFM | RigMode::AIS | RigMode::VDES => -120 + (raw as i32 * 6), + RigMode::FM | RigMode::WFM | RigMode::AIS | RigMode::VDES | RigMode::MARINE => { + -120 + (raw as i32 * 6) + } _ => -127 + (raw as i32 * 6), } } diff --git a/src/trx-server/trx-backend/trx-backend-ft450d/src/lib.rs b/src/trx-server/trx-backend/trx-backend-ft450d/src/lib.rs index e7d612e..02009c6 100644 --- a/src/trx-server/trx-backend/trx-backend-ft450d/src/lib.rs +++ b/src/trx-server/trx-backend/trx-backend-ft450d/src/lib.rs @@ -513,7 +513,7 @@ fn encode_mode(mode: &RigMode) -> DynResult { RigMode::USB => Ok('2'), RigMode::CW => Ok('3'), RigMode::FM => Ok('4'), - RigMode::AIS | RigMode::VDES => Ok('4'), + RigMode::AIS | RigMode::VDES | RigMode::MARINE => Ok('4'), RigMode::AM => Ok('5'), RigMode::DIG => Ok('6'), RigMode::CWR => Ok('7'), diff --git a/src/trx-server/trx-backend/trx-backend-ft817/src/lib.rs b/src/trx-server/trx-backend/trx-backend-ft817/src/lib.rs index 74d9cdd..487d62a 100644 --- a/src/trx-server/trx-backend/trx-backend-ft817/src/lib.rs +++ b/src/trx-server/trx-backend/trx-backend-ft817/src/lib.rs @@ -590,7 +590,7 @@ fn encode_mode(mode: &RigMode) -> u8 { RigMode::AM => 0x04, RigMode::WFM => 0x06, RigMode::FM => 0x08, - RigMode::AIS | RigMode::VDES => 0x08, + RigMode::AIS | RigMode::VDES | RigMode::MARINE => 0x08, RigMode::DIG => 0x0A, RigMode::PKT => 0x0C, RigMode::Other(_) => 0x00, diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs index 1aeb3c4..d6796fd 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/demod.rs @@ -156,7 +156,7 @@ impl Demodulator { RigMode::AM => Self::Am, RigMode::FM => Self::Fm, RigMode::WFM => Self::Wfm, - RigMode::AIS | RigMode::VDES => Self::Fm, + RigMode::AIS | RigMode::VDES | RigMode::MARINE => Self::Fm, RigMode::CW | RigMode::CWR => Self::Cw, RigMode::DIG => Self::Passthrough, // VHF/UHF packet radio (APRS, AX.25) is FM-encoded AFSK. diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs index 4a6ba89..21af42f 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/dsp/channel.rs @@ -45,7 +45,7 @@ fn default_bandwidth_for_mode(mode: &RigMode) -> u32 { RigMode::FM => 12_500, RigMode::WFM => 180_000, RigMode::AIS => 25_000, - RigMode::VDES => 100_000, + RigMode::VDES | RigMode::MARINE => 100_000, RigMode::Other(_) => 3_000, } } diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs index e5fe5cd..dc771fc 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs @@ -58,7 +58,7 @@ impl SoapySdrRig { match mode { RigMode::LSB | RigMode::USB | RigMode::DIG => 3_000, RigMode::PKT | RigMode::AIS => 25_000, - RigMode::VDES => 100_000, + RigMode::VDES | RigMode::MARINE => 100_000, RigMode::CW | RigMode::CWR => 500, RigMode::AM => 9_000, RigMode::FM => 12_500,