[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 <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -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_FEC_TAIL_BITS: usize = 10;
|
||||||
const TER_MCS1_100_SYNC_BITS: &[u8; TER_MCS1_100_SYNC_SYMBOLS] = b"111111001101010000011001010";
|
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 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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct VdesDecoder {
|
pub struct VdesDecoder {
|
||||||
@@ -117,14 +119,32 @@ impl VdesDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let framed = extract_candidate_frame(&symbols)?;
|
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 payload_symbols = framed.payload_symbols();
|
||||||
let deinterleaved = deinterleave_100khz_frame(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 (fec_input_symbols, fec_tail_symbols) = split_fec_frame(&deinterleaved);
|
||||||
let coded_bits = dibits_to_bits(fec_input_symbols);
|
let coded_bits = dibits_to_bits(fec_input_symbols);
|
||||||
let decoded_bits = viterbi_decode_rate_half(&coded_bits);
|
let decoded_bits = viterbi_decode_rate_half(&coded_bits);
|
||||||
if decoded_bits.is_empty() {
|
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 parsed = parse_vdes_payload(&decoded_bits);
|
||||||
let payload_bits = if parsed.payload_bits.is_empty() {
|
let payload_bits = if parsed.payload_bits.is_empty() {
|
||||||
@@ -133,8 +153,6 @@ impl VdesDecoder {
|
|||||||
parsed.payload_bits.as_slice()
|
parsed.payload_bits.as_slice()
|
||||||
};
|
};
|
||||||
let raw_bytes = pack_bits_msb(payload_bits);
|
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
|
let link_text = link_id
|
||||||
.map(|value| format!("LID {}", value))
|
.map(|value| format!("LID {}", value))
|
||||||
.unwrap_or_else(|| "LID ?".to_string());
|
.unwrap_or_else(|| "LID ?".to_string());
|
||||||
@@ -308,7 +326,7 @@ fn extract_candidate_frame(symbols: &[u8]) -> Option<FrameSlice> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if best_score <= 0.5 {
|
if best_score <= MIN_SYNC_CANDIDATE_SCORE {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,6 +345,53 @@ fn extract_candidate_frame(symbols: &[u8]) -> Option<FrameSlice> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
fn syncword_score(symbols: &[u8], rotation: u8) -> (f32, u8) {
|
||||||
if symbols.len() < TER_MCS1_100_SYNC_SYMBOLS {
|
if symbols.len() < TER_MCS1_100_SYNC_SYMBOLS {
|
||||||
return (0.0, u8::MAX);
|
return (0.0, u8::MAX);
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ function renderFt8Row(msg) {
|
|||||||
const rfHz = Number.isFinite(msg.freq_hz) && Number.isFinite(baseHz) ? (baseHz + msg.freq_hz) : null;
|
const rfHz = Number.isFinite(msg.freq_hz) && Number.isFinite(baseHz) ? (baseHz + msg.freq_hz) : null;
|
||||||
const freq = Number.isFinite(rfHz) ? rfHz.toFixed(0) : "--";
|
const freq = Number.isFinite(rfHz) ? rfHz.toFixed(0) : "--";
|
||||||
const renderedMessage = renderFt8Message(rawMessage);
|
const renderedMessage = renderFt8Message(rawMessage);
|
||||||
const receiverHtml = msg.receiver
|
row.innerHTML = `<span class="ft8-time">${fmtTime(msg.ts_ms)}</span><span class="ft8-snr">${snr}</span><span class="ft8-dt">${dt}</span><span class="ft8-freq">${freq}</span><span class="ft8-msg">${renderedMessage}</span>`;
|
||||||
? `<span class="decode-rig-badge" style="--decode-rig-color:${msg.receiver.color};">${msg.receiver.label}</span>`
|
|
||||||
: "";
|
|
||||||
row.innerHTML = `<span class="ft8-time">${fmtTime(msg.ts_ms)}</span>${receiverHtml}<span class="ft8-snr">${snr}</span><span class="ft8-dt">${dt}</span><span class="ft8-freq">${freq}</span><span class="ft8-msg">${renderedMessage}</span>`;
|
|
||||||
applyFt8FilterToRow(row);
|
applyFt8FilterToRow(row);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ function renderWsprRow(msg) {
|
|||||||
const freq = Number.isFinite(rfHz) ? rfHz.toFixed(0) : "--";
|
const freq = Number.isFinite(rfHz) ? rfHz.toFixed(0) : "--";
|
||||||
const message = (msg.message || "").toString();
|
const message = (msg.message || "").toString();
|
||||||
row.dataset.message = message.toUpperCase();
|
row.dataset.message = message.toUpperCase();
|
||||||
const receiverHtml = msg.receiver
|
row.innerHTML = `<span class="ft8-time">${fmtWsprTime(msg.ts_ms)}</span><span class="ft8-snr">${snr}</span><span class="ft8-dt">${dt}</span><span class="ft8-freq">${freq}</span><span class="ft8-msg">${escapeWsprHtml(message)}</span>`;
|
||||||
? `<span class="decode-rig-badge" style="--decode-rig-color:${msg.receiver.color};">${msg.receiver.label}</span>`
|
|
||||||
: "";
|
|
||||||
row.innerHTML = `<span class="ft8-time">${fmtWsprTime(msg.ts_ms)}</span>${receiverHtml}<span class="ft8-snr">${snr}</span><span class="ft8-dt">${dt}</span><span class="ft8-freq">${freq}</span><span class="ft8-msg">${escapeWsprHtml(message)}</span>`;
|
|
||||||
applyWsprFilterToRow(row);
|
applyWsprFilterToRow(row);
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ pub enum RigMode {
|
|||||||
FM,
|
FM,
|
||||||
AIS,
|
AIS,
|
||||||
VDES,
|
VDES,
|
||||||
|
MARINE,
|
||||||
DIG,
|
DIG,
|
||||||
PKT,
|
PKT,
|
||||||
Other(String),
|
Other(String),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub fn parse_mode(s: &str) -> RigMode {
|
|||||||
"WFM" => RigMode::WFM,
|
"WFM" => RigMode::WFM,
|
||||||
"AIS" => RigMode::AIS,
|
"AIS" => RigMode::AIS,
|
||||||
"VDES" => RigMode::VDES,
|
"VDES" => RigMode::VDES,
|
||||||
|
"MARINE" => RigMode::MARINE,
|
||||||
"DIG" | "DIGI" => RigMode::DIG,
|
"DIG" | "DIGI" => RigMode::DIG,
|
||||||
"PKT" | "PACKET" => RigMode::PKT,
|
"PKT" | "PACKET" => RigMode::PKT,
|
||||||
other => RigMode::Other(other.to_string()),
|
other => RigMode::Other(other.to_string()),
|
||||||
@@ -45,6 +46,7 @@ pub fn mode_to_string(mode: &RigMode) -> String {
|
|||||||
RigMode::WFM => "WFM".to_string(),
|
RigMode::WFM => "WFM".to_string(),
|
||||||
RigMode::AIS => "AIS".to_string(),
|
RigMode::AIS => "AIS".to_string(),
|
||||||
RigMode::VDES => "VDES".to_string(),
|
RigMode::VDES => "VDES".to_string(),
|
||||||
|
RigMode::MARINE => "MARINE".to_string(),
|
||||||
RigMode::DIG => "DIG".to_string(),
|
RigMode::DIG => "DIG".to_string(),
|
||||||
RigMode::PKT => "PKT".to_string(),
|
RigMode::PKT => "PKT".to_string(),
|
||||||
RigMode::Other(s) => s.clone(),
|
RigMode::Other(s) => s.clone(),
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ fn default_audio_bandwidth_for_mode(mode: &trx_core::rig::state::RigMode) -> u32
|
|||||||
RigMode::FM => 12_500,
|
RigMode::FM => 12_500,
|
||||||
RigMode::WFM => 180_000,
|
RigMode::WFM => 180_000,
|
||||||
RigMode::AIS => 25_000,
|
RigMode::AIS => 25_000,
|
||||||
RigMode::VDES => 100_000,
|
RigMode::VDES | RigMode::MARINE => 100_000,
|
||||||
RigMode::Other(_) => 3_000,
|
RigMode::Other(_) => 3_000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,6 +268,7 @@ fn parse_rig_mode(
|
|||||||
"FM" => RigMode::FM,
|
"FM" => RigMode::FM,
|
||||||
"AIS" => RigMode::AIS,
|
"AIS" => RigMode::AIS,
|
||||||
"VDES" => RigMode::VDES,
|
"VDES" => RigMode::VDES,
|
||||||
|
"MARINE" => RigMode::MARINE,
|
||||||
"DIG" => RigMode::DIG,
|
"DIG" => RigMode::DIG,
|
||||||
"PKT" => RigMode::PKT,
|
"PKT" => RigMode::PKT,
|
||||||
_ => initial_mode.clone(),
|
_ => initial_mode.clone(),
|
||||||
|
|||||||
@@ -773,7 +773,9 @@ fn map_signal_strength(mode: &RigMode, raw: u8) -> i32 {
|
|||||||
// FT-817 returns 0-15 for signal strength
|
// FT-817 returns 0-15 for signal strength
|
||||||
// Map to approximate dBm / S-units
|
// Map to approximate dBm / S-units
|
||||||
match mode {
|
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),
|
_ => -127 + (raw as i32 * 6),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -513,7 +513,7 @@ fn encode_mode(mode: &RigMode) -> DynResult<char> {
|
|||||||
RigMode::USB => Ok('2'),
|
RigMode::USB => Ok('2'),
|
||||||
RigMode::CW => Ok('3'),
|
RigMode::CW => Ok('3'),
|
||||||
RigMode::FM => Ok('4'),
|
RigMode::FM => Ok('4'),
|
||||||
RigMode::AIS | RigMode::VDES => Ok('4'),
|
RigMode::AIS | RigMode::VDES | RigMode::MARINE => Ok('4'),
|
||||||
RigMode::AM => Ok('5'),
|
RigMode::AM => Ok('5'),
|
||||||
RigMode::DIG => Ok('6'),
|
RigMode::DIG => Ok('6'),
|
||||||
RigMode::CWR => Ok('7'),
|
RigMode::CWR => Ok('7'),
|
||||||
|
|||||||
@@ -590,7 +590,7 @@ fn encode_mode(mode: &RigMode) -> u8 {
|
|||||||
RigMode::AM => 0x04,
|
RigMode::AM => 0x04,
|
||||||
RigMode::WFM => 0x06,
|
RigMode::WFM => 0x06,
|
||||||
RigMode::FM => 0x08,
|
RigMode::FM => 0x08,
|
||||||
RigMode::AIS | RigMode::VDES => 0x08,
|
RigMode::AIS | RigMode::VDES | RigMode::MARINE => 0x08,
|
||||||
RigMode::DIG => 0x0A,
|
RigMode::DIG => 0x0A,
|
||||||
RigMode::PKT => 0x0C,
|
RigMode::PKT => 0x0C,
|
||||||
RigMode::Other(_) => 0x00,
|
RigMode::Other(_) => 0x00,
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ impl Demodulator {
|
|||||||
RigMode::AM => Self::Am,
|
RigMode::AM => Self::Am,
|
||||||
RigMode::FM => Self::Fm,
|
RigMode::FM => Self::Fm,
|
||||||
RigMode::WFM => Self::Wfm,
|
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::CW | RigMode::CWR => Self::Cw,
|
||||||
RigMode::DIG => Self::Passthrough,
|
RigMode::DIG => Self::Passthrough,
|
||||||
// VHF/UHF packet radio (APRS, AX.25) is FM-encoded AFSK.
|
// VHF/UHF packet radio (APRS, AX.25) is FM-encoded AFSK.
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ fn default_bandwidth_for_mode(mode: &RigMode) -> u32 {
|
|||||||
RigMode::FM => 12_500,
|
RigMode::FM => 12_500,
|
||||||
RigMode::WFM => 180_000,
|
RigMode::WFM => 180_000,
|
||||||
RigMode::AIS => 25_000,
|
RigMode::AIS => 25_000,
|
||||||
RigMode::VDES => 100_000,
|
RigMode::VDES | RigMode::MARINE => 100_000,
|
||||||
RigMode::Other(_) => 3_000,
|
RigMode::Other(_) => 3_000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ impl SoapySdrRig {
|
|||||||
match mode {
|
match mode {
|
||||||
RigMode::LSB | RigMode::USB | RigMode::DIG => 3_000,
|
RigMode::LSB | RigMode::USB | RigMode::DIG => 3_000,
|
||||||
RigMode::PKT | RigMode::AIS => 25_000,
|
RigMode::PKT | RigMode::AIS => 25_000,
|
||||||
RigMode::VDES => 100_000,
|
RigMode::VDES | RigMode::MARINE => 100_000,
|
||||||
RigMode::CW | RigMode::CWR => 500,
|
RigMode::CW | RigMode::CWR => 500,
|
||||||
RigMode::AM => 9_000,
|
RigMode::AM => 9_000,
|
||||||
RigMode::FM => 12_500,
|
RigMode::FM => 12_500,
|
||||||
|
|||||||
Reference in New Issue
Block a user