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:
-
Default โ auto-detect (the "generic" path). Call
connect()with no profile (orPrinterProfile.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. -
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 customPrinterProfileso it binds directly without guessing. -
Special case โ auto-detect / generic bind the wrong characteristic. Some modules expose vendor-specific characteristics (e.g.
fff0/fff1/fff2and 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:- Open the bundled
PrinterDebugPage. It lists every service/characteristic with its properties (W/WNR/R/N). - Tap the correct WRITE chip (and a NOTIFY chip if available), then Apply picked โ the current connection rebinds live, no reconnect.
- 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.
- Note the working UUIDs down and pass them as a custom
PrinterProfile(see below) so production code binds directly and skips this step.
- Open the bundled
| 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
textFontSizefont, 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 (CPCLEG/ ESCESC *). 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 โSETSPactually controls character spacing, not line spacing; usecharSpacing()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.
code128via the generalbarcode()needs a code-set prefix in the data ({A,{B,{C). The olderbarcode128()helper encodes automatically. Barcode/QR command dialects vary by device: ifbarcode128()prints the{Cโฆdata as text orqrcode()prints the raw URL, switch tobarcode128Raw()/qrcodeSelect()(both let the printer encode internally).
Transport-agnostic. The builders only generate command bytes โ
build()returns aList<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 usingCpclBuilder/EscBuilderand just writebuild()'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 0andESC *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. |
Print-complete detection
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 everydispose(), or you'll drop the printer between screens. - Call
disconnect()once when you're truly done (e.g. logout, or app shutdown viaWidgetsBindingObserver.didChangeAppLifecycleState). This cancels the 10-second reconnect timer and closes the streams. - After
disconnect()the singleton is reusable โ justconnect()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
textFontSizemappings. - Optional persistence extension package (
cpcl_esc_printer_prefs).
License
MIT โ see LICENSE.
่ฏทไฝ่ ๅๆฏๅๅก โ
ๅฆๆ่ฟไธชๆไปถๅธฎไฝ ็ไธไบๅฏนๆฅ่็ๆๅฐๆบ็ๆถ้ด๏ผๆฌข่ฟๆ่ตๆฏๆไธไธ๏ฝ ไฝ ็ๆฏไธไปฝ้ผๅฑ๏ผ้ฝๆฏๆๆ็ปญ็ปดๆคๅๆดๆฐ็ๅจๅใๅผๆบไธๆ๏ผๆ่ฐขๆไฝ ๐
| ๆฏไปๅฎ | ๅพฎไฟกๆฏไป |
|---|---|
![]() |
![]() |
ๆ่ต็บฏๅฑ่ชๆฟ๏ผไธๆ่ตไนๅฎๅ จๅฏไปฅๆพๅฟไฝฟ็จใๆ้ฎ้ขๆฌข่ฟๆ Issue๏ผๆไผ่ฎค็ๅๅคใ
Libraries
- cpcl_esc_printer
- Public API of the
cpcl_esc_printerplugin.cpcl_esc_printerๆไปถ็ๅ ฌๅผ APIใ

