Build an App that Prints from a Thermal Bluetooth Printer Using Flutter

Written by altynberg | Published 2022/01/24
Tech Story Tags: flutter | flutter-tutorial | flutter-for-mobile-app | flutter-app | bluettoth | portable-printers | printer | flutter-top-story | hackernoon-es

TLDRTo accomplish this, we have to connect to a printer via Bluetooth and send printable data like text, image, or QR code. We will use these plugins: flutter_blue and esc_pos_utils. We'll use this plugin to connect the Bluetooth device and to send the data. It is not only for thermal printers but also can be used for any Bluetooth device. I chose this plugin because it supports iOS, Android, and macOS. We can scan and get available devices like this: FlutterBlue.via the TL;DR App

Today we are going to learn how to print via the thermal Bluetooth printer in Flutter. To accomplish this, we have to connect to a printer via Bluetooth and send printable data like text, image, or QR code. We will use these plugins:

flutter_blue: ^0.8.0

esc_pos_utils: ^1.1.0

flutter_blue

We'll use this plugin to connect to the Bluetooth device and to send the data. It is not only for thermal printers but also can be used for any Bluetooth device. I chose this plugin because it supports iOS, Android, and macOS.

https://pub.dev/packages/flutter_blue

esc_pos_utils

This plugin will help to generate ESC/POS commands to send it via flutter_blue. It supports images, barcodes, text styling, tables, paper cut, paper feed, and much more.

https://pub.dev/packages/esc_pos_utils

Setup

We may need permission setups, so go to the flutter_blue page and make setup according to instructions.

Scan for devices

We can scan and get available devices like this:

FlutterBlue flutterBlue = FlutterBlue.instance;

// Start scanning
flutterBlue.startScan(timeout: Duration(seconds: 4));

// Listen to scan results
var subscription = flutterBlue.scanResults.listen((results) {
    // do something with scan results
    for (ScanResult r in results) {
        print('${r.device.name} found! rssi: ${r.rssi}');
    }
});

// Stop scanning
flutterBlue.stopScan();

But let's create a widget where

  1. We can show the list of devices.
  2. And when a device is selected generate, ESC/POS commands and send them to the selected device.

import 'package:blue_thermal_printing/blue_print.dart';
import 'package:esc_pos_utils/esc_pos_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blue/flutter_blue.dart';

FlutterBlue flutterBlue = FlutterBlue.instance;

class PrintingWidget extends StatefulWidget {
  const PrintingWidget({Key? key}) : super(key: key);

  @override
  _PrintingWidgetState createState() => _PrintingWidgetState();
}

class _PrintingWidgetState extends State<PrintingWidget> {
  List<ScanResult>? scanResult;

  @override
  void initState() {
    super.initState();
    findDevices();
  }

  void findDevices() {
    flutterBlue.startScan(timeout: const Duration(seconds: 4));
    flutterBlue.scanResults.listen((results) {
      setState(() {
        scanResult = results;
      });
    });
    flutterBlue.stopScan();
  }

  void printWithDevice(BluetoothDevice device) async {
    await device.connect();
    final gen = Generator(PaperSize.mm58, await CapabilityProfile.load());
    final printer = BluePrint();
    printer.add(gen.qrcode('https://altospos.com'));
    printer.add(gen.text('Hello'));
    printer.add(gen.text('World', styles: const PosStyles(bold: true)));
    printer.add(gen.feed(1));
    await printer.printData(device);
    device.disconnect();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Bluetooth devices')),
      body: ListView.separated(
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(scanResult![index].device.name),
            subtitle: Text(scanResult![index].device.id.id),
            onTap: () => printWithDevice(scanResult![index].device),
          );
        },
        separatorBuilder: (context, index) => const Divider(),
        itemCount: scanResult?.length ?? 0,
      ),
    );
  }
}

Let's look at the printWithDevice method.

  1. First, we connect to the given device.
  2. The Generator is a class of the esc_pos_utils plugin. With the help of this class, we can create ESC/POS commands like this: generator.text('Hello').
  3. We can add these generated commands to the BluePrint instance (we will create this class further).
  4. At the end, we send all generated data to the Bluetooth printer and disconnect when finished.

Printing

Now let's see the BluePrint class. In short, it collects ESC/POS commands and sends these data to a given printer.

  • void add(List<int> data) - To add commands.
  • List<List<int>> getChunks() - If we need to print very large data like an image, we may encounter problems because some devices will fail because of the big package size. So we need to send the data in chunks.
  • printData(BluetoothDevice device) - Sends chunked data to the device via BluetoothCharacteristic.

import 'dart:math';
import 'package:flutter_blue/flutter_blue.dart';

class BluePrint {
  BluePrint({this.chunkLen = 512});

  final int chunkLen;
  final _data = List<int>.empty(growable: true);

  void add(List<int> data) {
    _data.addAll(data);
  }

  List<List<int>> getChunks() {
    final chunks = List<List<int>>.empty(growable: true);
    for (var i = 0; i < _data.length; i += chunkLen) {
      chunks.add(_data.sublist(i, min(i + chunkLen, _data.length)));
    }
    return chunks;
  }

  Future<void> printData(BluetoothDevice device) async {
    final data = getChunks();
    final characs = await _getCharacteristics(device);
    for (var i = 0; i < characs.length; i++) {
      if (await _tryPrint(characs[i], data)) {
        break;
      }
    }
  }

  Future<bool> _tryPrint(
    BluetoothCharacteristic charac,
    List<List<int>> data,
  ) async {
    for (var i = 0; i < data.length; i++) {
      try {
        await charac.write(data[i]);
      } catch (e) {
        return false;
      }
    }
    return true;
  }

  Future<List<BluetoothCharacteristic>> _getCharacteristics(
    BluetoothDevice device,
  ) async {
    final services = await device.discoverServices();
    final res = List<BluetoothCharacteristic>.empty(growable: true);
    for (var i = 0; i < services.length; i++) {
      res.addAll(services[i].characteristics);
    }
    return res;
  }
}

I needed to add a Bluetooth thermal printer feature when I was working on Alto's POS project. It was my first Bluetooth experience with Flutter, and because of the little material about it, I had to try many things before making it work properly on every device. So I decided to write this article in hope that it will be useful for others.

The GitHub project is available here: https://github.com/usenbekov/blue-thermal-printing



Written by altynberg | Software Engineer
Published by HackerNoon on 2022/01/24