nblib
The NIIMBOT printer protocol for the B1: packet framing, 1-bit raster encoding, print-job orchestration, and a Web Bluetooth transport. Browser-first ESM, no dependencies.
A port of niimbluelib (MIT), verified byte-for-byte against it. See ACKNOWLEDGEMENTS.md for what came from where, why this exists separately, and where the two deliberately differ.
Status: printed on a real B1, first attempt, 2026-08-21. The encoder was verified byte for byte against niimbluelib across hundreds of rasters before that; the transport and the print sequence have now met hardware too. Only the default path is confirmed, though — other stock types, densities and the
leftfeed direction are still untested, so reports are welcome.
bun add @slastra/nblib
Use
import { buildPage, imageDataToRows, printJob, LabelType } from '@slastra/nblib';
import { connect } from '@slastra/nblib/web-bluetooth';
const link = await connect(); // must be called from a user gesture
const ctx = canvas.getContext('2d')!;
const rows = imageDataToRows(ctx.getImageData(0, 0, canvas.width, canvas.height));
const page = buildPage(rows, { direction: 'top' });
await printJob(link, [() => Promise.resolve(page)], {
density: 3,
labelType: LabelType.WithGaps,
onProgress: (done, total) => console.log(`${done}/${total}`)
});
printJob takes builders rather than pages so a thousand-label batch does not
rasterize a thousand labels up front; page i+1 renders while page i is
still on the wire.
Print direction is a property of the label
The single thing most likely to go wrong. direction says which edge of the
design leaves the printer first, which decides which of the design's two
dimensions has to fit across the print head:
direction |
leading edge | crosses the head | must be ≤ 384 |
|---|---|---|---|
top |
the design's top | its width | width |
left |
the design's left | its height | height |
left rotates the raster 90° clockwise on the way out. Pick the wrong one and
the label prints a quarter turn off — which is invisible on square stock and
obvious on anything else. buildPage refuses a raster that will not fit across
the head, and says so:
raster is 400 dots across the head, which is 384 dots wide — try printDirection "left"
The B1
| Resolution | 203 dpi, 8 dots/mm |
| Print head | 384 dots — 48 mm, not the 50 mm the stock is |
| Stock | gap, black-mark, or transparent |
| Density | 1–5, default 3 |
| Model id | 4096 |
That 48 mm is the trap: 50 × 30 mm stock is 400 dots wide, and 400 dots do not fit across a 384-dot head. Design to 384.
How it works
- Packets are
55 55 <cmd> <len> <data> <xor> aa aa, big-endian throughout, with one length byte — so data caps at 255 bytes and the raster goes one packet per row rather than as a stream. Connect is the only packet that carries a leading0x03. - Rows compress three ways: identical consecutive rows collapse to a repeat count, blank runs carry no data at all, and a row with six dots or fewer is sent as a list of dot positions instead of a bitmap. Each row packet carries three count bytes the printer uses to pace head current.
- The transport is request/response. Almost every command has a specific
reply that must land before the next one goes out, so a
Linkhas to hand packets back as well as push bytes — and buffer them, because a reply can arrive before anyone asks for it. The raster is the exception: those three commands are one-way and go as one burst. - Discovery matches on device name, never on service UUID. A service-UUID
filter makes Chrome push a
SetDiscoveryFilterUUID list to BlueZ, which segfaultsbluetoothd5.87 on desktop Linux and takes the whole Bluetooth stack down with it. There is a test guarding this.
Transports
Link is two methods — send and receive — so anything that moves bytes and
hands packets back can drive a job. Web Bluetooth ships here; Web Serial or a
node BLE stack would slot in unchanged.
Development
bun install
bun test # 83 tests: reference vectors, round-trips, job flow, discovery
bun run check # tsc
bun run build # dist/
The encoder's expected bytes were generated by driving niimbluelib over the same rasters. Regenerate them the same way if the encoder ever changes on purpose — never by pasting in what this code currently emits.
Browser support
Web Bluetooth is Chromium-only. Chrome on Linux additionally needs
chrome://flags/#enable-web-bluetooth and a relaunch;
#enable-web-bluetooth-new-permissions-backend is optional but lets a known
printer reconnect with no chooser. Web Bluetooth also requires a secure
context, so serve over https or localhost.
Licence
MIT