npm.io
0.1.1 • Published 22h ago

@slastra/nblib

Licence
MIT
Version
0.1.1
Deps
0
Size
129 kB
Vulns
0
Weekly
0

nblib

license

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 left feed 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 leading 0x03.
  • 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 Link has 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 SetDiscoveryFilter UUID list to BlueZ, which segfaults bluetoothd 5.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

Keywords