flutter_zpl_printer 0.1.1
flutter_zpl_printer: ^0.1.1 copied to clipboard
Discover, connect to, and print on Zebra ZPL label printers over Bluetooth LE, Wi-Fi/TCP, and USB. Pure-Dart protocol stack, no Link-OS SDK required.
flutter_zpl_printer #
Discover, connect to, and print on Zebra ZPL label printers from Flutter over Bluetooth LE, Wi-Fi / TCP, and USB.
The plugin talks to the printer in Zebra's own protocols (SGD, ZPL ~HS status,
Zebra BLE GATT services, USB printer class) from Dart. It does not depend on the
Link-OS SDK, so there are no proprietary binaries to download or copy.
It pairs well with flutter_zpl_generator
for building the ZPL you send.
Upgrading from 0.0.1? 0.1.0 is a full rewrite with a new API. See the migration table in the changelog.
๐ Integration guide: availability checks, discovery, transport fallback, printing, settings, error handling, and troubleshooting, based on a production app.
Platform support #
| Transport | iOS | macOS | Windows | Android |
|---|---|---|---|---|
| Bluetooth LE | โ | โ | โ | โ |
| Wi-Fi / TCP | โ | โ | โ | โ |
| USB | โ not possible | โช not tested | โ fails in testing | โช not tested, libusb not bundled |
โ works in hardware testing ยท โ fails in hardware testing ยท โช code exists, not verified on hardware ยท โ platform limitation
USB, in short: experimental. It hasn't been confirmed working on any platform yet. It is
untested on macOS and Android, it fails on Windows in our testing (cause not confirmed yet), and
iOS doesn't allow it (USB calls throw UsbUnsupportedOnPlatformException). Use Bluetooth LE or
Wi-Fi for production printing.
Installation #
dependencies:
flutter_zpl_printer: ^0.1.1
import 'package:flutter_zpl_printer/flutter_zpl_printer.dart';
Platform setup #
iOS: ios/Runner/Info.plist
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Used to connect to Zebra printers over Bluetooth.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Used to discover Zebra printers on your local network.</string>
universal_ble (the BLE dependency) requires iOS 13.1 or later.
macOS: entitlements and Info.plist
macos/Runner/DebugProfile.entitlements and Release.entitlements:
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/> <!-- needed to receive UDP discovery replies -->
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.usb</key>
<true/>
macos/Runner/Info.plist:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Used to connect to Zebra printers over Bluetooth.</string>
The libusb dylib is bundled through the plugin's podspec. Nothing else to install.
Windows
Bluetooth LE and Wi-Fi need no extra setup. For USB, read Known issues โ Windows USB first.
Android: AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
Request Bluetooth permissions at runtime (for example with permission_handler) before scanning.
Quick start #
1. Find a printer #
// USB + UDP broadcast/multicast + BLE, merged and de-duplicated.
await for (final printer in DiscoveryService.discoverAll()) {
print('${printer.connectionType.displayName}: ${printer.friendlyName} @ ${printer.address}');
}
Or one transport at a time:
BleDiscovery.discoverZebra(timeout: const Duration(seconds: 10)); // Bluetooth LE
NetworkDiscovery.discover(); // UDP broadcast on port 4201
UsbDiscovery.enumerate(); // attached USB printers
2. Connect #
// From a discovery result: picks the right Connection type for you.
final printer = await ZebraPrinter.connect(discovered.createConnection());
// Or build a connection yourself:
final ble = BleConnection(deviceId); // Bluetooth LE
final tcp = TcpConnection.zpl('192.168.1.50'); // Wi-Fi, port 9100
final usb = UsbConnection(UsbDeviceAddress.parse('usb://0A5F:0027/XXSERIAL')); // USB
3. Print and query #
final status = await printer.getStatus();
if (status.isReadyToPrint) {
await printer.printZpl('^XA^FO50,50^A0N,40,40^FDHello Zebra^FS^XZ');
} else if (status.isHeadOpen) {
print('Close the print head');
} else if (status.isPaperOut) {
print('Load labels');
}
final model = await printer.getSetting(PrinterSgdKey.deviceProductName.value);
final firmware = await printer.getSetting(PrinterSgdKey.applName.value);
await printer.calibrate();
await printer.printConfigurationLabel();
await printer.disconnect();
4. Handle errors #
Every transport throws ConnectionException or a subclass:
try {
await printer.printZpl(zpl);
} on UsbDeviceBusyException catch (e) {
showError(e.remediation ?? e.message); // tells the user how to fix it
} on ConnectionException catch (e) {
showError(e.message);
}
5. Print images #
Use flutter_zpl_generator to turn images into ZPL,
then send the result with printZpl. This is the image path that has been tested on real printers.
This package's own printImage / GraphicsUtil has a known issue in 0.1.x.
import 'dart:typed_data';
import 'package:flutter_zpl_generator/flutter_zpl_generator.dart' as zpl; // prefix: both packages define ZplPrintMode
import 'package:flutter_zpl_printer/flutter_zpl_printer.dart';
Future<void> printPicture(ZebraPrinter printer, Uint8List pngBytes) async {
// Print width in dots, as reported by the printer (fallback: 384 dots,
// a 2-inch 203 dpi mobile printer).
final raw = await printer.getSetting(PrinterSgdKey.ezplPrintWidth.value);
final width = int.tryParse(raw.trim()) ?? 384;
final label = await zpl.ZplGenerator(
config: zpl.ZplConfiguration(
printWidth: width,
printMode: zpl.ZplPrintMode.tearOff,
),
autoLabelLengthFromFirstImage: true,
commands: [
zpl.ZplImageDownload(
image: pngBytes,
targetWidth: width,
ditheringAlgorithm: zpl.ZplDitheringAlgorithm.threshold,
),
const zpl.ZplImageRecall(), // x: 0, y: 0, graphicName: 'IMG'
],
).build();
await printer.printZpl(label);
}
ZplImageDownload sends uncompressed hex by default (compression: ZplImageCompression.none),
which every Zebra printer accepts. Threshold dithering is what the tested app uses: zPrint found
that Floyd-Steinberg's dense dot coverage can trip the print head's thermal protection on a ZQ620.
The example app shows all three transports end to end. The integration guide covers the production details.
Features #
- Connections:
BleConnection,TcpConnection,UsbConnection, plusMultichannelBleConnection/MultichannelTcpConnection(separate print and status channels) andReconnectableConnection(auto-reconnect with exponential backoff). - Discovery: UDP broadcast, directed broadcast, multicast, TCP subnet search, BLE scan, USB enumeration,
and USB hot-plug events (
UsbHotplugStream.events()). - Status: full
~HSparsing (isReadyToPrint,isPaperOut,isHeadOpen,isRibbonOut,isPaused,isHeadTooHot, labels remaining, and more). - SGD (Set/Get/Do):
Sgd.get,Sgd.set,Sgd.doCommand, and a catalog of verified keys inPrinterSgdKey. - Files and formats: list, store, and delete files on
E:/R:; store formats and print them with^FNfield data (FormatUtil.printStoredFormat). - Graphics: convert PNG/JPEG to GRF or Z64 and print or store it (
GraphicsUtil). For printing images, preferflutter_zpl_generator; see the Z64 known issue. - More utilities:
FontUtil,AlertUtil,ProfileUtil(backup / restore),FirmwareUtil,ZplSanitizer.
SGD example #
final darkness = await Sgd.get(PrinterSgdKey.headDarknessSwitch.value, connection);
await Sgd.set(PrinterSgdKey.deviceFriendlyName.value, 'Warehouse-01', connection);
// Sgd.doCommand waits for a reply. Actions like device.reset never send one,
// so write those directly:
await connection.write(Uint8List.fromList(utf8.encode('! U1 do "device.reset" ""\r\n')));
USB details #
Check the platform table first. USB is part of DiscoveryService.discoverAll()
by default, and USB failures there are swallowed, so other transports keep working.
// Plug/unplug events: macOS and Android. Not emitted on Windows yet.
UsbHotplugStream.events().listen((e) => print('${e.type} โ ${e.address}'));
const cfg = ConnectionConfig(
usbBulkTimeoutMs: 8000, // default 5000
usbMaxChunkSize: 0, // 0 = use the endpoint's wMaxPacketSize
usbStallRetries: 3,
);
final conn = UsbConnection(address, config: cfg);
libusb 1.0.29 is bundled under third_party/libusb/ and linked dynamically
(LGPL-2.1-or-later; see third_party/libusb/1.0.29/LICENSE).
Testing your app #
import 'package:flutter_zpl_printer/flutter_zpl_printer_testing.dart';
final fake = FakeUsbPlatform()..devices.add(UsbDeviceRecord(
vendorId: 0x0A5F, productId: 0x0027, path: '/fake', hasPermission: true,
serialNumber: 'XX1', interfaceNumber: 0, bulkInEndpoint: 0x81, bulkOutEndpoint: 0x01,
));
final conn = UsbConnection.withPlatform(UsbDeviceAddress.parse('usb://0A5F:0027/XX1'), fake);
Known issues #
Windows USB #
Status: USB printing on Windows failed when tested with Zebra printers. The cause is not confirmed yet. Bluetooth LE and Wi-Fi work on Windows. Until this is fixed, treat Windows USB as experimental and offer Wi-Fi or Bluetooth.
What the Windows USB code does in 0.1.0:
- Lists printers with Windows SetupAPI.
- Opens them through libusb (
libusb-1.0.dll), assuming interface 0, bulk endpoints0x01/0x81, and 64-byte packets. It doesn't read these from the printer yet. - Does not emit plug/unplug events.
UsbHotplugStream.events()stays silent on Windows.
Possible causes we're investigating:
libusb-1.0.dllis not bundled. If the build log sayslibusb-1.0.dll not found, opens fail withUsbLibLoadException. Workaround: put the official libusb 1.0.29VS2022/MS64/dll/libusb-1.0.dllnext to your app's.exe.- The printer is bound to the Windows printer driver. With Zebra Setup Utilities / ZDesigner installed,
Windows binds the printer to
usbprintand libusb can't claim it. You getUsbDeviceBusyException; show itsremediationtext. Rebinding to WinUSB with Zadig gives libusb access, but then the Windows print queue can't use the printer. - The assumed endpoints don't match the printer. That would show as a transfer timeout or stall on write.
If you try it, please report your printer model, Windows version, the driver shown in Device Manager, and the exception text on the issue tracker.
Android USB #
Android USB needs libusb-1.0.so built per ABI with the NDK. 0.1.0 does not ship those binaries,
so Android USB calls fail with UsbLibLoadException. See tool/fetch_libusb.sh.
Image compression (Z64) #
printer.printImage(...) and GraphicsUtil.printImage(...) compress images with Z64 by default, and
0.1.x computes the Z64 checksum over the raw bitmap. Zebra's ZPL II Programming Guide says the CRC must
be "calculated over the :encoded_data field" and that "a CRC mismatch is treated as an aborted
download", so a printer that checks it can drop the image. This path has not been tested on hardware.
Workarounds:
- Recommended: build image labels with
flutter_zpl_generatorand send them withprintZpl(example). - Or pass
useCompression: falsetoGraphicsUtil.printImageto send uncompressed hex.
Bluetooth LE / Wi-Fi printing, printZpl, and images built with flutter_zpl_generator are not affected.
Other limitations #
- Classic Bluetooth (iOS MFi, Android SPP) is not supported. Use Bluetooth LE.
- Status parsing targets ZPL. CPCL printers connect and print, but status objects are ZPL-specific.
- TLS connections are not supported. TCP is plaintext.
Contributing #
Issues and pull requests are welcome at github.com/minhtri1401/flutter_zpl_printer.
This package is not affiliated with or endorsed by Zebra Technologies. Zebra, Link-OS, and ZPL are trademarks of Zebra Technologies Corp.
License #
MIT for this package. Bundled libusb is LGPL-2.1-or-later.