cpcl_esc_printer 0.2.0 copy "cpcl_esc_printer: ^0.2.0" to clipboard
cpcl_esc_printer: ^0.2.0 copied to clipboard

Flutter BLE thermal printer plugin with CPCL / ESC-POS / TSPL builders — text, barcode, QR, tables, images, plus optional Material scan/debug UI.

example/lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';

import 'package:barcode/barcode.dart';
import 'package:cpcl_esc_printer/cpcl_esc_printer.dart';
import 'package:fast_gbk/fast_gbk.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() => runApp(const ExampleApp());

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'cpcl_esc_printer example',
      theme: ThemeData(useMaterial3: true),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  // ---- Example-only persistence keys (plugin does NOT do this itself) ----
  static const _kLastDeviceId = 'last_printer_id';
  static const _kLastDeviceName = 'last_printer_name';

  final _manager = PrinterManager.instance;
  StreamSubscription<PrinterConnectionState>? _stateSub;
  PrinterConnectionState _state = PrinterConnectionState.disconnected;
  String? _lastId;
  String? _lastName;

  // Encoding toggle: GBK for Chinese thermal printers, UTF-8 otherwise.
  bool _useGbk = true;
  TextEncoder get _encoder => _useGbk ? gbk.encode : utf8.encode;

  @override
  void initState() {
    super.initState();
    _stateSub = _manager.stateStream.listen((s) {
      if (mounted) setState(() => _state = s);
      // Persist the device once it's ready (example-only).
      if (s == PrinterConnectionState.ready) _saveLastDevice();
    });
    _loadLastDevice();
  }

  @override
  void dispose() {
    _stateSub?.cancel();
    super.dispose();
  }

  // ---- Permissions (example-only, via permission_handler) ----
  Future<bool> _ensurePermissions() async {
    final statuses = await [
      Permission.bluetoothScan,
      Permission.bluetoothConnect,
      Permission.locationWhenInUse, // only needed for scanning on Android < 12
    ].request();
    bool granted(Permission p) {
      final s = statuses[p];
      return s != null && (s.isGranted || s.isLimited);
    }

    // BLUETOOTH_SCAN/CONNECT are the load-bearing permissions on Android 12+
    // (scan is declared neverForLocation). Location is only required for
    // scanning on Android < 12, so it must not block the gate on newer devices.
    final ok =
        granted(Permission.bluetoothScan) && granted(Permission.bluetoothConnect);
    if (!ok && mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Bluetooth permissions are required.')),
      );
    }
    return ok;
  }

  // ---- Persistence (example-only, via shared_preferences) ----
  Future<void> _loadLastDevice() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _lastId = prefs.getString(_kLastDeviceId);
      _lastName = prefs.getString(_kLastDeviceName);
    });
  }

  Future<void> _saveLastDevice() async {
    final id = _manager.connectedDeviceId;
    if (id == null) return;
    final name = _manager.connectedDeviceName ?? '';
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_kLastDeviceId, id);
    await prefs.setString(_kLastDeviceName, name);
    setState(() {
      _lastId = id;
      _lastName = name;
    });
  }

  Future<void> _scan() async {
    if (!await _ensurePermissions()) return;
    if (!mounted) return;
    await Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (_) => PrinterScannerPage(encoder: _encoder),
      ),
    );
  }

  Future<void> _reconnectLast() async {
    if (_lastId == null) return;
    if (!await _ensurePermissions()) return;
    try {
      await _manager.connect(
        BleDevice(id: _lastId!, name: _lastName ?? '', rssi: 0),
        profile: PrinterProfile.autoDetect,
      );
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context)
            .showSnackBar(SnackBar(content: Text('Reconnect failed: $e')));
      }
    }
  }

  // Rasterize a barcode/QR (pure-Dart `barcode` geometry) into a MonoBitmap so
  // it can be printed via EscBuilder.image() — works on printers that don't
  // render ESC/POS barcode/QR commands (they print raster fine).
  MonoBitmap _codeBitmap(Barcode bc, String data,
      {required int width, required int height}) {
    final widthBytes = (width + 7) >> 3;
    final out = Uint8List(widthBytes * height);
    for (final e in bc.make(data,
        width: width.toDouble(), height: height.toDouble(), drawText: false)) {
      if (e is BarcodeBar && e.black) {
        final x0 = e.left.floor().clamp(0, width);
        final x1 = (e.left + e.width).ceil().clamp(0, width);
        final y0 = e.top.floor().clamp(0, height);
        final y1 = (e.top + e.height).ceil().clamp(0, height);
        for (var y = y0; y < y1; y++) {
          for (var x = x0; x < x1; x++) {
            out[y * widthBytes + (x >> 3)] |= 0x80 >> (x & 7);
          }
        }
      }
    }
    return MonoBitmap(width: width, height: height, bytes: out);
  }

  // Full ESC/POS receipt with a barcode and QR rendered as raster images.
  Future<void> _printReceipt() async {
    if (!_manager.isReady) {
      ScaffoldMessenger.of(context)
          .showSnackBar(const SnackBar(content: Text('Connect first.')));
      return;
    }
    const orderNo = '20240717001';
    const w = 48;
    final divider = '-' * w;
    final barcodeBmp =
        _codeBitmap(Barcode.code128(), orderNo, width: 360, height: 80);
    final qrBmp = _codeBitmap(
        Barcode.qrCode(), 'https://m.example.com/r/$orderNo',
        width: 180, height: 180);
    final bytes = EscBuilder(encoder: _encoder)
        .align(EscAlign.center)
        .fontSize(17)
        .text('阳光超市')
        .fontSize(0)
        .printMode(0)
        .text('欢迎光临')
        .text('电话 000-00000000')
        .align(EscAlign.left)
        .text(divider)
        .text('单号 $orderNo')
        .text('时间 2024-07-17 12:30')
        .text('收银 001 张三')
        .text(divider)
        .table(
          [
            ['商品', '数量', '金额'],
            ['可乐', '2', '6.00'],
            ['面包', '1', '4.50'],
            ['牛奶', '3', '15.00'],
          ],
          columnWidths: [24, 8, 16],
          align: [EscAlign.left, EscAlign.center, EscAlign.right],
          rule: true,
        )
        .text(divider)
        .twoColumns('合计', '25.50', leftWidth: w - 5)
        .twoColumns('实付(微信)', '25.50', leftWidth: w - 5)
        .text(divider)
        .align(EscAlign.center)
        .image(barcodeBmp)
        .feed()
        .image(qrBmp)
        .feed()
        .text('谢谢惠顾 欢迎再次光临')
        .feed(lines: 3)
        .build();
    await _manager.writeRaw(bytes);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('cpcl_esc_printer example')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Card(
              child: Padding(
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text('State: ${_state.name}'),
                    const SizedBox(height: 4),
                    Text('Last printer: ${_lastName ?? '-'}  ${_lastId ?? ''}'),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                const Text('Encoding: '),
                const SizedBox(width: 8),
                Expanded(
                  child: SegmentedButton<bool>(
                    segments: const [
                      ButtonSegment(value: true, label: Text('GBK (中文)')),
                      ButtonSegment(value: false, label: Text('UTF-8')),
                    ],
                    selected: {_useGbk},
                    onSelectionChanged: (s) => setState(() => _useGbk = s.first),
                  ),
                ),
              ],
            ),
            const SizedBox(height: 16),
            ElevatedButton.icon(
              icon: const Icon(Icons.bluetooth_searching),
              label: const Text('Scan printers'),
              onPressed: _scan,
            ),
            const SizedBox(height: 8),
            OutlinedButton.icon(
              icon: const Icon(Icons.autorenew),
              label: Text(_lastId == null
                  ? 'No saved printer'
                  : 'Reconnect last (${_lastName ?? _lastId})'),
              onPressed: _lastId == null ? null : _reconnectLast,
            ),
            const SizedBox(height: 8),
            OutlinedButton(
              onPressed: () async {
                if (!_manager.isReady) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('Connect first.')),
                  );
                  return;
                }
                final bytes = CpclBuilder(width: 400, height: 200, encoder: _encoder)
                    .text('Hello 你好', 20, 20, size: 2)
                    .qrcode('https://pub.flutter-io.cn', 20, 60, u: 5)
                    .build();
                await _manager.writeRaw(bytes);
              },
              child: const Text('Quick CPCL print'),
            ),
            const SizedBox(height: 8),
            OutlinedButton(
              onPressed: _printReceipt,
              child: const Text('小票打印(条码+二维码图形)'),
            ),
            const Spacer(),
            const Text(
              'Persistence & permissions live in the EXAMPLE only.\n'
              'The plugin itself depends on flutter_blue_plus alone.',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 12, color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
140
points
136
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter BLE thermal printer plugin with CPCL / ESC-POS / TSPL builders — text, barcode, QR, tables, images, plus optional Material scan/debug UI.

Repository (GitHub)
View/report issues

Topics

#bluetooth #thermal-printer #esc-pos #cpcl #tspl

License

MIT (license)

Dependencies

flutter, flutter_blue_plus

More

Packages that depend on cpcl_esc_printer