npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@slastra/nblib

v0.3.0

Published

NIIMBOT B1 label printer protocol: packet framing, 1-bit raster encoding, print-job orchestration, and a Web Bluetooth transport. Browser-first, dependency-free ESM, ported from MultiMote's niimbluelib.

Readme

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', align: 'center' });

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.

A narrower design does not centre itself

The printer starts every row at dot 0 and has no notion of a margin, so a 40 mm design on a 48 mm head prints hard against one edge with all 8 mm of slack on the other. align decides where the slack goes:

| align | | | -------- | --------------------------------------------------------------------- | | left | default, and what the printer does on its own | | center | splits the slack — usually what a design smaller than its stock wants | | right | all the slack on the near side |

center centres on the head, and the head is not necessarily centred on the stock. If a label lands consistently off by a millimetre or two, that is the head offset rather than the arithmetic, and right is the one-word test that says which way it goes.

Reading the roll's RFID tag

Genuine NIIMBOT stock carries an NFC tag. readRfidInfo returns null for both kinds of nothing — untagged stock, and a printer with no reader — since from the caller's side those are the same answer.

import { readRfidInfo } from '@slastra/nblib';

const tag = await readRfidInfo(link);
// { uuid, barcode, serial, total, used, type, raw }

type is a LabelType value from the same enumeration printJob's labelType takes, so the tag's answer can be fed straight back to the printer. There are no dimensions on the tag — it carries identity and a counter, not a media size. raw is always present: the payload is variable-length in two places, so an unfamiliar firmware is visible as bytes rather than decoded into confident nonsense.

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.

The first handshake

A B1 routinely ignores the opening Connect packet. gatt.connect() and startNotifications() both resolve before the link has really settled — connection parameters and MTU are still being negotiated — so the first handshake goes out into a link that swallows it, and a single attempt fails on a cold connection and works on the next. That is why connect() retries: handshakeAttempts (default 3) tries, settleMs (default 250) apart. Only silence is retried; a refusal is the printer's final answer and is thrown straight through.

Licence

MIT