flutter_classic_bluetooth v1.0.0
pub.dev GitHub

ESP32

An ESP32 running BluetoothSerial is a plain SPP device, so nothing special is needed on the Flutter side.

Install#

flutter pub add flutter_classic_bluetooth
import 'package:flutter_classic_bluetooth/flutter_classic_bluetooth.dart';

The sketch#

On the board, BluetoothSerial from the Arduino ESP32 core exposes a standard serial port. The name you pass to begin is what shows up in a scan.

#include "BluetoothSerial.h"

BluetoothSerial SerialBT;

void setup() {
  SerialBT.begin("ESP32-Device");
}

void loop() {
  if (SerialBT.available()) {
    String line = SerialBT.readStringUntil('\n');
    SerialBT.println("echo: " + line);
  }
}

This needs a classic ESP32 or ESP32-S3. The ESP32-C3, C6 and S2 have no Bluetooth Classic radio, only BLE, so BluetoothSerial will not compile for them and no amount of Flutter code will reach them.

Finding it#

final bluetooth = FlutterClassicBluetooth();

final devices = await bluetooth.scan(timeout: const Duration(seconds: 10));
final esp = devices.firstWhere((d) => d.name == 'ESP32-Device');

After the first pairing, skip the scan entirely and read it from getPairedDevices, which is instant.

Connecting and talking#

final connection = await bluetooth.connect(
  address: esp.address,
  timeout: const Duration(seconds: 8),
);

connection.input.lines().listen((line) {
  print('ESP32: $line');
});

await connection.output.writeLine('hello');

writeLine appends the newline the sketch's readStringUntil is waiting for. Without a delimiter the board sits there holding a partial line, which looks exactly like a connection that is not working.

Command and reply#

When the board answers each command, sendAndReceive is less code than wiring up a listener and a completer.

final reply = await connection.sendAndReceive('status');
print(reply);   // echo: status

Streaming sensor data#

A board that pushes readings continuously is the other common shape. Parse each line as it lands.

connection.input.lines().listen((line) {
  final parts = line.split(',');
  if (parts.length != 2) return;          // ignore boot noise
  final temp = double.tryParse(parts[0]);
  final humidity = double.tryParse(parts[1]);
  if (temp != null && humidity != null) {
    setState(() => reading = (temp, humidity));
  }
});

Guard the parse. An ESP32 prints boot messages over the same port before your sketch takes over, and those will reach you as lines.

Surviving a reset#

Boards get power cycled and reflashed constantly during development. A reconnecting link saves restarting the app every time.

final link = bluetooth.connectWithReconnect(
  address: esp.address,
  policy: const BtcReconnectPolicy(
    initialBackoff: Duration(seconds: 1),
    maxBackoff: Duration(seconds: 15),
  ),
);

link.input.lines().listen((line) => print('ESP32: $line'));
await link.sendLine('hello');

Common problems#

SymptomUsually
Board never appears in a scanAn ESP32-C3, C6 or S2, which has no Bluetooth Classic radio
Connects, then drops immediatelyBrownout. USB power is often not enough while the radio transmits
Nothing arrives in DartThe sketch is not sending a delimiter, so lines never completes one
First lines are garbageBoot log on the same port. Skip lines that do not parse
Connect fails after reflashingThe old bond is stale. Unpair and pair again