diff --git a/Cargo.lock b/Cargo.lock index 80f299d..826ef72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -343,6 +343,26 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.66.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn", +] + [[package]] name = "bindgen" version = "0.72.1" @@ -356,7 +376,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 2.1.1", "shlex", "syn", ] @@ -600,7 +620,7 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" dependencies = [ - "bindgen", + "bindgen 0.72.1", ] [[package]] @@ -1212,6 +1232,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.177" @@ -1409,6 +1435,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1530,6 +1565,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1725,6 +1766,12 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -1908,6 +1955,28 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "soapysdr" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7497ace07aab956a89bc84c74478879ae099be8e061b59d8f80bfeacec3d9bda" +dependencies = [ + "log", + "num-complex", + "soapysdr-sys", +] + +[[package]] +name = "soapysdr-sys" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb5e50f86d0bf0c3312b77fce8737f760ce30adfa22baae97ffdd66a939356b" +dependencies = [ + "bindgen 0.66.1", + "cc", + "pkg-config", +] + [[package]] name = "socket2" version = "0.5.10" @@ -2278,6 +2347,7 @@ dependencies = [ "tracing", "trx-backend-ft450d", "trx-backend-ft817", + "trx-backend-soapysdr", "trx-core", ] @@ -2303,6 +2373,18 @@ dependencies = [ "trx-core", ] +[[package]] +name = "trx-backend-soapysdr" +version = "0.1.0" +dependencies = [ + "num-complex", + "serde", + "soapysdr", + "tokio", + "tracing", + "trx-core", +] + [[package]] name = "trx-client" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b3d8ec6..680b02a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "src/trx-server/trx-backend", "src/trx-server/trx-backend/trx-backend-ft817", "src/trx-server/trx-backend/trx-backend-ft450d", + "src/trx-server/trx-backend/trx-backend-soapysdr", "src/trx-client", "src/trx-client/trx-frontend", "src/trx-client/trx-frontend/trx-frontend-http", diff --git a/SDR.md b/SDR.md index d82e228..f9c0a18 100644 --- a/SDR.md +++ b/SDR.md @@ -24,7 +24,7 @@ This document specifies the requirements for a SoapySDR-based RX-only backend (` | ID | Status | Task | Touches | Needs | |----|--------|------|---------|-------| -| SDR-04 | `[ ]` | Create crate scaffold: `Cargo.toml` (deps: `soapysdr`, `num-complex`, `tokio`), empty `lib.rs` | `src/trx-server/trx-backend/trx-backend-soapysdr/` | SDR-01, SDR-02 | +| SDR-04 | `[x]` | Create crate scaffold: `Cargo.toml` (deps: `soapysdr`, `num-complex`, `tokio`), empty `lib.rs` | `src/trx-server/trx-backend/trx-backend-soapysdr/` | SDR-01, SDR-02 | | SDR-05 | `[ ]` | Implement `demod.rs`: SSB (USB/LSB), AM envelope, FM quadrature, CW narrow BPF+envelope | `…/src/demod.rs` | SDR-04 | | SDR-06 | `[ ]` | Implement `dsp.rs`: IQ broadcast loop (SoapySDR read thread → `broadcast::Sender>>`); per-channel mixer → FIR LPF → decimator → demod → frame accumulator → `broadcast::Sender>` | `…/src/dsp.rs` | SDR-04, SDR-05 | | SDR-07 | `[ ]` | Implement `SoapySdrRig` in `lib.rs`: `RigCat` (RX methods + `not_supported` stubs for TX), `AudioSource`, gain control (manual/auto with fallback), primary channel freq/mode tracking | `…/src/lib.rs` | SDR-03, SDR-06 | diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index 63451b1..5e8d211 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -290,6 +290,12 @@ async fn main() -> DynResult<()> { resolved.rig, addr ); } + RigAccess::Sdr { args } => { + info!( + "Starting trx-server (rig: {}, access: sdr {})", + resolved.rig, args + ); + } } if let Some(ref cs) = resolved.callsign { diff --git a/src/trx-server/src/rig_task.rs b/src/trx-server/src/rig_task.rs index 7b92e05..8cc0738 100644 --- a/src/trx-server/src/rig_task.rs +++ b/src/trx-server/src/rig_task.rs @@ -91,6 +91,7 @@ pub async fn run_rig_task( match &config.access { RigAccess::Serial { path, baud } => info!("Serial: {} @ {} baud", path, baud), RigAccess::Tcp { addr } => info!("TCP CAT: {}", addr), + RigAccess::Sdr { args } => info!("SDR: {}", args), } let mut rig: Box = config diff --git a/src/trx-server/trx-backend/Cargo.toml b/src/trx-server/trx-backend/Cargo.toml index da0f079..e04d8bc 100644 --- a/src/trx-server/trx-backend/Cargo.toml +++ b/src/trx-server/trx-backend/Cargo.toml @@ -11,12 +11,13 @@ edition = "2021" default = ["ft817", "ft450d"] ft817 = ["dep:trx-backend-ft817"] ft450d = ["dep:trx-backend-ft450d"] -soapysdr = [] # implementation wired in SDR-04 +soapysdr = ["dep:trx-backend-soapysdr"] [dependencies] trx-core = { path = "../../trx-core" } trx-backend-ft817 = { path = "trx-backend-ft817", optional = true } trx-backend-ft450d = { path = "trx-backend-ft450d", optional = true } +trx-backend-soapysdr = { path = "./trx-backend-soapysdr", optional = true } tokio = { workspace = true, features = ["full"] } tokio-serial = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/src/trx-server/trx-backend/src/lib.rs b/src/trx-server/trx-backend/src/lib.rs index 1af407e..2828dd4 100644 --- a/src/trx-server/trx-backend/src/lib.rs +++ b/src/trx-server/trx-backend/src/lib.rs @@ -124,10 +124,7 @@ fn ft450d_factory(access: RigAccess) -> DynResult> { #[cfg(feature = "soapysdr")] fn soapysdr_factory(access: RigAccess) -> DynResult> { match access { - RigAccess::Sdr { args } => { - // trx_backend_soapysdr will be wired in once SDR-04 lands - Err(format!("soapysdr backend not yet implemented (args: {args})").into()) - } + RigAccess::Sdr { args } => Ok(Box::new(trx_backend_soapysdr::SoapySdrRig::new(&args)?)), _ => Err("soapysdr backend requires Sdr access type".into()), } } diff --git a/src/trx-server/trx-backend/trx-backend-soapysdr/Cargo.toml b/src/trx-server/trx-backend/trx-backend-soapysdr/Cargo.toml new file mode 100644 index 0000000..54c7904 --- /dev/null +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/Cargo.toml @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2025 Stanislaw Grams +# +# SPDX-License-Identifier: BSD-2-Clause + +[package] +name = "trx-backend-soapysdr" +version = "0.1.0" +edition = "2021" +license = "BSD-2-Clause" + +[dependencies] +trx-core = { path = "../../../trx-core" } +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"] 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 new file mode 100644 index 0000000..1ca8cc5 --- /dev/null +++ b/src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: 2025 Stanislaw Grams +// +// SPDX-License-Identifier: BSD-2-Clause + +use std::pin::Pin; + +use trx_core::radio::freq::{Band, Freq}; +use trx_core::rig::{ + Rig, RigAccessMethod, RigCapabilities, RigCat, RigInfo, RigStatusFuture, +}; +use trx_core::rig::response::RigError; +use trx_core::{DynResult, RigMode}; + +/// RX-only backend for any SoapySDR-compatible device. +pub struct SoapySdrRig { + info: RigInfo, + freq: Freq, + mode: RigMode, +} + +impl SoapySdrRig { + /// Construct a new `SoapySdrRig` from a SoapySDR device args string. + /// + /// The `args` value follows SoapySDR's key=value comma-separated convention + /// (e.g. `"driver=rtlsdr"` or `"driver=airspy,serial=00000001"`). + pub fn new(args: &str) -> DynResult { + tracing::info!("initialising SoapySDR backend (args={:?})", args); + + let info = RigInfo { + manufacturer: "SoapySDR".to_string(), + model: "Generic SDR".to_string(), + revision: "".to_string(), + capabilities: RigCapabilities { + min_freq_step_hz: 1, + // Broad RX-only coverage: DC through 6 GHz as a single band. + supported_bands: vec![Band { + low_hz: 0, + high_hz: 6_000_000_000, + tx_allowed: false, + }], + supported_modes: vec![ + RigMode::LSB, + RigMode::USB, + RigMode::CW, + RigMode::CWR, + RigMode::AM, + RigMode::WFM, + RigMode::FM, + RigMode::DIG, + RigMode::PKT, + ], + num_vfos: 1, + lockable: false, + attenuator: false, + preamp: false, + rit: false, + rpt: false, + split: false, + lock: false, + }, + // There is no serial/TCP access for SDR devices; use a dummy TCP + // placeholder so `RigAccessMethod` (which has no SDR variant) can + // still carry the args string in a human-readable form. + access: RigAccessMethod::Tcp { + addr: format!("soapysdr:{}", args), + }, + }; + + Ok(Self { + info, + freq: Freq { hz: 14_074_000 }, + mode: RigMode::USB, + }) + } +} + +// --------------------------------------------------------------------------- +// Rig +// --------------------------------------------------------------------------- + +impl Rig for SoapySdrRig { + fn info(&self) -> &RigInfo { + &self.info + } +} + +// --------------------------------------------------------------------------- +// RigCat +// --------------------------------------------------------------------------- + +impl RigCat for SoapySdrRig { + // -- Supported RX methods ----------------------------------------------- + + fn get_status<'a>(&'a mut self) -> RigStatusFuture<'a> { + Box::pin(async move { Ok((self.freq, self.mode.clone(), None)) }) + } + + fn set_freq<'a>( + &'a mut self, + freq: Freq, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + tracing::debug!("SoapySdrRig: set_freq -> {} Hz", freq.hz); + self.freq = freq; + Ok(()) + }) + } + + fn set_mode<'a>( + &'a mut self, + mode: RigMode, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + tracing::debug!("SoapySdrRig: set_mode -> {:?}", mode); + self.mode = mode; + Ok(()) + }) + } + + fn get_signal_strength<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + // RSSI mapping will be implemented in SDR-07; return 0 for now. + Box::pin(async move { Ok(0) }) + } + + // -- TX / unsupported methods ------------------------------------------- + + fn set_ptt<'a>( + &'a mut self, + _ptt: bool, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + Err(Box::new(RigError::not_supported("set_ptt")) as Box) + }) + } + + fn power_on<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + Err(Box::new(RigError::not_supported("power_on")) as Box) + }) + } + + fn power_off<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + Err(Box::new(RigError::not_supported("power_off")) as Box) + }) + } + + fn get_tx_power<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + Err(Box::new(RigError::not_supported("get_tx_power")) as Box) + }) + } + + fn get_tx_limit<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + Err(Box::new(RigError::not_supported("get_tx_limit")) as Box) + }) + } + + fn set_tx_limit<'a>( + &'a mut self, + _limit: u8, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + Err(Box::new(RigError::not_supported("set_tx_limit")) as Box) + }) + } + + fn toggle_vfo<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + Err(Box::new(RigError::not_supported("toggle_vfo")) as Box) + }) + } + + fn lock<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + Err(Box::new(RigError::not_supported("lock")) as Box) + }) + } + + fn unlock<'a>( + &'a mut self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + Err(Box::new(RigError::not_supported("unlock")) as Box) + }) + } + + /// Returns `None` for now; will be overridden with `Some(self)` in SDR-07 + /// once the IQ DSP pipeline is in place. + fn as_audio_source(&self) -> Option<&dyn trx_core::rig::AudioSource> { + None + } +}