Send and receive
A connection is a byte stream in and an ordered sink out. The work is turning bytes into messages.
Install#
flutter pub add flutter_classic_bluetoothimport 'package:flutter_classic_bluetooth/flutter_classic_bluetooth.dart';Writing#
await connection.output.writeString('AT');
await connection.output.writeLine('AT+GMR');
await connection.output.writeBytes([0x01, 0x02, 0x03]);
await connection.output.add(Uint8List.fromList([0xFF]));Writes are queued and delivered in call order, so you do not have to await each one to keep them in sequence. When you do need to know everything has gone out:
connection.output.writeString('one');
connection.output.writeString('two');
await connection.output.allSent;Reading raw bytes#
connection.input.listen(
(bytes) => print('RX ${bytes.length}'),
onDone: () => print('device disconnected'),
onError: (e) => print('link error: $e'),
);onDone firing is how you learn the other end went away.
Bytes are not messages#
This is the mistake that costs the most time. RFCOMM is a stream, not a datagram service. One write on the device does not produce one event in Dart. A 40 byte reply can arrive as 40 separate events, or two writes can arrive merged into one. Any code shaped like if (utf8.decode(bytes) == 'OK') works on your desk and fails in the field.
Split the stream on whatever the protocol actually delimits with.
Line based protocols#
connection.input.lines().listen((line) {
print('device said: $line');
});lines buffers across events and emits one string per line, handling both \n and \r\n. Pass maxLineLength to cap the buffer so a device stuck without a delimiter cannot grow it without bound.
connection.input.lines(maxLineLength: 4096).listen(handleLine);Binary frames#
For a protocol delimited by a byte sequence rather than a newline:
connection.input
.frames(delimiter: Uint8List.fromList([0x0D, 0x0A]))
.listen((frame) => decode(frame));Command and response#
Write a command, wait for the reply, in one call. This is the common shape for AT command devices.
final version = await connection.sendAndReceive('AT+GMR');
final ok = await connection.sendAndReceive(
'AT',
where: (line) => line == 'OK',
timeout: const Duration(seconds: 3),
);It subscribes before writing, so a device that answers immediately is never missed. where skips lines you do not care about, which matters on devices that echo the command back before replying. No matching line inside the timeout throws BtcTimeoutException.
One caveat: this is a single outstanding request at a time. Do not fire several in parallel on one connection and expect the answers to pair up.
Text encodings#
Everything defaults to UTF-8. Devices that speak Latin-1 or a code page need it stated.
await connection.output.writeString('café', encoding: latin1);
connection.input.lines(encoding: latin1).listen(handleLine);Decoding without splitting#
When a device streams text with no framing at all and you just want it on screen:
connection.input.decoded().listen((chunk) => append(chunk));Chunk boundaries do not fall on character boundaries, so this decodes across events rather than per event, which keeps multi-byte characters intact.