#![doc = include_str!("../readme.md")]
pub use error::{Error, Result};
use futures::Stream;
pub use metrics::{RfMetrics, RfMetricsCalculator, iq_level_dbfs};
use num_complex::Complex;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::pin::Pin;
use std::task::{Context, Poll};
#[cfg(feature = "airspy")]
pub mod airspy;
pub mod dsp;
pub mod error;
#[cfg(feature = "hackrf")]
pub mod hackrf;
pub mod iqread;
pub mod metrics;
#[cfg(feature = "pluto")]
pub mod pluto;
#[cfg(feature = "rtlsdr")]
pub mod rtlsdr;
#[cfg(feature = "soapy")]
pub mod soapy;
pub fn expanduser(path: PathBuf) -> PathBuf {
if let Some(stripped) = path.to_str().and_then(|p| p.strip_prefix("~"))
&& let Some(home_dir) = dirs::home_dir()
{
return home_dir.join(stripped.trim_start_matches('/'));
}
path
}
pub fn parse_si_value<T>(s: &str) -> Result<T>
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Display,
{
let s = s.trim();
let (num_str, multiplier) = if let Some(stripped) = s.strip_suffix('G') {
(stripped, 1_000_000_000.0)
} else if let Some(stripped) = s.strip_suffix('M') {
(stripped, 1_000_000.0)
} else if let Some(stripped) = s.strip_suffix(['k', 'K']) {
(stripped, 1_000.0)
} else {
return s
.parse()
.map_err(|e| Error::other(format!("Invalid numeric value '{}': {}", s, e)));
};
let value_f64: f64 = num_str
.parse()
.map_err(|e| Error::other(format!("Invalid numeric value '{}': {}", num_str, e)))?;
let result = value_f64 * multiplier;
let result_str = format!("{:.0}", result);
result_str.parse().map_err(|e| {
Error::other(format!(
"Failed to convert '{}' to target type: {}",
result_str, e
))
})
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum Gain {
Auto,
Manual(f64),
Elements(Vec<GainElement>),
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct GainElement {
pub name: GainElementName,
pub value_db: f64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub enum GainElementName {
Tuner,
Lna,
Mix,
Vga,
Pga,
Custom(String),
}
impl<'de> Deserialize<'de> for Gain {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, MapAccess, Visitor};
use std::fmt;
struct GainVisitor;
impl<'de> Visitor<'de> for GainVisitor {
type Value = Gain;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(r#""auto", a number (e.g., 49.6), or a map of gain elements (e.g., { LNA = 5, VGA = 5 })"#)
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Gain, E>
where
E: de::Error,
{
if value.eq_ignore_ascii_case("auto") {
Ok(Gain::Auto)
} else {
Err(de::Error::custom(format!(
"expected \"auto\", got \"{}\"",
value
)))
}
}
fn visit_i64<E>(self, value: i64) -> std::result::Result<Gain, E>
where
E: de::Error,
{
Ok(Gain::Manual(value as f64))
}
fn visit_u64<E>(self, value: u64) -> std::result::Result<Gain, E>
where
E: de::Error,
{
Ok(Gain::Manual(value as f64))
}
fn visit_f64<E>(self, value: f64) -> std::result::Result<Gain, E>
where
E: de::Error,
{
Ok(Gain::Manual(value))
}
fn visit_map<M>(self, mut map: M) -> std::result::Result<Gain, M::Error>
where
M: MapAccess<'de>,
{
let mut elements = Vec::new();
while let Some(key) = map.next_key::<String>()? {
let value: f64 = map.next_value()?;
let name = match key.to_uppercase().as_str() {
"TUNER" => GainElementName::Tuner,
"LNA" => GainElementName::Lna,
"MIX" => GainElementName::Mix,
"VGA" => GainElementName::Vga,
"PGA" => GainElementName::Pga,
custom => GainElementName::Custom(custom.to_string()),
};
elements.push(GainElement {
name,
value_db: value,
});
}
if elements.is_empty() {
Err(de::Error::custom("gain elements map cannot be empty"))
} else {
Ok(Gain::Elements(elements))
}
}
}
deserializer.deserialize_any(GainVisitor)
}
}
impl Gain {
pub fn parse(input: &str) -> error::Result<Self> {
let input = input.trim();
if input.eq_ignore_ascii_case("auto") {
return Ok(Gain::Auto);
}
if let Ok(value) = input.parse::<f64>() {
return Ok(Gain::Manual(value));
}
let mut elements = Vec::new();
for part in input.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
let (name, value) = part.split_once('=').ok_or_else(|| {
Error::other(format!(
"Invalid gain element '{}', expected format NAME=VALUE",
part
))
})?;
let value_db = value.trim().parse::<f64>().map_err(|_| {
Error::other(format!("Invalid gain value '{}', expected a number", value))
})?;
let name = match name.trim().to_ascii_uppercase().as_str() {
"TUNER" => GainElementName::Tuner,
"LNA" => GainElementName::Lna,
"MIX" => GainElementName::Mix,
"VGA" => GainElementName::Vga,
"PGA" => GainElementName::Pga,
other => GainElementName::Custom(other.to_string()),
};
elements.push(GainElement { name, value_db });
}
if elements.is_empty() {
return Err(Error::other(format!("Invalid gain setting: '{}'", input)));
}
Ok(Gain::Elements(elements))
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum IqFormat {
Cu8,
Cs8,
Cs16,
Cf32,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DeviceConfig {
#[cfg(feature = "pluto")]
Pluto(pluto::PlutoConfig),
#[cfg(feature = "rtlsdr")]
RtlSdr(rtlsdr::RtlSdrConfig),
#[cfg(feature = "soapy")]
Soapy(soapy::SoapyConfig),
#[cfg(feature = "airspy")]
Airspy(airspy::AirspyConfig),
#[cfg(feature = "hackrf")]
HackRf(hackrf::HackRfConfig),
}
impl std::str::FromStr for DeviceConfig {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let parts: Vec<&str> = s.splitn(2, "://").collect();
if parts.len() != 2 {
return Err(Error::other(
"Invalid device URL: missing '://' separator".to_string(),
));
}
let scheme = parts[0];
#[allow(unused_variables)]
let rest = parts[1];
match scheme {
#[cfg(feature = "rtlsdr")]
"rtlsdr" => {
let (device_part, query) = if let Some(q_pos) = rest.find('?') {
(&rest[..q_pos], &rest[q_pos + 1..])
} else {
(rest, "")
};
let device_index = if device_part.is_empty() {
0
} else {
device_part.parse::<usize>().map_err(|_| {
Error::other(format!("Invalid device index: {}", device_part))
})?
};
let mut center_freq: Option<u32> = None;
let mut sample_rate: Option<u32> = None;
let mut gain = Gain::Auto;
let mut bias_tee = false;
let mut ppm: i32 = 0;
for param in query.split('&') {
if param.is_empty() {
continue;
}
let kv: Vec<&str> = param.splitn(2, '=').collect();
if kv.len() != 2 {
continue;
}
match kv[0] {
"freq" | "frequency" => {
center_freq = Some(parse_si_value(kv[1])?);
}
"rate" | "sample_rate" => {
sample_rate = Some(parse_si_value(kv[1])?);
}
"gain" => {
if kv[1].to_lowercase() == "auto" {
gain = Gain::Auto;
} else {
let gain_db: f64 = kv[1].parse().map_err(|_| {
Error::other(format!("Invalid gain: {}", kv[1]))
})?;
gain = Gain::Manual(gain_db);
}
}
"bias_tee" | "bias-tee" => {
bias_tee = kv[1].to_lowercase() == "true" || kv[1] == "1";
}
"ppm" | "freq_correction" | "freq-correction" => {
ppm = kv[1]
.parse::<i32>()
.map_err(|_| Error::other(format!("Invalid ppm: {}", kv[1])))?;
}
_ => {} }
}
let center_freq = center_freq
.ok_or_else(|| Error::other("Missing freq parameter".to_string()))?;
let sample_rate = sample_rate
.ok_or_else(|| Error::other("Missing rate parameter".to_string()))?;
Ok(DeviceConfig::RtlSdr(rtlsdr::RtlSdrConfig {
device: rtlsdr::DeviceSelector::Index(device_index),
center_freq,
sample_rate,
gain,
bias_tee,
freq_correction_ppm: ppm,
}))
}
#[cfg(feature = "soapy")]
"soapy" => {
let (args_part, query) = if let Some(q_pos) = rest.find('?') {
(&rest[..q_pos], &rest[q_pos + 1..])
} else {
(rest, "")
};
let args = args_part.to_string();
let mut center_freq: Option<f64> = None;
let mut sample_rate: Option<f64> = None;
let mut gain = Gain::Auto;
let channel = 0;
let mut bias_tee = false;
for param in query.split('&') {
if param.is_empty() {
continue;
}
let kv: Vec<&str> = param.splitn(2, '=').collect();
if kv.len() != 2 {
continue;
}
match kv[0] {
"freq" | "frequency" => {
center_freq = Some(parse_si_value(kv[1])?);
}
"rate" | "sample_rate" => {
sample_rate = Some(parse_si_value(kv[1])?);
}
"gain" => {
gain = Gain::parse(kv[1])?;
}
"bias_tee" | "bias-tee" => {
bias_tee = kv[1].to_lowercase() == "true" || kv[1] == "1";
}
_ => {} }
}
let center_freq = center_freq
.ok_or_else(|| Error::other("Missing freq parameter".to_string()))?;
let sample_rate = sample_rate
.ok_or_else(|| Error::other("Missing rate parameter".to_string()))?;
Ok(DeviceConfig::Soapy(soapy::SoapyConfig {
args,
center_freq,
sample_rate,
channel,
gain,
bias_tee,
}))
}
#[cfg(feature = "pluto")]
"pluto" => {
let rest_trimmed = rest.strip_prefix('/').unwrap_or(rest);
let (uri_part, query) = if let Some(q_pos) = rest_trimmed.find('?') {
(&rest_trimmed[..q_pos], &rest_trimmed[q_pos + 1..])
} else {
(rest_trimmed, "")
};
let uri = uri_part.to_string();
let mut center_freq: Option<i64> = None;
let mut sample_rate: Option<i64> = None;
let mut gain = Gain::Manual(40.0);
for param in query.split('&') {
if param.is_empty() {
continue;
}
let kv: Vec<&str> = param.splitn(2, '=').collect();
if kv.len() != 2 {
continue;
}
match kv[0] {
"freq" | "frequency" => {
center_freq = Some(parse_si_value(kv[1])?);
}
"rate" | "sample_rate" => {
sample_rate = Some(parse_si_value(kv[1])?);
}
"gain" => {
if kv[1].to_lowercase() == "auto" {
gain = Gain::Auto;
} else {
let gain_db: f64 = kv[1].parse().map_err(|_| {
Error::other(format!("Invalid gain: {}", kv[1]))
})?;
gain = Gain::Manual(gain_db);
}
}
_ => {} }
}
let center_freq = center_freq
.ok_or_else(|| Error::other("Missing freq parameter".to_string()))?;
let sample_rate = sample_rate
.ok_or_else(|| Error::other("Missing rate parameter".to_string()))?;
Ok(DeviceConfig::Pluto(pluto::PlutoConfig {
uri,
center_freq,
sample_rate,
gain,
}))
}
#[cfg(feature = "airspy")]
"airspy" => {
let (device_part, query) = if let Some(q_pos) = rest.find('?') {
(&rest[..q_pos], &rest[q_pos + 1..])
} else {
(rest, "")
};
let device_index = if device_part.is_empty() {
0
} else {
device_part.parse::<usize>().map_err(|_| {
Error::other(format!("Invalid airspy device index: {}", device_part))
})?
};
let mut center_freq: Option<u32> = None;
let mut sample_rate: Option<u32> = None;
let mut gain = Gain::Auto;
let mut lna_gain: Option<u8> = None;
let mut mixer_gain: Option<u8> = None;
let mut vga_gain: Option<u8> = None;
let mut gain_mode = airspy::AirspyGainMode::default();
for param in query.split('&') {
if param.is_empty() {
continue;
}
let kv: Vec<&str> = param.splitn(2, '=').collect();
if kv.len() != 2 {
continue;
}
match kv[0] {
"freq" | "frequency" => {
center_freq = Some(parse_si_value(kv[1])?);
}
"rate" | "sample_rate" => {
sample_rate = Some(parse_si_value(kv[1])?);
}
"gain" => {
if kv[1].to_lowercase() == "auto" {
gain = Gain::Auto;
} else {
let gain_db: f64 = kv[1].parse().map_err(|_| {
Error::other(format!("Invalid gain: {}", kv[1]))
})?;
gain = Gain::Manual(gain_db);
}
}
"lna" | "lna_gain" => {
let v: u8 = kv[1].parse().map_err(|_| {
Error::other(format!("Invalid lna gain: {}", kv[1]))
})?;
lna_gain = Some(v);
}
"mix" | "mixer" | "mixer_gain" => {
let v: u8 = kv[1].parse().map_err(|_| {
Error::other(format!("Invalid mixer gain: {}", kv[1]))
})?;
mixer_gain = Some(v);
}
"if" | "if_gain" | "vga" | "vga_gain" => {
let v: u8 = kv[1].parse().map_err(|_| {
Error::other(format!("Invalid IF/VGA gain: {}", kv[1]))
})?;
vga_gain = Some(v);
}
"gain_mode" | "gain-mode" => match kv[1].to_lowercase().as_str() {
"linearity" | "linear" => {
gain_mode = airspy::AirspyGainMode::Linearity;
}
"sensitivity" | "sensitive" => {
gain_mode = airspy::AirspyGainMode::Sensitivity;
}
_ => {
return Err(Error::other(format!(
"Invalid gain_mode '{}': expected 'linearity' or 'sensitivity'",
kv[1]
)));
}
},
_ => {} }
}
let center_freq = center_freq
.ok_or_else(|| Error::other("Missing freq parameter".to_string()))?;
let sample_rate = sample_rate
.ok_or_else(|| Error::other("Missing rate parameter".to_string()))?;
let mut cfg =
airspy::AirspyConfig::new(device_index, center_freq, sample_rate, gain);
cfg.lna_gain = lna_gain;
cfg.mixer_gain = mixer_gain;
cfg.vga_gain = vga_gain;
cfg.gain_mode = gain_mode;
Ok(DeviceConfig::Airspy(cfg))
}
#[cfg(feature = "hackrf")]
"hackrf" => {
let (device_part, query) = if let Some(q_pos) = rest.find('?') {
(&rest[..q_pos], &rest[q_pos + 1..])
} else {
(rest, "")
};
let device_index = if device_part.is_empty() {
0
} else {
device_part.parse::<usize>().map_err(|_| {
Error::other(format!("Invalid hackrf device index: {}", device_part))
})?
};
let mut center_freq: Option<u64> = None;
let mut sample_rate: Option<u32> = None;
let mut gain = Gain::Auto;
let mut amp_enable = false;
let mut bias_tee = false;
for param in query.split('&') {
if param.is_empty() {
continue;
}
let kv: Vec<&str> = param.splitn(2, '=').collect();
if kv.len() != 2 {
continue;
}
match kv[0] {
"freq" | "frequency" => {
center_freq = Some(parse_si_value(kv[1])?);
}
"rate" | "sample_rate" => {
sample_rate = Some(parse_si_value(kv[1])?);
}
"gain" => {
gain = Gain::parse(kv[1])?;
}
"amp" | "amp_enable" => {
amp_enable = kv[1].to_lowercase() == "true" || kv[1] == "1";
}
"bias_tee" | "bias-tee" => {
bias_tee = kv[1].to_lowercase() == "true" || kv[1] == "1";
}
_ => {} }
}
let center_freq = center_freq
.ok_or_else(|| Error::other("Missing freq parameter".to_string()))?;
let sample_rate = sample_rate
.ok_or_else(|| Error::other("Missing rate parameter".to_string()))?;
Ok(DeviceConfig::HackRf(hackrf::HackRfConfig {
device_index,
center_freq,
sample_rate,
gain,
amp_enable,
bias_tee,
}))
}
_ => Err(Error::other(format!("Unknown device scheme: {}", scheme))),
}
}
}
impl std::fmt::Display for IqFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IqFormat::Cu8 => write!(f, "cu8"),
IqFormat::Cs8 => write!(f, "cs8"),
IqFormat::Cs16 => write!(f, "cs16"),
IqFormat::Cf32 => write!(f, "cf32"),
}
}
}
impl std::str::FromStr for IqFormat {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"cu8" => Ok(IqFormat::Cu8),
"cs8" => Ok(IqFormat::Cs8),
"cs16" => Ok(IqFormat::Cs16),
"cf32" => Ok(IqFormat::Cf32),
_ => Err(Error::format(format!("Invalid IQ format: '{}'", s))),
}
}
}
pub enum IqSource {
IqFile(iqread::IqRead<std::io::BufReader<std::fs::File>>),
IqStdin(iqread::IqRead<std::io::BufReader<std::io::Stdin>>),
IqTcp(iqread::IqRead<std::io::BufReader<std::net::TcpStream>>),
#[cfg(feature = "pluto")]
PlutoSdr(pluto::PlutoSdrReader),
#[cfg(feature = "rtlsdr")]
RtlSdr(rtlsdr::RtlSdrReader),
#[cfg(feature = "soapy")]
SoapySdr(soapy::SoapySdrReader),
#[cfg(feature = "airspy")]
Airspy(airspy::AirspySdrReader),
#[cfg(feature = "hackrf")]
HackRf(hackrf::HackRfReader),
}
impl Iterator for IqSource {
type Item = error::Result<Vec<Complex<f32>>>;
fn next(&mut self) -> Option<Self::Item> {
match self {
IqSource::IqFile(source) => source.next(),
IqSource::IqStdin(source) => source.next(),
IqSource::IqTcp(source) => source.next(),
#[cfg(feature = "pluto")]
IqSource::PlutoSdr(source) => source.next(),
#[cfg(feature = "rtlsdr")]
IqSource::RtlSdr(source) => source.next(),
#[cfg(feature = "soapy")]
IqSource::SoapySdr(source) => source.next(),
#[cfg(feature = "airspy")]
IqSource::Airspy(source) => source.next(),
#[cfg(feature = "hackrf")]
IqSource::HackRf(source) => source.next(),
}
}
}
impl IqSource {
pub fn from_file<P: AsRef<std::path::Path>>(
path: P,
center_freq: u32,
sample_rate: u32,
chunk_size: usize,
iq_format: IqFormat,
) -> error::Result<Self> {
let source =
iqread::IqRead::from_file(path, center_freq, sample_rate, chunk_size, iq_format)?;
Ok(IqSource::IqFile(source))
}
pub fn from_stdin(
center_freq: u32,
sample_rate: u32,
chunk_size: usize,
iq_format: IqFormat,
) -> error::Result<IqSource> {
let source = iqread::IqRead::from_stdin(center_freq, sample_rate, chunk_size, iq_format);
Ok(IqSource::IqStdin(source))
}
pub fn from_tcp(
addr: &str,
port: u16,
center_freq: u32,
sample_rate: u32,
chunk_size: usize,
iq_format: IqFormat,
) -> error::Result<Self> {
let source =
iqread::IqRead::from_tcp(addr, port, center_freq, sample_rate, chunk_size, iq_format)?;
Ok(IqSource::IqTcp(source))
}
pub fn from_device_config(config: DeviceConfig) -> error::Result<Self> {
match config {
#[cfg(feature = "pluto")]
DeviceConfig::Pluto(cfg) => {
let source = pluto::PlutoSdrReader::new(&cfg)?;
Ok(IqSource::PlutoSdr(source))
}
#[cfg(feature = "rtlsdr")]
DeviceConfig::RtlSdr(cfg) => {
let source = rtlsdr::RtlSdrReader::new(&cfg)?;
Ok(IqSource::RtlSdr(source))
}
#[cfg(feature = "soapy")]
DeviceConfig::Soapy(cfg) => {
let source = soapy::SoapySdrReader::new(&cfg)?;
Ok(IqSource::SoapySdr(source))
}
#[cfg(feature = "airspy")]
DeviceConfig::Airspy(cfg) => {
let source = airspy::AirspySdrReader::new(&cfg)?;
Ok(IqSource::Airspy(source))
}
#[cfg(feature = "hackrf")]
DeviceConfig::HackRf(cfg) => {
let source = hackrf::HackRfReader::new(&cfg)?;
Ok(IqSource::HackRf(source))
}
}
}
#[cfg(feature = "pluto")]
pub fn from_pluto(
uri: &str,
center_freq: i64,
sample_rate: i64,
gain: f64,
) -> error::Result<Self> {
let config = pluto::PlutoConfig {
uri: uri.to_string(),
center_freq,
sample_rate,
gain: Gain::Manual(gain),
};
let source = pluto::PlutoSdrReader::new(&config)?;
Ok(IqSource::PlutoSdr(source))
}
#[cfg(feature = "rtlsdr")]
pub fn from_rtlsdr(
device_index: usize,
center_freq: u32,
sample_rate: u32,
gain: Option<i32>,
) -> error::Result<Self> {
let config = rtlsdr::RtlSdrConfig {
device: rtlsdr::DeviceSelector::Index(device_index),
center_freq,
sample_rate,
gain: match gain {
Some(g) => Gain::Manual((g as f64) / 10.0),
None => Gain::Auto,
},
bias_tee: false,
freq_correction_ppm: 0,
};
let source = rtlsdr::RtlSdrReader::new(&config)?;
Ok(IqSource::RtlSdr(source))
}
#[cfg(feature = "soapy")]
pub fn from_soapy(
args: &str,
channel: usize,
center_freq: u32,
sample_rate: u32,
gain: Gain,
) -> error::Result<Self> {
let config = soapy::SoapyConfig {
args: args.to_string(),
center_freq: center_freq as f64,
sample_rate: sample_rate as f64,
channel,
gain,
bias_tee: false,
};
let source = soapy::SoapySdrReader::new(&config)?;
Ok(IqSource::SoapySdr(source))
}
#[cfg(feature = "airspy")]
pub fn from_airspy(
device_index: usize,
center_freq: u32,
sample_rate: u32,
gain: Gain,
) -> error::Result<Self> {
let config = airspy::AirspyConfig::new(device_index, center_freq, sample_rate, gain);
let source = airspy::AirspySdrReader::new(&config)?;
Ok(IqSource::Airspy(source))
}
}
pub enum IqAsyncSource {
IqAsyncFile(iqread::IqAsyncRead<tokio::io::BufReader<tokio::fs::File>>),
IqAsyncStdin(iqread::IqAsyncRead<tokio::io::BufReader<tokio::io::Stdin>>),
IqAsyncTcp(iqread::IqAsyncRead<tokio::io::BufReader<tokio::net::TcpStream>>),
#[cfg(feature = "pluto")]
PlutoSdr(pluto::AsyncPlutoSdrReader),
#[cfg(feature = "rtlsdr")]
RtlSdr(rtlsdr::AsyncRtlSdrReader),
#[cfg(feature = "soapy")]
SoapySdr(soapy::AsyncSoapySdrReader),
#[cfg(feature = "airspy")]
Airspy(airspy::AsyncAirspySdrReader),
#[cfg(feature = "hackrf")]
HackRf(hackrf::AsyncHackRfReader),
}
impl IqAsyncSource {
pub fn tune(&self, _center_freq: u32) -> error::Result<()> {
match self {
#[cfg(feature = "rtlsdr")]
IqAsyncSource::RtlSdr(source) => source.tune(_center_freq),
#[cfg(feature = "airspy")]
IqAsyncSource::Airspy(source) => source.tune(_center_freq),
#[cfg(feature = "hackrf")]
IqAsyncSource::HackRf(source) => source.tune(_center_freq as u64),
_ => Err(error::Error::other(
"Retune is only supported for RTL-SDR/Airspy/HackRF async sources".to_string(),
)),
}
}
pub fn set_gain(&self, _gain: Gain) -> error::Result<()> {
match self {
#[cfg(feature = "rtlsdr")]
IqAsyncSource::RtlSdr(source) => {
source.adjust(crate::rtlsdr::RtlSdrMessage::Gain(_gain))
}
#[cfg(feature = "airspy")]
IqAsyncSource::Airspy(source) => {
source.adjust(crate::airspy::AirspyMessage::Gain(_gain))
}
#[cfg(feature = "hackrf")]
IqAsyncSource::HackRf(source) => source.set_gain(_gain),
_ => Err(error::Error::other(
"Runtime gain adjustment is not supported for this source type",
)),
}
}
pub async fn from_file<P: AsRef<std::path::Path>>(
path: P,
center_freq: u32,
sample_rate: u32,
chunk_size: usize,
iq_format: IqFormat,
) -> error::Result<Self> {
let source =
iqread::IqAsyncRead::from_file(path, center_freq, sample_rate, chunk_size, iq_format)
.await?;
Ok(IqAsyncSource::IqAsyncFile(source))
}
pub fn from_stdin(
center_freq: u32,
sample_rate: u32,
chunk_size: usize,
iq_format: IqFormat,
) -> Self {
let source =
iqread::IqAsyncRead::from_stdin(center_freq, sample_rate, chunk_size, iq_format);
IqAsyncSource::IqAsyncStdin(source)
}
pub async fn from_tcp(
addr: &str,
port: u16,
center_freq: u32,
sample_rate: u32,
chunk_size: usize,
iq_format: IqFormat,
) -> error::Result<Self> {
let source = iqread::IqAsyncRead::from_tcp(
addr,
port,
center_freq,
sample_rate,
chunk_size,
iq_format,
)
.await?;
Ok(IqAsyncSource::IqAsyncTcp(source))
}
#[cfg(feature = "pluto")]
pub async fn from_pluto(
uri: &str,
center_freq: i64,
sample_rate: i64,
gain: f64,
) -> error::Result<Self> {
let config = pluto::PlutoConfig {
uri: uri.to_string(),
center_freq,
sample_rate,
gain: Gain::Manual(gain),
};
let source = pluto::AsyncPlutoSdrReader::new(&config).await?;
Ok(IqAsyncSource::PlutoSdr(source))
}
#[cfg(feature = "rtlsdr")]
pub async fn from_rtlsdr(
device_index: usize,
center_freq: u32,
sample_rate: u32,
gain: Option<i32>,
) -> error::Result<Self> {
let config = rtlsdr::RtlSdrConfig {
device: rtlsdr::DeviceSelector::Index(device_index),
center_freq,
sample_rate,
gain: match gain {
Some(g) => Gain::Manual((g as f64) / 10.0),
None => Gain::Auto,
},
bias_tee: false,
freq_correction_ppm: 0,
};
let async_reader = rtlsdr::AsyncRtlSdrReader::new(&config)?;
Ok(IqAsyncSource::RtlSdr(async_reader))
}
#[cfg(feature = "soapy")]
pub async fn from_soapy(
args: &str,
channel: usize,
center_freq: u32,
sample_rate: u32,
gain: Gain,
) -> error::Result<Self> {
let config = soapy::SoapyConfig {
args: args.to_string(),
center_freq: center_freq as f64,
sample_rate: sample_rate as f64,
channel,
gain,
bias_tee: false,
};
let async_reader = soapy::AsyncSoapySdrReader::new(&config)?;
Ok(IqAsyncSource::SoapySdr(async_reader))
}
#[cfg(feature = "airspy")]
pub async fn from_airspy(
device_index: Option<usize>,
center_freq: u32,
sample_rate: u32,
gain: Gain,
lna_gain: Option<u8>,
mixer_gain: Option<u8>,
vga_gain: Option<u8>,
) -> error::Result<Self> {
let config = airspy::AirspyConfig {
device: match device_index {
Some(idx) => airspy::DeviceSelector::Index(idx),
None => airspy::DeviceSelector::Index(0),
},
center_freq,
sample_rate,
gain,
bias_tee: false,
packing: false,
lna_gain,
mixer_gain,
vga_gain,
gain_mode: airspy::AirspyGainMode::default(),
};
let async_reader = airspy::AsyncAirspySdrReader::new(&config)?;
Ok(IqAsyncSource::Airspy(async_reader))
}
#[cfg(any(
feature = "rtlsdr",
feature = "pluto",
feature = "soapy",
feature = "airspy",
feature = "hackrf"
))]
pub async fn from_device_config(config: &DeviceConfig) -> error::Result<Self> {
match config {
#[cfg(feature = "rtlsdr")]
DeviceConfig::RtlSdr(cfg) => {
let async_reader = rtlsdr::AsyncRtlSdrReader::new(cfg)?;
Ok(IqAsyncSource::RtlSdr(async_reader))
}
#[cfg(feature = "pluto")]
DeviceConfig::Pluto(cfg) => {
let async_reader = pluto::AsyncPlutoSdrReader::new(cfg).await?;
Ok(IqAsyncSource::PlutoSdr(async_reader))
}
#[cfg(feature = "soapy")]
DeviceConfig::Soapy(cfg) => {
let async_reader = soapy::AsyncSoapySdrReader::new(cfg)?;
Ok(IqAsyncSource::SoapySdr(async_reader))
}
#[cfg(feature = "airspy")]
DeviceConfig::Airspy(cfg) => {
let async_reader = airspy::AsyncAirspySdrReader::new(cfg)?;
Ok(IqAsyncSource::Airspy(async_reader))
}
#[cfg(feature = "hackrf")]
DeviceConfig::HackRf(cfg) => {
let async_reader = hackrf::AsyncHackRfReader::new(cfg)?;
Ok(IqAsyncSource::HackRf(async_reader))
}
}
}
}
impl Stream for IqAsyncSource {
type Item = error::Result<Vec<Complex<f32>>>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.get_mut() {
IqAsyncSource::IqAsyncFile(source) => Pin::new(source).poll_next(cx),
IqAsyncSource::IqAsyncStdin(source) => Pin::new(source).poll_next(cx),
IqAsyncSource::IqAsyncTcp(source) => Pin::new(source).poll_next(cx),
#[cfg(feature = "pluto")]
IqAsyncSource::PlutoSdr(source) => Pin::new(source).poll_next(cx),
#[cfg(feature = "rtlsdr")]
IqAsyncSource::RtlSdr(source) => Pin::new(source).poll_next(cx),
#[cfg(feature = "soapy")]
IqAsyncSource::SoapySdr(source) => Pin::new(source).poll_next(cx),
#[cfg(feature = "airspy")]
IqAsyncSource::Airspy(source) => Pin::new(source).poll_next(cx),
#[cfg(feature = "hackrf")]
IqAsyncSource::HackRf(source) => Pin::new(source).poll_next(cx),
}
}
}
fn convert_bytes_to_complex(format: IqFormat, buffer: &[u8]) -> Vec<Complex<f32>> {
match format {
IqFormat::Cu8 => buffer
.chunks_exact(2)
.map(|c| Complex::new((c[0] as f32 - 127.5) / 128.0, (c[1] as f32 - 127.5) / 128.0))
.collect(),
IqFormat::Cs8 => buffer
.chunks_exact(2)
.map(|c| Complex::new((c[0] as i8) as f32 / 128.0, (c[1] as i8) as f32 / 128.0))
.collect(),
IqFormat::Cs16 => buffer
.chunks_exact(4)
.map(|c| {
Complex::new(
i16::from_le_bytes([c[0], c[1]]) as f32 / 32768.0,
i16::from_le_bytes([c[2], c[3]]) as f32 / 32768.0,
)
})
.collect(),
IqFormat::Cf32 => buffer
.chunks_exact(8)
.map(|c| {
Complex::new(
f32::from_le_bytes([c[0], c[1], c[2], c[3]]),
f32::from_le_bytes([c[4], c[5], c[6], c[7]]),
)
})
.collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_iqformat_display() {
assert_eq!(IqFormat::Cu8.to_string(), "cu8");
assert_eq!(IqFormat::Cs8.to_string(), "cs8");
assert_eq!(IqFormat::Cs16.to_string(), "cs16");
assert_eq!(IqFormat::Cf32.to_string(), "cf32");
}
#[test]
fn test_iqformat_fromstr() {
assert_eq!("cu8".parse::<IqFormat>().unwrap(), IqFormat::Cu8);
assert_eq!("cs8".parse::<IqFormat>().unwrap(), IqFormat::Cs8);
assert_eq!("cs16".parse::<IqFormat>().unwrap(), IqFormat::Cs16);
assert_eq!("cf32".parse::<IqFormat>().unwrap(), IqFormat::Cf32);
}
#[test]
fn test_iqformat_fromstr_case_insensitive() {
assert_eq!("CU8".parse::<IqFormat>().unwrap(), IqFormat::Cu8);
assert_eq!("Cs8".parse::<IqFormat>().unwrap(), IqFormat::Cs8);
assert_eq!("CS16".parse::<IqFormat>().unwrap(), IqFormat::Cs16);
assert_eq!("CF32".parse::<IqFormat>().unwrap(), IqFormat::Cf32);
}
#[test]
fn test_iqformat_fromstr_invalid() {
assert!("invalid".parse::<IqFormat>().is_err());
assert!("cu16".parse::<IqFormat>().is_err());
assert!("".parse::<IqFormat>().is_err());
}
#[test]
fn test_iqformat_roundtrip() {
let formats = [IqFormat::Cu8, IqFormat::Cs8, IqFormat::Cs16, IqFormat::Cf32];
for format in formats {
let s = format.to_string();
let parsed: IqFormat = s.parse().unwrap();
assert_eq!(parsed, format);
}
}
#[test]
#[cfg(feature = "pluto")]
fn test_pluto_uri_parsing() {
use std::str::FromStr;
let config = DeviceConfig::from_str("pluto://192.168.2.1?freq=1090M&rate=2.4M").unwrap();
if let DeviceConfig::Pluto(pluto_config) = config {
assert_eq!(pluto_config.uri, "192.168.2.1");
} else {
panic!("Expected PlutoSDR config");
}
let config = DeviceConfig::from_str("pluto://ip:192.168.2.1?freq=1090M&rate=2.4M").unwrap();
if let DeviceConfig::Pluto(pluto_config) = config {
assert_eq!(pluto_config.uri, "ip:192.168.2.1");
} else {
panic!("Expected PlutoSDR config");
}
let config = DeviceConfig::from_str("pluto:///usb:1.18.5?freq=1090M&rate=2.4M").unwrap();
if let DeviceConfig::Pluto(pluto_config) = config {
assert_eq!(pluto_config.uri, "usb:1.18.5");
} else {
panic!("Expected PlutoSDR config");
}
let config = DeviceConfig::from_str("pluto:///usb:?freq=1090M&rate=2.4M").unwrap();
if let DeviceConfig::Pluto(pluto_config) = config {
assert_eq!(pluto_config.uri, "usb:");
} else {
panic!("Expected PlutoSDR config");
}
}
}