Skip to main content

librtlsdr_rs/
lib.rs

1//! Pure-Rust port of librtlsdr — RTL2832U USB control + tuner drivers.
2//!
3//! Talks to RTL-SDR dongles directly over USB via [`rusb`], without the
4//! C `librtlsdr` library or its headers. Covers all five tuner families
5//! shipped in real-world dongles (R820T / R820T2 / R828D, E4000,
6//! FC0012, FC0013, FC2580).
7//!
8//! # Quick start
9//!
10//! ```no_run
11//! use librtlsdr_rs::{RtlSdrDevice, RtlSdrError};
12//!
13//! # fn main() -> Result<(), RtlSdrError> {
14//! // Open the first dongle plugged in.
15//! let mut dev = RtlSdrDevice::open(0)?;
16//!
17//! // Tune to 100 MHz, 2.048 Msps.
18//! dev.set_center_freq(100_000_000)?;
19//! dev.set_sample_rate(2_048_000)?;
20//!
21//! // Manual gain at 14.4 dB (`144` in tenths-of-dB happens to be
22//! // an exact step on the R820T2 gain table). For arbitrary user
23//! // input, snap to the nearest step via [`RtlSdrDevice::closest_gain`]
24//! // first — passing a value not in the tuner's table results in
25//! // the tuner silently rounding (or, on the E4000, returning an
26//! // `InvalidGain` error).
27//! dev.set_tuner_gain_mode(true)?;
28//! dev.set_tuner_gain(144)?;
29//!
30//! // Read 64 KB of interleaved I/Q samples.
31//! dev.reset_buffer()?;
32//! let mut buf = vec![0u8; 65_536];
33//! let n = dev.read_sync(&mut buf)?;
34//! assert!(n > 0);
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! # Public surface
40//!
41//! The committed surface is intentionally narrow:
42//!
43//! - [`RtlSdrDevice`] — the device handle. All control + streaming
44//!   methods live here. Open via [`RtlSdrDevice::open`].
45//! - Free enumeration helpers — [`get_device_count`],
46//!   [`get_device_name`], [`get_device_usb_strings`],
47//!   [`get_index_by_serial`].
48//! - [`RtlSdrError`] — the unified error type returned by every
49//!   fallible operation.
50//! - [`TunerType`] — the IC family identifier returned by
51//!   [`RtlSdrDevice::tuner_type`].
52//!
53//! Sample values are interleaved unsigned 8-bit I/Q pairs, the native
54//! RTL-SDR format. Convert to centred `i8` (or `f32` in `[-1, 1]`) at
55//! the consumer if needed; we don't impose a sample type on the read
56//! path.
57//!
58//! # USB context + threading
59//!
60//! [`RtlSdrDevice`] holds an `Arc<rusb::DeviceHandle>` internally so
61//! the device handle can be cloned across threads — `rusb::DeviceHandle`
62//! is `Sync`, which makes the type *shareable* between threads. The
63//! control methods on [`RtlSdrDevice`] take `&mut self` and serialise
64//! on the caller; that's the supported single-thread pattern.
65//!
66//! For raw bulk reads on a worker thread (e.g. an `rtl_tcp`-style
67//! server), call [`RtlSdrDevice::usb_handle`] to clone the underlying
68//! handle and use [`RtlSdrDevice::BULK_ENDPOINT`] for the endpoint
69//! address. **Note that `Sync` alone does not guarantee that
70//! concurrent bulk and control transfers on the same handle are
71//! safe** — `rusb`'s docs don't make that claim explicitly, and
72//! libusb's caveats restrict per-resource concurrent access. If you
73//! mix concurrent bulk and control on one handle, treat it as an
74//! unsupported design assumption you've verified against your
75//! libusb version + dongle hardware. The safer pattern is to
76//! quiesce control calls (or do them all from one thread) while a
77//! bulk-read worker is in flight.
78//!
79//! # Faithful-port note
80//!
81//! The crate is a port of the C `librtlsdr` source — register
82//! addresses, magic constants, and per-tuner I2C tables are
83//! transcribed directly from the upstream code. Some internal items
84//! aren't currently called from Rust but are kept for completeness so
85//! future hardware-feature work is a register-table read away rather
86//! than a re-port. Hardware register manipulation requires extensive
87//! integer casts inherent in a faithful port of C driver code.
88//!
89//! [`rusb`]: https://docs.rs/rusb
90
91// Allow cast-heavy code throughout this crate (hardware register port)
92#![allow(
93    clippy::cast_possible_truncation,
94    clippy::cast_sign_loss,
95    clippy::cast_possible_wrap,
96    clippy::cast_precision_loss,
97    clippy::cast_lossless,
98    clippy::similar_names,
99    clippy::collapsible_if,
100    clippy::struct_excessive_bools,
101    clippy::wildcard_imports,
102    clippy::neg_multiply,
103    clippy::range_plus_one,
104    clippy::manual_range_contains,
105    clippy::needless_range_loop,
106    clippy::implicit_saturating_sub,
107    clippy::doc_markdown
108)]
109
110// Faithful-port modules: `constants` and `reg` transcribe register
111// addresses and hardware magic numbers from upstream `librtlsdr`'s C
112// source. We keep the full table around even when not currently
113// called from Rust so future hardware-feature work is a register-
114// table read away rather than a re-port. Scoped `dead_code` allow
115// (rather than crate-level) means accidental dead paths in `device`,
116// `error`, or any future addition still get caught by the lint. Per
117// #630 CR round 2.
118#[allow(dead_code)]
119pub(crate) mod constants;
120pub mod device;
121pub mod error;
122#[allow(dead_code)]
123pub(crate) mod reg;
124// `tuner` is the internal abstraction layer over the five tuner-IC
125// backends (R820T2 / E4000 / FC0012 / FC0013 / FC2580). The `Tuner`
126// trait takes raw `rusb::DeviceHandle` parameters because the per-IC
127// I2C transactions need direct USB control-transfer access — that's
128// not a shape we want in the committed semver surface. External
129// consumers control the tuner through `RtlSdrDevice` methods
130// (`set_tuner_gain`, `set_tuner_bandwidth`, etc.) which dispatch
131// internally to the right backend. Per #630 CR round 1.
132pub(crate) mod tuner;
133pub(crate) mod usb;
134
135pub use device::{
136    DeviceInfo, ReaderIter, RtlSdrDevice, RtlSdrDeviceBuilder, RtlSdrReader, SampleIter,
137    get_device_count, get_device_name, get_device_usb_strings, get_index_by_serial, list_devices,
138};
139pub use error::{RtlSdrError, TunerError};
140// Re-export `Block` from the internal `reg` module. Block became
141// part of the public API in 0.2 because `RtlSdrError::RegisterAccess`
142// now carries `block: Block` for diagnostic context. Per #16.
143pub use reg::Block;
144// Re-export `TunerType` from the internal `reg` module. The
145// rendered docs use the source's own doc on `reg::TunerType`
146// (which is more comprehensive than a one-line summary on the
147// re-export). Per audit issue #19.
148pub use reg::TunerType;
149// Re-export `rusb` so consumers pattern-matching on
150// `RtlSdrError::Usb(rusb::Error::...)` can do so without taking a
151// direct `rusb` dependency in their own `Cargo.toml` — and, more
152// importantly, without risking a dep version mismatch where the
153// consumer resolves a different `rusb` minor than this crate
154// (which would make the inner `rusb::Error` types non-compatible
155// at the type level). Per audit #15.
156pub use rusb;