core_rtlsdr 0.0.1
core_rtlsdr: ^0.0.1 copied to clipboard
Testable radio engine for RTL-SDR on Android/Flutter: tuning, streaming, demodulation, gain, squelch, stereo/RDS, spectrum, recording, band scan and presets as ChangeNotifier controllers on top of the [...]
core_rtlsdr #
Testable radio engine for RTL-SDR (RTL2832U dongles) on Android/Flutter,
built on top of the driver_rtlsdr
plugin (source).
driver_rtlsdr exposes the native core as raw FFI/USB primitives — tuning,
streaming, gain, stats — and deliberately has no opinion on session logic or
UI. core_rtlsdr is the layer in between: a set of ChangeNotifier
controllers (tuning, streaming, demodulation, gain, squelch, stereo, RDS,
spectrum, recording, band scan, presets) that turn those primitives 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, no
emulator, and no Android device at all.
Why this exists #
Every app built directly on driver_rtlsdr (or on raw FFI) ends up
reimplementing the same ~1000 lines of controller logic: a stats-polling
timer with correct bytesPerSecond bookkeeping, a stereo-pilot-aware RDS
poller, a band scanner that samples RF level fast enough to not miss a
station, a presets store. That logic is genuinely reusable — it doesn't
depend on dart:ffi, doesn't depend on Android, and shouldn't have to be
re-tested by hand on a real dongle every time it changes. core_rtlsdr
extracts it once, tests it against a fake, and lets every consumer (this
package's own example/, and — the point of this package — a future
widget_rtlsdr UI library) depend on the same tested implementation.
What this package provides #
RtlSdrDriver: the seam. An interface covering everything a radio session needs from the native core, with plain-Dart models (RadioStats,RdsInfo) instead of raw FFI structs.NativeRtlSdrDriveris the real, Android-only implementation (a thin adapter overdriver_rtlsdr'sNativeBindings). Every controller below depends on this interface, never on FFI directly.RadioController: tuning, streaming, demodulation mode (WFM/NFM/AM), gain (auto/manual), squelch, stereo toggle, live stats (IQ throughput, RF/audio level, ring buffer overflow). OwnsspectrumControllerandrdsControllerand starts/stops them together with streaming.SpectrumController: higher-rate polling (~25 fps default) of the captured band's spectrum —getSpectrumDb, ready to plot.RdsController: PI/PTY/TP/TA/PS/RadioText decoding, only meaningful once the stereo pilot is locked and RDS is enabled.RecordingController+defaultRecordingPath: records the demodulated PCM to a WAV file at a path you choose (a ready-made timestamped path under app-specific external storage is one call away).ScanController: sweeps a frequency range sampling RF level fast enough to catch a station in a ~70 ms step, mode/band agnostic (works for NFM/PMR scanning exactly like commercial WFM).PresetsController+PresetsRepository(SharedPreferencesPresetsRepository,InMemoryPresetsRepository): save/recall a frequency+mode+gain combination, storage backend swappable via the repository interface.UsbState/UsbChannel/DemodMode: re-exported fromdriver_rtlsdrso a consumer only ever needs to depend on this one package.
What this package deliberately does NOT provide #
- UI: zero widgets, same stance as
driver_rtlsdr.example/builds a plain Material UI directly against these controllers to prove they're sufficient on their own — a richer, reusable widget library (waterfall view, draggable spectrum tuner, themed panels) is exactly the job of a futurewidget_rtlsdrpackage (see below). - A foreground service: keeping the process alive in the background
during streaming is a UX decision for each app. Use
RadioController(driver, onStreamingStarted: ..., onStreamingStopped: ...)to hook your own in. - Where to save recordings:
RecordingController.startRecordingtakes an absolute path;defaultRecordingPathis a convenience, not a requirement.
Installation #
dependencies:
core_rtlsdr: ^0.0.1
core_rtlsdr depends on driver_rtlsdr
(pulled in transitively — no need to depend on it directly), which is
Android-only and requires minSdk = 26. See driver_rtlsdr's own README
for the Android manifest integration (USB auto-open intent filter);
example/android/ already has
it wired up as a reference.
Usage #
// 1. USB permission flow (unchanged from driver_rtlsdr).
final usbState = UsbState();
final usbChannel = UsbChannel(state: usbState);
await usbChannel.refreshConnectedDevices();
// ... requestPermission(), listen for usbState.status == deviceReady ...
// 2. Once ready, build a driver and the controllers around it.
final driver = NativeRtlSdrDriver();
final radio = RadioController(driver);
radio.setFrequencyHz(101500000); // 101.5 MHz
radio.setDemodMode(DemodMode.wfm);
radio.startStreaming();
// 3. Listen like any ChangeNotifier (works with `provider`, `ListenableBuilder`, etc.)
radio.addListener(() {
print('RF level: ${radio.rfLevelDbfs} dBFS');
print('RDS: ${radio.rdsController.info.programService}');
});
See example/ for a complete, runnable app: USB permission → tuning →
mode/gain/squelch → stereo/RDS → spectrum → recording → band scan →
presets.
Testing without hardware #
Every controller depends on RtlSdrDriver, never on FFI directly, so
package:core_rtlsdr/testing.dart exports FakeRtlSdrDriver — a pure-Dart,
in-memory implementation you can poke directly in a test:
import 'package:core_rtlsdr/core_rtlsdr.dart';
import 'package:core_rtlsdr/testing.dart';
test('scan finds a hit above the threshold', () async {
final driver = FakeRtlSdrDriver(
signalLevelForFrequency: (hz) => hz == 101500000 ? -10.0 : -90.0,
);
final radio = RadioController(driver)..startStreaming();
final scanner = ScanController(settleDelay: Duration.zero, sampleGap: Duration.zero);
await scanner.startScan(radio, startHz: 101000000, endHz: 102000000, stepHz: 100000);
expect(scanner.results.map((h) => h.frequencyHz), contains(101500000));
});
This is the same reason driver_rtlsdr only unit-tests FFI struct layout
on the host and leaves everything else to on-device integration_test —
except here, because the actual radio logic lives above the FFI seam
instead of being entangled with it, that logic gets full unit test
coverage without ever touching a device. See test/ in this package (44+
tests) for the full suite, and example/integration_test/ for the
on-device check that NativeRtlSdrDriver actually reaches
libnative_rtlsdr.so.
Building widget_rtlsdr on top of this package #
This package exists to make that next package straightforward. A UI layer
built on core_rtlsdr should:
- Depend only on
core_rtlsdr(never ondriver_rtlsdrordart:ffidirectly) — every native concept it needs (frequency, stats, RDS, spectrum bins, demod mode) already has a plain-Dart shape here. - Take the controllers it needs as constructor parameters (
RadioController,SpectrumController,ScanController, ...) rather than constructing them — that's what lets an app compose them withprovider/riverpod/ whatever it already uses, exactly likeexample/lib/app.dartdoes. - Test against
FakeRtlSdrDriver(package:core_rtlsdr/testing.dart), so its own CI never needs an emulator either — reserve on-deviceintegration_testfor the one thing that actually requires a device: confirming widgets render correctly against a live-ish stream of updates. - Look at
example/lib/widgets/in this package for the data each section needs (e.g.SpectrumBarsshows whatSpectrumController.binslooks like to consume) — deliberately built with plainContainers/Sliders/ListTiles, leaving the actual design system, waterfall/canvas rendering, and theming towidget_rtlsdr.
Tests #
test/— pure Dart/Flutter unit tests, run on the host (no Android or dongle needed): every controller againstFakeRtlSdrDriver,Preset/SharedPreferencesPresetsRepository. Run with:flutter test.example/test/— a widget test of the example app's initial (no-device) state.example/integration_test/— runs on a real Android device/emulator; confirmsNativeRtlSdrDriverreacheslibnative_rtlsdr.soand a real FFI call round-trips, without needing a dongle physically connected. Run with:cd example && flutter test integration_test.- Validation against real hardware: inherited from
driver_rtlsdr— see that package's README and../../app/flutter/rtl-sdr mobile/docs/how-it-was-built.mdfor the results of validating the underlying native core against a real RTL2838U dongle. This package's own controller logic (stats math, scan stepping, RDS string decoding, preset round-tripping) is covered by the unit tests above and doesn't require re-validation on hardware when it changes — only the native core does.
License #
GPLv2, or (at your option) any later version — see LICENSE.
This package depends on driver_rtlsdr, which links librtlsdr (GPLv2),
requiring that any software using it be distributed under the GPL — hence
the same choice here.
Contributing #
Contributions are welcome! See CONTRIBUTING.md for how to set up your environment, coding conventions, and the PR process.