[feat](trx-backend-soapysdr): add crate scaffold with RigCat stub

Introduces the trx-backend-soapysdr crate with a compilable SoapySdrRig
struct that satisfies the Rig + RigCat trait bounds.  RX methods
(get_status, set_freq, set_mode, get_signal_strength) are implemented;
TX-only methods return RigError::not_supported.  as_audio_source returns
None for now (overridden in SDR-07).  Wires the crate into the workspace
and trx-backend (feature "soapysdr"), and fixes the non-exhaustive match
on RigAccess::Sdr in trx-server main.rs and rig_task.rs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-02-24 19:51:52 +01:00
parent b00f054bd4
commit 2ee217a1a1
9 changed files with 326 additions and 8 deletions
+6
View File
@@ -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 {
+1
View File
@@ -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<dyn RigCat> = config
+2 -1
View File
@@ -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"] }
+1 -4
View File
@@ -124,10 +124,7 @@ fn ft450d_factory(access: RigAccess) -> DynResult<Box<dyn RigCat>> {
#[cfg(feature = "soapysdr")]
fn soapysdr_factory(access: RigAccess) -> DynResult<Box<dyn RigCat>> {
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()),
}
}
@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
#
# 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"]
@@ -0,0 +1,208 @@
// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// 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<Self> {
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<Box<dyn std::future::Future<Output = DynResult<()>> + 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<Box<dyn std::future::Future<Output = DynResult<()>> + 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<Box<dyn std::future::Future<Output = DynResult<u8>> + 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<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
Box::pin(async move {
Err(Box::new(RigError::not_supported("set_ptt")) as Box<dyn std::error::Error + Send + Sync>)
})
}
fn power_on<'a>(
&'a mut self,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
Box::pin(async move {
Err(Box::new(RigError::not_supported("power_on")) as Box<dyn std::error::Error + Send + Sync>)
})
}
fn power_off<'a>(
&'a mut self,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
Box::pin(async move {
Err(Box::new(RigError::not_supported("power_off")) as Box<dyn std::error::Error + Send + Sync>)
})
}
fn get_tx_power<'a>(
&'a mut self,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<u8>> + Send + 'a>> {
Box::pin(async move {
Err(Box::new(RigError::not_supported("get_tx_power")) as Box<dyn std::error::Error + Send + Sync>)
})
}
fn get_tx_limit<'a>(
&'a mut self,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<u8>> + Send + 'a>> {
Box::pin(async move {
Err(Box::new(RigError::not_supported("get_tx_limit")) as Box<dyn std::error::Error + Send + Sync>)
})
}
fn set_tx_limit<'a>(
&'a mut self,
_limit: u8,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
Box::pin(async move {
Err(Box::new(RigError::not_supported("set_tx_limit")) as Box<dyn std::error::Error + Send + Sync>)
})
}
fn toggle_vfo<'a>(
&'a mut self,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
Box::pin(async move {
Err(Box::new(RigError::not_supported("toggle_vfo")) as Box<dyn std::error::Error + Send + Sync>)
})
}
fn lock<'a>(
&'a mut self,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
Box::pin(async move {
Err(Box::new(RigError::not_supported("lock")) as Box<dyn std::error::Error + Send + Sync>)
})
}
fn unlock<'a>(
&'a mut self,
) -> Pin<Box<dyn std::future::Future<Output = DynResult<()>> + Send + 'a>> {
Box::pin(async move {
Err(Box::new(RigError::not_supported("unlock")) as Box<dyn std::error::Error + Send + Sync>)
})
}
/// 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
}
}