RtlSdrManager 0.8.1

dotnet add package RtlSdrManager --version 0.8.1
                    
NuGet\Install-Package RtlSdrManager -Version 0.8.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RtlSdrManager" Version="0.8.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RtlSdrManager" Version="0.8.1" />
                    
Directory.Packages.props
<PackageReference Include="RtlSdrManager" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RtlSdrManager --version 0.8.1
                    
#r "nuget: RtlSdrManager, 0.8.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RtlSdrManager@0.8.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RtlSdrManager&version=0.8.1
                    
Install as a Cake Addin
#tool nuget:?package=RtlSdrManager&version=0.8.1
                    
Install as a Cake Tool

RTL-SDR Manager Library for .NET

NuGet Version NuGet Downloads License: GPL v3 .NET

A modern .NET library for managing RTL-SDR devices

RTL-SDR Manager provides a high-level, type-safe API for controlling RTL2832U-based software-defined radio devices from .NET applications. The library handles device lifecycle, tuner configuration, and sample acquisition with support for synchronous and asynchronous operations, multiple simultaneous devices, and advanced features such as KerberosSDR coherent arrays.

Features

  • Async/Await Support — Non-blocking sample reading with concurrent queue buffering for real-time signal processing.

  • Multiple Tuner Support — Works with E4000, R820T/R828D, FC0012, FC0013, and FC2580 tuner chips.

  • Advanced Configuration — Gain control, frequency correction (PPM), direct sampling for HF reception, and bias tee power control.

  • KerberosSDR Ready — Frequency dithering and GPIO control for coherent SDR arrays used in direction finding.

  • Type-Safe Frequency API — Strongly-typed Frequency values with factory methods (FromHz, FromKHz, FromMHz, FromGHz) and arithmetic operations.

  • Cross-Platform — Runs on Windows, Linux, and macOS via platform-specific native interop.

  • High Performance — Uses LibraryImport source-generated P/Invoke for optimal native library calls.

  • Trim and AOT Ready — Publishes cleanly from Native AOT applications, for smaller and faster-starting deployments on Raspberry Pi and other embedded targets.

  • Production Ready — Proper exception handling, IDisposable patterns, null safety, and scoped console output suppression.

Installation

Via NuGet Package Manager

# .NET CLI
dotnet add package RtlSdrManager

# Package Manager Console (Visual Studio)
Install-Package RtlSdrManager

# PackageReference (in .csproj)
<PackageReference Include="RtlSdrManager" Version="0.8.1" />

Prerequisites

The librtlsdr native library must be installed on the system. Any 2.x release works, but 2.0.3 or later is recommended: earlier versions can block indefinitely inside the native reader when a device is unplugged during an asynchronous reading, so the failure surfaces as a stop timeout from StopReadSamplesAsync() rather than as the underlying error (see Asynchronous Sample Reading).

Windows:

# Using Chocolatey
choco install rtl-sdr

# Or download from: https://github.com/osmocom/rtl-sdr/releases

Linux (Ubuntu/Debian):

sudo apt-get install librtlsdr-dev

macOS:

brew install librtlsdr

Quick Start

Basic Usage

using RtlSdrManager;
using RtlSdrManager.Modes;

// Get the singleton device manager
var manager = RtlSdrDeviceManager.Instance;

// Check available devices
Console.WriteLine($"Found {manager.CountDevices} RTL-SDR device(s)");

// Open the first device with a friendly name
manager.OpenManagedDevice(0, "my-rtl-sdr");

// Configure the device
var device = manager["my-rtl-sdr"];
device.CenterFrequency = Frequency.FromMHz(1090);  // ADS-B frequency
device.SampleRate = Frequency.FromMHz(2);
device.TunerGainMode = TunerGainModes.AGC;
device.AGCMode = AGCModes.Enabled;
device.ResetDeviceBuffer();

Console.WriteLine($"Tuner: {device.TunerType}");
Console.WriteLine($"Center Frequency: {device.CenterFrequency.MHz} MHz");

Console Output Suppression

By default, librtlsdr diagnostic messages (such as "Found Rafael Micro R820T tuner" and "[R82XX] PLL not locked!") are shown on stdout/stderr. The library provides a global static property to suppress these messages during device operations:

// Suppress librtlsdr diagnostic output globally
RtlSdrDeviceManager.SuppressLibraryConsoleOutput = true;

// Device operations are now silent
manager.OpenManagedDevice(0, "my-rtl-sdr");
var device = manager["my-rtl-sdr"];
device.SampleRate = Frequency.FromMHz(2.4);

// Re-enable output
RtlSdrDeviceManager.SuppressLibraryConsoleOutput = false;
device.CenterFrequency = Frequency.FromMHz(1090);  // Shows librtlsdr messages

Suppression is scoped to individual device operations using reference-counted file descriptor redirection. Stdout and stderr are redirected to /dev/null (Unix/macOS) or NUL (Windows) only for the duration of each native call, then restored. This prevents interference with console applications and avoids file descriptor corruption when multiple devices operate concurrently.

For detailed documentation, see Console Output Suppression.

Synchronous Sample Reading

// Read samples synchronously (blocking)
var samples = device.ReadSamples(256 * 1024);

foreach (var sample in samples)
{
    // Process I/Q samples
    Console.WriteLine($"I: {sample.I}, Q: {sample.Q}");
}

Asynchronous Sample Reading

// Configure async buffer
device.MaxAsyncBufferSize = 512 * 1024;
device.DropSamplesOnFullBuffer = true;

// Start async reading in background
device.StartReadSamplesAsync();

// Option 1: Event-based. The handler runs on the driver's callback thread, so it must
// hand work off rather than do it: anything slow here stalls the transfer pipeline and
// costs samples.
var pending = new ConcurrentQueue<List<IQData>>();

device.SamplesAvailable += (sender, args) =>
{
    // Drain and hand over. Process on your own thread, not this one.
    pending.Enqueue(device.GetSamplesFromAsyncBuffer(args.SampleCount));
};

// Option 2: Manual polling (for custom processing logic)
while (running)
{
    var samples = device.GetSamplesFromAsyncBuffer(16 * 1024);
    if (samples.Count > 0)
    {
        // Process the batch of I/Q samples
        Console.WriteLine($"Received {samples.Count} samples");
    }
    else
    {
        await Task.Delay(100);
    }
}

// Stop reading when done
device.StopReadSamplesAsync();

// Clean up
manager.CloseManagedDevice("my-rtl-sdr");

If the reading stops on its own — a full buffer with DropSamplesOnFullBuffer disabled, a throwing SamplesAvailable handler, or a device failure — the error is captured and rethrown by StopReadSamplesAsync(), and stays observable via AsyncReadException until the next StartReadSamplesAsync().

Unplugging a device mid-reading belongs to that last case, but how it surfaces depends on the native library. With librtlsdr 2.0.3 or later, the native reader returns promptly, and you get the underlying error. With earlier versions it can block indefinitely instead, and StopReadSamplesAsync() gives up after five seconds with a stop-timeout error — the device is then left untouched on purpose, so no native callback runs against freed state.

Raw Buffer Mode (Zero-Copy)

For high-throughput applications, raw buffer mode eliminates per-sample object allocation by delivering raw byte buffers directly from the native callback via ArrayPool<byte>:

Choosing a read mode

Asynchronous reading offers two delivery modes, selected by UseRawBufferMode (default false). The mode is captured once when StartReadSamplesAsync is called, so set it beforehand; toggling it mid-stream has no effect until the next start.

Default (UseRawBufferMode = false) Raw buffer mode (UseRawBufferMode = true)
Delivery per-sample IQData structs via ConcurrentQueue<IQData> pooled byte[] buffers via Channel
Consume with GetSamplesFromAsyncBuffer(...).I / .Q GetRawSamplesFromAsyncBuffer() → raw bytes
Ease of use high — no buffer lifetime to manage manual — must call buffer.Return() exactly once
Per-sample overhead enqueue/dequeue + per-slot bookkeeping none (one memcpy per buffer)
Best for moderate sample rates, simplicity high sample rates, minimal GC pressure

Since v0.7.1 the default mode stores each IQData as two bytes internally (the .I / .Q accessors stay int), so it is already compact; raw buffer mode remains the choice when you want to avoid per-sample handling entirely.

// Enable raw buffer mode before starting async reading
device.UseRawBufferMode = true;
device.MaxAsyncBufferSize = 512 * 1024;
device.DropSamplesOnFullBuffer = true;

device.StartReadSamplesAsync(requestedSamples: 131072);

device.SamplesAvailable += (sender, args) =>
{
    var buffer = device.GetRawSamplesFromAsyncBuffer();
    if (buffer == null) return;

    try
    {
        // Access raw interleaved I/Q bytes: [I0, Q0, I1, Q1, ...]
        ReadOnlySpan<byte> raw = buffer.Data.AsSpan(0, buffer.ByteLength);

        for (int i = 0; i < buffer.ByteLength; i += 2)
        {
            byte iSample = raw[i];
            byte qSample = raw[i + 1];
            // Process sample pair...
        }
    }
    finally
    {
        buffer.Return(); // Return pooled buffer — must be called exactly once
    }
};

// Stop reading when done
device.StopReadSamplesAsync();

Manual Gain Control

// Switch to manual gain mode
device.TunerGainMode = TunerGainModes.Manual;

// Get supported gain values
var gains = device.SupportedTunerGains;
Console.WriteLine($"Supported gains: {string.Join(", ", gains)} dB");

// Set specific gain
device.TunerGain = 42.1; // dB

// Or use convenience methods
device.SetMaximumTunerGain();
device.SetMinimumTunerGain();

Frequency Operations

// Create frequencies with different units
var freq1 = Frequency.FromHz(1090_000_000);
var freq2 = Frequency.FromKHz(1090_000);
var freq3 = Frequency.FromMHz(1090);
var freq4 = Frequency.FromGHz(1.09);

// Convert between units
Console.WriteLine($"{freq1.Hz} Hz");
Console.WriteLine($"{freq1.KHz} KHz");
Console.WriteLine($"{freq1.MHz} MHz");
Console.WriteLine($"{freq1.GHz} GHz");

// Arithmetic operations
var shifted = freq1 + Frequency.FromKHz(100); // Add 100 KHz offset
var doubled = freq1 * 2;

// Comparison
if (freq1 > Frequency.FromMHz(100))
{
    Console.WriteLine("Above 100 MHz");
}

Advanced Features

Bias Tee (for powering external LNAs)
// Enable bias tee on GPIO 0 (most common)
device.SetBiasTee(BiasTeeModes.Enabled);

// Or target a specific GPIO pin (0..7), on any tuner
device.SetBiasTeeGPIO(gpio: 1, BiasTeeModes.Enabled);

// The bias tee stays powered after the device is closed, so turn it off explicitly
device.SetBiasTee(BiasTeeModes.Disabled);

Note: GPIO pins 4 and 6 are reserved. Pin 4 is pulsed to reset the tuner while the device is opened, and pin 6 selects the band filter on FC0012 tuners. They are not blocked, but using them as a bias tee control will produce confusing behavior.

Direct Sampling (HF reception)
// Enable direct sampling on I-ADC, before choosing a frequency
device.DirectSamplingMode = DirectSamplingModes.InPhaseADCInputEnabled;

// Or on Q-ADC
device.DirectSamplingMode = DirectSamplingModes.QuadratureADCInputEnabled;

// The reachable range is now the ADC's: 0 Hz to half the crystal frequency,
// which is 0 - 14.4 MHz on the usual 28.8 MHz crystal
Console.WriteLine(string.Join(" and ", device.SupportedFrequencyRanges));

device.CenterFrequency = Frequency.FromMHz(7.1);  // 40 m amateur band

// Disable, then set a frequency the tuner can reach
device.DirectSamplingMode = DirectSamplingModes.Disabled;
device.CenterFrequency = Frequency.FromMHz(145);

Frequencies above half the crystal are received by aliasing rather than tuned to directly: subtract the wanted frequency from the crystal frequency. See Direct Sampling for the detail.

Frequency Correction (PPM)
// Set frequency correction in PPM
device.FrequencyCorrection = 10; // +10 PPM
KerberosSDR Support
// Enable KerberosSDR mode (required for these features)
device.KerberosSDRMode = KerberosSDRModes.Enabled;

// Enable frequency dithering (for R820T only)
device.FrequencyDitheringMode = FrequencyDitheringModes.Enabled;

// Control GPIO pins directly
device.SetGPIO(gpio: 1, GPIOModes.Enabled);

Documentation

Detailed guides for specific use cases:

Sample Applications

The samples/ directory contains complete working examples:

  • Demo1 — Event-based async sample reading, handing work off the callback thread
  • Demo2 — Manual polling from the async buffer, with transfer buffer count and drop count
  • Demo3 — Synchronous sample reading
  • Demo4 — Device discovery and configuration: re-enumeration, opening by serial, reachable range
  • Demo5 — Raw buffer mode for zero-copy sample processing, with pooled-buffer hand-off

Building from Source

Requirements

  • .NET 10.0 SDK or later
  • librtlsdr native library

Build Commands

# Clone the repository
git clone https://github.com/nandortoth/rtlsdr-manager.git
cd rtlsdr-manager

# Build the entire solution
dotnet build

# Run the test suite
dotnet test

# Verify behavior that needs a real device (attach a dongle first)
tools/test-verify.sh

# Create NuGet packages
dotnet pack --configuration Release

# Or use the convenience script
./build.sh

The test suite covers only hardware-independent components, so it needs no dongle and no librtlsdr installation. Everything that depends on a device is checked by tools/HwVerify instead, which opens the first device, prints PASS/FAIL/SKIP per check, and exits nonzero if anything failed.

tools/test-verify.sh runs the harness between two device health checks, which is the recommended form: a dongle that has been heavily cycled stops sustaining a stream while still opening, and that looks exactly like a code regression. See the Contributing Guide for what the harness changes on the device and how to add checks.

Build Output

The build process creates:

  • NuGet packagesartifacts/packages/
  • Library binariesartifacts/binaries/RtlSdrManager/
  • Sample binariesartifacts/binaries/Samples/

Running Samples

# Using the convenience script
./runsample.sh

# Or manually
dotnet run --project samples/RtlSdrManager.Samples

Architecture

rtlsdr-manager/
├── src/
│   └── RtlSdrManager/           # Main library
│       ├── Exceptions/          # Custom exception types
│       ├── Hardware/            # Hardware type definitions
│       ├── Interop/             # P/Invoke wrappers
│       └── Modes/               # Enumeration types
├── tests/
│   └── RtlSdrManager.Tests/     # xUnit test suite
├── tools/                       # Contributor tooling; most of it needs a dongle
│   ├── HwVerify/                # Verification harness
│   ├── HwHealth/                # Sustained-delivery health probe
│   ├── HwStress/                # Cycling stress tool
│   ├── HwCommon/                # Helpers shared by the above
│   ├── build-native.sh          # Build a native librtlsdr to test against
│   └── test-*.sh                # Procedures composing the tools above
├── patches/                     # Patches against the native library
├── samples/
│   └── RtlSdrManager.Samples/   # Example applications
└── docs/                        # Documentation

System Requirements

  • .NET Runtime — 10.0 or later
  • Operating System — Windows, Linux, macOS
  • Hardware — RTL-SDR compatible device (RTL2832U-based)
  • Native Library — librtlsdr installed on the system (2.x; 2.0.3 or later recommended)

Supported Devices

This library supports RTL-SDR devices with the following tuners:

Tuner Frequency Range Notes
Elonics E4000 52 -- 2200 MHz No longer manufactured. Often cannot lock near 1100 -- 1250 MHz; the exact gap varies by device
Rafael Micro R820T 24 -- 1766 MHz Most common, excellent performance
Rafael Micro R828D 24 -- 1766 MHz Similar to R820T
Fitipower FC0012 22 -- 948.6 MHz Basic performance
Fitipower FC0013 22 -- 1100 MHz Basic performance
FCI FC2580 146 -- 308 MHz, 438 -- 924 MHz Good performance

Known Limitations

Ending an asynchronous reading can terminate the process

Ending an asynchronous reading releases the device's transfer buffers before every canceled transfer has finished reporting. If one reports afterwards, it writes into released memory and the process dies abruptly, with no managed exception to catch. This is a defect in the native library, not in this one.

It has two shapes, depending on what happens first: a late transfer completion faulting on the USB event thread, or CloseManagedDevice() tearing the device down while transfers are still outstanding.

Use one reading per process, and retune while it runs. Changing CenterFrequency during a reading is safe and needs no stop, so a scanner or sweep should start once, retune between measurements, and stop once at the end:

device.StartReadSamplesAsync();

foreach (var frequency in frequencies)
{
    device.CenterFrequency = frequency;
    device.ResetDeviceBuffer();
    Analyse(frequency, device.GetSamplesFromAsyncBuffer(16 * 1024));
}

device.StopReadSamplesAsync();

Do not stop and start repeatedly, and do not close and reopen the device between readings. Both reach the defect readily, so neither is a way around the other.

Unaffected: ReadSamples(), the synchronous API, on every platform; and a process that streams once and exits, which is the usual shape.

Observed on macOS. The native code path lacks the same protection on Linux, so it is likely affected there too, and Windows has a partial mitigation that may make it less exposed. A fix has been submitted to steve-m/librtlsdr; once a corrected librtlsdr is released, upgrading it removes the problem.

Contributing

Contributions are welcome. Please read the Contributing Guide for development setup, coding standards, and the pull request process. This project follows the Contributor Covenant Code of Conduct.

License

RTL-SDR Manager Library for .NET is free software, released under the GNU General Public License v3.0 or later.

Acknowledgments

Contact

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

v0.8.1 (2026-09-06):

           CHANGED:
           - The guidance for asynchronous reading was wrong and is corrected. It
             recommended closing and reopening the device between readings; that does not
             avoid the defect below, because closing is one of the two ways it manifests.
             Use one reading per process, retuning CenterFrequency while it runs.
           - The event-based example processed samples inside the SamplesAvailable handler.
             That handler runs on the driver's callback thread, where slow work stalls the
             transfer pipeline and costs samples, so the example now hands the batch off and
             processes it elsewhere.

           ADDED:
           - Trim and Native AOT compatibility. The library can now be used from
             AOT-published and trimmed applications without trim warnings. No effect on
             other consumers.
           - Worked examples for the APIs added in 0.8.0, which shipped without any. The
             samples now cover opening a device by serial number, the reachable frequency
             range, and the transfer buffer count, alongside the dropped-sample counter and
             the captured asynchronous error.

           KNOWN LIMITATION:
           - Ending an asynchronous reading can terminate the process. The native library
             releases its transfer buffers before every canceled transfer has reported, so a
             late completion writes into released memory. Use one reading per process and
             retune CenterFrequency while it runs; do not stop and restart, and do not close
             and reopen between readings, as both reach the defect. ReadSamples() and a
             process that streams once are unaffected. Observed on macOS; a fix has been
             submitted upstream.

           See CHANGELOG.md for complete details and previous releases.