flutter_usb_serial_manager
ðĪ Android only. This plugin wraps Android's native USB Host API and does not support iOS, web, or desktop â see Supported platforms.
flutter_usb_serial_manager is a Flutter plugin for USB serial communication on Android â list connected USB devices, request USB host permission, open a serial connection at any baud rate, and read or write raw bytes over USB OTG. It also ships a built-in Modbus RTU soil-sensor helper (one-shot reads and live interval streams) for agriculture / IoT projects that talk to NPK, temperature, humidity, EC, salinity and pH probes over USB-to-RS485.
Whether you're building a USB serial terminal, a Modbus RTU sensor dashboard, a barcode/RFID reader integration, or any Android app that needs low-level USB-to-serial (CDC/FTDI/CH34x/CP210x) communication, this plugin gives you a small, typed, stream-friendly Dart API on top of native Android USB Host APIs.
Table of contents
- Features
- Supported platforms
- Screenshots
- Getting started
- Usage
- API reference
- Example app
- Troubleshooting / FAQ
- Roadmap
- Releasing
- Contributing
- License
- Credits
Features
- ð Device discovery â enumerate every USB device currently attached via USB OTG.
- ð Permission handling â check and request Android's runtime USB permission dialog.
- ð Connect/disconnect â open a serial connection to any device at a custom baud rate.
- ðĪðĨ Raw read & write â send and receive raw bytes over the serial connection.
- ðą Modbus RTU soil-sensor helper â one call to read temperature, humidity, EC, salinity, NPK and pH from a standard Modbus soil sensor, no manual frame-building required.
- ðĄ Live data streams â subscribe to a
Streamfor interval-based raw reads or soil-sensor samples instead of polling. - ð§Đ Typed Dart models â
UsbDevice,RawReadConfig,SoilSensorConfigwith sane defaults and IDE autocompletion. - ðŠķ Small surface area â a focused API instead of a heavyweight framework, easy to wrap in your own repository/BLoC layer.
Supported platforms
| Platform | Support | Notes |
|---|---|---|
| Android | â API 24+ | Uses Android's USB Host API â device must support USB OTG. |
| iOS | â Not supported | iOS does not expose generic USB-host/serial access to third-party apps without Apple's MFi program. Not currently planned. |
| Web / Desktop | â Not supported | Out of scope for this plugin. |
Screenshots
The example app included in this repo is a full "test console" that exercises every method in the plugin â device discovery, permissions, connection, raw read/write, Modbus soil-sensor reads, and live streams.
Device discovery & permission |
Connect, write & read |
Live Modbus soil-sensor stream |
Getting started
1. Add the dependency
dependencies:
flutter_usb_serial_manager: ^0.0.1
Then fetch it:
flutter pub get
2. Android setup (required)
This plugin talks to Android's USB Host API, so a couple of one-time additions are needed in your app's android/app/src/main/AndroidManifest.xml (not the plugin's).
-
Declare the USB host feature â add this as a direct child of
<manifest>:<uses-feature android:name="android.hardware.usb.host" android:required="false" />required="false"keeps your app installable on devices without USB host support; only devices that actually plug in a USB accessory will exercise this plugin. -
(Optional, recommended) Auto-launch on device attach. Add an intent filter to your launcher
<activity>so Android offers to open your app the moment a USB device is plugged in, instead of requiring the user to open it first:<activity android:name=".MainActivity" ...> <!-- your existing intent-filters --> <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>and create
android/app/src/main/res/xml/device_filter.xml:<?xml version="1.0" encoding="utf-8"?> <resources> <usb-device /> </resources>An empty
<usb-device />matches every USB device, which is convenient for development. For production, narrow it down to your target hardware'svendor-id/product-id(you can read these fromUsbDevice.vendorIdviagetDeviceList()). -
Minimum SDK. Make sure
minSdkVersion(minSdkinandroid/app/build.gradle.kts) is 24 or higher.
Note on native dependency resolution: this plugin depends on a native Android library published on JitPack, and already declares that repository for its own build. If your app centralizes repositories with
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) }inandroid/settings.gradle.kts, addmaven { url = uri("https://jitpack.io") }there as well so Gradle is allowed to resolve it.
3. iOS
Not supported â see Supported platforms. Calling any method on iOS will throw a MissingPluginException.
Usage
Import the package:
import 'package:flutter_usb_serial_manager/flutter_usb_serial_manager.dart';
import 'package:flutter_usb_serial_manager/modals/usb_device.dart';
import 'package:flutter_usb_serial_manager/modals/raw_read_config.dart';
import 'package:flutter_usb_serial_manager/modals/soil_sensor_config.dart';
final usb = FlutterUsbSerialManager();
List devices & connect
// 1. Discover attached USB devices.
final devices = await usb.getDeviceList();
final device = devices.first;
// 2. Ask the user for permission (shows Android's system dialog).
final granted = await usb.requestUsbPermission(device);
if (!granted) return;
// 3. Open the serial connection.
final connected = await usb.connect(device, baudRate: 9600);
print('Connected: $connected');
Write and read raw bytes
await usb.write('Hello device');
final response = await usb.read(
config: const RawReadConfig(bufferSize: 1024, timeout: 1000),
);
print('Received: $response');
Raw
write/readonly make sense for devices that understand plain bytes you send them (e.g. a microcontroller echoing text). Modbus RTU devices â like the soil sensor helper below â expect a specific binary frame, so usereadSoilDatafor those instead.
Modbus soil-sensor helper
One-shot read:
final sample = await usb.readSoilData(
config: const SoilSensorConfig(
slaveId: 1,
startAddress: 0x0000,
registerCount: 8,
responseDelayMs: 300,
),
);
print(sample);
// {temperatureC: 24.3, humidityPercent: 41.0, ecUsCm: 210.0,
// salinityMgL: 120.0, nitrogenMgKg: 30.0, phosphorusMgKg: 12.0,
// potassiumMgKg: 18.0, ph: 6.8}
Continuous stream:
await usb.onReadSoilDataInterval(intervalMs: 1000);
final subscription = usb.soilDataStream.listen((sample) {
print('Soil sample: $sample');
});
// later
await usb.offReadSoilDataInterval();
await subscription.cancel();
Live raw serial stream
await usb.onReadInterval(
config: const RawReadConfig(bufferSize: 1024, timeout: 1000),
intervalMs: 500,
);
final subscription = usb.serialDataStream.listen((chunk) {
print('Serial chunk: $chunk');
});
// later
await usb.offReadInterval();
await subscription.cancel();
Always call disconnect() when you're done:
await usb.disconnect();
API reference
FlutterUsbSerialManager
| Method | Returns | Description |
|---|---|---|
getDeviceList() |
Future<List<UsbDevice>> |
Lists USB devices currently attached to the host. |
hasPermission(UsbDevice device) |
Future<bool> |
Whether the app already has permission to access device. |
requestUsbPermission(UsbDevice device) |
Future<bool> |
Shows the Android USB-permission dialog and resolves with the user's decision. |
connect(UsbDevice device, {int baudRate = 9600}) |
Future<bool> |
Opens a serial connection to device. |
isConnected() |
Future<bool> |
Whether a device is currently connected. |
getConnectedDevice() |
Future<UsbDevice?> |
The currently connected device, or null. |
disconnect() |
Future<void> |
Closes the current serial connection. |
write(String data) |
Future<void> |
Writes data to the connected device. |
read({RawReadConfig config}) |
Future<String> |
Reads raw bytes once, according to config. |
readSoilData({SoilSensorConfig config}) |
Future<Map<String, double>?> |
Reads one Modbus soil-sensor sample. |
onReadInterval({RawReadConfig config, int intervalMs = 1000}) |
Future<void> |
Starts pushing raw reads to serialDataStream every intervalMs. |
offReadInterval() |
Future<void> |
Stops the interval started by onReadInterval. |
onReadSoilDataInterval({SoilSensorConfig config, int intervalMs = 1000}) |
Future<void> |
Starts pushing soil-sensor samples to soilDataStream every intervalMs. |
offReadSoilDataInterval() |
Future<void> |
Stops the interval started by onReadSoilDataInterval. |
serialDataStream |
Stream<String> |
Raw bytes pushed while an onReadInterval is active. |
soilDataStream |
Stream<Map<String, double>> |
Soil samples pushed while an onReadSoilDataInterval is active. |
Models
UsbDevice
| Field | Type | Description |
|---|---|---|
vendorId |
int |
USB vendor ID. |
productId |
int |
USB product ID. |
manufacturer |
String? |
Manufacturer name, when the OS can report it. |
RawReadConfig â mirrors the native RawReadConfig.
| Field | Type | Default | Description |
|---|---|---|---|
bufferSize |
int |
1024 |
Max bytes to read per call. |
timeout |
int |
1000 |
Read timeout, in milliseconds. |
SoilSensorConfig â mirrors the native SoilSensorConfig, for standard Modbus RTU soil sensors.
| Field | Type | Default | Description |
|---|---|---|---|
slaveId |
int |
1 |
Modbus slave/unit address. |
startAddress |
int |
0x0000 |
First register to read. |
registerCount |
int |
8 |
Number of registers to read. |
responseDelayMs |
int |
300 |
Delay to wait for the sensor's response. |
Example app
The example/ directory contains a runnable Flutter app that doubles as a manual test console for every method above â device list, permission flow, connect/disconnect, write/read, Modbus soil-sensor reads, and both live streams with a scrolling log. It's the fastest way to verify your hardware works before wiring the plugin into your own UI.
cd example
flutter run
Troubleshooting / FAQ
hasPermission() / connect() always fails with DEVICE_NOT_FOUND.
Make sure you're passing back a UsbDevice you got from getDeviceList() in the same session â device identity is matched by vendorId/productId against the currently attached devices.
The USB permission dialog never appears.
Confirm android.hardware.usb.host is declared and that the device is actually connected in USB host/OTG mode (some cables/adapters are charge-only). Also check hasPermission() first â if permission was already granted previously, no dialog is shown.
read() / write() don't seem to do anything.
Raw read/write only work with devices that understand plain bytes you send them. A Modbus RTU sensor won't respond to arbitrary text â use readSoilData() / onReadSoilDataInterval() instead, which build the correct Modbus frame internally.
Gradle can't resolve the native dependency.
See the Android setup note about JitPack and dependencyResolutionManagement.
Roadmap
Configurable Modbus function codes beyond the built-in soil-sensor profile.Parity/stop-bit/data-bit configuration forconnect().Unit tests for the platform channel layer.
iOS support is not on the roadmap â see Supported platforms for why.
Releasing
Releases are automated with two GitHub Actions workflows:
tag-release.ymlâ triggers wheneverversion:inpubspec.yamlchanges onmain. It creates the matchingvX.Y.Zgit tag and a GitHub Release, using the matchingCHANGELOG.mdsection plus GitHub's auto-generated commit/PR notes as the release body.publish.ymlâ triggers on thatvX.Y.Ztag push and publishes to pub.dev via pub.dev's official OIDC "trusted publishing" reusable workflow, so no long-lived pub.dev credentials are stored in this repo.
To cut a release: bump version: in pubspec.yaml, add a matching section to CHANGELOG.md, and merge to main â the rest happens automatically.
One-time setup required before this works (pub.dev only lets you automate publishing for a package that already exists there):
- Publish the first version manually, once:
flutter pub publishfrom the repo root. - Go to pub.dev/packages/flutter_usb_serial_manager/admin â Automated publishing and add this GitHub repository (
DeveloperRejaul/flutter_usb_serial_manager) with workflow filepublish.ymlas a trusted publisher. See the automated publishing guide for details. - From then on, every version bump merged to
mainis tagged, released, and published automatically â no more manualpub publish.
Only the pub.dev package owner (signed in with the Google account used for step 1) can do steps 1â2.
Contributing
Issues and pull requests are welcome â bug reports, docs fixes, tests, and new features all help.
- Read the Contributing guide for the development setup, project structure, coding style, commit message convention, and the pull request checklist.
- This project follows a Code of Conduct; please be kind and respectful in issues, PRs, and discussions.
- Found a bug? Use the bug report template. Have an idea? Use the feature request template.
- Every pull request runs against the checklist in
.github/PULL_REQUEST_TEMPLATE.mdâflutter analyzemust pass, and native changes need to be verified on a real device via the example app.
License
Licensed under the Apache License, Version 2.0.
Copyright 2026 Rezaul Karim
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Credits
Built on top of the native Android library DeveloperRejaul/usb-serial, which in turn wraps mik3y/usb-serial-for-android for the low-level USB CDC/FTDI/CH34x/CP210x driver support.