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_bluetoothimport '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: statusStreaming 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#
| Symptom | Usually |
|---|---|
| Board never appears in a scan | An ESP32-C3, C6 or S2, which has no Bluetooth Classic radio |
| Connects, then drops immediately | Brownout. USB power is often not enough while the radio transmits |
| Nothing arrives in Dart | The sketch is not sending a delimiter, so lines never completes one |
| First lines are garbage | Boot log on the same port. Skip lines that do not parse |
| Connect fails after reflashing | The old bond is stale. Unpair and pair again |