cpcl_esc_printer

English | ็ฎ€ไฝ“ไธญๆ–‡

๐Ÿ™ ๆ„Ÿ่ฐข flutter_blue_plus โ€” ๆœฌๆ’ไปถ็š„่“็‰™่ฟžๆŽฅๅฑ‚ๅŸบไบŽๅฎƒๆž„ๅปบใ€‚ๆ„Ÿ่ฐขไฝœ่€…็š„ๅผ€ๆบ่ดก็Œฎ๏ผ Thanks to flutter_blue_plus, on which this plugin's BLE layer is built.

Lightweight Flutter plugin for BLE thermal printers (label + receipt).

  • ๐Ÿ”Œ Connect / auto-reconnect / write-with-completion โ€” built on flutter_blue_plus 1.36.8 (the last BSD-3 / free release before the 2.0.0 commercial-license switch). Supports Android / iOS / macOS / Linux / Web.
  • ๐Ÿท CpclBuilder โ€” CPCL label commands for common BLE label printers.
  • ๐Ÿงพ EscBuilder โ€” ESC/POS receipt commands.
  • ๐Ÿงฉ PrinterProfile โ€” customise service / write / notify UUIDs, or use auto-detect. Built-in generic preset.
  • ๐Ÿงช Optional Material scanner + debug page.

Install

dependencies:
  cpcl_esc_printer: ^0.1.0

Platform setup

Android โ€” AndroidManifest.xml

<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
                 android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

android/app/build.gradle โ€” minSdkVersion 21.

iOS โ€” Info.plist

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Used to connect to Bluetooth printers.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Used to connect to Bluetooth printers.</string>

iOS Deployment Target โ‰ฅ 12.0.

Windows โ€” not supported (yet)

flutter_blue_plus 1.36.8 does not include a Windows implementation, so this plugin doesn't support Windows. The community package flutter_blue_plus_windows exists, but it pins flutter_blue_plus <1.35.0 โ€” incompatible with the ^1.36.8 constraint here โ€” and hasn't been updated in a long time, so it isn't wired in. If Windows is a hard requirement, you'd need to fork and pin an older flutter_blue_plus; otherwise use Android / iOS / macOS / Linux / Web.

Runtime permissions โ€” not requested by this plugin

Because we don't ship permission_handler, request permissions in your app before calling startScan():

import 'package:permission_handler/permission_handler.dart';

await [
  Permission.bluetoothScan,
  Permission.bluetoothConnect,
  Permission.locationWhenInUse, // required on Android < 12
].request();

You can substitute any permission library. If any required permission is denied, startScan() will throw and the scanner page shows the error via a SnackBar.

60-second quick start

import 'package:cpcl_esc_printer/cpcl_esc_printer.dart';
import 'package:flutter/material.dart';

// 1. Open scanner page.
Navigator.push(
  context,
  MaterialPageRoute(builder: (_) => const PrinterScannerPage()),
);

// 2. Or connect programmatically.
final m = PrinterManager.instance;
await m.startScan();
m.scanResults.listen((devices) async {
  if (devices.isNotEmpty) {
    await m.stopScan();
    await m.connect(devices.first, profile: PrinterProfile.autoDetect);
  }
});

// 3. Print a CPCL label.
final bytes = CpclBuilder(width: 400, height: 240)
    .text('Hello', 20, 20, size: 2)
    .qrcode('cpcl_esc_printer', 20, 60, u: 5)
    .build();
await m.writeRaw(bytes);

Characteristic selection flow

A BLE printer exposes a GATT service containing characteristics; this plugin needs one to write print data to and (optionally) one to receive notify (print-complete) callbacks. The flow is designed so it "just works" by default, with a manual fallback when it doesn't:

                       connect(device, profile)
                                 โ”‚
       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ–ผ                         โ–ผ                           โ–ผ
 profile has UUIDs        autoDetect (default)         nothing works
 (generic / custom)        scan services, pick               โ”‚
  โ†’ use them               first write+notify pair           โ”‚
       โ”‚                         โ”‚                           โ–ผ
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             open PrinterDebugPage,
                   โ–ผ                            tap WRITE + NOTIFY chips,
              โœ… ready to print                 "Apply picked" โ†’ โœ… ready

Step by step:

  1. Default โ€” auto-detect (the "generic" path). Call connect() with no profile (or PrinterProfile.autoDetect). The plugin discovers all services and picks the first characteristic that supports write, plus the first that supports notify. This covers the majority of printers, so most users never configure anything.

  2. Known model โ€” use the preset or custom UUIDs. If you already know the printer's UUIDs, pass PrinterProfile.generic (the common Nordic-UART service) or a custom PrinterProfile so it binds directly without guessing.

  3. Special case โ€” auto-detect / generic bind the wrong characteristic. Some modules expose vendor-specific characteristics (e.g. fff0/fff1/fff2 and other non-standard sets), or several writable characteristics where the first isn't the right one. There's no preset for these โ€” they vary too much to ship reliably. Instead, discover the working pair yourself:

    1. Open the bundled PrinterDebugPage. It lists every service/characteristic with its properties (W/WNR/R/N).
    2. Tap the correct WRITE chip (and a NOTIFY chip if available), then Apply picked โ€” the current connection rebinds live, no reconnect.
    3. Print a test to confirm. The page shows the currently bound Service / Write / Notify UUIDs at the top (selectable, each with a copy button), and every characteristic row has its own copy button.
    4. Note the working UUIDs down and pass them as a custom PrinterProfile (see below) so production code binds directly and skips this step.
Preset When to use
PrinterProfile.autoDetect (default) Unknown printer โ€” scans all services, picks the first write + notify pair. The recommended starting point.
PrinterProfile.generic Nordic-UART-based BLE label printers (49535343-โ€ฆ).
Custom Anything else โ€” fill in your own UUIDs found via the debug page (see below).

Custom UUIDs

const my = PrinterProfile(
  serviceUuid:     '0000ff00-0000-1000-8000-00805f9b34fb',
  writeCharUuid:   '0000ff02-0000-1000-8000-00805f9b34fb',
  notifyCharUuid:  '0000ff01-0000-1000-8000-00805f9b34fb',
  autoMtu: true,       // chunk using the negotiated MTU (fast). See below.
  mtu: 60,             // fallback/cap when autoMtu is false; payload = mtu - 3
  chunkDelayMs: 10,    // delay between chunk writes
  printTimeoutMs: 5000,// wait for print-complete notify
  fallbackDelayMs: 2000,// used when notify characteristic absent
  writeWithoutResponse: false,
);
await PrinterManager.instance.connect(device, profile: my);

MTU-aware chunking (print speed)

By default (autoMtu: true) writes are chunked using the connection's negotiated MTU โ€” Android auto-negotiates ~512 bytes, iOS/macOS ~135-255 โ€” instead of a fixed 60. This cuts round-trips 4-9ร— on large jobs (images, long labels), so image printing is much faster. Read the live value via PrinterManager.instance.negotiatedMtu.

Set autoMtu: false to force the conservative fixed mtu if an old/flaky printer misbehaves with large writes.

Live picker via debug page

The bundled PrinterDebugPage lists every service/characteristic and lets you tap two chips (WRITE + NOTIFY) โ†’ Apply picked. The current profile is patched in place โ€” great for reverse-engineering a new device.

It also has three test-print buttons:

  • ๅญ—ไฝ“ๆ‰“ๅฐ Font โ€” prints every CPCL size preset and textFontSize font, labelled, so you can see each font's real on-paper size.
  • ๆ ‡็ญพๆ‰“ๅฐ Label(CPCL) โ€” a simple express/courier waybill sample with both a QR code and a Code128 1D barcode.
  • ๅฐ็ฅจๆ‰“ๅฐ Receipt(ESC) โ€” a supermarket cashier receipt sample with an items table, totals, a Code128 1D barcode and a QR code.
  • CPCLๅ›พ็‰‡ๆ‰“ๅฐ / ESCๅ›พ็‰‡ๆ‰“ๅฐ Image โ€” captures the on-screen Flutter logo via RepaintBoundary โ†’ MonoBitmap, then prints it (CPCL EG / ESC ESC *). Demonstrates the zero-dependency widget-to-bitmap path.

The sample text is Chinese. Pass a GBK encoder so it prints correctly:

import 'package:fast_gbk/fast_gbk.dart';
PrinterScannerPage(encoder: gbk.encode);   // forwarded to the debug page
// or open the debug page directly:
PrinterDebugPage(encoder: gbk.encode);

Without an encoder the samples fall back to UTF-8 (ASCII/numbers print fine, Chinese may be garbled on GBK printers).

CpclBuilder โ€” label printing

CpclBuilder(width: 620, height: 320)
  .bold(2)
  .magnify(width: 2, height: 2)
  .text('Title', 10, 10, size: 3)
  .magnify()               // reset
  .textFontSize('code', 10, 60, font: 4)
  .qrcode('BQM12345', 10, 100, m: 1, u: 6)
  .barcode128('12345', 200, 100, height: 60)
  .line(0, 200, 620, 200)
  .box(0, 210, 620, 300)
  .build();

Text size presets (1-5) map to CPCL font/size argument pairs (matches the classic mapping used by common CPCL demos):

size CPCL args Visual
1 5 0 small
2 0 2 medium
3 5 1 large
4 4 2 x-large
5 10 5 xx-large

Need vendor-specific fonts? Use textFontSize() and pass the raw font / size values from your printer's programming manual.

More CPCL commands

Method CPCL
justify(CpclJustify.center) CENTER / LEFT / RIGHT
underline(true) UNDERLINE ON/OFF
inverseLine(x0,y0,x1,y1) INVERSE-LINE (white-on-black)
pattern(CpclPattern.crossHatch) PATTERN 100-106
contrast(2) CONTRAST 0-3 (darkness)
speed(3) SPEED 0-5
serialCount(1) COUNT (auto serial number)
pageWidth(576) PAGE-WIDTH
beep(1) BEEP
cut(partial: false) CUT / PARTIAL-CUT
gapSense() / barSense() / barSenseLeft() media sensing (gap / right mark / left mark)
charSpacing(5) SETSP (character spacing)
prefeed(100) / postfeed(160) PREFEED / POSTFEED
pace() / wait(80) PACE / WAIT (batch pacing / delay)
background(110) / bkText('ๅทฒ', x, y) BACKGROUND / BKTEXT (watermark)
reprint() REPRINT (auto-reprint on fault, device-specific)
encoding('GB18030') / country('CHINA') code page
scaleText('PLL_LAT.CSF', 20, 20, x, y, 'HI') SCALE-TEXT (outline font)
barcode(CpclBarcode.ean13, '690...', x, y) any linear symbology (+ vertical: true)
pdf417('data', x, y) / datamatrix('data', x, y) PDF-417 / DataMatrix 2D
concat(x, y, [...]) / multiline([...], lineHeight: 40) CONCAT / MULTILINE
barcodeText(7, 0, 5) / barcodeTextOff() HRI text under bars
graphics(widthBytes, h, x, y, bytes) EG / VEG bitmap

lineSpacing() is deprecated โ€” SETSP actually controls character spacing, not line spacing; use charSpacing() instead.

EscBuilder โ€” receipt printing

EscBuilder()
  .align(EscAlign.center).fontSize(17).text('RECEIPT')
  .fontSize(0).feed()
  .align(EscAlign.left)
  .text('Order: XS-1001')
  .text('---------------------------')
  .twoColumns('Item A', 'ยฅ12.00', leftWidth: 20)
  .twoColumns('Item B', 'ยฅ8.50',  leftWidth: 20)
  .text('---------------------------')
  .threeColumns('Total', '', 'ยฅ20.50', leftWidth: 12, centerWidth: 10)
  .align(EscAlign.center)
  .qrcode('https://example.com', size: 6)
  .feed(lines: 3)
  .build();

More ESC/POS commands

Method ESC/POS
feedDots(24) ESC J
absolutePosition(n) / relativePosition(n) ESC $ / ESC \
leftMargin(n) / printAreaWidth(n) GS L / GS W
printMode(mode) ESC ! (combined bitfield)
reverse(true) GS B (white-on-black)
rotate90(true) ESC V
upsideDown(true) ESC {
doubleStrike(true) ESC G
selectFont(1) ESC M (A/B/C)
charSpacing(n) ESC SP
chineseMode(true) / quadChinese(true) FS & / FS . / FS W
codeTable(n) / intlCharSet(n) ESC t / ESC R
barcodeHeight(n)/barcodeWidth(n)/hriPosition(...)/hriFont(n) GS h / GS w / GS H / GS f
barcode(EscBarcode.ean13, '69...') GS k (function B)
raster(widthBytes, h, bytes) GS v 0 bitmap
cut(partial: true) ESC i / ESC m
cashDrawer() ESC p (kick drawer)
buzzer(times: 2) ESC B
realtimeStatus(EscStatusType.paperSensor) DLE EOT n (reply on notify)
transmitStatus(n: 1) GS r n (reply on notify)
realtimeRequest(EscRealtimeRequest.recover) DLE ENQ n (realtime error recovery)
realtimeDrawerPulse(t: 2) DLE DC4 (realtime drawer kick)
autoStatusBack(mask) GS a n (auto status report on notify)
transmitId(n: 1) GS I n (printer id/info, reply on notify)
deviceStatus() GS 0x99 โ€” vendor status; decode reply with DeviceStatus.parse
feedToRightMark() / feedToLeftMark() SO / FF (feed to black mark)
cutGsV(feedUnits: 3) GS V (alternative cut to cut())
codePage(EscCodePage.gbk) FS c (GBK / Big5 code page)
qrcodeGsk('...', ecc: 4) GS o + GS q + GS k m=11 (alternative QR to qrcode())
qrcodeSelect('...', size: 6) GS Z select + ESC Z print (alternative QR for devices that reject qrcode())
barcode128Raw('...') GS k m=0x18 + raw data + NUL (printer encodes; alternative to barcode128())
selectPeripheral(n) ESC = (select peripheral device)
panelKeys(false) ESC c 5 (enable/disable feed key)
chineseCodeFormat(n) ESC 9 (Chinese code format)
userDefinedCharset(true) / cancelUserChar(code) ESC % / ESC ?

Page mode (printers that support it):

Method ESC/POS
pageMode() / standardMode() ESC L / ESC S
pageArea(x, y, w, h) ESC W (print area)
pageDirection(n) ESC T (print direction 0โ€“3)
printPage() ESC FF (print page buffer)
pageAbsoluteVertical(n) / pageRelativeVertical(n) GS $ / GS \

Some commands above are vendor extensions (e.g. deviceStatus, codePage, qrcodeGsk, the black-mark feeds) and are provided as alternatives to the standard-ESC/POS methods โ€” pick whichever your device actually implements.

code128 via the general barcode() needs a code-set prefix in the data ({A, {B, {C). The older barcode128() helper encodes automatically. Barcode/QR command dialects vary by device: if barcode128() prints the {Cโ€ฆ data as text or qrcode() prints the raw URL, switch to barcode128Raw() / qrcodeSelect() (both let the printer encode internally).

Transport-agnostic. The builders only generate command bytes โ€” build() returns a List<int>. This plugin sends them over BLE, but the same bytes work over any channel: if your printer only speaks classic Bluetooth SPP (e.g. some legacy models) or USB/serial, keep using CpclBuilder / EscBuilder and just write build()'s output through your own connection.

Custom / vendor commands

Command sets vary by brand. When a command isn't wrapped by the builder, use the escape hatches instead of forking the plugin:

Builder Method Appends
CpclBuilder raw(String) CPCL fragment, no newline
CpclBuilder rawLine(String) CPCL fragment + \n (CPCL is line-based)
EscBuilder raw(List<int>) raw bytes verbatim
EscBuilder rawText(String) text via the builder's encoder (GBK/UTF-8)

Wrap your own named helpers with a Dart extension, so brand-specific commands read just like the built-in ones and stay chainable:

extension AcmeCpcl on CpclBuilder {
  CpclBuilder acmeCutter() => rawLine('ACME-CUT 1');
}

final bytes = CpclBuilder(width: 400, height: 200)
    .text('Hi', 10, 10)
    .acmeCutter()   // your vendor command, chained
    .build();
extension AcmeEsc on EscBuilder {
  EscBuilder acmeDensity(int n) => raw([0x1D, 0x7C, n]);
}

Tables

Both builders have a table() helper โ€” but they work differently because the two protocols are fundamentally different.

ESC/POS โ€” monospace text table

Columns are space-padded (CJK counts as 2 chars). Optional dashed rule under the header. Relies on the printer's monospace font.

EscBuilder()
  .table(
    [
      ['Item', 'Qty', 'Amount'],
      ['Cola', '2', 'ยฅ6.00'],
      ['Bread', '1', 'ยฅ4.50'],
    ],
    columnWidths: [16, 6, 10],                       // in characters
    align: [EscAlign.left, EscAlign.center, EscAlign.right],
    rule: true,                                      // dashed line under header
  )
  .feed(lines: 2)
  .build();

CPCL โ€” grid table with borders

Coordinate-based: draws grid lines with LINE and places TEXT in each cell.

CpclBuilder(width: 500, height: 300)
  .table(
    10, 20,
    columnWidths: [220, 100, 120],                   // in dots
    rowHeight: 44,
    rows: [
      ['Item', 'Qty', 'Amount'],
      ['Cola', '2', '6.00'],
      ['Bread', '1', '4.50'],
    ],
    border: true,                                    // set false for text-only
    size: 1,
  )
  .build();

For per-cell styling beyond what table() offers (bold headers, mixed fonts), compose cells yourself with text() / line() / box().

Chinese text โ€” pluggable encoder

Most Chinese thermal printers expect GBK, not UTF-8. This plugin does not hard-depend on a GBK library โ€” plug one in:

dependencies:
  fast_gbk: ^1.0.0   # add this yourself
import 'package:fast_gbk/fast_gbk.dart';

final bytes = CpclBuilder(encoder: gbk.encode)
  .text('ไธญๆ–‡ๆ ‡็ญพ', 10, 10, size: 2)
  .build();

Without an encoder override you get UTF-8, which prints ASCII fine but mojibake for Chinese on most modules.

Image printing

Both builders can print monochrome bitmaps. The plugin is dependency-free here: it does NOT decode PNG/JPG โ€” you provide pixels, MonoBitmap packs them into 1-bit printer data (MSB-first, 1 = black).

// You already have RGBA pixels (width*height*4 bytes):
final bmp = MonoBitmap.fromRgba(
  rgba, width, height,
  threshold: 128,   // black if luminance < threshold
  dither: false,    // Floydโ€“Steinberg for photos
);

// CPCL label:
final cpcl = CpclBuilder(width: 400, height: 300)
  .image(bmp, 20, 20)
  .build();

// ESC/POS receipt:
final esc = EscBuilder()
  .align(EscAlign.center)
  .image(bmp)        // GS v 0 raster
  .feed(lines: 2)
  .build();

Two ESC/POS image modes โ€” pick what your printer supports

Method Command Use when
EscBuilder.image(bmp) GS v 0 raster Modern printers. One command, simplest.
EscBuilder.imageBitImage(bmp) ESC * 33 (24-dot column) Old / cheap ESC/POS clones that print garbage or nothing with GS v 0. Widest compatibility.

Both take the same MonoBitmap โ€” if one prints blank or scrambled, switch to the other. imageBitImage mirrors the classic column-band approach used by many production POS apps.

Some devices don't support ESC/POS images at all. Label-oriented devices (CPCL/TSPL class) often emulate ESC/POS text but print both GS v 0 and ESC * image data as stray characters. On those, print images (and barcodes / QR) via the CPCL builder โ€” CpclBuilder.image(bmp, x, y) โ€” which they render natively. Rule of thumb: receipt-class devices โ†’ ESC/POS image; label/CPCL-class devices โ†’ CPCL image. The same split applies to barcodes and QR codes (see the barcode/QR notes above).

Also available: MonoBitmap.fromGrayscale(gray, w, h), and the low-level CpclBuilder.graphics(...) / EscBuilder.raster(...) if you already have packed 1-bit bytes.

Decoding a PNG/asset (optional image dependency)

MonoBitmap takes raw pixels, so to print an actual image file, decode it in your app with the pure-Dart image package (kept out of this plugin's dependencies on purpose):

dependencies:
  image: ^4.0.0   # add this yourself, only if you need file decoding
import 'package:image/image.dart' as img;
import 'package:flutter/services.dart' show rootBundle;

final data = await rootBundle.load('assets/logo.png');
final decoded = img.decodeImage(data.buffer.asUint8List())!;
// Resize to the printer's dot width (e.g. 384 dots for 58mm, 576 for 80mm):
final resized = img.copyResize(decoded, width: 384);

// Extract RGBA bytes:
final rgba = resized.getBytes(order: img.ChannelOrder.rgba);
final bmp = MonoBitmap.fromRgba(rgba, resized.width, resized.height, dither: true);

await PrinterManager.instance.writeRaw(
  EscBuilder().align(EscAlign.center).image(bmp).feed(lines: 3).build(),
);

Rendering a Flutter widget to a bitmap (no extra dependency)

Wrap a widget in a RepaintBoundary, capture it via dart:ui, then feed the pixels to MonoBitmap.fromRgba. The bundled PrinterDebugPage already does exactly this (its CPCLๅ›พ็‰‡ๆ‰“ๅฐ / ESCๅ›พ็‰‡ๆ‰“ๅฐ buttons capture the on-screen Flutter logo) โ€” read its source for a complete, dependency-free recipe.

PrinterManager API

Member Purpose
startScan({timeout}) / stopScan() BLE scan control.
scanResults (Stream) Deduplicated List<BleDevice> snapshots.
connect(device, {profile}) Connect + discover characteristics.
disconnect() Cancels reconnect timer, closes streams.
writeRaw(bytes) Chunked write, waits for print-complete. Serialised.
state / stateStream PrinterConnectionState.
notifyStream Raw bytes received from the notify characteristic.
discoverAll() Enumerate all services / characteristics (for UI).
useCharacteristics({...}) Override current write / notify at runtime.
connectedDevice / profile / isReady Getters (connectedDevice returns the raw flutter_blue_plus device).
connectedDeviceId / connectedDeviceName String getters โ€” no flutter_blue_plus type leaked.
boundServiceUuid / boundWriteUuid / boundNotifyUuid The actually-resolved UUIDs (incl. auto-detect), or null before ready.
negotiatedMtu Live negotiated MTU (23 before negotiation). Drives autoMtu chunking.

writeRaw() writes in mtu - 3 byte chunks and then waits for the printer to send a byte on the notify characteristic (or a printTimeoutMs timeout). Consecutive writeRaw() calls are queued, so multi-label jobs never interleave. If no notify characteristic is available, a fixed fallbackDelayMs is used instead.

Errors & exceptions

Call Throws when Handle by
startScan() Bluetooth is off, or required runtime permissions are denied. Wrap in try/catch; prompt the user to enable BT / grant permission. The bundled scanner page already surfaces this via a SnackBar.
connect() Device unreachable, or no writable characteristic found after discovery. Catch, show a retry; try PrinterProfile.autoDetect or the debug page to pick a characteristic manually.
writeRaw() Called before a printer is ready โ€” throws StateError('Printer not ready...'). Guard with if (PrinterManager.instance.isReady), or catch and re-connect.

writeRaw() resolving does not guarantee ink on paper โ€” it means the printer acknowledged (notify byte) or the printTimeoutMs / fallbackDelayMs elapsed. There's no paper-out / low-battery signal in CPCL/ESC itself; poll the printer's own status command if your model supports one.

Lifecycle

PrinterManager.instance is a process-wide singleton and holds the live BLE connection. It does not auto-dispose:

  • Keep the connection open across pages โ€” don't disconnect() on every dispose(), or you'll drop the printer between screens.
  • Call disconnect() once when you're truly done (e.g. logout, or app shutdown via WidgetsBindingObserver.didChangeAppLifecycleState). This cancels the 10-second reconnect timer and closes the streams.
  • After disconnect() the singleton is reusable โ€” just connect() again.

What's intentionally NOT included (and how to add it)

To avoid clashing with your app's dependency versions, the plugin skips a few conveniences that most apps end up implementing their own way. Suggested extensions:

1. Auto-reconnect to last device on app launch

Store the last known device id yourself (e.g. via shared_preferences) and call connect() at startup:

final lastId = prefs.getString('last_printer_id');
if (lastId != null) {
  await PrinterManager.instance.connect(
    BleDevice(id: lastId, name: '', rssi: 0),
    profile: myProfile,
  );
}

The manager already retries every 10 seconds while disconnected.

2. Permission handling

See "Runtime permissions" above. Wrap startScan() behind your permission gate.

3. Image / bitmap printing

Supported โ€” see Image printing. Monochrome packing (MonoBitmap) is built in and dependency-free; PNG/JPG decoding is delegated to the optional image package so it never conflicts with your app's version.

4. Persistent connection state

PrinterManager is a singleton but stateful in memory. If you kill the process, connection state is lost โ€” restore it in your bootstrap code.

Roadmap

  • TSPL / TSC label protocol builder (for TSC-family label printers).
  • Extra font-table presets for vendor-specific textFontSize mappings.
  • Optional persistence extension package (cpcl_esc_printer_prefs).

License

MIT โ€” see LICENSE.

่ฏทไฝœ่€…ๅ–ๆฏๅ’–ๅ•ก โ˜•

ๅฆ‚ๆžœ่ฟ™ไธชๆ’ไปถๅธฎไฝ ็œไธ‹ไบ†ๅฏนๆŽฅ่“็‰™ๆ‰“ๅฐๆœบ็š„ๆ—ถ้—ด๏ผŒๆฌข่ฟŽๆ‰“่ตๆ”ฏๆŒไธ€ไธ‹๏ฝž ไฝ ็š„ๆฏไธ€ไปฝ้ผ“ๅŠฑ๏ผŒ้ƒฝๆ˜ฏๆˆ‘ๆŒ็ปญ็ปดๆŠคๅ’Œๆ›ดๆ–ฐ็š„ๅŠจๅŠ›ใ€‚ๅผ€ๆบไธๆ˜“๏ผŒๆ„Ÿ่ฐขๆœ‰ไฝ  ๐Ÿ™

ๆ”ฏไป˜ๅฎ ๅพฎไฟกๆ”ฏไป˜
ๆ”ฏไป˜ๅฎๆ‰“่ต ๅพฎไฟกๆ‰“่ต

ๆ‰“่ต็บฏๅฑž่‡ชๆ„ฟ๏ผŒไธๆ‰“่ตไนŸๅฎŒๅ…จๅฏไปฅๆ”พๅฟƒไฝฟ็”จใ€‚ๆœ‰้—ฎ้ข˜ๆฌข่ฟŽๆ Issue๏ผŒๆˆ‘ไผš่ฎค็œŸๅ›žๅคใ€‚

Libraries

cpcl_esc_printer
Public API of the cpcl_esc_printer plugin. cpcl_esc_printer ๆ’ไปถ็š„ๅ…ฌๅผ€ APIใ€‚