Merge branch 'sdr_autoselect'

This commit is contained in:
Wouter Bokslag
2026-03-25 19:38:47 +01:00
10 changed files with 452 additions and 453 deletions

18
Cargo.lock generated
View File

@@ -150,7 +150,7 @@ dependencies = [
[[package]]
name = "bluestation-bs"
version = "0.5.5"
version = "0.5.6"
dependencies = [
"clap",
"ctrlc",
@@ -719,7 +719,7 @@ dependencies = [
[[package]]
name = "net-tnmm-test"
version = "0.5.5"
version = "0.5.6"
dependencies = [
"clap",
"tetra-config",
@@ -734,7 +734,7 @@ dependencies = [
[[package]]
name = "net-tnmm-test-quic"
version = "0.5.5"
version = "0.5.6"
dependencies = [
"quinn",
"rcgen",
@@ -886,7 +886,7 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "pdu-tool"
version = "0.5.5"
version = "0.5.6"
dependencies = [
"clap",
"tetra-config",
@@ -1501,7 +1501,7 @@ dependencies = [
[[package]]
name = "tetra-config"
version = "0.5.5"
version = "0.5.6"
dependencies = [
"chrono-tz",
"serde",
@@ -1511,7 +1511,7 @@ dependencies = [
[[package]]
name = "tetra-core"
version = "0.5.5"
version = "0.5.6"
dependencies = [
"const_format",
"git-version",
@@ -1523,7 +1523,7 @@ dependencies = [
[[package]]
name = "tetra-entities"
version = "0.5.5"
version = "0.5.6"
dependencies = [
"as-any",
"bitcode",
@@ -1553,7 +1553,7 @@ dependencies = [
[[package]]
name = "tetra-pdus"
version = "0.5.5"
version = "0.5.6"
dependencies = [
"tetra-config",
"tetra-core",
@@ -1563,7 +1563,7 @@ dependencies = [
[[package]]
name = "tetra-saps"
version = "0.5.5"
version = "0.5.6"
dependencies = [
"tetra-core",
"tracing",

View File

@@ -17,7 +17,7 @@ members = [
]
[workspace.package]
version = "0.5.5"
version = "0.5.6"
edition = "2024"
authors = ["Wouter Bokslag / Midnight Blue"]
license = "MIT"

View File

@@ -92,9 +92,9 @@ fn main() {
eprintln!("░▀█▀░█▀▀░▀█▀░█▀▄░█▀█░░░░░█▀▄░█░░░█░█░█▀▀░█▀▀░▀█▀░█▀█░▀█▀░▀█▀░█▀█░█▀█");
eprintln!("░░█░░█▀▀░░█░░█▀▄░█▀█░▄▄▄░█▀▄░█░░░█░█░█▀▀░▀▀█░░█░░█▀█░░█░░░█░░█░█░█░█");
eprintln!("░░▀░░▀▀▀░░▀░░▀░▀░▀░▀░░░░░▀▀░░▀▀▀░▀▀▀░▀▀▀░▀▀▀░░▀░░▀░▀░░▀░░▀▀▀░▀▀▀░▀░▀\n");
eprintln!(" Wouter Bokslag / Midnight Blue");
eprintln!(" -> https://github.com/MidnightBlueLabs/tetra-bluestation");
eprintln!(" -> https://midnightblue.nl\n");
eprintln!(" Wouter Bokslag / Midnight Blue");
eprintln!(" https://github.com/MidnightBlueLabs/tetra-bluestation");
eprintln!(" Version: {}", tetra_core::STACK_VERSION);
let args = Args::parse();
let mut cfg = load_config_from_toml(&args.config);

View File

@@ -33,25 +33,9 @@ impl StackConfig {
// Check input device settings
match self.phy_io.backend {
PhyBackend::SoapySdr => {
let Some(ref soapy_cfg) = self.phy_io.soapysdr else {
if self.phy_io.soapysdr.is_none() {
return Err("soapysdr configuration must be provided for Soapysdr backend");
};
// Validate that exactly one hardware configuration is present
let config_count = [
soapy_cfg.io_cfg.iocfg_usrpb2xx.is_some(),
soapy_cfg.io_cfg.iocfg_limesdr.is_some(),
soapy_cfg.io_cfg.iocfg_sxceiver.is_some(),
soapy_cfg.io_cfg.iocfg_pluto.is_some(),
]
.iter()
.filter(|&&x| x)
.count();
if config_count != 1 {
return Err(
"soapysdr backend requires exactly one hardware configuration (iocfg_usrpb2xx, iocfg_limesdr, iocfg_sxceiver or iocfg_pluto)",
);
}
}
PhyBackend::None => {} // For testing
PhyBackend::Undefined => {

View File

@@ -17,7 +17,7 @@ pub fn from_toml_str(toml_str: &str) -> Result<SharedConfig, Box<dyn std::error:
let root: TomlConfigRoot = toml::from_str(toml_str)?;
// Various sanity checks
let expected_config_version = "0.5";
let expected_config_version = "0.6";
if !root.config_version.eq(expected_config_version) {
return Err(format!(
"Unrecognized config_version: {}, expect {}",
@@ -33,8 +33,13 @@ pub fn from_toml_str(toml_str: &str) -> Result<SharedConfig, Box<dyn std::error:
return Err(format!("Unrecognized fields: phy_io::{:?}", sorted_keys(&root.phy_io.extra)).into());
}
if let Some(ref soapy) = root.phy_io.soapysdr {
if !soapy.extra.is_empty() {
return Err(format!("Unrecognized fields: phy_io.soapysdr::{:?}", sorted_keys(&soapy.extra)).into());
let extra_keys = sorted_keys(&soapy.extra);
let extra_keys_filtered = extra_keys
.iter()
.filter(|key| !(key.starts_with("rx_gain_") || key.starts_with("tx_gain_")))
.collect::<Vec<&&str>>();
if !extra_keys_filtered.is_empty() {
return Err(format!("Unrecognized fields: phy_io.soapysdr::{:?}", extra_keys_filtered).into());
}
}
if !root.net_info.extra.is_empty() {

View File

@@ -3,7 +3,7 @@ use std::collections::HashMap;
use serde::Deserialize;
use toml::Value;
use crate::bluestation::{CfgLimeSdr, CfgPluto, CfgSoapySdr, CfgSxCeiver, CfgUsrpB2xx, SoapySdrDto, SoapySdrIoCfg};
use crate::bluestation::{CfgSoapySdr, SoapySdrDto};
/// The PHY layer backend type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
@@ -46,58 +46,51 @@ pub struct PhyIoDto {
pub fn phy_dto_to_cfg(src: PhyIoDto) -> CfgPhyIo {
let soapysdr = src.soapysdr.map(|soapy_dto| {
let mut soapy_cfg = CfgSoapySdr {
CfgSoapySdr {
ul_freq: soapy_dto.rx_freq,
dl_freq: soapy_dto.tx_freq,
ppm_err: soapy_dto.ppm_err.unwrap_or(0.0),
io_cfg: SoapySdrIoCfg::default(),
};
if let Some(usrp_dto) = soapy_dto.iocfg_usrpb2xx {
soapy_cfg.io_cfg.iocfg_usrpb2xx = Some(CfgUsrpB2xx {
rx_ant: usrp_dto.rx_ant,
tx_ant: usrp_dto.tx_ant,
rx_gain_pga: usrp_dto.rx_gain_pga,
tx_gain_pga: usrp_dto.tx_gain_pga,
});
device: soapy_dto.device,
fs: soapy_dto.sample_rate,
rx_ch: soapy_dto.rx_channel,
tx_ch: soapy_dto.tx_channel,
rx_ant: soapy_dto.rx_antenna,
tx_ant: soapy_dto.tx_antenna,
rx_gains: soapy_dto
.extra
.iter()
.filter_map(|(key, value)| {
key.strip_prefix("rx_gain_").map(|gain_name| {
(
gain_name.to_string().to_lowercase(),
match value {
Value::Integer(v) => *v as f64,
Value::Float(v) => *v,
// TODO: should this error be returned somehow?
_ => panic!("RX gain value must be a number"),
},
)
})
})
.collect(),
tx_gains: soapy_dto
.extra
.iter()
.filter_map(|(key, value)| {
key.strip_prefix("tx_gain_").map(|gain_name| {
(
gain_name.to_string().to_lowercase(),
match value {
Value::Integer(v) => *v as f64,
Value::Float(v) => *v,
// TODO: should this error be returned somehow?
_ => panic!("TX gain value must be a number"),
},
)
})
})
.collect(),
}
if let Some(lime_dto) = soapy_dto.iocfg_limesdr {
soapy_cfg.io_cfg.iocfg_limesdr = Some(CfgLimeSdr {
rx_ant: lime_dto.rx_ant,
tx_ant: lime_dto.tx_ant,
rx_gain_lna: lime_dto.rx_gain_lna,
rx_gain_tia: lime_dto.rx_gain_tia,
rx_gain_pga: lime_dto.rx_gain_pga,
tx_gain_pad: lime_dto.tx_gain_pad,
tx_gain_iamp: lime_dto.tx_gain_iamp,
});
}
if let Some(sx_dto) = soapy_dto.iocfg_sxceiver {
soapy_cfg.io_cfg.iocfg_sxceiver = Some(CfgSxCeiver {
rx_ant: sx_dto.rx_ant,
tx_ant: sx_dto.tx_ant,
rx_gain_lna: sx_dto.rx_gain_lna,
rx_gain_pga: sx_dto.rx_gain_pga,
tx_gain_dac: sx_dto.tx_gain_dac,
tx_gain_mixer: sx_dto.tx_gain_mixer,
});
}
if let Some(pluto_dto) = soapy_dto.iocfg_pluto {
soapy_cfg.io_cfg.iocfg_pluto = Some(CfgPluto {
rx_ant: pluto_dto.rx_ant,
tx_ant: pluto_dto.tx_ant,
rx_gain_pga: pluto_dto.rx_gain_pga,
tx_gain_pga: pluto_dto.tx_gain_pga,
uri: pluto_dto.uri,
loopback: pluto_dto.loopback,
timestamp_every: pluto_dto.timestamp_every,
usb_direct: pluto_dto.usb_direct,
direct: pluto_dto.direct,
});
}
soapy_cfg
});
CfgPhyIo {

View File

@@ -2,95 +2,6 @@ use serde::Deserialize;
use std::collections::HashMap;
use toml::Value;
/// Configuration for different SDR hardware devices
#[derive(Debug, Clone)]
pub struct SoapySdrIoCfg {
/// USRP B2xx series configuration (B200, B210)
pub iocfg_usrpb2xx: Option<CfgUsrpB2xx>,
/// LimeSDR configuration
pub iocfg_limesdr: Option<CfgLimeSdr>,
/// SXceiver configuration
pub iocfg_sxceiver: Option<CfgSxCeiver>,
/// Pluto timestamp configuration
pub iocfg_pluto: Option<CfgPluto>,
}
impl SoapySdrIoCfg {
pub fn get_soapy_driver_name(&self) -> &'static str {
if self.iocfg_usrpb2xx.is_some() {
"uhd"
} else if self.iocfg_limesdr.is_some() {
"lime"
} else if self.iocfg_sxceiver.is_some() {
"sx"
} else if self.iocfg_pluto.is_some() {
"plutosdr"
} else {
"unknown"
}
}
}
impl Default for SoapySdrIoCfg {
fn default() -> Self {
Self {
iocfg_usrpb2xx: None,
iocfg_limesdr: None,
iocfg_sxceiver: None,
iocfg_pluto: None,
}
}
}
/// Configuration for Ettus USRP B2xx series
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CfgUsrpB2xx {
pub rx_ant: Option<String>,
pub tx_ant: Option<String>,
pub rx_gain_pga: Option<f64>,
pub tx_gain_pga: Option<f64>,
}
/// Configuration for LimeSDR
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CfgLimeSdr {
pub rx_ant: Option<String>,
pub tx_ant: Option<String>,
pub rx_gain_lna: Option<f64>,
pub rx_gain_tia: Option<f64>,
pub rx_gain_pga: Option<f64>,
pub tx_gain_pad: Option<f64>,
pub tx_gain_iamp: Option<f64>,
}
/// Configuration for SXceiver
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CfgSxCeiver {
pub rx_ant: Option<String>,
pub tx_ant: Option<String>,
pub rx_gain_lna: Option<f64>,
pub rx_gain_pga: Option<f64>,
pub tx_gain_dac: Option<f64>,
pub tx_gain_mixer: Option<f64>,
}
/// Configuration for Pluto timestamp
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CfgPluto {
pub rx_ant: Option<String>,
pub tx_ant: Option<String>,
pub rx_gain_pga: Option<f64>,
pub tx_gain_pga: Option<f64>,
pub uri: Option<String>,
pub usb_direct: Option<bool>,
pub direct: Option<bool>,
pub timestamp_every: Option<usize>,
pub loopback: Option<bool>,
}
/// SoapySDR configuration
#[derive(Debug, Clone)]
pub struct CfgSoapySdr {
@@ -100,8 +11,25 @@ pub struct CfgSoapySdr {
pub dl_freq: f64,
/// PPM frequency error correction
pub ppm_err: f64,
/// Hardware-specific I/O configuration
pub io_cfg: SoapySdrIoCfg,
/// Argument string to select a specific SDR device.
/// If None, devices will be enumerated until the first supported device is found.
pub device: Option<String>,
/// RX antenna. Device specific default will be used if None.
pub rx_ant: Option<String>,
/// TX antenna. Device specific default will be used if None.
pub tx_ant: Option<String>,
/// RX gain values.
/// Device specific defaults will be used for gains that are not set.
pub rx_gains: HashMap<String, f64>,
/// TX gain values.
/// Device specific defaults will be used for gains that are not set.
pub tx_gains: HashMap<String, f64>,
/// RX and TX sample rate. Device specific default will be used if None.
pub fs: Option<f64>,
/// RX channel number
pub rx_ch: Option<usize>,
/// TX channel number
pub tx_ch: Option<usize>,
}
impl CfgSoapySdr {
@@ -126,53 +54,15 @@ pub struct SoapySdrDto {
pub tx_freq: f64,
pub ppm_err: Option<f64>,
pub iocfg_usrpb2xx: Option<UsrpB2xxDto>,
pub iocfg_limesdr: Option<LimeSdrDto>,
pub iocfg_sxceiver: Option<SXceiverDto>,
pub iocfg_pluto: Option<PlutoDto>,
pub device: Option<String>,
pub rx_antenna: Option<String>,
pub tx_antenna: Option<String>,
pub sample_rate: Option<f64>,
pub rx_channel: Option<usize>,
pub tx_channel: Option<usize>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
#[derive(Deserialize)]
pub struct UsrpB2xxDto {
pub rx_ant: Option<String>,
pub tx_ant: Option<String>,
pub rx_gain_pga: Option<f64>,
pub tx_gain_pga: Option<f64>,
}
#[derive(Deserialize)]
pub struct LimeSdrDto {
pub rx_ant: Option<String>,
pub tx_ant: Option<String>,
pub rx_gain_lna: Option<f64>,
pub rx_gain_tia: Option<f64>,
pub rx_gain_pga: Option<f64>,
pub tx_gain_pad: Option<f64>,
pub tx_gain_iamp: Option<f64>,
}
#[derive(Deserialize)]
pub struct SXceiverDto {
pub rx_ant: Option<String>,
pub tx_ant: Option<String>,
pub rx_gain_lna: Option<f64>,
pub rx_gain_pga: Option<f64>,
pub tx_gain_dac: Option<f64>,
pub tx_gain_mixer: Option<f64>,
}
#[derive(Deserialize)]
pub struct PlutoDto {
pub rx_ant: Option<String>,
pub tx_ant: Option<String>,
pub rx_gain_pga: Option<f64>,
pub tx_gain_pga: Option<f64>,
pub uri: Option<String>,
pub loopback: Option<bool>,
pub timestamp_every: Option<usize>,
pub usb_direct: Option<bool>,
pub direct: Option<bool>,
}

View File

@@ -2,6 +2,62 @@
use tetra_config::bluestation::{StackMode, sec_phy_soapy::*};
/// Enum of all supported devices
pub enum SupportedDevice {
LimeSdr(LimeSdrModel),
SXceiver,
PlutoSdr,
Usrp(UsrpModel),
}
#[derive(Debug, PartialEq)]
pub enum LimeSdrModel {
LimeSdrUsb,
LimeSdrMiniV2,
LimeNetMicro,
/// Other LimeSDR models with FX3 driver
OtherFx3,
/// Other LimeSDR models with FT601 driver
OtherFt601,
}
#[derive(Debug, PartialEq)]
pub enum UsrpModel {
B200,
B210,
Other,
}
impl SupportedDevice {
/// Detect an SDR device based on driver key and hardware key.
/// Return None if the device is not supported.
pub fn detect(driver_key: &str, hardware_key: &str) -> Option<Self> {
match (driver_key, hardware_key) {
("FX3", "LimeSDR-USB") => Some(Self::LimeSdr(LimeSdrModel::LimeSdrUsb)),
("FX3", _) => Some(Self::LimeSdr(LimeSdrModel::OtherFx3)),
("FT601", "LimeSDR-Mini_v2") => Some(Self::LimeSdr(LimeSdrModel::LimeSdrMiniV2)),
("FT601", "LimeNET-Micro") => Some(Self::LimeSdr(LimeSdrModel::LimeNetMicro)),
("FT601", _) => Some(Self::LimeSdr(LimeSdrModel::OtherFt601)),
("sx", _) => Some(Self::SXceiver),
("PlutoSDR", _) => Some(Self::PlutoSdr),
// USRP B210 seems to report as ("b200", "B210"),
// but the driver key is also known to be "uhd" in some cases.
// The reason is unknown but might be due to
// gateware, firmware or driver version differences.
// Try to detect USRP correctly in all cases.
("b200", "B200") | ("uhd", "B200") => Some(Self::Usrp(UsrpModel::B200)),
("b200", "B210") | ("uhd", "B210") => Some(Self::Usrp(UsrpModel::B210)),
("b200", _) | ("uhd", _) => Some(Self::Usrp(UsrpModel::Other)),
// TODO: add other USRP models if needed
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub struct SdrSettings {
/// Settings template, holding which SDR is used
@@ -12,6 +68,10 @@ pub struct SdrSettings {
pub use_get_hardware_time: bool,
/// Receive and transmit sample rate.
pub fs: f64,
/// Receive channel number
pub rx_ch: usize,
/// Transmit channel number
pub tx_ch: usize,
/// Receive antenna
pub rx_ant: Option<String>,
/// Transmit antenna
@@ -25,72 +85,74 @@ pub struct SdrSettings {
pub rx_args: Vec<(String, String)>,
/// Transmit stream arguments
pub tx_args: Vec<(String, String)>,
/// Additional device arguments
pub dev_args: Vec<(String, String)>,
}
/// Get device arguments based on IO configuration.
///
/// This is separate from SdrSettings because device arguments
/// must be known before opening the device,
/// whereas SdrSettings may depend on information
/// that is obtained after the device has been opened.
pub fn get_device_arguments(io_cfg: &SoapySdrIoCfg, _mode: StackMode) -> Vec<(String, String)> {
let mut args = Vec::<(String, String)>::new();
let driver = io_cfg.get_soapy_driver_name();
args.push(("driver".to_string(), driver.to_string()));
// Additional device arguments for devices that need them
match driver {
"plutosdr" => {
let cfg: &Option<CfgPluto> = &io_cfg.iocfg_pluto;
// If cfg is None, use default which sets all optional fields to None.
let cfg_pluto = if let Some(cfg) = cfg { &cfg } else { &CfgPluto::default() };
args.push((
"direct".to_string(),
cfg_pluto.direct.map_or("1", |v| if v { "1" } else { "0" }).to_string(),
));
args.push(("timestamp_every".to_string(), cfg_pluto.timestamp_every.unwrap_or(1500).to_string()));
if let Some(ref uri) = cfg_pluto.uri {
args.push(("uri".to_string(), uri.to_string()));
}
if let Some(loopback) = cfg_pluto.loopback {
args.push(("loopback".to_string(), (if loopback { "1" } else { "0" }).to_string()));
}
}
_ => {}
}
args
pub enum Error {
InvalidConfiguration,
}
impl SdrSettings {
/// Get settings based on SDR type
pub fn get_settings(io_cfg: &SoapySdrIoCfg, driver_key: &str, hardware_key: &str, mode: StackMode) -> Self {
match (driver_key, hardware_key) {
("FX3", "LimeSDR-USB") => Self::settings_limesdr(&io_cfg.iocfg_limesdr, mode, LimeSdrModel::LimeSdrUsb),
("FX3", "LimeSDR-Mini_v2") => Self::settings_limesdr(&io_cfg.iocfg_limesdr, mode, LimeSdrModel::LimeSdrMiniV2),
("FX3", _) => Self::settings_limesdr(&io_cfg.iocfg_limesdr, mode, LimeSdrModel::OtherFx3),
/// Get settings based on SDR type and SoapySDR configuration
pub fn get_settings(cfg: &CfgSoapySdr, device: SupportedDevice, mode: StackMode) -> Result<Self, Error> {
let mut settings = Self::get_defaults(cfg, device, mode);
// TODO: remove one of these once we know whether LimeSDR-Mini_v2 reports FX3 or FT601
("FT601", "LimeSDR-Mini_v2") => Self::settings_limesdr(&io_cfg.iocfg_limesdr, mode, LimeSdrModel::LimeSdrMiniV2),
("FT601", "LimeNET-Micro") => Self::settings_limesdr(&io_cfg.iocfg_limesdr, mode, LimeSdrModel::LimeNetMicro),
("FT601", _) => Self::settings_limesdr(&io_cfg.iocfg_limesdr, mode, LimeSdrModel::OtherFt601),
// Override settings if specified in configuration
if let Some(fs) = cfg.fs {
settings.fs = fs;
}
if let Some(ch) = cfg.rx_ch {
settings.rx_ch = ch;
}
if let Some(ch) = cfg.tx_ch {
settings.tx_ch = ch;
}
if let Some(ant) = &cfg.rx_ant {
settings.rx_ant = Some(ant.clone());
}
if let Some(ant) = &cfg.tx_ant {
settings.tx_ant = Some(ant.clone());
}
("sx", _) => Self::settings_sxceiver(&io_cfg.iocfg_sxceiver, mode),
let mut cfg_gains = cfg.rx_gains.clone();
for (name, value) in settings.rx_gain.iter_mut() {
if let Some(gain) = cfg_gains.remove(&(*name.to_lowercase())) {
*value = gain;
}
}
if !cfg_gains.is_empty() {
tracing::error!("Unsupported RX gains for {}: {:?}", settings.name, cfg_gains);
return Err(Error::InvalidConfiguration);
}
// USRP B210 seems to report as ("b200", "B210"),
// but the driver key is also known to be "uhd" in some cases.
// The reason is unknown but might be due to
// gateware, firmware or driver version differences.
// Try to detect USRP correctly in all cases.
("b200", "B200") | ("uhd", "B200") => Self::settings_usrp(&io_cfg.iocfg_usrpb2xx, mode, UsrpModel::B200),
("b200", "B210") | ("uhd", "B210") => Self::settings_usrp(&io_cfg.iocfg_usrpb2xx, mode, UsrpModel::B210),
("b200", _) | ("uhd", _) => Self::settings_usrp(&io_cfg.iocfg_usrpb2xx, mode, UsrpModel::Other),
// TODO: add other USRP models if needed
("PlutoSDR", _) => Self::settings_pluto(&io_cfg.iocfg_pluto, mode),
let mut cfg_gains = cfg.tx_gains.clone();
for (name, value) in settings.tx_gain.iter_mut() {
if let Some(gain) = cfg_gains.remove(&(*name.to_lowercase())) {
*value = gain;
}
}
if !cfg_gains.is_empty() {
tracing::error!("Unsupported TX gains for {}: {:?}", settings.name, cfg_gains);
return Err(Error::InvalidConfiguration);
}
_ => Self::unknown(mode),
// TODO: check for extra gain fields in cfg
Ok(settings)
}
/// Get default settings based on SDR type
fn get_defaults(cfg: &CfgSoapySdr, device: SupportedDevice, mode: StackMode) -> Self {
match device {
SupportedDevice::LimeSdr(model) => Self::settings_limesdr(mode, model),
SupportedDevice::SXceiver => Self::settings_sxceiver(mode, cfg.fs),
SupportedDevice::PlutoSdr => Self::settings_pluto(mode),
SupportedDevice::Usrp(model) => Self::settings_usrp(mode, model),
}
}
@@ -101,7 +163,7 @@ impl SdrSettings {
/// more fields are added to SdrSettings to handle some special cases.
fn default(mode: StackMode) -> Self {
Self {
name: "".to_string(), // should be always overridden
name: String::new(), // should be always overridden
// With FCFB bin spacing of 500 Hz and overlap factor or 1/4,
// FFT size becomes fs/500 and must be a multiple of 4.
@@ -122,23 +184,16 @@ impl SdrSettings {
tx_ant: None,
rx_gain: vec![],
tx_gain: vec![],
rx_ch: 0,
tx_ch: 0,
rx_args: vec![],
tx_args: vec![],
dev_args: vec![],
}
}
fn unknown(mode: StackMode) -> Self {
SdrSettings {
name: "Unknown SDR device".to_string(),
..Self::default(mode)
}
}
fn settings_limesdr(cfg: &Option<CfgLimeSdr>, mode: StackMode, model: LimeSdrModel) -> Self {
// If cfg is None, use default which sets all optional fields to None.
let cfg = if let Some(cfg) = cfg { &cfg } else { &CfgLimeSdr::default() };
SdrSettings {
fn settings_limesdr(mode: StackMode, model: LimeSdrModel) -> Self {
Self {
name: match model {
LimeSdrModel::LimeSdrUsb => "LimeSDR USB",
LimeSdrModel::LimeSdrMiniV2 => "LimeSDR Mini 2.0",
@@ -149,33 +204,23 @@ impl SdrSettings {
.to_string(),
rx_ant: Some(
cfg.rx_ant.clone().unwrap_or(
match model {
LimeSdrModel::LimeSdrUsb => "LNAL",
_ => "LNAW",
}
.to_string(),
),
),
tx_ant: Some(
cfg.tx_ant.clone().unwrap_or(
match model {
LimeSdrModel::LimeSdrUsb => "BAND1",
_ => "BAND2",
}
.to_string(),
),
match model {
LimeSdrModel::LimeSdrUsb => "LNAL",
_ => "LNAW",
}
.to_string(),
),
rx_gain: vec![
("LNA".to_string(), cfg.rx_gain_lna.unwrap_or(18.0)),
("TIA".to_string(), cfg.rx_gain_tia.unwrap_or(6.0)),
("PGA".to_string(), cfg.rx_gain_pga.unwrap_or(10.0)),
],
tx_gain: vec![
("PAD".to_string(), cfg.tx_gain_pad.unwrap_or(22.0)),
("IAMP".to_string(), cfg.tx_gain_iamp.unwrap_or(6.0)),
],
tx_ant: Some(
match model {
LimeSdrModel::LimeSdrUsb => "BAND1",
_ => "BAND2",
}
.to_string(),
),
rx_gain: vec![("LNA".to_string(), 18.0), ("TIA".to_string(), 6.0), ("PGA".to_string(), 10.0)],
tx_gain: vec![("PAD".to_string(), 22.0), ("IAMP".to_string(), 6.0)],
// Minimum latency for BS/MS, maximum throughput for monitor
rx_args: vec![("latency".to_string(), if mode == StackMode::Mon { "1" } else { "0" }.to_string())],
@@ -185,28 +230,25 @@ impl SdrSettings {
}
}
fn settings_sxceiver(cfg: &Option<CfgSxCeiver>, mode: StackMode) -> Self {
// If cfg is None, use default which sets all optional fields to None.
let cfg = if let Some(cfg) = cfg { &cfg } else { &CfgSxCeiver::default() };
fn settings_sxceiver(mode: StackMode, fs_override: Option<f64>) -> Self {
// TODO: pass detected clock rate or list of supported sample rates
// to get_settings and choose sample rate accordingly.
let fs = 600e3;
SdrSettings {
// Ok, it is not strictly needed now that sample rate can be overridden.
// That added another minor issue, though:
// sample rate affects the optimal period size
// and override is applied after it is computed.
// OK, duplicate handle sample rate override here
// as an ugly little extra special case...
let fs = fs_override.unwrap_or(600e3);
Self {
name: "SXceiver".to_string(),
fs,
rx_ant: Some(cfg.rx_ant.clone().unwrap_or("RX".to_string())),
tx_ant: Some(cfg.tx_ant.clone().unwrap_or("TX".to_string())),
rx_ant: Some("RX".to_string()),
tx_ant: Some("TX".to_string()),
rx_gain: vec![
("LNA".to_string(), cfg.rx_gain_lna.unwrap_or(42.0)),
("PGA".to_string(), cfg.rx_gain_pga.unwrap_or(16.0)),
],
tx_gain: vec![
("DAC".to_string(), cfg.tx_gain_dac.unwrap_or(9.0)),
("MIXER".to_string(), cfg.tx_gain_mixer.unwrap_or(30.0)),
],
rx_gain: vec![("LNA".to_string(), 42.0), ("PGA".to_string(), 16.0)],
tx_gain: vec![("DAC".to_string(), 9.0), ("MIXER".to_string(), 30.0)],
rx_args: vec![("period".to_string(), block_size(fs).to_string())],
tx_args: vec![("period".to_string(), block_size(fs).to_string())],
@@ -215,11 +257,8 @@ impl SdrSettings {
}
}
fn settings_usrp(cfg: &Option<CfgUsrpB2xx>, mode: StackMode, model: UsrpModel) -> Self {
// If cfg is None, use default which sets all optional fields to None.
let cfg = if let Some(cfg) = cfg { &cfg } else { &CfgUsrpB2xx::default() };
SdrSettings {
fn settings_usrp(mode: StackMode, model: UsrpModel) -> Self {
Self {
name: match model {
UsrpModel::B200 => "USRP B200",
UsrpModel::B210 => "USRP B210",
@@ -227,24 +266,18 @@ impl SdrSettings {
}
.to_string(),
rx_ant: Some(cfg.rx_ant.clone().unwrap_or("TX/RX".to_string())),
tx_ant: Some(cfg.tx_ant.clone().unwrap_or("TX/RX".to_string())),
rx_ant: Some("TX/RX".to_string()),
tx_ant: Some("TX/RX".to_string()),
rx_gain: vec![("PGA".to_string(), cfg.rx_gain_pga.unwrap_or(50.0))],
tx_gain: vec![("PGA".to_string(), cfg.tx_gain_pga.unwrap_or(35.0))],
rx_args: vec![],
tx_args: vec![],
rx_gain: vec![("PGA".to_string(), 50.0)],
tx_gain: vec![("PGA".to_string(), 35.0)],
..Self::default(mode)
}
}
fn settings_pluto(cfg: &Option<CfgPluto>, mode: StackMode) -> Self {
// If cfg is None, use default which sets all optional fields to None.
let cfg = if let Some(cfg) = cfg { &cfg } else { &CfgPluto::default() };
SdrSettings {
fn settings_pluto(mode: StackMode) -> Self {
Self {
name: "Pluto".to_string(),
// get_hardware_time is apparently not implemented for pluto.
use_get_hardware_time: false,
@@ -253,38 +286,23 @@ impl SdrSettings {
// That would allow a power-of-two FFT size for lower CPU use.
fs: 1e6,
rx_ant: Some(cfg.rx_ant.clone().unwrap_or("A_BALANCED".to_string())),
tx_ant: Some(cfg.tx_ant.clone().unwrap_or("A".to_string())),
rx_ant: Some("A_BALANCED".to_string()),
tx_ant: Some("A".to_string()),
rx_gain: vec![("PGA".to_string(), cfg.rx_gain_pga.unwrap_or(20.0))],
tx_gain: vec![("PGA".to_string(), cfg.tx_gain_pga.unwrap_or(89.0))],
rx_gain: vec![("PGA".to_string(), 20.0)],
tx_gain: vec![("PGA".to_string(), 89.0)],
rx_args: vec![],
tx_args: vec![],
dev_args: vec![
("direct".to_string(), "1".to_string()),
("timestamp_every".to_string(), "1500".to_string()),
("loopback".to_string(), "0".to_string()),
],
..Self::default(mode)
}
}
}
#[derive(Debug, PartialEq)]
enum LimeSdrModel {
LimeSdrUsb,
LimeSdrMiniV2,
LimeNetMicro,
/// Other LimeSDR models with FX3 driver
OtherFx3,
/// Other LimeSDR models with FT601 driver
OtherFt601,
}
#[derive(Debug, PartialEq)]
enum UsrpModel {
B200,
B210,
Other,
}
/// Get processing block size in samples for a given sample rate.
/// This can be used to optimize performance for some SDRs.
pub fn block_size(fs: f64) -> usize {

View File

@@ -1,12 +1,11 @@
use soapysdr;
use tetra_config::bluestation::SharedConfig;
use tetra_config::bluestation::{SharedConfig, StackMode, sec_phy_soapy::CfgSoapySdr};
use tetra_config::bluestation::StackMode;
use tetra_pdus::phy::traits::rxtx_dev::RxTxDevError;
use super::dsp_types::*;
use super::soapy_settings;
use super::soapy_settings::SdrSettings;
use super::soapy_settings::{SdrSettings, SupportedDevice};
use super::soapy_time::{ticks_to_time_ns, time_ns_to_ticks};
type StreamType = ComplexSample;
@@ -29,6 +28,7 @@ pub struct SoapyIo {
/// so that sample counter startsB210 from 0 even if timestamp does not.
initial_time: Option<i64>,
rx_next_count: SampleCount,
prev_time_ns: i64,
/// If false, timestamp of latest RX read is used to estimate
/// current hardware time. This is used in case get_hardware_time
@@ -62,9 +62,6 @@ macro_rules! soapycheck {
impl SoapyIo {
pub fn new(cfg: &SharedConfig) -> Result<Self, soapysdr::Error> {
let rx_ch = 0;
let tx_ch = 0;
let binding = cfg.config();
let soapy_cfg = binding
.phy_io
@@ -72,11 +69,17 @@ impl SoapyIo {
.as_ref()
.expect("SoapySdr config must be set for SoapySdr PhyIo");
let mode = cfg.config().stack_mode;
let (dev, sdr_settings) = open_device(&soapy_cfg, mode)?;
let rx_ch = sdr_settings.rx_ch;
let tx_ch = sdr_settings.tx_ch;
// Get PPM corrected freqs
let (dl_corrected, _) = soapy_cfg.dl_freq_corrected();
let (ul_corrected, _) = soapy_cfg.ul_freq_corrected();
let mode = cfg.config().stack_mode;
let (rx_freq, tx_freq) = match mode {
StackMode::Bs => (
Some(ul_corrected - SOAPY_FREQ_OFFSET), // Offset RX center frequency from carrier frequency
@@ -91,32 +94,9 @@ impl SoapyIo {
}
};
let dev_args_str = soapy_settings::get_device_arguments(&soapy_cfg.io_cfg, mode);
tracing::info!("Using device arguments: {:?}", dev_args_str);
let mut dev_args = soapysdr::Args::new();
for (key, value) in dev_args_str {
dev_args.set(key, value);
}
let dev = soapycheck!("open SoapySDR device", soapysdr::Device::new(dev_args));
let rx_enabled = rx_freq.is_some();
let tx_enabled = tx_freq.is_some();
// Get default settings based on detected hardware
let driver_key = dev.driver_key().unwrap_or_default();
let hardware_key = dev.hardware_key().unwrap_or_default();
let sdr_settings = SdrSettings::get_settings(&soapy_cfg.io_cfg, &driver_key, &hardware_key, mode);
tracing::info!(
"Got driver key '{}' hardware_key '{}', using settings for {}",
driver_key,
hardware_key,
sdr_settings.name,
);
tracing::info!("Using: {:?}", sdr_settings);
let mut rx_fs: f64 = 0.0;
if rx_enabled {
soapycheck!(
@@ -207,11 +187,7 @@ impl SoapyIo {
tx_fs,
initial_time: None,
rx_next_count: 0,
// TODO: if SoapyRemote support is added back,
// always set use_get_hardware_time to false when SoapyRemote is used.
// The setting was originally added to deal with unacceptably slow
// get_hardware_time over SoapyRemote but turns out it is needed
// for some SDR devices as well, so it now a part of sdr_settings.
prev_time_ns: -1,
use_get_hardware_time: sdr_settings.use_get_hardware_time,
dev,
rx,
@@ -226,13 +202,26 @@ impl SoapyIo {
Ok(len) => {
// Get timestamp, set initial time if not yet set
let time = rx.time_ns();
if self.initial_time.is_none() {
// rust-soapysdr does not let us if a timestamp was available
// so we have to guess by checking whether it has changed from its previous value.
let timestamp_available = time != self.prev_time_ns;
self.prev_time_ns = time;
if self.initial_time.is_none() && timestamp_available {
self.initial_time = Some(time - ticks_to_time_ns(self.rx_next_count, self.rx_fs));
tracing::trace!("Set initial_time to {} ns", self.initial_time.unwrap());
};
// Re-compute total count from timestamp (gracefully handles lost samples).
let mut count = time_ns_to_ticks(time - self.initial_time.unwrap(), self.rx_fs);
let mut count = if timestamp_available {
time_ns_to_ticks(time - self.initial_time.unwrap(), self.rx_fs)
} else {
// If timestamp was not available,
// assume the read continues right after the previous read.
// Some drivers, particularly SoapyRemote,
// may provide a timestamp only in some of the reads.
self.rx_next_count
};
// Smooth tiny timestamp jitter (e.g. +/-1 sample) to keep counters monotonic
// This is known to happen for LimeSDR Mini v2 after some time
@@ -346,3 +335,142 @@ impl SoapyIo {
self.tx.is_some()
}
}
// Messy logic related to opening a device follows...
/// Struct to temporarily hold stuff related to opening and detecting a device
struct OpenedDevice {
dev_args: soapysdr::Args,
dev: soapysdr::Device,
driver_key: String,
hardware_key: String,
detected_device: SupportedDevice,
soapyremote_used: bool,
}
fn open_given_device(dev_args: soapysdr::Args) -> Result<OpenedDevice, soapysdr::Error> {
let soapyremote_used = match dev_args.get("driver") {
Some("remote") => true,
_ => false,
};
tracing::info!("Trying to open a device with arguments: {}", dev_args);
let dev_args_copy: soapysdr::Args = dev_args.iter().collect();
let dev = match soapysdr::Device::new(dev_args_copy) {
Ok(dev) => dev,
Err(err) => {
tracing::info!("Skipping a SoapySDR device because opening failed: {}", err);
return Err(err);
}
};
let driver_key = dev.driver_key().unwrap_or_default();
let hardware_key = dev.hardware_key().unwrap_or_default();
// Check whether the device is supported
if let Some(detected_device) = SupportedDevice::detect(&driver_key, &hardware_key) {
tracing::info!(
"Found supported device with driver_key '{}' hardware_key '{}'",
driver_key,
hardware_key
);
Ok(OpenedDevice {
dev_args,
dev,
driver_key,
hardware_key,
detected_device,
soapyremote_used,
})
} else {
tracing::info!(
"Skipping unsupported device with driver_key '{}' hardware_key '{}'",
driver_key,
hardware_key
);
Err(soapysdr::Error {
code: soapysdr::ErrorCode::NotSupported,
message: "Unsupported device".to_string(),
})
}
}
/// Enumerate devices and find the first supported device
fn find_supported_device(filter_args: soapysdr::Args) -> Result<OpenedDevice, soapysdr::Error> {
for dev_args in soapycheck!("Enumerate SoapySDR devices", soapysdr::enumerate(filter_args)) {
//tracing::info!("Trying to open a device with arguments: {}", args_formatted);
match open_given_device(dev_args) {
Ok(opened_device) => return Ok(opened_device),
Err(_) => {}
}
}
return Err(soapysdr::Error {
code: soapysdr::ErrorCode::NotSupported,
message: "No supported devices found".to_string(),
});
}
/// Open a given device if argument string is given,
/// automatically find the first supported device if not.
fn open_device(soapy_cfg: &CfgSoapySdr, mode: StackMode) -> Result<(soapysdr::Device, SdrSettings), soapysdr::Error> {
let mut opened_device = if let Some(arg_string) = &soapy_cfg.device {
open_given_device(arg_string.as_str().into())
} else {
find_supported_device(soapysdr::Args::new())
}?;
let mut sdr_settings = match SdrSettings::get_settings(&soapy_cfg, opened_device.detected_device, mode) {
Ok(sdr_settings) => sdr_settings,
Err(soapy_settings::Error::InvalidConfiguration) => {
return Err(soapysdr::Error {
code: soapysdr::ErrorCode::Other,
message: "Invalid SDR device configuration".to_string(),
});
}
};
if opened_device.soapyremote_used {
// Getting hardware time may be too slow over SoapyRemote
tracing::info!("SoapyRemote detected, forcing use_get_hardware_time=false");
sdr_settings.use_get_hardware_time = false;
}
tracing::info!("Using settings: {:?}", sdr_settings);
// If additional driver arguments are needed, reopen the device with them
if sdr_settings.dev_args.len() > 0 {
// Append additional arguments from settings
for (key, value) in &sdr_settings.dev_args {
opened_device.dev_args.set(key.as_str(), value.as_str());
}
tracing::info!("Reopening device with additional arguments: {}", opened_device.dev_args);
// Make sure device gets closed first. Not sure if needed.
std::mem::drop(opened_device.dev);
opened_device.dev = soapycheck!(
"open SoapySDR device with additional arguments",
soapysdr::Device::new(opened_device.dev_args)
);
// Make sure it is still the same device.
// Unlikely to change, but who knows if a device got connected just in between,
// or if the device broke from first opening attempt and something else got opened
// because device arguments were not precise enough to guarantee a specific device.
let new_driver_key = opened_device.dev.driver_key().unwrap_or_default();
let new_hardware_key = opened_device.dev.hardware_key().unwrap_or_default();
if new_driver_key != opened_device.driver_key || new_hardware_key != opened_device.hardware_key {
tracing::info!(
"Expected the same driver_key='{}' hardware_key='{}' after reopen, got driver_key='{}' hardware_key='{}'",
opened_device.driver_key,
opened_device.hardware_key,
new_driver_key,
new_hardware_key
);
return Err(soapysdr::Error {
code: soapysdr::ErrorCode::Other,
message: "Reopened a different device".to_string(),
});
}
}
Ok((opened_device.dev, sdr_settings))
}

View File

@@ -2,7 +2,7 @@
# This is an example configuration file for the TETRA base station stack
# DO NOT RUN without editing to stay within legal limits of your jurisdiction
config_version = "0.5"
config_version = "0.6"
# Stack operation mode: "Bs" (Base Station), "Ms" (Mobile Station), or "Mon" (Monitor)
stack_mode = "Bs"
@@ -28,55 +28,36 @@ backend = "SoapySdr"
# !!! Make sure to also edit all related fields in the cell_info section to fit this frequency.
tx_freq = 438025000
rx_freq = 433025000
ppm_err = 0.0 # Adjust if your SDR has a non-negligible tuning error
# Sane defaults for LimeSDR
# [phy_io.soapysdr.iocfg_limesdr]
# rx_ant = "LNAL"
# tx_ant = "BAND1"
# rx_gain_lna = 18.0
# rx_gain_tia = 6.0
# rx_gain_pga = 10.0
# tx_gain_pad = 22.0
# tx_gain_iamp = 3.0
# Adjust if your SDR has a non-negligible tuning error
# ppm_err = 0.0
# Sane defaults for LimeSDR Mini v2
# [phy_io.soapysdr.iocfg_limesdr]
# rx_ant = "LNAW"
# tx_ant = "BAND2"
# rx_gain_lna = 18.0
# rx_gain_tia = 6.0
# rx_gain_pga = 10.0
# tx_gain_pad = 22.0
# tx_gain_iamp = 3.0
################################################################################################
## OPTIONAL: specific device, antenna, gain selection ##
## Make sure to check your device for valid settings using SoapySDRUtil --probe ##
## or check: https://github.com/MidnightBlueLabs/tetra-bluestation-docs/wiki/03-Configuration ##
################################################################################################
# Sane defaults for SXceiver
# [phy_io.soapysdr.iocfg_sxceiver]
# rx_ant = "RX"
# tx_ant = "TX"
# rx_gain_lna = 42.0
# rx_gain_pga = 16.0
# tx_gain_dac = 9.0
# tx_gain_mixer = 30.0
# Optional device selection arguments.
# If not specified, the first supported device found is selected automatically.
# You may need to specify this if you have multiple supported device connected,
# or if your device cannot be automatically detected by enumeration.
# For example, to use a Pluto+ with a given IP address:
# device = "driver=plutosdr,uri=ip:192.168.42.42"
# To select a LimeSDR with a given serial number (check with SoapySDRUtil --find):
# device = "driver=lime,serial=123456789"
# Sane defaults for Original Adalm Pluto and Pluto+
# !!! See Wiki for full parameter breakdown and setup requirements.
# [phy_io.soapysdr.iocfg_pluto]
# rx_ant = "A_BALANCED"
# tx_ant = "A"
# rx_gain_pga = 20.0
# tx_gain_pga = 89.0
# uri = "ip:192.168.42.42" # Pluto+ only, do not set for original Pluto. Other options: "usb:1.3.5", or hostname.
# direct = true #Advanced driver parameter. Do not change unless really needed.
# timestamp_every = 1500 #Advanced driver parameter. Do not change unless really needed.
# loopback = false #Advanced driver parameter. Do not change unless really needed.
# Optional antenna selection to override device-specific defaults
# Check antenna names with SoapySDRUtil --probe
# rx_antenna = "LNAW"
# tx_antenna = "BAND2"
# Sane defaults for Ettus USRP B210 (and probably B200)
[phy_io.soapysdr.iocfg_usrpb2xx]
rx_ant = "TX/RX"
tx_ant = "TX/RX"
rx_gain_pga = 50.0
tx_gain_pga = 35.0
# Optional gain values to override device-specific defaults.
# Check valid gain names with SoapySDRUtil --probe
# For example, to increase transmit power on a LimeSDR:
# tx_gain_pad = 50.0
# To adjust LNA gain to optimize RX performance on a LimeSDR or SXceiver:
# rx_gain_lna = 30.0
###############################################################################