diff --git a/src/trx-server/Cargo.toml b/src/trx-server/Cargo.toml index af279ba..d8663f7 100644 --- a/src/trx-server/Cargo.toml +++ b/src/trx-server/Cargo.toml @@ -11,7 +11,6 @@ build = "build.rs" [features] default = ["soapysdr"] soapysdr = ["trx-backend/soapysdr"] -soapysdr-sys = ["soapysdr", "trx-backend/soapysdr-sys"] [dependencies] tokio = { workspace = true, features = ["full"] } @@ -27,8 +26,8 @@ bytes = "1" cpal = "0.15" opus = "0.3" trx-app = { path = "../trx-app" } -trx-backend = { path = "trx-backend" } -trx-backend-soapysdr = { path = "trx-backend/trx-backend-soapysdr", optional = true } +trx-backend = { path = "trx-backend", features = ["soapysdr"] } +trx-backend-soapysdr = { path = "trx-backend/trx-backend-soapysdr" } trx-core = { path = "../trx-core" } trx-aprs = { path = "../decoders/trx-aprs" } trx-cw = { path = "../decoders/trx-cw" } diff --git a/src/trx-server/trx-backend/Cargo.toml b/src/trx-server/trx-backend/Cargo.toml index 7cde56b..e04d8bc 100644 --- a/src/trx-server/trx-backend/Cargo.toml +++ b/src/trx-server/trx-backend/Cargo.toml @@ -12,7 +12,6 @@ default = ["ft817", "ft450d"] ft817 = ["dep:trx-backend-ft817"] ft450d = ["dep:trx-backend-ft450d"] soapysdr = ["dep:trx-backend-soapysdr"] -soapysdr-sys = ["soapysdr", "trx-backend-soapysdr/soapysdr-sys"] [dependencies] trx-core = { path = "../../trx-core" } diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/Cargo.toml b/src/trx-server/trx-backend/trx-backend-soapysdr/Cargo.toml index 54c7904..a38e34a 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/Cargo.toml +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/Cargo.toml @@ -14,9 +14,4 @@ tokio = { workspace = true, features = ["sync", "rt"] } serde = { workspace = true } tracing = { workspace = true } num-complex = "0.4" -# soapysdr is an optional system-library dep gated behind a feature -soapysdr = { version = "0.3", optional = true } - -[features] -default = [] -soapysdr-sys = ["dep:soapysdr"] +soapysdr = "0.3" 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 2448f74..e1f3f65 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 @@ -4,8 +4,6 @@ pub mod demod; pub mod dsp; - -#[cfg(feature = "soapysdr-sys")] pub mod real_iq_source; use std::pin::Pin; @@ -38,8 +36,7 @@ impl SoapySdrRig { /// /// # Parameters /// - `args`: SoapySDR device args string (e.g. `"driver=rtlsdr"`). - /// When `soapysdr-sys` feature is enabled, opens a real device. - /// Otherwise, uses `MockIqSource` for testing. + /// Opens a real hardware device via SoapySDR. /// - `channels`: per-channel tuples of /// `(channel_if_hz, initial_mode, audio_bandwidth_hz, fir_taps)`. /// - `gain_mode`: `"auto"` or `"manual"`. @@ -78,28 +75,15 @@ impl SoapySdrRig { ); } - // Create IQ source: real device or mock depending on feature flag. - let iq_source: Box = { - #[cfg(feature = "soapysdr-sys")] - { - let device = real_iq_source::RealIqSource::new( - args, - initial_freq.hz() as f64, - sdr_sample_rate as f64, - 1_500_000.0, // default 1.5 MHz bandwidth - gain_db, - )?; - Box::new(device) - } - #[cfg(not(feature = "soapysdr-sys"))] - { - tracing::warn!( - "soapysdr-sys feature not enabled; using MockIqSource (silence). \ - Compile with --features soapysdr to use real hardware." - ); - Box::new(dsp::MockIqSource) - } - }; + // Create real IQ source from hardware device. + let iq_source: Box = + Box::new(real_iq_source::RealIqSource::new( + args, + initial_freq.hz as f64, + sdr_sample_rate as f64, + 1_500_000.0, // default 1.5 MHz bandwidth + gain_db, + )?); let pipeline = dsp::SdrPipeline::start( iq_source, diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs b/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs index 0843555..bb7aa9f 100644 --- a/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/real_iq_source.rs @@ -5,8 +5,7 @@ //! Real SoapySDR device IQ source implementation. use num_complex::Complex; -use soapysdr::SoapySDRError; -use std::ffi::CString; +use soapysdr::Device; use crate::dsp::IqSource; @@ -14,9 +13,8 @@ use crate::dsp::IqSource; /// /// Reads IQ samples directly from a SoapySDR-compatible device. pub struct RealIqSource { - device: soapysdr::Device, - stream: soapysdr::RxStream, - buffer_size: usize, + _device: Device, + buffer: Vec>, } impl RealIqSource { @@ -27,7 +25,7 @@ impl RealIqSource { /// - `center_freq_hz`: Center frequency in Hz /// - `sample_rate_hz`: IQ sample rate in Hz /// - `bandwidth_hz`: Hardware filter bandwidth in Hz - /// - `gain_db`: RX gain in dB (used for manual gain mode) + /// - `gain_db`: RX gain in dB /// /// # Returns /// A configured RealIqSource or an error string if initialization fails. @@ -38,34 +36,17 @@ impl RealIqSource { bandwidth_hz: f64, gain_db: f64, ) -> Result { - // Parse device arguments. - let kwargs = Self::parse_device_args(args)?; + tracing::info!("Initializing SoapySDR device with args: {}", args); - // Create device. - let device = soapysdr::Device::new(kwargs).map_err(|e| { + // Create device from arguments string. + let device = Device::new(args).map_err(|e| { format!( "Failed to open SoapySDR device (args={}): {}", args, e ) })?; - tracing::info!( - "Opened SoapySDR device: {}", - device - .driver_key() - .map(|k| k.to_string_lossy().into_owned()) - .unwrap_or_else(|_| "unknown".to_string()) - ); - - // Get RX antenna and print available options. - if let Ok(antennas) = device.list_antennas(soapysdr::Direction::Rx, 0) { - let antenna_list = antennas - .iter() - .map(|a| a.to_string_lossy().into_owned()) - .collect::>() - .join(", "); - tracing::info!("Available RX antennas: {}", antenna_list); - } + tracing::info!("SoapySDR device opened successfully"); // Set sample rate. device @@ -74,7 +55,7 @@ impl RealIqSource { let actual_rate = device .sample_rate(soapysdr::Direction::Rx, 0) - .map_err(|e| format!("Failed to read sample rate: {}", e))?; + .unwrap_or(sample_rate_hz); tracing::info!( "Set sample rate to {} Hz (actual: {} Hz)", sample_rate_hz, @@ -83,133 +64,64 @@ impl RealIqSource { // Set center frequency. device - .set_frequency(soapysdr::Direction::Rx, 0, center_freq_hz) + .set_frequency(soapysdr::Direction::Rx, 0, center_freq_hz, ()) .map_err(|e| format!("Failed to set frequency: {}", e))?; let actual_freq = device .frequency(soapysdr::Direction::Rx, 0) - .map_err(|e| format!("Failed to read frequency: {}", e))?; + .unwrap_or(center_freq_hz); tracing::info!( "Set center frequency to {} Hz (actual: {} Hz)", center_freq_hz, actual_freq ); - // Set bandwidth. + // Set bandwidth if specified. if bandwidth_hz > 0.0 { - device - .set_bandwidth(soapysdr::Direction::Rx, 0, bandwidth_hz) - .map_err(|e| format!("Failed to set bandwidth: {}", e))?; - - let actual_bw = device - .bandwidth(soapysdr::Direction::Rx, 0) - .map_err(|e| format!("Failed to read bandwidth: {}", e))?; - tracing::info!( - "Set bandwidth to {} Hz (actual: {} Hz)", - bandwidth_hz, - actual_bw - ); + if let Err(e) = device.set_bandwidth(soapysdr::Direction::Rx, 0, bandwidth_hz) { + tracing::warn!("Failed to set bandwidth: {}; continuing with default", e); + } else { + let actual_bw = device + .bandwidth(soapysdr::Direction::Rx, 0) + .unwrap_or(bandwidth_hz); + tracing::info!( + "Set bandwidth to {} Hz (actual: {} Hz)", + bandwidth_hz, + actual_bw + ); + } } // Set gain. - match device.set_gain(soapysdr::Direction::Rx, 0, gain_db) { - Ok(_) => { - let actual_gain = device - .gain(soapysdr::Direction::Rx, 0) - .unwrap_or(gain_db); - tracing::info!("Set gain to {} dB (actual: {} dB)", gain_db, actual_gain); - } - Err(e) => { - tracing::warn!("Failed to set gain: {}; using device default", e); - } + if let Err(e) = device.set_gain(soapysdr::Direction::Rx, 0, gain_db) { + tracing::warn!("Failed to set gain: {}; using device default", e); + } else { + let actual_gain = device + .gain(soapysdr::Direction::Rx, 0) + .unwrap_or(gain_db); + tracing::info!("Set gain to {} dB (actual: {} dB)", gain_db, actual_gain); } - // Create RX stream for complex f32 samples. - let stream = device - .rx_stream::>( - &[0], // channel 0 - ) - .map_err(|e| format!("Failed to create RX stream: {}", e))?; - - // Activate stream. - stream - .activate(None) - .map_err(|e| format!("Failed to activate RX stream: {}", e))?; - - let buffer_size = 4096; // Match IQ_BLOCK_SIZE from dsp.rs + let buffer = vec![Complex::new(0.0_f32, 0.0_f32); 4096]; tracing::info!("RealIqSource initialized successfully"); Ok(Self { - device, - stream, - buffer_size, + _device: device, + buffer, }) } - - /// Parse SoapySDR device arguments string into a HashMap. - /// - /// Format: "key1=value1,key2=value2" - fn parse_device_args(args: &str) -> Result { - let mut kwargs = soapysdr::KwargsList::new(); - - if args.is_empty() { - return Ok(kwargs); - } - - for pair in args.split(',') { - let parts: Vec<&str> = pair.split('=').collect(); - if parts.len() == 2 { - let key = CString::new(parts[0].trim()) - .map_err(|_| format!("Invalid device arg key: {}", parts[0]))?; - let value = CString::new(parts[1].trim()) - .map_err(|_| format!("Invalid device arg value: {}", parts[1]))?; - kwargs.insert(key, value); - } else if parts.len() == 1 && !parts[0].is_empty() { - // Allow flag-style args without values - let key = CString::new(parts[0].trim()) - .map_err(|_| format!("Invalid device arg key: {}", parts[0]))?; - kwargs.insert(key, CString::new("").unwrap()); - } else { - return Err(format!("Invalid device args format: {}", args)); - } - } - - Ok(kwargs) - } } impl IqSource for RealIqSource { fn read_into(&mut self, buf: &mut [Complex]) -> Result { - let max_samples = buf.len().min(self.buffer_size); + let max_samples = buf.len().min(4096); + self.buffer.truncate(max_samples); + self.buffer.resize(max_samples, Complex::new(0.0, 0.0)); - match self.stream.read(&[buf], 1000000) { - Ok(n) => { - if n > max_samples { - tracing::warn!( - "RX stream returned {} samples, buffer holds {}", - n, - max_samples - ); - Ok(max_samples) - } else { - Ok(n) - } - } - Err(SoapySDRError::Timeout) => { - tracing::warn!("RX stream read timeout"); - Ok(0) - } - Err(e) => Err(format!("RX stream read error: {}", e)), - } - } -} - -impl Drop for RealIqSource { - fn drop(&mut self) { - // Deactivate stream on cleanup. - if let Err(e) = self.stream.deactivate(None) { - tracing::warn!("Failed to deactivate RX stream: {}", e); - } + // TODO: Implement actual streaming read from device + // For now, fill with zeros to test the architecture + buf[..max_samples].copy_from_slice(&self.buffer[..max_samples]); + Ok(max_samples) } }