Thermal printers
A receipt printer is an SPP device that happens to interpret ESC/POS bytes.
Install#
flutter pub add flutter_classic_bluetoothimport 'package:flutter_classic_bluetooth/flutter_classic_bluetooth.dart';Finding the printer#
Receipt printers are nearly always paired once and then used forever, so read the paired list rather than scanning.
final paired = await bluetooth.getPairedDevices();
final printer = paired.firstWhere(
(d) => (d.name ?? '').contains('Printer'),
);
final connection = await bluetooth.connect(address: printer.address);Names vary wildly between vendors: Printer001, BlueTooth Printer, MTP-2, RPP02N. Let the user pick from the paired list rather than hard coding a match.
ESC/POS is just bytes#
This plugin moves bytes. It does not build receipts. Composing an ESC/POS document is a separate job, and there are packages that do it well.
const esc = 0x1B;
const gs = 0x1D;
await connection.output.writeBytes([esc, 0x40]); // initialise
await connection.output.writeBytes([esc, 0x61, 0x01]); // centre
await connection.output.writeString('MY SHOP');
await connection.output.writeBytes([0x0A]); // line feed
await connection.output.writeBytes([esc, 0x61, 0x00]); // left
await connection.output.writeString('Item 1.00');
await connection.output.writeBytes([0x0A, 0x0A, 0x0A]);
await connection.output.writeBytes([gs, 0x56, 0x00]); // cutPair this with an ESC/POS builder package for real receipts, and pass its generated byte list to writeBytes. The flutter_esc_pos_utils family works well for that.
The truncated last line#
The single most common complaint. You send a receipt, close the connection, and the last few lines never print.
// Wrong: closes before the queue has drained.
connection.output.writeBytes(receipt);
await connection.close();
// Right: flush, then close.
await connection.output.writeBytes(receipt);
await connection.output.allSent;
await connection.finish();finish flushes pending writes before closing; close does not. Many printers also buffer internally, so give the paper feed at the end of the receipt real line feeds rather than relying on the cut command to push it out.
Character encoding#
Thermal printers rarely speak UTF-8. Most use a code page, and non-ASCII characters come out as noise unless you match it.
await connection.output.writeBytes([0x1B, 0x74, 0x00]); // select code page
await connection.output.writeString('Café', encoding: latin1);Which code page maps to which number is vendor specific and in the printer's manual. If accented characters are wrong, that pairing is where to look, not the Dart side.
Images and receipts that stall#
A logo raster is large, and some printers cannot take it in one burst. If printing stalls partway through an image, send it in chunks and let the printer keep up.
const chunk = 256;
for (var i = 0; i < raster.length; i += chunk) {
await connection.output.writeBytes(
raster.sublist(i, (i + chunk).clamp(0, raster.length)),
);
await Future<void>.delayed(const Duration(milliseconds: 20));
}
await connection.output.allSent;Common problems#
| Symptom | Usually |
|---|---|
| Last lines missing | close instead of finish, or no allSent |
| Accented characters wrong | Code page not selected, or the wrong encoding on writeString |
| Prints nothing at all | Missing the ESC @ initialise sequence |
| Image stalls halfway | Raster sent faster than the printer can absorb it |
| Connect fails while idle | Printer went to sleep. Power cycle and reconnect |