[feat](trx-rs): split VDES frontend and decoder path
Add a dedicated VDES plugin tab and live bar, stop reusing the AIS vessel UI, and serve a separate VDES frontend script. Rework the SDR backend so VDES receives a single 100 kHz IQ tap, then replace the fake AIS-clone decoder path with an early M.2092-1 oriented complex-baseband scaffold using burst detection, coarse pi/4-QPSK slicing, and TER-MCS-1.100 frame heuristics. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
@@ -82,6 +82,7 @@ impl IqSource for MockIqSource {
|
||||
|
||||
pub struct SdrPipeline {
|
||||
pub pcm_senders: Vec<broadcast::Sender<Vec<f32>>>,
|
||||
pub iq_senders: Vec<broadcast::Sender<Vec<Complex<f32>>>>,
|
||||
pub channel_dsps: Vec<Arc<Mutex<ChannelDsp>>>,
|
||||
/// Latest FFT magnitude bins (dBFS, FFT-shifted), updated ~10 Hz.
|
||||
pub spectrum_buf: Arc<Mutex<Option<Vec<f32>>>>,
|
||||
@@ -113,10 +114,12 @@ impl SdrPipeline {
|
||||
const PCM_BROADCAST_CAPACITY: usize = 32;
|
||||
|
||||
let mut pcm_senders = Vec::with_capacity(channels.len());
|
||||
let mut iq_senders = Vec::with_capacity(channels.len());
|
||||
let mut channel_dsps: Vec<Arc<Mutex<ChannelDsp>>> = Vec::with_capacity(channels.len());
|
||||
|
||||
for &(channel_if_hz, ref mode, audio_bandwidth_hz, fir_taps) in channels {
|
||||
let (pcm_tx, _pcm_rx) = broadcast::channel::<Vec<f32>>(PCM_BROADCAST_CAPACITY);
|
||||
let (iq_tx, _iq_rx) = broadcast::channel::<Vec<Complex<f32>>>(IQ_BROADCAST_CAPACITY);
|
||||
let dsp = ChannelDsp::new(
|
||||
channel_if_hz,
|
||||
mode,
|
||||
@@ -129,8 +132,10 @@ impl SdrPipeline {
|
||||
wfm_stereo,
|
||||
fir_taps,
|
||||
pcm_tx.clone(),
|
||||
iq_tx.clone(),
|
||||
);
|
||||
pcm_senders.push(pcm_tx);
|
||||
iq_senders.push(iq_tx);
|
||||
channel_dsps.push(Arc::new(Mutex::new(dsp)));
|
||||
}
|
||||
|
||||
@@ -159,6 +164,7 @@ impl SdrPipeline {
|
||||
|
||||
Self {
|
||||
pcm_senders,
|
||||
iq_senders,
|
||||
channel_dsps,
|
||||
spectrum_buf,
|
||||
sdr_sample_rate,
|
||||
|
||||
@@ -44,7 +44,8 @@ fn default_bandwidth_for_mode(mode: &RigMode) -> u32 {
|
||||
RigMode::AM => 9_000,
|
||||
RigMode::FM => 12_500,
|
||||
RigMode::WFM => 180_000,
|
||||
RigMode::AIS | RigMode::VDES => 25_000,
|
||||
RigMode::AIS => 25_000,
|
||||
RigMode::VDES => 100_000,
|
||||
RigMode::Other(_) => 3_000,
|
||||
}
|
||||
}
|
||||
@@ -68,14 +69,17 @@ pub struct ChannelDsp {
|
||||
frame_buf_offset: usize,
|
||||
pub frame_size: usize,
|
||||
pub pcm_tx: broadcast::Sender<Vec<f32>>,
|
||||
pub iq_tx: broadcast::Sender<Vec<Complex<f32>>>,
|
||||
scratch_mixed_i: Vec<f32>,
|
||||
scratch_mixed_q: Vec<f32>,
|
||||
scratch_filtered_i: Vec<f32>,
|
||||
scratch_filtered_q: Vec<f32>,
|
||||
scratch_decimated: Vec<Complex<f32>>,
|
||||
scratch_iq_tap: Vec<Complex<f32>>,
|
||||
pub mixer_phase: f64,
|
||||
pub mixer_phase_inc: f64,
|
||||
decim_counter: usize,
|
||||
iq_tap_counter: usize,
|
||||
resample_phase: f64,
|
||||
resample_phase_inc: f64,
|
||||
wfm_decoder: Option<WfmStereoDecoder>,
|
||||
@@ -141,6 +145,7 @@ impl ChannelDsp {
|
||||
let rate_changed = self.decim_factor != next_decim_factor;
|
||||
self.decim_factor = next_decim_factor;
|
||||
self.decim_counter = 0;
|
||||
self.iq_tap_counter = 0;
|
||||
self.resample_phase = 0.0;
|
||||
self.resample_phase_inc = if self.sdr_sample_rate == 0 {
|
||||
1.0
|
||||
@@ -181,6 +186,7 @@ impl ChannelDsp {
|
||||
wfm_stereo: bool,
|
||||
fir_taps: usize,
|
||||
pcm_tx: broadcast::Sender<Vec<f32>>,
|
||||
iq_tx: broadcast::Sender<Vec<Complex<f32>>>,
|
||||
) -> Self {
|
||||
let output_channels = output_channels.max(1);
|
||||
let audio_bandwidth_hz = Self::clamp_bandwidth_for_mode(mode, audio_bandwidth_hz);
|
||||
@@ -224,14 +230,17 @@ impl ChannelDsp {
|
||||
frame_buf_offset: 0,
|
||||
frame_size,
|
||||
pcm_tx,
|
||||
iq_tx,
|
||||
scratch_mixed_i: Vec::with_capacity(IQ_BLOCK_SIZE),
|
||||
scratch_mixed_q: Vec::with_capacity(IQ_BLOCK_SIZE),
|
||||
scratch_filtered_i: Vec::with_capacity(IQ_BLOCK_SIZE),
|
||||
scratch_filtered_q: Vec::with_capacity(IQ_BLOCK_SIZE),
|
||||
scratch_decimated: Vec::with_capacity(IQ_BLOCK_SIZE / decim_factor.max(1) + 1),
|
||||
scratch_iq_tap: Vec::with_capacity(IQ_BLOCK_SIZE / decim_factor.max(1) + 1),
|
||||
mixer_phase: 0.0,
|
||||
mixer_phase_inc,
|
||||
decim_counter: 0,
|
||||
iq_tap_counter: 0,
|
||||
resample_phase: 0.0,
|
||||
resample_phase_inc: if sdr_sample_rate == 0 {
|
||||
1.0
|
||||
@@ -357,6 +366,25 @@ impl ChannelDsp {
|
||||
self.scratch_decimated
|
||||
.reserve(capacity - self.scratch_decimated.capacity());
|
||||
}
|
||||
if self.mode == RigMode::VDES && self.iq_tx.receiver_count() > 0 {
|
||||
self.scratch_iq_tap.clear();
|
||||
if self.scratch_iq_tap.capacity() < capacity {
|
||||
self.scratch_iq_tap
|
||||
.reserve(capacity - self.scratch_iq_tap.capacity());
|
||||
}
|
||||
for idx in 0..n {
|
||||
self.iq_tap_counter += 1;
|
||||
if self.iq_tap_counter >= self.decim_factor {
|
||||
self.iq_tap_counter = 0;
|
||||
let fi = filtered_i.get(idx).copied().unwrap_or(0.0);
|
||||
let fq = filtered_q.get(idx).copied().unwrap_or(0.0);
|
||||
self.scratch_iq_tap.push(Complex::new(fi, fq));
|
||||
}
|
||||
}
|
||||
if !self.scratch_iq_tap.is_empty() {
|
||||
let _ = self.iq_tx.send(self.scratch_iq_tap.clone());
|
||||
}
|
||||
}
|
||||
let decimated = &mut self.scratch_decimated;
|
||||
if self.wfm_decoder.is_some() {
|
||||
for idx in 0..n {
|
||||
|
||||
@@ -57,7 +57,8 @@ impl SoapySdrRig {
|
||||
fn default_bandwidth_for_mode(mode: &RigMode) -> u32 {
|
||||
match mode {
|
||||
RigMode::LSB | RigMode::USB | RigMode::DIG => 3_000,
|
||||
RigMode::PKT | RigMode::AIS | RigMode::VDES => 25_000,
|
||||
RigMode::PKT | RigMode::AIS => 25_000,
|
||||
RigMode::VDES => 100_000,
|
||||
RigMode::CW | RigMode::CWR => 500,
|
||||
RigMode::AM => 9_000,
|
||||
RigMode::FM => 12_500,
|
||||
@@ -297,6 +298,19 @@ impl SoapySdrRig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe_iq_channel(
|
||||
&self,
|
||||
channel_idx: usize,
|
||||
) -> tokio::sync::broadcast::Receiver<Vec<num_complex::Complex<f32>>> {
|
||||
if let Some(sender) = self.pipeline.iq_senders.get(channel_idx) {
|
||||
sender.subscribe()
|
||||
} else {
|
||||
let (tx, rx) = tokio::sync::broadcast::channel(1);
|
||||
drop(tx);
|
||||
rx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -353,7 +367,7 @@ impl RigCat for SoapySdrRig {
|
||||
let half_span_hz = i128::from(self.pipeline.sdr_sample_rate) / 2;
|
||||
let current_center_hz = i128::from(self.center_hz);
|
||||
let target_lo_hz = i128::from(freq.hz);
|
||||
let target_hi_hz = if matches!(self.mode, RigMode::AIS | RigMode::VDES) {
|
||||
let target_hi_hz = if self.mode == RigMode::AIS {
|
||||
i128::from(freq.hz) + i128::from(AIS_CHANNEL_SPACING_HZ)
|
||||
} else {
|
||||
i128::from(freq.hz)
|
||||
|
||||
Reference in New Issue
Block a user