core_rtlsdr 0.2.0 copy "core_rtlsdr: ^0.2.0" to clipboard
core_rtlsdr: ^0.2.0 copied to clipboard

PlatformAndroid

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 #

pub package pub points pub likes CI 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/USB/LSB), gain (auto/manual), squelch, stereo toggle, live stats (IQ throughput, RF/audio level, ring buffer overflow, PCM/I·Q recording bytes written). 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/defaultIqRecordingPath: records the demodulated PCM to a WAV file, and/or the raw interleaved I/Q (pre-demodulation, .cu8) to its own file — independent of each other, at a path you choose (ready-made timestamped paths under app-specific external storage are one call away), or straight into the public Downloads folder via startRecordingToDownloads/ startIqRecordingToDownloads (Android MediaStore, API 29+, with an automatic fallback to app-specific storage below that). shareRecording/ shareIqRecording hand the last completed recording to Android's native share sheet either way.
  • 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.2.0

core_rtlsdr depends on driver_rtlsdr (pulled in transitively — no need to depend on it directly), which is Android-only. See "Android setup" below for the two things your app needs beyond pubspec.yaml.

Android setup #

  1. minSdk = 26 in your app's android/app/build.gradle.kts — required by the Oboe/AAudio low-latency audio path driver_rtlsdr uses internally:

    android {
        defaultConfig {
            minSdk = maxOf(flutter.minSdkVersion, 26)
        }
    }
    
  2. USB auto-open intent filter, on your launcher <activity> in android/app/src/main/AndroidManifest.xml — this is what makes Android offer to open your app automatically when the dongle is plugged in (optional, but the whole point of a USB-OTG radio app):

    <activity ...>
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
        <intent-filter>
            <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
        </intent-filter>
        <meta-data
            android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
            android:resource="@xml/device_filter" />
    </activity>
    

    @xml/device_filter (the VID/PID list of supported dongles) and the android.hardware.usb.host permission both come from driver_rtlsdr's own manifest, merged into your build automatically by Gradle's resource merger — there's nothing to copy. See example/android/app/src/main/AndroidManifest.xml in this repo for a working reference.

That's it — no native build step of your own, no CMake/NDK configuration: driver_rtlsdr's Gradle plugin handles compiling the vendored native core for you the first time you flutter pub get/build.

Usage #

Every snippet below assumes:

import 'package:core_rtlsdr/core_rtlsdr.dart';

They build on each other — sections 1 and 2 are the minimum to get audio out of the speaker; everything else is additive and independent of each other (skip to whichever you need).

1. Detect the dongle and wait for it to be ready #

The native driver isn't usable until Android has granted USB permission and driver_rtlsdr's Kotlin side has opened the device — that's the deviceReady event on [UsbState].

final usbState = UsbState();
final usbChannel = UsbChannel(state: usbState);

// Call once, e.g. in initState()/on app start — covers the case where the
// dongle is already plugged in when your app opens (there's no ATTACHED
// intent to catch in that case).
await usbChannel.refreshConnectedDevices();

usbState.addListener(() {
  switch (usbState.status) {
    case UsbConnectionStatus.noDevice:
      print('No RTL-SDR dongle connected.');
    case UsbConnectionStatus.attached:
      usbChannel.requestPermission(); // a dongle showed up — ask for permission
    case UsbConnectionStatus.permissionRequested:
      print('Waiting for the permission dialog...');
    case UsbConnectionStatus.permissionDenied:
      print('User denied USB permission.');
    case UsbConnectionStatus.permissionGranted:
      print('Permission granted, native driver opening...');
    case UsbConnectionStatus.deviceReady:
      onDeviceReady(); // only now is it safe to construct NativeRtlSdrDriver()
  }
  if (usbState.lastError != null) print('USB error: ${usbState.lastError}');
});

// Don't forget, when the screen/app is done with it:
// usbChannel.dispose();

2. Tune and start streaming #

late final RadioController radio;

void onDeviceReady() {
  radio = RadioController(NativeRtlSdrDriver());

  radio.setFrequencyHz(101500000); // 101.5 MHz
  radio.setDemodMode(DemodMode.wfm);
  radio.startStreaming();

  radio.addListener(() {
    print('${(radio.frequencyHz / 1e6).toStringAsFixed(1)} MHz  '
        'RF: ${radio.rfLevelDbfs.toStringAsFixed(1)} dBFS  '
        '${(radio.bytesPerSecond / 1e6).toStringAsFixed(2)} MB/s');
    if (radio.lastError != null) print('Radio error: ${radio.lastError}');
  });
}

// Call when leaving the screen / shutting the radio session down.
void dispose() {
  radio.dispose(); // stops streaming if still running
}

RadioController polls stats on a timer (every 500ms by default — statsInterval is a constructor parameter) and calls notifyListeners() on every tick — use it with provider's ChangeNotifierProvider, ListenableBuilder, or addListener directly, as above. See section 10 for the full provider composition.

3. Demodulation mode and squelch #

// WFM (commercial FM, stereo+RDS capable), NFM (narrowband — PMR/ham/etc.),
// AM, or USB/LSB (single sideband — ham/shortwave). Switching mode while
// streaming restarts it automatically to apply immediately — no need to
// call stopStreaming()/startStreaming() yourself.
radio.setDemodMode(DemodMode.nfm);
// radio.setDemodMode(DemodMode.usb); // or .lsb — single sideband

// Squelch applies to NFM/AM/USB/LSB — WFM (commercial broadcast) doesn't use it.
if (radio.demodMode.supportsSquelch) {
  radio.setSquelchThresholdDb(-40); // dBFS
  print(radio.squelchOpen ? 'Signal present' : 'Squelched');
}

4. Gain: automatic or manual #

// Fetch the gains this specific tuner supports (tenths of a dB) — call
// once after deviceReady; the list doesn't change at runtime.
radio.refreshGainList(); // populates radio.gainList

radio.setGainAuto(true); // AGC (the default)

// Or pick a manual gain from radio.gainList (e.g. the strongest available):
radio.setGainTenthDb(radio.gainList.last); // switches gainAuto off automatically
print('Gain: ${radio.gainTenthDb / 10} dB');

5. Stereo and RDS (WFM only) #

radio.setStereoEnabled(true); // on by default

radio.addListener(() {
  if (radio.demodMode == DemodMode.wfm) {
    print(radio.stereoLocked ? 'Stereo (pilot locked)' : 'Mono');
  }
});

// RDS decoding is a separate controller, owned by RadioController and
// started/stopped automatically together with streaming.
radio.rdsController.setEnabled(true);
radio.rdsController.addListener(() {
  final info = radio.rdsController.info;
  if (info.syncLocked) {
    print('${info.programService} — ${info.radioText}'); // e.g. "BBC R1 — Now playing..."
  }
});

6. Spectrum data (for plotting) #

// Also owned by RadioController, started/stopped with streaming. Polls
// faster than the main stats (~25fps by default) — kept as its own
// ChangeNotifier so a waterfall/spectrum widget can listen to just this
// and skip rebuilding the rest of a radio panel every frame.
radio.spectrumController.addListener(() {
  final List<double> bins = radio.spectrumController.bins; // dB; bin 0 = lower band edge
  // Feed `bins` to a chart/canvas — see
  // example/lib/widgets/spectrum_bars.dart for a minimal bar-chart
  // consumer, or build a waterfall view on top of it.
});

7. Recording to a WAV file #

final recording = RecordingController(radio.driver); // same driver instance as radio

Future<void> startRecording() async {
  final path = await defaultRecordingPath(
    frequencyHz: radio.frequencyHz,
    mode: radio.demodMode,
  ); // .../Android/data/<pkg>/files/Recordings/rtlsdr_<timestamp>_<freq>MHz_<mode>.wav
  await recording.startRecording(path);
}

void stopRecording() => recording.stopRecording();

recording.addListener(() {
  if (recording.isRecording) {
    print('Recording to ${recording.currentFilePath}');
  } else if (recording.lastRecordingPath != null) {
    print('Saved ${recording.lastRecordingPath}');
  }
});

defaultRecordingPath is a convenience — pass any absolute path you like to startRecording instead (e.g. one resolved via your own path_provider/file_picker logic, or a user-chosen location).

8. Raw I/Q recording #

// Same RecordingController as above — dumps the interleaved 8-bit unsigned
// I/Q exactly as the dongle sends it (the ".cu8" convention rtl_sdr/GNU
// Radio/gqrx use), tapped before decimation/demodulation. Independent of
// startRecording/stopRecording — both can run at once.
Future<void> startIqRecording() async {
  final path = await defaultIqRecordingPath(
    frequencyHz: radio.frequencyHz,
  ); // .../Android/data/<pkg>/files/Recordings/rtlsdr_iq_<timestamp>_<freq>MHz.cu8
  await recording.startIqRecording(path);
}

void stopIqRecording() => recording.stopIqRecording();

recording.addListener(() {
  if (recording.isIqRecording) {
    print('I/Q recording to ${recording.currentIqFilePath}');
  } else if (recording.lastIqRecordingPath != null) {
    print('I/Q saved ${recording.lastIqRecordingPath}');
  }
});

// Bytes written so far, polled the same way as the PCM recording:
radio.addListener(() => print('${radio.iqRecordingBytesWritten} bytes'));

9. Recording to Downloads, and sharing #

// Same RecordingController, PCM or I/Q — records into the real, shared
// Downloads folder (Downloads/Recordings/...) via Android's MediaStore
// (API 29+) instead of app-specific storage, so the file shows up in the
// user's Files app / Downloads app without needing storage permissions.
// On Android below API 29 (no MediaStore.Downloads collection to insert
// into), this falls back to startRecording + defaultRecordingPath
// automatically — no error, no extra handling needed on your side.
await recording.startRecordingToDownloads(
  frequencyHz: radio.frequencyHz,
  mode: radio.demodMode,
);
await recording.startIqRecordingToDownloads(frequencyHz: radio.frequencyHz);

recording.stopRecording(); // or stopIqRecording() — same as before

// Share the last completed recording via Android's native share sheet.
// Works no matter which flow started it: a Downloads recording shares by
// its content:// URI directly, a plain-path recording is resolved to a
// shareable URI through a bundled FileProvider first.
await recording.shareRecording();
await recording.shareIqRecording();

RecordingController takes an optional downloadsChannel: constructor argument (defaults to DownloadsChannel(), re-exported from driver_rtlsdr) — swap in FakeDownloadsChannel from package:core_rtlsdr/testing.dart to unit-test any of the above without an Android device (see "Testing without hardware" below).

10. Band scanning #

final scanner = ScanController(); // defaults: commercial FM band, 100kHz steps, -25dB threshold

scanner.addListener(() {
  print('Scanning: ${(scanner.progress * 100).toStringAsFixed(0)}%');
});

// Streaming must already be started (see section 2).
await scanner.startScan(radio);
// Or a custom range/mode, e.g. a PMR band in NFM:
// radio.setDemodMode(DemodMode.nfm);
// await scanner.startScan(radio, startHz: 446000000, endHz: 446200000, stepHz: 6250, thresholdDb: -30);

for (final hit in scanner.results) {
  print('${(hit.frequencyHz / 1e6).toStringAsFixed(3)} MHz '
      'at ${hit.rfLevelDbfs.toStringAsFixed(1)} dBFS');
}

scanner.applyHit(radio, scanner.results.first); // tune to a hit
scanner.stopScan(); // cancel mid-scan

11. Presets (save/recall frequency + mode + gain) #

final presets = PresetsController(const SharedPreferencesPresetsRepository());
await presets.load();

await presets.add(Preset(
  name: 'Local NPR',
  frequencyHz: radio.frequencyHz,
  mode: radio.demodMode,
  gainAuto: radio.gainAuto,
  gainTenthDb: radio.gainTenthDb,
));

// Or straight from a scan hit:
scanner.saveHitAsPreset(presets, radio, scanner.results.first, 'Found station');

presets.applyTo(radio, presets.presets.first); // recall one
await presets.remove(presets.presets.first);

No persistence wanted (a quick prototype, or a test)? Use InMemoryPresetsRepository() instead — same PresetsController API, or implement PresetsRepository yourself against your own backend (a database, a server, ...).

12. Putting it all together with provider #

This is exactly the shape example/lib/app.dart uses — a composition root that wires everything up once, with screens/widgets reading it via context.watch/context.read:

import 'package:core_rtlsdr/core_rtlsdr.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => UsbState()),
        ProxyProvider<UsbState, UsbChannel>(
          update: (_, usbState, previous) => previous ?? UsbChannel(state: usbState),
          dispose: (_, channel) => channel.dispose(),
        ),
        Provider<RtlSdrDriver>(
          create: (_) => NativeRtlSdrDriver(),
          dispose: (_, driver) => driver.dispose(),
        ),
        ChangeNotifierProvider(
          create: (context) => RadioController(context.read<RtlSdrDriver>()),
        ),
        ChangeNotifierProvider(
          create: (context) => RecordingController(context.read<RtlSdrDriver>()),
        ),
        ChangeNotifierProvider(create: (_) => ScanController()),
        ChangeNotifierProvider(
          create: (_) => PresetsController(const SharedPreferencesPresetsRepository())..load(),
        ),
      ],
      child: MaterialApp(home: const HomeScreen()),
    );
  }
}

// Anywhere deeper in the widget tree:
class TuneButton extends StatelessWidget {
  const TuneButton({super.key});

  @override
  Widget build(BuildContext context) {
    final radio = context.watch<RadioController>(); // rebuilds on every stats tick
    return FilledButton(
      onPressed: () => context.read<RadioController>().startStreaming(),
      child: Text(radio.isStreaming ? 'Streaming' : 'Start'),
    );
  }
}

See example/lib/ for the full, working version of this — a USB status card plus a tuner card covering every section above, all wired through provider — that you can flutter run on a real device.

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));
});

A few more, showing common patterns:

// Simulate a native call failing and assert your error-handling path.
test('setFrequencyHz surfaces a native failure via lastError', () {
  final driver = FakeRtlSdrDriver()..failNextCall = true;
  final radio = RadioController(driver);

  radio.setFrequencyHz(101500000);

  expect(radio.lastError, isNotNull);
  expect(radio.frequencyHz, isNot(101500000)); // unchanged
});

// Drive RDS without a real broadcast — just set the fake's rdsInfo directly.
test('RdsController surfaces the program service once synced', () {
  final driver = FakeRtlSdrDriver()
    ..rdsInfo = const RdsInfo(
      syncLocked: true,
      piCode: 0x1234,
      pty: 10,
      tp: true,
      ta: false,
      programService: 'BBC R1',
      radioText: 'Now playing something',
      generation: 1,
    );
  final rds = RdsController(driver)..refresh();

  expect(rds.info.programService, 'BBC R1');
});

// Test a widget/controller that needs presets, with no real storage.
test('PresetsController works against InMemoryPresetsRepository', () async {
  final presets = PresetsController(InMemoryPresetsRepository());
  await presets.add(const Preset(
    name: 'Test station',
    frequencyHz: 101500000,
    mode: DemodMode.wfm,
    gainAuto: true,
    gainTenthDb: 0,
  ));

  expect(presets.presets, hasLength(1));
});

// Test Downloads-folder recording + sharing without an Android device —
// FakeDownloadsChannel tracks every call so you can assert on it directly.
test('startRecordingToDownloads uses the downloads channel', () async {
  final driver = FakeRtlSdrDriver();
  final downloads = FakeDownloadsChannel();
  final recording = RecordingController(driver, downloadsChannel: downloads);

  await recording.startRecordingToDownloads(
    frequencyHz: 101500000,
    mode: DemodMode.wfm,
  );

  expect(recording.isRecording, isTrue);
  expect(downloads.openCalls, hasLength(1));
  expect(downloads.openCalls.single.mimeType, 'audio/wav');
});

// failNextOpen simulates the legacy-Android fallback (pre-API 29, no
// MediaStore.Downloads collection) — startRecordingToDownloads falls back
// to app-specific storage automatically instead of surfacing an error.
test('startRecordingToDownloads falls back below API 29', () async {
  final driver = FakeRtlSdrDriver();
  final downloads = FakeDownloadsChannel()..failNextOpen = true;
  final recording = RecordingController(driver, downloadsChannel: downloads);

  await recording.startRecordingToDownloads(
    frequencyHz: 101500000,
    mode: DemodMode.wfm,
  );

  expect(recording.isRecording, isTrue); // still recording, just not via MediaStore
  expect(driver.recordingPath, isNotNull); // fell back to startRecording(path)
});

FakeRtlSdrDriver's state (rfLevelDbfs, rdsInfo, spectrumDb, gainList, failNextCall, ...) is all plain mutable fields — poke whatever the scenario needs directly, no mocking framework required. FakeDownloadsChannel (also from testing.dart) is the same idea for startRecordingToDownloads/shareRecording, tracking openCalls/ finishedFds/shareCalls instead of touching a real MediaStore.

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 (60+ 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
140
points
35
downloads

Documentation

API reference

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
Contributing

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