Skip to content

@thermal-label/transport ​

USB, TCP, Serial, WebUSB, Web Serial, and Web Bluetooth transport classes for the thermal-label ecosystem. Every class implements Transport from @thermal-label/contracts, so drivers program against one pull-based API regardless of the underlying channel.

Install ​

bash
pnpm add @thermal-label/transport

Native peers are optional — install only what your code path needs:

bash
pnpm add usb           # for UsbTransport (Node)
pnpm add serialport    # for SerialTransport (Node)

Browser-only consumers download neither.

What's in the box ​

ExportRuntimePurpose
UsbTransportNodelibusb bindings for USB Printer Class devices. Interface 0 by default; bInterfaceNumber opt-in for composite devices.
TcpTransportNodeRaw TCP (port 9100 / JetDirect by default) with partial-read buffering.
SerialTransportNodeserialport wrapper for /dev/rfcomm*, /dev/ttyUSB*, COM<n>.
WebUsbTransportBrowsernavigator.usb picker + USBDevice wrapper.
WebSerialTransportBrowsernavigator.serial picker + SerialPort wrapper (covers BT SPP + USB-serial).
WebBluetoothTransportBrowserGATT transport for BLE printers (Niimbot, Phomemo, Brother BLE, …).
enumerateUsbDevicesNodeList connected USB printers matching one or more registries, serial included.
enumerateNetworkDevices / identifyNetworkDeviceNodeFind network printers by SNMP broadcast, or ask one host what it is.
snmpGet / snmpBroadcastNodeDependency-free SNMPv1 GET primitives with the PRINTER_MIB OID table.
matchDevice / buildUsbFiltersBothHelpers for matching and requesting USB devices.
matchModelNameBothMatch a self-reported model string (hrDeviceDescr, MDL:, usb_MDL) against DeviceEntry.modelNames.
buildBluetoothRequestOptionsBothBuild navigator.bluetooth.requestDevice options from a BluetoothConfig.
buildSerialRequestOptionsBothBuild navigator.serial.requestPort options.
discoverAllBothAggregate PrinterDiscovery implementations across drivers.

Subpath imports ​

Transport classes are split by runtime so browser bundlers never see the usb native addon:

ts
// Node
import { UsbTransport, TcpTransport, SerialTransport } from '@thermal-label/transport/node';

// Browser
import {
  WebUsbTransport,
  WebSerialTransport,
  WebBluetoothTransport,
} from '@thermal-label/transport/web';

// Platform-neutral helpers
import { matchDevice, matchModelName, buildUsbFilters, discoverAll } from '@thermal-label/transport';

The root entry (@thermal-label/transport) is safe to import from either environment — it contains only discovery helpers.

Examples ​

UsbTransport (Node) ​

ts
import { UsbTransport } from '@thermal-label/transport/node';

const transport = await UsbTransport.open(0x04f9, 0x209d); // Brother QL-820NWB
await transport.write(new Uint8Array([0x1b, 0x40]));       // ESC @ (reset)
const status = await transport.read(32, 2000);              // up to 32 bytes, 2s timeout
await transport.close();

Opens the device, claims interface 0, detaches the usblp kernel driver on Linux when present, and locates Bulk IN / OUT endpoints. read() maps libusb timeouts to TransportTimeoutError.

Composite devices (interface selection) ​

For composite USB devices that expose more than one printer-class interface behind a single VID/PID — the LabelWriter 450 Duo is the canonical case — pass bInterfaceNumber to claim a specific interface. Two transports against the same (vid, pid) share the underlying libusb handle via an internal refcount, so they can coexist and close independently.

ts
const label = await UsbTransport.open(0x0922, 0x1003, { bInterfaceNumber: 0 });
const tape  = await UsbTransport.open(0x0922, 0x1003, { bInterfaceNumber: 1 });
// …
await label.close();   // releases interface 0; libusb handle stays open
await tape.close();    // releases interface 1; libusb handle closes

UsbTransport.openDevice(descriptor, { bInterfaceNumber }) accepts the same option. WebUsbTransport.fromDevice(device, { interfaceNumber }) is the browser-side equivalent.

TcpTransport (Node) ​

ts
import { TcpTransport } from '@thermal-label/transport/node';

const transport = await TcpTransport.connect('192.0.2.42'); // default port 9100
await transport.write(payload);
await transport.close();

read(n) accumulates bytes across data events and only resolves once the buffer holds n bytes. Remainder bytes stay buffered for the next call.

SerialTransport (Node) ​

ts
import { SerialTransport } from '@thermal-label/transport/node';

// Bluetooth SPP printer (Linux, after `bluetoothctl pair` + `rfcomm bind`)
const transport = await SerialTransport.open('/dev/rfcomm0');
await transport.write(payload);

// USB-to-serial — baud rate matters
const wired = await SerialTransport.open('/dev/ttyUSB0', 115200);

macOS: classic Bluetooth SPP was removed years ago. macOS users connect via USB or TCP instead.

WebUsbTransport (Browser) ​

ts
import { WebUsbTransport } from '@thermal-label/transport/web';
import { buildUsbFilters } from '@thermal-label/transport';
import { DEVICES } from '@thermal-label/labelwriter-web';

const filters = buildUsbFilters(DEVICES);
const transport = await WebUsbTransport.request(filters);

WebUsbTransport.fromDevice(device) wraps a USBDevice from navigator.usb.getDevices() for previously-paired devices.

WebSerialTransport (Browser) ​

ts
import { WebSerialTransport } from '@thermal-label/transport/web';

const transport = await WebSerialTransport.request();
await transport.write(payload);

Chrome/Edge desktop and Chrome Android only. Firefox and Safari do not implement Web Serial. The browser picker lists USB-serial adapters and any Bluetooth SPP devices already paired at the OS level.

WebBluetoothTransport (Browser) ​

ts
import { WebBluetoothTransport } from '@thermal-label/transport/web';

const transport = await WebBluetoothTransport.request({
  serviceUuid: '0000ff00-0000-1000-8000-00805f9b34fb',
  txCharacteristicUuid: '0000ff02-0000-1000-8000-00805f9b34fb',
  rxCharacteristicUuid: '0000ff01-0000-1000-8000-00805f9b34fb',
  namePrefix: 'QL-820',
  mtu: 512,
});

write() chunks data to mtu bytes (default 20) and sends sequentially via writeValueWithoutResponse. read(n) buffers incoming characteristicvaluechanged notifications and resolves once n bytes are available.

Enumeration (Node) ​

ts
import { enumerateUsbDevices, enumerateNetworkDevices, identifyNetworkDevice } from '@thermal-label/transport/node';
import { DEVICES } from '@thermal-label/brother-ql-core';

const usb = await enumerateUsbDevices(Object.values(DEVICES));
// [{ descriptor, serialNumber?, connectionId: '1:5' }]

const lan = await enumerateNetworkDevices(Object.values(DEVICES), { windowMs: 1000 });
// [{ descriptor, host: '192.168.1.67', port: 9100, serialNumber?, modelName, connectionId: '192.168.1.67:9100' }]

const one = await identifyNetworkDevice('192.168.1.67', Object.values(DEVICES));
// undefined → answered, but not in this registry; rejects → could not be asked

Port 9100 on a network label printer is write-only, so the model, serial and loaded media come from SNMP (standard Printer-MIB, community public). The broadcast covers every non-internal IPv4 subnet the host sits on; on another subnet identifyNetworkDevice still works over unicast. With SNMP disabled on the printer (or a non-default community) it rejects, and drivers fall back to an explicit deviceKey. Neither helper opens a TCP connection, and snmpGet only accepts a GetResponse from the target's own address and port with the request id it sent. Answers from anything else on the LAN are dropped.

snmpGet(host, oids, opts) and snmpBroadcast(oid, opts) are the primitives underneath, exported for drivers that map Printer-MIB status themselves; PRINTER_MIB names the OIDs (hrPrinterStatus, hrPrinterDetectedErrorState, prtInputMediaName, prtMarkerLifeCount, …).

Discovery aggregation ​

ts
import { discoverAll } from '@thermal-label/transport';
import { discovery as labelwriter } from '@thermal-label/labelwriter-node';
import { discovery as brotherQl } from '@thermal-label/brother-ql-node';

const printers = await discoverAll([labelwriter, brotherQl]);

discoverAll uses Promise.allSettled, so a failing driver does not block discovery from the others.

Compatibility ​

Node≥ 24 (for UsbTransport, TcpTransport, SerialTransport)
BrowsersChrome/Edge 89+ (WebUSB, WebSerial); Web Bluetooth on Chrome/Edge desktop and Chrome Android
Optional peersusb (Node USB), serialport (Node Serial)
LicenseMIT

Source on GitHub · npm · API reference