core_rtlsdr library
Testable radio engine for RTL-SDR on Android/Flutter.
driver_rtlsdr (this package's dependency) exposes the native core as
raw FFI/USB primitives and deliberately has no opinion on session logic
or UI. core_rtlsdr is the layer in between: RtlSdrDriver gives every
piece of native state (tuning, streaming, gain, stats, RDS, spectrum,
recording) a plain-Dart shape, and a set of ChangeNotifier controllers
— RadioController, SpectrumController, RdsController,
RecordingController, ScanController, PresetsController — turn that
into ready-to-use radio behavior, the same shape validated in the
rtl-sdr mobile reference app, but decoupled from any one app and unit
-testable on a host with no dongle or emulator (see
package:core_rtlsdr/testing.dart).
Building a UI on top of this package
- UsbState + UsbChannel (re-exported from
driver_rtlsdr): detect the dongle, request permission, listen fordeviceReady. - Once ready, create a NativeRtlSdrDriver and a RadioController around it. RadioController owns a SpectrumController and RdsController and starts/stops them with streaming.
- Optionally add ScanController (band scan) and PresetsController (backed by SharedPreferencesPresetsRepository or your own PresetsRepository).
- A widget layer (e.g. a future
widget_rtlsdr) only ever needs to depend on this package — never ondriver_rtlsdrordart:ffidirectly — and can be built/tested againstpackage:core_rtlsdr/testing.dart'sFakeRtlSdrDriverwith no Android device at all.
See example/ for a minimal, fully working app exercising every
controller, and this package's README for the full picture.
Classes
- InMemoryPresetsRepository
- Non-persistent PresetsRepository — presets live only for the process lifetime. Useful for tests, and for apps that don't want persistence.
- NativeRtlSdrDriver
-
RtlSdrDriver backed by the real native core —
driver_rtlsdr'sNativeBindings, FFI intolibnative_rtlsdr.so. Only usable on Android, and only after the USB permission flow has produced adeviceReadyevent (seeUsbState/UsbChannel, re-exported fromcore_rtlsdr.dart). - Preset
- A saved frequency/mode/gain combination, for quick recall.
- PresetsController
- User-managed list of Presets, backed by a PresetsRepository.
- PresetsRepository
- Persists the list of user Presets. PresetsController depends on this interface rather than a concrete storage mechanism, so it can be unit-tested with InMemoryPresetsRepository and swapped for a different backend (e.g. a database) without touching controller code.
- RadioController
-
Controls tuning/streaming/demodulation/gain of the dongle through an
RtlSdrDriver — once the native driver has already opened the device
(
deviceReady, seeUsbState/UsbChannel). - RadioStats
-
One statistics snapshot from the native driver — decoupled from the raw
ShimStatsFFI struct so callers (and fakes) never needdart:ffi. - RdsController
-
Polls RtlSdrDriver.getRdsInfo (PI/PTY/TP/TA/PS/RadioText) — only
meaningful when info's
syncLockedis true (WFM, stereo pilot locked, RDS enabled — see RadioController.setStereoEnabled/setEnabled). - RdsInfo
- One RDS snapshot (PI/PTY/TP/TA/PS/RadioText) — only meaningful when syncLocked (WFM, stereo pilot locked, RDS enabled).
- RecordingController
- Records the same PCM already going to the speaker to a WAV file, via RtlSdrDriver.startRecording/stopRecording — the tap happens inside the native DSP thread; this only orchestrates the file path and state for the UI.
- RtlSdrDriver
-
Everything a radio session needs from the native RTL-SDR core — the seam
the rest of
core_rtlsdris built against, instead of every controller callingdriver_rtlsdr'sNativeBindings(raw FFI) directly. - ScanController
-
Scans a frequency range looking for active stations: tunes each step,
waits for the tuner to settle (AGC/PLL), and measures the RF level (via
RadioController.sampleRfLevelDbfsNow, not the already-cached
rfLevelDbfs— that only updates every RadioController's stats interval, too slow to stay fresh against a scan step of a few tens of milliseconds) — above the threshold becomes a ScanHit. - ScanHit
- A frequency with an RF signal above the threshold, found during a scan.
-
Persists the preset list as JSON in a single
shared_preferencesvalue — a short list of favorites doesn't need a real database. - SpectrumController
- Polls RtlSdrDriver.getSpectrumDb at a higher rate than RadioController's general stats (~25fps vs ~2fps) — kept as its own ChangeNotifier so a waterfall/spectrum widget can listen to just this and avoid rebuilding the rest of a radio panel every frame.
- UsbChannel
-
Bridge to the Kotlin side: requests/reads the RTL-SDR dongle's USB
permission state via MethodChannel and listens for attach/detach/
permission events via EventChannel. Kotlin
(
DriverRtlsdrPlugin/UsbAttachReceiver) is the only part of the driver that can talk toandroid.hardware.usb.UsbManager. - UsbDeviceInfo
- UsbState
- Observable state of the USB connection with the RTL-SDR dongle.
Enums
- DemodMode
-
Mirrors
demod_mode_tfrom rtlsdr_shim.h — the numeric values matter (passed directly toshim_set_demod_modevia FFI). - UsbConnectionStatus
-
Connection status with the RTL-SDR dongle, mirroring the lifecycle of
Android's USB permission flow (
UsbManager).
Functions
-
buildIqRecordingFileName(
{required int frequencyHz, DateTime? now}) → String -
Builds the
rtlsdr_iq_<timestamp>_<freq>MHz.cu8file name used by defaultIqRecordingPath — split out as a pure function (nodart:io) so it's unit-testable without a platform to resolve a directory against..cu8matches the interleaved 8-bit unsigned I/Q formatrtl_sdr/GNU Radio/gqrx use for raw captures. -
buildRecordingFileName(
{required int frequencyHz, required DemodMode mode, DateTime? now}) → String -
Builds the
rtlsdr_<timestamp>_<freq>MHz_<mode>.wavfile name used by defaultRecordingPath — split out as a pure function (nodart:io) so it's unit-testable without a platform to resolve a directory against. -
defaultIqRecordingPath(
{required int frequencyHz, String subdirectory = 'Recordings'}) → Future< String> -
Builds a timestamped raw I/Q path under app-specific external storage
(
<external>/[subdirectory]/rtlsdr_iq_<timestamp>_<freq>MHz.cu8), creating the directory if needed. Raw I/Q capture happens before demodulation, so unlike defaultRecordingPath there is no demod mode to embed in the name. -
defaultRecordingPath(
{required int frequencyHz, required DemodMode mode, String subdirectory = 'Recordings'}) → Future< String> -
Builds a timestamped WAV path under app-specific external storage
(
<external>/[subdirectory]/rtlsdr_<timestamp>_<freq>MHz_<mode>.wav), creating the directory if needed. A convenience for the common case — apps with different naming/location needs can build their own path and call RecordingController.startRecording directly instead.