mod eeprom;
mod enumerate;
mod frequency;
mod gain;
mod sampling;
mod streaming;
pub use enumerate::{
DeviceInfo, get_device_count, get_device_name, get_device_usb_strings, get_index_by_serial,
list_devices,
};
pub use streaming::SampleIter;
mod builder;
pub use builder::RtlSdrDeviceBuilder;
mod reader;
pub use reader::{ReaderIter, RtlSdrReader};
#[cfg(feature = "tokio")]
mod streaming_tokio;
#[cfg(feature = "tokio")]
pub use streaming_tokio::TokioSampleStream;
#[cfg(feature = "smol")]
mod streaming_smol;
#[cfg(feature = "smol")]
pub use streaming_smol::SmolSampleStream;
use crate::constants::*;
use crate::error::RtlSdrError;
use crate::reg::{AsyncStatus, Block, TunerType};
use crate::tuner::Tuner;
use crate::tuner::r82xx::{R82xxConfig, R82xxPriv};
use crate::usb;
pub struct RtlSdrDevice {
pub(crate) handle: std::sync::Arc<rusb::DeviceHandle<rusb::GlobalContext>>,
pub(crate) reader_busy: std::sync::Arc<std::sync::atomic::AtomicBool>,
pub(crate) tuner_type: TunerType,
pub(crate) tuner: Option<Box<dyn Tuner>>,
pub(crate) rtl_xtal: u32,
pub(crate) tun_xtal: u32,
pub(crate) rate: u32,
pub(crate) freq: u32,
pub(crate) bw: u32,
pub(crate) offs_freq: u32,
pub(crate) corr: i32,
pub(crate) gain: i32,
pub(crate) direct_sampling: i32,
pub(crate) fir: [i32; FIR_LEN],
#[allow(dead_code)]
pub(crate) async_status: AsyncStatus,
pub(crate) manufact: String,
pub(crate) product: String,
pub(crate) serial: String,
pub(crate) dev_lost: bool,
pub(crate) driver_active: bool,
}
impl RtlSdrDevice {
pub const BULK_ENDPOINT: u8 = crate::constants::BULK_ENDPOINT;
#[must_use]
pub fn builder() -> RtlSdrDeviceBuilder {
RtlSdrDeviceBuilder::default()
}
#[must_use]
pub fn list() -> Vec<DeviceInfo> {
enumerate::list_devices()
}
#[must_use]
pub fn reader(&self) -> RtlSdrReader {
RtlSdrReader {
handle: std::sync::Arc::clone(&self.handle),
busy: std::sync::Arc::clone(&self.reader_busy),
}
}
pub fn open(index: u32) -> Result<Self, RtlSdrError> {
let (device, _dd) = enumerate::find_device_by_index(index)?;
let handle = device.open()?;
let driver_active = handle.kernel_driver_active(0).unwrap_or(false);
if driver_active {
let _ = handle.detach_kernel_driver(0);
}
handle.claim_interface(0)?;
let mut dev = Self {
handle: std::sync::Arc::new(handle),
reader_busy: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
tuner_type: TunerType::Unknown,
tuner: None,
rtl_xtal: DEF_RTL_XTAL_FREQ,
tun_xtal: DEF_RTL_XTAL_FREQ,
rate: 0,
freq: 0,
bw: 0,
offs_freq: 0,
corr: 0,
gain: 0,
direct_sampling: 0,
fir: FIR_DEFAULT,
async_status: AsyncStatus::Inactive,
manufact: String::new(),
product: String::new(),
serial: String::new(),
dev_lost: true,
driver_active,
};
if usb::write_reg(
&dev.handle,
Block::Usb,
crate::reg::usb_reg::USB_SYSCTL,
0x09,
1,
)
.is_err()
{
tracing::warn!("dummy write failed, resetting device");
let _ = dev.handle.reset();
}
usb::init_baseband(&dev.handle, &dev.fir)?;
dev.dev_lost = false;
if let Ok(dd) = dev.handle.device().device_descriptor() {
dev.manufact = dev
.handle
.read_manufacturer_string_ascii(&dd)
.unwrap_or_default();
dev.product = dev
.handle
.read_product_string_ascii(&dd)
.unwrap_or_default();
dev.serial = dev
.handle
.read_serial_number_string_ascii(&dd)
.unwrap_or_default();
}
usb::set_i2c_repeater(&dev.handle, true)?;
dev.probe_tuner();
if dev.tun_xtal == DEF_RTL_XTAL_FREQ {
dev.tun_xtal = dev.rtl_xtal;
}
match dev.tuner_type {
TunerType::R828D | TunerType::R820T => {
usb::demod_write_reg(&dev.handle, 1, 0xb1, 0x1a, 1)?;
usb::demod_write_reg(&dev.handle, 0, 0x08, 0x4d, 1)?;
dev.set_if_freq(R82XX_IF_FREQ)?;
usb::demod_write_reg(&dev.handle, 1, 0x15, 0x01, 1)?;
}
TunerType::Unknown => {
tracing::warn!("No supported tuner found, enabling direct sampling");
let _ = dev.set_direct_sampling(1);
}
_ => {}
}
if let Some(tuner) = &mut dev.tuner {
tuner.init(&dev.handle)?;
}
usb::set_i2c_repeater(&dev.handle, false)?;
Ok(dev)
}
fn probe_tuner(&mut self) {
if let Ok(reg) = usb::i2c_read_reg(&self.handle, E4K_I2C_ADDR, E4K_CHECK_ADDR)
&& reg == E4K_CHECK_VAL
{
tracing::info!("Found Elonics E4000 tuner");
self.tuner_type = TunerType::E4000;
return;
}
if let Ok(reg) = usb::i2c_read_reg(&self.handle, FC0013_I2C_ADDR, FC0013_CHECK_ADDR)
&& reg == FC0013_CHECK_VAL
{
tracing::info!("Found Fitipower FC0013 tuner");
self.tuner_type = TunerType::Fc0013;
return;
}
if let Ok(reg) = usb::i2c_read_reg(&self.handle, R820T_I2C_ADDR, R82XX_CHECK_ADDR)
&& reg == R82XX_CHECK_VAL
{
tracing::info!("Found Rafael Micro R820T tuner");
self.tuner_type = TunerType::R820T;
self.create_r82xx_tuner();
return;
}
if let Ok(reg) = usb::i2c_read_reg(&self.handle, R828D_I2C_ADDR, R82XX_CHECK_ADDR)
&& reg == R82XX_CHECK_VAL
{
tracing::info!("Found Rafael Micro R828D tuner");
let is_v4 = self.is_blog_v4();
if is_v4 {
tracing::info!("RTL-SDR Blog V4 Detected");
}
self.tuner_type = TunerType::R828D;
self.create_r82xx_tuner();
return;
}
let _ = usb::set_gpio_output(&self.handle, 4);
let _ = usb::set_gpio_bit(&self.handle, 4, true);
let _ = usb::set_gpio_bit(&self.handle, 4, false);
if let Ok(reg) = usb::i2c_read_reg(&self.handle, FC2580_I2C_ADDR, FC2580_CHECK_ADDR)
&& (reg & 0x7f) == FC2580_CHECK_VAL
{
tracing::info!("Found FCI 2580 tuner");
self.tuner_type = TunerType::Fc2580;
return;
}
if let Ok(reg) = usb::i2c_read_reg(&self.handle, FC0012_I2C_ADDR, FC0012_CHECK_ADDR)
&& reg == FC0012_CHECK_VAL
{
tracing::info!("Found Fitipower FC0012 tuner");
let _ = usb::set_gpio_output(&self.handle, 6);
self.tuner_type = TunerType::Fc0012;
return;
}
tracing::warn!("No supported tuner found");
}
fn create_r82xx_tuner(&mut self) {
let (i2c_addr, chip) = match self.tuner_type {
TunerType::R828D => {
let is_v4 = self.is_blog_v4();
if !is_v4 {
self.tun_xtal = R828D_XTAL_FREQ;
}
(
R828D_I2C_ADDR,
crate::tuner::r82xx::constants::R82xxChip::R828D,
)
}
_ => (
R820T_I2C_ADDR,
crate::tuner::r82xx::constants::R82xxChip::R820T,
),
};
let xtal = self.get_tuner_xtal();
let config = R82xxConfig {
i2c_addr,
xtal,
rafael_chip: chip,
max_i2c_msg_len: 8,
use_predetect: false,
};
let mut r82xx = R82xxPriv::new(&config);
let is_v4 = self.is_blog_v4();
r82xx.set_blog_v4(is_v4);
self.tuner = Some(Box::new(r82xx));
}
pub(crate) fn set_if_freq(&self, freq: u32) -> Result<(), RtlSdrError> {
let rtl_xtal = self.get_rtl_xtal();
let if_freq = -((f64::from(freq) * (1u64 << 22) as f64) / f64::from(rtl_xtal)) as i32;
let tmp = ((if_freq >> 16) & 0x3f) as u16;
usb::demod_write_reg(&self.handle, 1, 0x19, tmp, 1)?;
let tmp = ((if_freq >> 8) & 0xff) as u16;
usb::demod_write_reg(&self.handle, 1, 0x1a, tmp, 1)?;
let tmp = (if_freq & 0xff) as u16;
usb::demod_write_reg(&self.handle, 1, 0x1b, tmp, 1)?;
Ok(())
}
pub(crate) fn set_sample_freq_correction(&self, ppm: i32) -> Result<(), RtlSdrError> {
let offs = (f64::from(-ppm) * (1u64 << 24) as f64 / 1_000_000.0) as i16;
let tmp = (offs & 0xff) as u16;
usb::demod_write_reg(&self.handle, 1, 0x3f, tmp, 1)?;
let tmp = ((offs >> 8) & 0x3f) as u16;
usb::demod_write_reg(&self.handle, 1, 0x3e, tmp, 1)?;
Ok(())
}
pub(crate) fn get_rtl_xtal(&self) -> u32 {
(f64::from(self.rtl_xtal) * (1.0 + f64::from(self.corr) / 1e6)) as u32
}
pub(crate) fn get_tuner_xtal(&self) -> u32 {
(f64::from(self.tun_xtal) * (1.0 + f64::from(self.corr) / 1e6)) as u32
}
#[must_use]
pub fn tuner_type(&self) -> TunerType {
self.tuner_type
}
#[must_use]
pub fn tuner_gains(&self) -> &'static [i32] {
self.tuner_type.gains()
}
#[must_use]
pub fn closest_gain(&self, desired_tenths_db: i32) -> Option<i32> {
closest_gain_in(self.tuner_gains(), desired_tenths_db)
}
#[must_use]
pub fn manufacturer(&self) -> &str {
&self.manufact
}
#[must_use]
pub fn product(&self) -> &str {
&self.product
}
#[must_use]
pub fn serial(&self) -> &str {
&self.serial
}
#[must_use]
pub fn center_freq(&self) -> u32 {
self.freq
}
#[must_use]
pub fn sample_rate(&self) -> u32 {
self.rate
}
#[must_use]
pub fn freq_correction(&self) -> i32 {
self.corr
}
#[must_use]
pub fn tuner_gain(&self) -> i32 {
self.gain
}
#[must_use]
pub fn direct_sampling(&self) -> i32 {
self.direct_sampling
}
#[must_use]
pub fn offset_tuning(&self) -> bool {
self.offs_freq > 0
}
#[must_use]
pub fn xtal_freq(&self) -> (u32, u32) {
(self.get_rtl_xtal(), self.get_tuner_xtal())
}
pub fn set_xtal_freq(&mut self, rtl_freq: u32, tuner_freq: u32) -> Result<(), RtlSdrError> {
if rtl_freq > 0 && (rtl_freq < MIN_RTL_XTAL_FREQ || rtl_freq > MAX_RTL_XTAL_FREQ) {
return Err(RtlSdrError::InvalidParameter(format!(
"RTL xtal freq out of range: {rtl_freq}"
)));
}
tracing::info!(
"set_xtal_freq: rtl={rtl_freq} Hz, tuner={tuner_freq} Hz \
(rtl 0 = unchanged; tuner 0 = follow current RTL xtal)"
);
if rtl_freq > 0 && self.rtl_xtal != rtl_freq {
self.rtl_xtal = rtl_freq;
if self.rate > 0 {
self.set_sample_rate(self.rate)?;
}
}
if self.tun_xtal != tuner_freq {
self.tun_xtal = if tuner_freq == 0 {
self.rtl_xtal
} else {
tuner_freq
};
if self.freq > 0 {
self.set_center_freq(self.freq)?;
}
}
Ok(())
}
pub fn is_blog_v4(&self) -> bool {
self.manufact == "RTLSDRBlog" && self.product == "Blog V4"
}
pub fn check_dongle_model(&self, manufact: &str, product: &str) -> bool {
self.manufact == manufact && self.product == product
}
}
impl Drop for RtlSdrDevice {
fn drop(&mut self) {
if !self.dev_lost {
if let Some(tuner) = &mut self.tuner {
if let Err(e) = usb::set_i2c_repeater(&self.handle, true) {
tracing::debug!("RtlSdrDevice::drop: set_i2c_repeater(true) failed: {e}");
}
if let Err(e) = tuner.exit(&self.handle) {
tracing::debug!("RtlSdrDevice::drop: tuner.exit failed: {e}");
}
if let Err(e) = usb::set_i2c_repeater(&self.handle, false) {
tracing::debug!("RtlSdrDevice::drop: set_i2c_repeater(false) failed: {e}");
}
}
if let Err(e) = usb::deinit_baseband(&self.handle) {
tracing::debug!("RtlSdrDevice::drop: deinit_baseband failed: {e}");
}
}
if let Err(e) = self.handle.release_interface(0) {
tracing::debug!("RtlSdrDevice::drop: release_interface failed: {e}");
}
if self.driver_active {
if let Err(e) = self.handle.attach_kernel_driver(0) {
tracing::debug!("RtlSdrDevice::drop: attach_kernel_driver failed: {e}");
}
}
}
}
fn closest_gain_in(gains: &[i32], desired: i32) -> Option<i32> {
gains
.iter()
.copied()
.min_by_key(|&g| (i64::from(g) - i64::from(desired)).abs())
}
const _: fn() = || {
fn assert_send<T: Send>() {}
assert_send::<RtlSdrDevice>();
};
impl std::fmt::Debug for RtlSdrDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RtlSdrDevice")
.field("handle_ptr", &std::sync::Arc::as_ptr(&self.handle))
.field("reader_busy", &self.reader_busy)
.field("tuner_type", &self.tuner_type)
.field("tuner_present", &self.tuner.is_some())
.field("rtl_xtal", &self.rtl_xtal)
.field("tun_xtal", &self.tun_xtal)
.field("rate", &self.rate)
.field("freq", &self.freq)
.field("bw", &self.bw)
.field("offs_freq", &self.offs_freq)
.field("corr", &self.corr)
.field("gain", &self.gain)
.field("direct_sampling", &self.direct_sampling)
.field("fir", &self.fir)
.field("async_status", &self.async_status)
.field("manufact", &self.manufact)
.field("product", &self.product)
.field("serial", &self.serial)
.field("dev_lost", &self.dev_lost)
.field("driver_active", &self.driver_active)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::closest_gain_in;
const R820T2_GAINS: &[i32] = &[
0, 9, 14, 27, 37, 77, 87, 125, 144, 157, 166, 197, 207, 229, 254, 280, 297, 328, 338, 364,
372, 386, 402, 421, 434, 439, 445, 480, 496,
];
#[test]
fn empty_table_returns_none() {
assert_eq!(closest_gain_in(&[], 250), None);
assert_eq!(closest_gain_in(&[], 0), None);
assert_eq!(closest_gain_in(&[], -100), None);
assert_eq!(closest_gain_in(&[], i32::MIN), None);
}
#[test]
fn exact_match_returns_self() {
for &g in R820T2_GAINS {
assert_eq!(
closest_gain_in(R820T2_GAINS, g),
Some(g),
"exact value {g} should round to itself"
);
}
}
#[test]
fn rounds_to_nearest_step() {
assert_eq!(closest_gain_in(R820T2_GAINS, 150), Some(144));
assert_eq!(closest_gain_in(R820T2_GAINS, 152), Some(157));
assert_eq!(closest_gain_in(R820T2_GAINS, 100), Some(87));
}
#[test]
fn out_of_range_clamps_to_endpoint() {
assert_eq!(closest_gain_in(R820T2_GAINS, -1000), Some(0));
assert_eq!(closest_gain_in(R820T2_GAINS, 10_000), Some(496));
}
#[test]
fn ties_resolve_deterministically() {
let table = &[0, 100];
assert_eq!(closest_gain_in(table, 50), Some(0));
}
#[test]
fn i32_min_does_not_overflow() {
assert_eq!(closest_gain_in(R820T2_GAINS, i32::MIN), Some(0));
assert_eq!(closest_gain_in(R820T2_GAINS, i32::MAX), Some(496));
}
#[test]
fn zero_step_is_distinguishable_from_empty_table() {
assert_eq!(closest_gain_in(R820T2_GAINS, 0), Some(0));
assert_eq!(closest_gain_in(&[], 0), None);
}
}