flutter_classic_bluetooth v1.0.0
pub.dev GitHub

Thermal printers

A receipt printer is an SPP device that happens to interpret ESC/POS bytes.

Install#

flutter pub add flutter_classic_bluetooth
import '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]);      // cut

Pair 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#

SymptomUsually
Last lines missingclose instead of finish, or no allSent
Accented characters wrongCode page not selected, or the wrong encoding on writeString
Prints nothing at allMissing the ESC @ initialise sequence
Image stalls halfwayRaster sent faster than the printer can absorb it
Connect fails while idlePrinter went to sleep. Power cycle and reconnect