[feat](trx-rs): add VDES hard-decision FEC stage

Improve the VDES decoder with sync/rotation metadata and a first hard-decision rate-1/2 Viterbi stage after deinterleaving, then surface the extra lock state in the VDES frontend. Also fix the strict clippy findings in AIS, frontend bookmarks, and the server audio stack signature.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-03-03 00:39:41 +01:00
parent 6e558303a7
commit 5e84fe2a82
6 changed files with 271 additions and 52 deletions
+1 -1
View File
@@ -344,7 +344,7 @@ fn decode_coord(raw: i32, invalid_abs: f64) -> Option<f64> {
} }
fn decode_sixbit_text(bits: &[u8], start: usize, len: usize) -> Option<String> { fn decode_sixbit_text(bits: &[u8], start: usize, len: usize) -> Option<String> {
if start.checked_add(len)? > bits.len() || len % 6 != 0 { if start.checked_add(len)? > bits.len() || !len.is_multiple_of(6) {
return None; return None;
} }
+228 -43
View File
@@ -12,9 +12,9 @@
//! - coarse symbol timing at the 76.8 ksps VDE-TER baseline //! - coarse symbol timing at the 76.8 ksps VDE-TER baseline
//! - `pi/4`-QPSK quadrant slicing //! - `pi/4`-QPSK quadrant slicing
//! //!
//! It intentionally stops at a raw burst payload stage. Full M.2092-1 FEC, //! It performs a first hard-decision FEC stage for the `TER-MCS-1.100` 1/2-rate
//! interleaving, link-layer parsing, and application payload decoding are not //! path after deinterleaving, but full M.2092-1 turbo/puncture handling,
//! implemented yet. //! link-layer parsing, and application payload decoding are not implemented yet.
use num_complex::Complex; use num_complex::Complex;
use trx_core::decode::VdesMessage; use trx_core::decode::VdesMessage;
@@ -28,6 +28,11 @@ const TER_MCS1_100_RAMP_SYMBOLS: usize = 32;
const TER_MCS1_100_SYNC_SYMBOLS: usize = 27; const TER_MCS1_100_SYNC_SYMBOLS: usize = 27;
const TER_MCS1_100_LINK_ID_SYMBOLS: usize = 16; const TER_MCS1_100_LINK_ID_SYMBOLS: usize = 16;
const TER_MCS1_100_PAYLOAD_SYMBOLS: usize = 1_877; const TER_MCS1_100_PAYLOAD_SYMBOLS: usize = 1_877;
const TER_MCS1_100_FEC_INPUT_SYMBOLS: usize = 1_872;
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];
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct VdesDecoder { pub struct VdesDecoder {
@@ -115,12 +120,27 @@ impl VdesDecoder {
let link_id = decode_link_id_from_symbols(&framed.symbols); let link_id = decode_link_id_from_symbols(&framed.symbols);
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);
let raw_bytes = pack_dibits_msb(&deinterleaved); 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;
}
let raw_bytes = pack_bits_msb(&decoded_bits);
let rms = burst_rms(&samples); let rms = burst_rms(&samples);
let mode = classify_vdes_burst(framed.symbols.len()); 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());
let tail_zero_bits = dibits_to_bits(fec_tail_symbols)
.into_iter()
.filter(|bit| *bit == 0)
.count();
let fec_state = format!(
"Hard-decision 1/2 Viterbi, tail {} / {} zero bits",
tail_zero_bits,
TER_MCS1_100_FEC_TAIL_BITS
);
Some(VdesMessage { Some(VdesMessage {
ts_ms: None, ts_ms: None,
@@ -129,7 +149,7 @@ impl VdesDecoder {
repeat: 0, repeat: 0,
mmsi: 0, mmsi: 0,
crc_ok: false, crc_ok: false,
bit_len: deinterleaved.len() * 2, bit_len: decoded_bits.len(),
raw_bytes, raw_bytes,
lat: None, lat: None,
lon: None, lon: None,
@@ -140,9 +160,16 @@ impl VdesDecoder {
vessel_name: Some(format!("VDES Frame {} sym", framed.symbols.len())), vessel_name: Some(format!("VDES Frame {} sym", framed.symbols.len())),
callsign: Some(format!("{} {} @{}", mode.label, link_text, framed.start_offset)), callsign: Some(format!("{} {} @{}", mode.label, link_text, framed.start_offset)),
destination: Some(format!( destination: Some(format!(
"TER-MCS-1.100 RMS {:.2} sync {:.2} turbo FEC pending", "TER-MCS-1.100 RMS {:.2} sync {:.0}% rot {}",
rms, framed.preamble_score rms,
framed.sync_score * 100.0,
framed.phase_rotation
)), )),
link_id,
sync_score: Some(framed.sync_score),
sync_errors: Some(framed.sync_errors),
phase_rotation: Some(framed.phase_rotation),
fec_state: Some(fec_state),
}) })
} }
@@ -183,7 +210,9 @@ struct BurstMode<'a> {
struct FrameSlice { struct FrameSlice {
start_offset: usize, start_offset: usize,
preamble_score: f32, sync_score: f32,
sync_errors: u8,
phase_rotation: u8,
symbols: Vec<u8>, symbols: Vec<u8>,
} }
@@ -213,61 +242,63 @@ fn classify_vdes_burst(symbols: usize) -> BurstMode<'static> {
} }
fn extract_candidate_frame(symbols: &[u8]) -> Option<FrameSlice> { fn extract_candidate_frame(symbols: &[u8]) -> Option<FrameSlice> {
if symbols.len() < TER_MCS1_100_SYNC_SYMBOLS { if symbols.len() < TER_MCS1_100_RAMP_SYMBOLS + TER_MCS1_100_SYNC_SYMBOLS {
return None; return None;
} }
let search_limit = symbols let search_limit = symbols
.len() .len()
.saturating_sub(TER_MCS1_100_BURST_SYMBOLS.saturating_sub(TER_MCS1_100_SYNC_SYMBOLS)); .saturating_sub(TER_MCS1_100_RAMP_SYMBOLS + TER_MCS1_100_SYNC_SYMBOLS);
let mut best_offset = 0usize; let mut best_offset = 0usize;
let mut best_score = f32::MIN; let mut best_score = 0.0_f32;
let mut best_errors = u8::MAX;
let mut best_rotation = 0u8;
for offset in 0..=search_limit { for offset in 0..=search_limit {
let sync_offset = offset + TER_MCS1_100_RAMP_SYMBOLS; let sync_offset = offset + TER_MCS1_100_RAMP_SYMBOLS;
if sync_offset >= symbols.len() { let sync_window = &symbols[sync_offset..sync_offset + TER_MCS1_100_SYNC_SYMBOLS];
break; for rotation in 0..4 {
} let (score, errors) = syncword_score(sync_window, rotation);
let score = preamble_like_score(&symbols[sync_offset..]); if score > best_score || (score == best_score && errors < best_errors) {
if score > best_score { best_score = score;
best_score = score; best_errors = errors;
best_offset = offset; best_rotation = rotation;
best_offset = offset;
}
} }
} }
if best_score <= 0.5 {
return None;
}
let available = symbols.len().saturating_sub(best_offset); let available = symbols.len().saturating_sub(best_offset);
if available < MIN_BURST_SYMBOLS { if available < MIN_BURST_SYMBOLS {
return None; return None;
} }
let take = available.min(TER_MCS1_100_BURST_SYMBOLS); let take = available.min(TER_MCS1_100_BURST_SYMBOLS);
let rotated = rotate_pi4_stream(&symbols[best_offset..best_offset + take], best_rotation);
Some(FrameSlice { Some(FrameSlice {
start_offset: best_offset, start_offset: best_offset,
preamble_score: best_score, sync_score: best_score,
symbols: symbols[best_offset..best_offset + take].to_vec(), sync_errors: best_errors,
phase_rotation: best_rotation,
symbols: rotated,
}) })
} }
fn preamble_like_score(symbols: &[u8]) -> f32 { 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 f32::MIN; return (0.0, u8::MAX);
} }
let window = &symbols[..TER_MCS1_100_SYNC_SYMBOLS]; let mut bit_errors = 0u8;
let mut score = 0.0_f32; for (idx, &dibit) in symbols.iter().take(TER_MCS1_100_SYNC_SYMBOLS).enumerate() {
for (idx, &dibit) in window.iter().enumerate() { let rotated = rotate_pi4_dibit(dibit, rotation);
if dibit == 0b00 || dibit == 0b11 { let expected = sync_reference_dibit(idx);
score += 1.0; bit_errors = bit_errors.saturating_add(dibit_bit_distance(rotated, expected) as u8);
} else {
score -= 1.5;
}
if idx > 0 {
if dibit != window[idx - 1] {
score += 0.4;
} else {
score -= 0.2;
}
}
} }
score / TER_MCS1_100_SYNC_SYMBOLS as f32 let max_bits = (TER_MCS1_100_SYNC_SYMBOLS * 2) as f32;
let score = 1.0 - (bit_errors as f32 / max_bits);
(score.clamp(0.0, 1.0), bit_errors)
} }
fn deinterleave_100khz_frame(symbols: &[u8]) -> Vec<u8> { fn deinterleave_100khz_frame(symbols: &[u8]) -> Vec<u8> {
@@ -290,6 +321,84 @@ fn deinterleave_100khz_frame(symbols: &[u8]) -> Vec<u8> {
out out
} }
fn split_fec_frame(symbols: &[u8]) -> (&[u8], &[u8]) {
let input_end = symbols.len().min(TER_MCS1_100_FEC_INPUT_SYMBOLS);
let tail_end = symbols
.len()
.min(TER_MCS1_100_FEC_INPUT_SYMBOLS + (TER_MCS1_100_FEC_TAIL_BITS / 2));
(&symbols[..input_end], &symbols[input_end..tail_end])
}
fn viterbi_decode_rate_half(coded_bits: &[u8]) -> Vec<u8> {
if coded_bits.len() < 2 {
return Vec::new();
}
let pair_count = coded_bits.len() / 2;
let mut metrics = [u16::MAX; 64];
let mut next_metrics = [u16::MAX; 64];
let mut predecessors = vec![[0u8; 64]; pair_count];
metrics[0] = 0;
for step in 0..pair_count {
next_metrics.fill(u16::MAX);
let recv0 = coded_bits[step * 2] & 1;
let recv1 = coded_bits[step * 2 + 1] & 1;
for (state, &metric) in metrics.iter().enumerate() {
if metric == u16::MAX {
continue;
}
for input_bit in 0..=1u8 {
let reg = ((state as u8) << 1) | input_bit;
let out = conv_encode_output(reg);
let branch = dibit_bit_distance(out, (recv0 << 1) | recv1) as u16;
let next_state = (reg & 0x3f) as usize;
let candidate = metric.saturating_add(branch);
if candidate < next_metrics[next_state] {
next_metrics[next_state] = candidate;
predecessors[step][next_state] = state as u8;
}
}
}
metrics = next_metrics;
}
let mut best_state = 0usize;
let mut best_metric = u16::MAX;
for (state, &metric) in metrics.iter().enumerate() {
if metric < best_metric {
best_metric = metric;
best_state = state;
}
}
if best_metric == u16::MAX {
return Vec::new();
}
let mut decoded = vec![0u8; pair_count];
let mut state = best_state;
for step in (0..pair_count).rev() {
let bit = (state as u8) & 1;
decoded[step] = bit;
state = predecessors[step][state] as usize;
}
decoded.truncate(TER_MCS1_100_FEC_OUTPUT_BITS.min(decoded.len()));
decoded
}
fn conv_encode_output(reg: u8) -> u8 {
let g0 = parity6_7(reg & 0o171);
let g1 = parity6_7(reg & 0o133);
(g0 << 1) | g1
}
fn parity6_7(value: u8) -> u8 {
(value.count_ones() as u8) & 1
}
fn decode_link_id_from_symbols(symbols: &[u8]) -> Option<u8> { fn decode_link_id_from_symbols(symbols: &[u8]) -> Option<u8> {
let start = TER_MCS1_100_RAMP_SYMBOLS + TER_MCS1_100_SYNC_SYMBOLS; let start = TER_MCS1_100_RAMP_SYMBOLS + TER_MCS1_100_SYNC_SYMBOLS;
let end = start + TER_MCS1_100_LINK_ID_SYMBOLS; let end = start + TER_MCS1_100_LINK_ID_SYMBOLS;
@@ -303,6 +412,35 @@ fn decode_link_id_from_symbols(symbols: &[u8]) -> Option<u8> {
decode_rm_1_5(&bits) decode_rm_1_5(&bits)
} }
fn sync_reference_dibit(idx: usize) -> u8 {
match TER_MCS1_100_SYNC_BITS[idx] {
b'1' => 0b11,
_ => 0b00,
}
}
fn rotate_pi4_dibit(dibit: u8, rotation: u8) -> u8 {
let pos = PI4_QPSK_DIBITS
.iter()
.position(|candidate| *candidate == (dibit & 0b11))
.unwrap_or(0);
PI4_QPSK_DIBITS[(pos + rotation as usize) % PI4_QPSK_DIBITS.len()]
}
fn rotate_pi4_stream(symbols: &[u8], rotation: u8) -> Vec<u8> {
if rotation == 0 {
return symbols.to_vec();
}
symbols
.iter()
.map(|dibit| rotate_pi4_dibit(*dibit, rotation))
.collect()
}
fn dibit_bit_distance(a: u8, b: u8) -> usize {
((a ^ b) & 0b11).count_ones() as usize
}
fn dibits_to_bits(symbols: &[u8]) -> Vec<u8> { fn dibits_to_bits(symbols: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(symbols.len() * 2); let mut out = Vec::with_capacity(symbols.len() * 2);
for &dibit in symbols { for &dibit in symbols {
@@ -312,6 +450,18 @@ fn dibits_to_bits(symbols: &[u8]) -> Vec<u8> {
out out
} }
fn bits_to_dibits(bits: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(bits.len().div_ceil(2));
let mut idx = 0usize;
while idx < bits.len() {
let hi = bits[idx] & 1;
let lo = bits.get(idx + 1).copied().unwrap_or(0) & 1;
out.push((hi << 1) | lo);
idx += 2;
}
out
}
fn decode_rm_1_5(bits: &[u8]) -> Option<u8> { fn decode_rm_1_5(bits: &[u8]) -> Option<u8> {
if bits.len() != 32 { if bits.len() != 32 {
return None; return None;
@@ -345,13 +495,13 @@ fn rm_1_5_codeword(value: u8) -> [u8; 32] {
let a4 = (value >> 1) & 1; let a4 = (value >> 1) & 1;
let a5 = value & 1; let a5 = value & 1;
let mut out = [0u8; 32]; let mut out = [0u8; 32];
for idx in 0..32 { for (idx, slot) in out.iter_mut().enumerate() {
let x1 = ((idx >> 4) & 1) as u8; let x1 = ((idx >> 4) & 1) as u8;
let x2 = ((idx >> 3) & 1) as u8; let x2 = ((idx >> 3) & 1) as u8;
let x3 = ((idx >> 2) & 1) as u8; let x3 = ((idx >> 2) & 1) as u8;
let x4 = ((idx >> 1) & 1) as u8; let x4 = ((idx >> 1) & 1) as u8;
let x5 = (idx & 1) as u8; let x5 = (idx & 1) as u8;
out[idx] = a0 ^ (a1 & x1) ^ (a2 & x2) ^ (a3 & x3) ^ (a4 & x4) ^ (a5 & x5); *slot = a0 ^ (a1 & x1) ^ (a2 & x2) ^ (a3 & x3) ^ (a4 & x4) ^ (a5 & x5);
} }
out out
} }
@@ -417,7 +567,7 @@ fn quantize_pi4_qpsk(sample: Complex<f32>) -> u8 {
} }
fn pack_dibits_msb(symbols: &[u8]) -> Vec<u8> { fn pack_dibits_msb(symbols: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity((symbols.len() + 3) / 4); let mut out = Vec::with_capacity(symbols.len().div_ceil(4));
let mut byte = 0u8; let mut byte = 0u8;
let mut count = 0usize; let mut count = 0usize;
@@ -425,19 +575,24 @@ fn pack_dibits_msb(symbols: &[u8]) -> Vec<u8> {
let shift = 6usize.saturating_sub((count % 4) * 2); let shift = 6usize.saturating_sub((count % 4) * 2);
byte |= (dibit & 0b11) << shift; byte |= (dibit & 0b11) << shift;
count += 1; count += 1;
if count % 4 == 0 { if count.is_multiple_of(4) {
out.push(byte); out.push(byte);
byte = 0; byte = 0;
} }
} }
if count % 4 != 0 { if !count.is_multiple_of(4) {
out.push(byte); out.push(byte);
} }
out out
} }
fn pack_bits_msb(bits: &[u8]) -> Vec<u8> {
let dibits = bits_to_dibits(bits);
pack_dibits_msb(&dibits)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -486,10 +641,40 @@ mod tests {
assert!(frame.symbols.len() >= MIN_BURST_SYMBOLS); assert!(frame.symbols.len() >= MIN_BURST_SYMBOLS);
} }
#[test]
fn syncword_score_prefers_correct_rotation() {
let sync: Vec<u8> = (0..TER_MCS1_100_SYNC_SYMBOLS)
.map(sync_reference_dibit)
.collect();
let rotated = rotate_pi4_stream(&sync, 2);
let (wrong_score, wrong_errors) = syncword_score(&rotated, 0);
let (right_score, right_errors) = syncword_score(&rotated, 2);
assert!(right_score > wrong_score);
assert!(right_errors < wrong_errors);
assert_eq!(right_errors, 0);
}
#[test] #[test]
fn deinterleave_preserves_length() { fn deinterleave_preserves_length() {
let symbols: Vec<u8> = (0..127).map(|idx| (idx % 4) as u8).collect(); let symbols: Vec<u8> = (0..127).map(|idx| (idx % 4) as u8).collect();
let out = deinterleave_100khz_frame(&symbols); let out = deinterleave_100khz_frame(&symbols);
assert_eq!(out.len(), symbols.len()); assert_eq!(out.len(), symbols.len());
} }
#[test]
fn viterbi_decodes_k7_rate_half_stream() {
let input: Vec<u8> = (0..TER_MCS1_100_FEC_OUTPUT_BITS)
.map(|idx| ((idx * 5 + 1) % 2) as u8)
.collect();
let mut state = 0u8;
let mut coded = Vec::with_capacity(input.len() * 2);
for &bit in &input {
state = ((state << 1) | bit) & 0x7f;
let dibit = conv_encode_output(state);
coded.push((dibit >> 1) & 1);
coded.push(dibit & 1);
}
let decoded = viterbi_decode_rate_half(&coded);
assert_eq!(decoded, input);
}
} }
@@ -79,8 +79,23 @@ function renderVdesRow(msg) {
const title = msg.vessel_name || "VDES Burst"; const title = msg.vessel_name || "VDES Burst";
const label = msg.callsign || "VDES"; const label = msg.callsign || "VDES";
const info = msg.destination || ""; const info = msg.destination || "";
const linkText = Number.isFinite(msg.link_id) ? `LID ${msg.link_id}` : "";
const syncText = Number.isFinite(msg.sync_score) ? `Sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : "";
const phaseText = Number.isFinite(msg.phase_rotation) ? `R${Number(msg.phase_rotation)}` : "";
const fecText = msg.fec_state || "";
const rawHex = vdesHexPreview(msg.raw_bytes); const rawHex = vdesHexPreview(msg.raw_bytes);
row.dataset.filterText = [title, label, info, rawHex, msg.message_type, msg.bit_len] row.dataset.filterText = [
title,
label,
info,
linkText,
syncText,
phaseText,
fecText,
rawHex,
msg.message_type,
msg.bit_len,
]
.filter(Boolean) .filter(Boolean)
.join(" ") .join(" ")
.toUpperCase(); .toUpperCase();
@@ -89,12 +104,16 @@ function renderVdesRow(msg) {
`<span class="vdes-time">${ts}</span>` + `<span class="vdes-time">${ts}</span>` +
`<span class="vdes-call">${escapeMapHtml(title)}</span>` + `<span class="vdes-call">${escapeMapHtml(title)}</span>` +
`<span class="vdes-badge">${escapeMapHtml(label)}</span>` + `<span class="vdes-badge">${escapeMapHtml(label)}</span>` +
(linkText ? `<span class="vdes-badge">${escapeMapHtml(linkText)}</span>` : "") +
(syncText ? `<span class="vdes-badge">${escapeMapHtml(syncText)}</span>` : "") +
(phaseText ? `<span class="vdes-badge">${escapeMapHtml(phaseText)}</span>` : "") +
`<span class="vdes-badge">T${escapeMapHtml(String(msg.message_type ?? "--"))}</span>` + `<span class="vdes-badge">T${escapeMapHtml(String(msg.message_type ?? "--"))}</span>` +
`</div>` + `</div>` +
`<div class="vdes-row-meta">` + `<div class="vdes-row-meta">` +
`<span>${escapeMapHtml(currentVdesCenterText())}</span>` + `<span>${escapeMapHtml(currentVdesCenterText())}</span>` +
`<span>${escapeMapHtml(`${msg.bit_len || 0} bits`)}</span>` + `<span>${escapeMapHtml(`${msg.bit_len || 0} bits`)}</span>` +
(info ? `<span>${escapeMapHtml(info)}</span>` : "") + (info ? `<span>${escapeMapHtml(info)}</span>` : "") +
(fecText ? `<span>${escapeMapHtml(fecText)}</span>` : "") +
`<span>${escapeMapHtml(vdesAgeText(msg._tsMs))}</span>` + `<span>${escapeMapHtml(vdesAgeText(msg._tsMs))}</span>` +
`</div>` + `</div>` +
`<div class="vdes-row-detail">` + `<div class="vdes-row-detail">` +
@@ -123,6 +142,9 @@ function updateVdesBar() {
const title = escapeMapHtml(msg.vessel_name || "Burst"); const title = escapeMapHtml(msg.vessel_name || "Burst");
const detail = [ const detail = [
`${msg.bit_len || 0} bits`, `${msg.bit_len || 0} bits`,
Number.isFinite(msg.link_id) ? `LID ${Number(msg.link_id)}` : null,
Number.isFinite(msg.sync_score) ? `sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : null,
Number.isFinite(msg.phase_rotation) ? `rot ${Number(msg.phase_rotation)}` : null,
msg.destination ? escapeMapHtml(msg.destination) : null, msg.destination ? escapeMapHtml(msg.destination) : null,
escapeMapHtml(vdesAgeText(msg._tsMs)), escapeMapHtml(vdesAgeText(msg._tsMs)),
] ]
@@ -193,6 +215,11 @@ window.onServerVdes = function(msg) {
vessel_name: msg.vessel_name, vessel_name: msg.vessel_name,
callsign: msg.callsign, callsign: msg.callsign,
destination: msg.destination, destination: msg.destination,
link_id: msg.link_id,
sync_score: msg.sync_score,
sync_errors: msg.sync_errors,
phase_rotation: msg.phase_rotation,
fec_state: msg.fec_state,
ts_ms: msg.ts_ms, ts_ms: msg.ts_ms,
}); });
}; };
@@ -95,7 +95,7 @@ impl BookmarkStore {
/// Returns true if any bookmark (other than `exclude_id`) has `freq_hz`. /// Returns true if any bookmark (other than `exclude_id`) has `freq_hz`.
pub fn freq_taken(&self, freq_hz: u64, exclude_id: Option<&str>) -> bool { pub fn freq_taken(&self, freq_hz: u64, exclude_id: Option<&str>) -> bool {
self.list().into_iter().any(|bm| { self.list().into_iter().any(|bm| {
bm.freq_hz == freq_hz && exclude_id.map_or(true, |ex| bm.id != ex) bm.freq_hz == freq_hz && exclude_id.is_none_or(|ex| bm.id != ex)
}) })
} }
} }
+10
View File
@@ -84,6 +84,16 @@ pub struct VdesMessage {
pub callsign: Option<String>, pub callsign: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<String>, pub destination: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub link_id: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sync_score: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sync_errors: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phase_rotation: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fec_state: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
+3 -6
View File
@@ -432,12 +432,9 @@ fn spawn_rig_audio_stack(
latitude: Option<f64>, latitude: Option<f64>,
longitude: Option<f64>, longitude: Option<f64>,
listen_override: Option<IpAddr>, listen_override: Option<IpAddr>,
sdr_pcm_rx: Option<broadcast::Receiver<Vec<f32>>>, sdr_pcm_rx: OptionalSdrPcmRx,
sdr_ais_pcm_rx: Option<( sdr_ais_pcm_rx: OptionalSdrAisPcmRx,
broadcast::Receiver<Vec<f32>>, sdr_vdes_iq_rx: OptionalSdrVdesIqRx,
broadcast::Receiver<Vec<f32>>,
)>,
sdr_vdes_iq_rx: Option<broadcast::Receiver<Vec<num_complex::Complex<f32>>>>,
) -> Vec<JoinHandle<()>> { ) -> Vec<JoinHandle<()>> {
let mut handles: Vec<JoinHandle<()>> = Vec::new(); let mut handles: Vec<JoinHandle<()>> = Vec::new();