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

  1. UsbState + UsbChannel (re-exported from driver_rtlsdr): detect the dongle, request permission, listen for deviceReady.
  2. Once ready, create a NativeRtlSdrDriver and a RadioController around it. RadioController owns a SpectrumController and RdsController and starts/stops them with streaming.
  3. Optionally add ScanController (band scan) and PresetsController (backed by SharedPreferencesPresetsRepository or your own PresetsRepository).
  4. A widget layer (e.g. a future widget_rtlsdr) only ever needs to depend on this package — never on driver_rtlsdr or dart:ffi directly — and can be built/tested against package:core_rtlsdr/testing.dart's FakeRtlSdrDriver with 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's NativeBindings, FFI into libnative_rtlsdr.so. Only usable on Android, and only after the USB permission flow has produced a deviceReady event (see UsbState/UsbChannel, re-exported from core_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, see UsbState/UsbChannel).
RadioStats
One statistics snapshot from the native driver — decoupled from the raw ShimStats FFI struct so callers (and fakes) never need dart:ffi.
RdsController
Polls RtlSdrDriver.getRdsInfo (PI/PTY/TP/TA/PS/RadioText) — only meaningful when info's syncLocked is 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_rtlsdr is built against, instead of every controller calling driver_rtlsdr's NativeBindings (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.
SharedPreferencesPresetsRepository
Persists the preset list as JSON in a single shared_preferences value — 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 to android.hardware.usb.UsbManager.
UsbDeviceInfo
UsbState
Observable state of the USB connection with the RTL-SDR dongle.

Enums

DemodMode
Mirrors demod_mode_t from rtlsdr_shim.h — the numeric values matter (passed directly to shim_set_demod_mode via 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.cu8 file name used by defaultIqRecordingPath — split out as a pure function (no dart:io) so it's unit-testable without a platform to resolve a directory against. .cu8 matches the interleaved 8-bit unsigned I/Q format rtl_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>.wav file name used by defaultRecordingPath — split out as a pure function (no dart: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.