mirror of
https://github.com/MidnightBlueLabs/tetra-bluestation.git
synced 2026-03-29 05:09:51 +00:00
Merge branch 'sdr_autoselect'
This commit is contained in:
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -17,7 +17,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.5"
|
||||
version = "0.5.6"
|
||||
edition = "2024"
|
||||
authors = ["Wouter Bokslag / Midnight Blue"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
||||
Reference in New Issue
Block a user