flutter_niimbot
An unofficial Flutter package for building label-printing experiences with NIIMBOT Bluetooth printers.
Current device support: NIIMBOT D11H only.
Support for other NIIMBOT models is not available yet.
This package is under active development. The label model, renderer, and D11H application facade are available through the stable public entry point.
Features
- Define label dimensions in millimeters
- Render text to a monochrome raster at 203 DPI
- Use normal or 90-degree rotated label orientation
- Configure text alignment, position, wrapping, size, and weight
- Work with typed BLE device, connection, service, and failure models
- Scan, connect, and print through the D11H application facade
- Warm-connection printing on iOS (no pre-print GATT refresh)
- Probe-aligned print characteristic discovery for real devices
Supported printers
| Manufacturer | Model | Status |
|---|---|---|
| NIIMBOT | D11H | Supported |
No other NIIMBOT printer model is currently supported or tested.
Installation
Add flutter_niimbot to your app:
dependencies:
flutter_niimbot: ^0.1.0-dev.12
Then fetch the dependency:
flutter pub get
Usage
Import the stable public entry point:
import 'package:flutter_niimbot/niimbot.dart';
Create and render a text label:
final document = LabelDocument(
size: LabelSize.d11h12x22,
orientation: LabelOrientation.rotated90,
elements: [
LabelText(
text: '상품명\n닉네임',
xMm: 0,
yMm: 1,
widthMm: 22,
heightMm: 10,
fontSizePt: 15,
alignment: LabelTextAlignment.start,
horizontalPosition: LabelHorizontalPosition.center,
wrap: true,
bold: true,
),
],
);
final raster = await const TextLabelRenderer().render(document);
Print through the high-level facade:
final printer = D11hPrinter();
try {
final devices = await printer.scan();
if (devices.isEmpty) {
throw StateError('No BLE printers found.');
}
await printer.connect(devices.first.deviceId);
await printer.printLabel(document);
// Or render first, then print on the warm connection:
// await printer.printRenderedLabel(raster);
} finally {
await printer.dispose();
}
Printing behavior
connect()discovers services, negotiates MTU, subscribes to the print characteristic, and settles briefly before the link is used.printLabel()andprintRenderedLabel()print on the current BLE connection. They do not disconnect and reconnect before each label.- Raster rows are coalesced into MTU-sized BLE writes rather than one write per row, matching the captured official-app traffic where a single write carries several frames.
- All printer operations are serialized through an internal queue.
scan()disconnects an active printer before discovery and ends with an explicitstopScan()so iOS scan results are not lost to timeout cleanup.
Continuous printing
Do not loop over printLabel() to print a run of labels. Each call opens
and closes a print job, and the printer re-runs its start-up and gap
calibration every time a job opens, so a loop is much slower than the official
app.
For several copies of the same label, pass copies so the raster is uploaded
once and the printer repeats it:
await printer.printLabel(document, copies: 10);
For a run of different labels, hand them over together so they share one job:
await printer.printLabels(<LabelDocument>[first, second, third]);
// Or, when the rasters are already rendered:
await printer.printRenderedLabels(<MonochromeRaster>[first, second, third]);
Tracking progress in a batch
A batch is one job, so it completes once. When you need to know about each label
— to update a counter, or to reconcile a quantity with a server — pass
onProgress. It reports the printer's cumulative printed-label count read from
the status poll, so it counts labels actually ejected rather than labels sent
over BLE:
var reported = 0;
final pending = <Future<void>>[];
await printer.printLabel(
document,
copies: 10,
onProgress: (printedLabels) {
// Queue the work; never await inside the callback.
while (reported < printedLabels) {
reported++;
pending.add(reportOnePrinted());
}
},
);
await Future.wait(pending);
onProgress is called synchronously from the print loop. Blocking in it stalls
the printer between labels, which is exactly the cost a single job removes — so
queue slow work and await it after the job returns. A callback that throws is
logged and ignored, since the labels are already committed to the printer.
Tuning write pacing
writeWithoutResponse has no flow control on iOS: flutter_reactive_ble calls
CBPeripheral.writeValue(type: .withoutResponse) and reports success
immediately without consulting canSendWriteWithoutResponse, so writing faster
than the link drains silently loses packets. Android routes through
RxAndroidBle, which waits for onCharacteristicWrite, and needs no pacing.
D11hPrinter therefore paces raster writes, defaulting to 8 ms between
coalesced packets. Tune it per platform when you want to:
final printer = D11hPrinter(
rasterInterWriteDelay: Platform.isAndroid
? Duration.zero
: const Duration(milliseconds: 8),
);
Coalescing several frames into one write is inferred from a single official-app
capture. If a printer misprints with it on, coalesceRasterWrites: false goes
back to one write per frame while keeping the single-job structure:
final printer = D11hPrinter(coalesceRasterWrites: false);
Measuring on a real printer
tool/d11h_probe has a Continuous print benchmark in the text-label card:
enter a label count, pick a print path, and it times the whole run.
| Path | What it measures |
|---|---|
| Single job + batched writes | The current behavior |
| Single job, one write per row | Isolates the batching win |
| Job per label, one write per row | Reproduces the old behavior |
Run the same count on the same roll for each path and compare the reported per-label time.
Media information
Media information is opt-in. The library does not read media automatically before printing, and it does not include built-in total-label counts. Applications provide their own roll profile when they want remaining-label estimates:
final info = await printer.readMediaInfo(
profile: D11hMediaRollProfile.d11h12x22,
);
print(info.state);
print(info.usageCounter);
print(info.remainingEstimate?.remainingLabels);
print(info.remainingEstimate?.remainingPercent);
D11hMediaRollProfile.fromTotalLabels() uses the observed D11H full-roll
counter of 256. With that profile, applications only provide the roll's total
label count; the current RFID counter determines the remaining labels and
percentage.
Built-in D11H profiles are available for the common rolls observed so far:
D11hMediaRollProfile.d11h12x22 // 260 labels
D11hMediaRollProfile.d11h12x30 // 195 labels
To auto-detect media after connecting, call readMediaInfo() immediately after
connect() in the app layer. The probe app does this so iOS testing shows the
loaded roll, counter, and remaining percentage as soon as the printer connects.
In the probe app, leave Total labels empty to use the selected 12x22/12x30
default, or enter a custom total when testing another roll.
Without a profile, the library reports loaded/not-loaded state, candidate identifiers, raw frames, and the observed counter, but remaining labels are unknown.
Print characteristic discovery
findD11hPrintCharacteristic() prefers FFF0/FFF1, then falls back to any
characteristic that supports notify and writeWithoutResponse, matching the
probe app's discovery logic on iOS.
Bluetooth setup
Applications using BLE functionality must configure the Android and iOS
Bluetooth permissions required by flutter_reactive_ble. Permission prompts
remain the responsibility of the application.
On iOS, avoid requesting Bluetooth permission through permission_handler
before scanning; let Core Bluetooth handle the system prompt.
The repository includes an internal D11H probe application under
tool/d11h_probe for protocol research and diagnostics.
API status
Use package:flutter_niimbot/niimbot.dart for the public API.
D11hPrinter, D11H characteristic discovery, label models, rendering, and BLE
transport types are exported by the stable entry point.
package:flutter_niimbot/niimbot_research.dart exposes low-level probe and
protocol research APIs. These APIs may change without notice and are not
covered by semantic-versioning guarantees.
Limitations
- Only NIIMBOT D11H has been characterized and tested.
- Text labels are supported; image, barcode, and QR-code elements are not yet part of the public renderer.
- A successful BLE write alone does not guarantee that a physical label was printed.
Disclaimer
This is an independent, unofficial project. It is not affiliated with, endorsed by, or sponsored by NIIMBOT.
License
See LICENSE.