core_rtlsdr 0.0.1 copy "core_rtlsdr: ^0.0.1" to clipboard
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 #

License: GPL v2 or later

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. NativeRtlSdrDriver is the real, Android-only implementation (a thin adapter over driver_rtlsdr's NativeBindings). 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). Owns spectrumController and rdsController and 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 from driver_rtlsdr so 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 future widget_rtlsdr package (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.startRecording takes an absolute path; defaultRecordingPath is 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:

  1. Depend only on core_rtlsdr (never on driver_rtlsdr or dart:ffi directly) — every native concept it needs (frequency, stats, RDS, spectrum bins, demod mode) already has a plain-Dart shape here.
  2. Take the controllers it needs as constructor parameters (RadioController, SpectrumController, ScanController, ...) rather than constructing them — that's what lets an app compose them with provider/riverpod/ whatever it already uses, exactly like example/lib/app.dart does.
  3. Test against FakeRtlSdrDriver (package:core_rtlsdr/testing.dart), so its own CI never needs an emulator either — reserve on-device integration_test for the one thing that actually requires a device: confirming widgets render correctly against a live-ish stream of updates.
  4. Look at example/lib/widgets/ in this package for the data each section needs (e.g. SpectrumBars shows what SpectrumController.bins looks like to consume) — deliberately built with plain Containers/ Sliders/ListTiles, leaving the actual design system, waterfall/canvas rendering, and theming to widget_rtlsdr.

Tests #

  • test/ — pure Dart/Flutter unit tests, run on the host (no Android or dongle needed): every controller against FakeRtlSdrDriver, 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; confirms NativeRtlSdrDriver reaches libnative_rtlsdr.so and 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.md for 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.

0
likes
0
points
61
downloads

Publisher

unverified uploader

Weekly Downloads

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 driver_rtlsdr plugin. Built so UI packages (e.g. widget_rtlsdr) never touch FFI directly.

Repository (GitHub)
View/report issues

Topics

#android #radio #sdr #ffi #hardware

License

unknown (license)

Dependencies

driver_rtlsdr, ffi, flutter, path_provider, shared_preferences

More

Packages that depend on core_rtlsdr