Published:
Many low-cost 4×6 and small label printers speak TSPL and come with a Bluetooth radio. Printing to one from a web app or a phone works — people do it from Android Chrome with a few dozen lines of JavaScript — but only if you first know which Bluetooth the printer speaks, and only on browsers that implement Web Bluetooth.
Bluetooth Low Energy (BLE) devices expose data through GATT: services that contain characteristics you read, write or subscribe to. A BLE label printer typically has one characteristic that accepts the print data. Bluetooth Classic printers usually offer a serial port profile (SPP) instead: a byte stream, like a cable. The printer's spec sheet or pairing behaviour tells you which one you have; some printers offer both.
That distinction decides what a web page can do. The Web Bluetooth API connects to BLE peripherals over GATT. For Classic devices, Chrome's own documentation points to the Web Serial API, which supports RFCOMM services such as SPP on paired devices in Chrome 117 on desktop.
Web Bluetooth is marked experimental and is not Baseline. It is
available in Chrome (desktop and Android), Edge, Opera and Samsung
Internet. Safari on macOS and iOS and Firefox do not support it. On iPhone
and iPad there is no supported path in Safari; third-party browsers that
add their own Bluetooth bridge exist, and one of the reports below used
one, but that is not part of WebKit. The API only works on HTTPS (a secure
context), and requestDevice() must be called from a user
gesture such as a button tap — so you need a "Connect printer" button.
1. Ask the user to pick the printer and declare the service you will use. 2. Connect and find a characteristic whose properties allow writing. 3. Send the TSPL job in small pieces.
// SERVICE_UUID: from your printer's documentation or a BLE scanner app
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: [SERVICE_UUID],
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService(SERVICE_UUID);
const chars = await service.getCharacteristics();
const out = chars.find(
(c) => c.properties.writeWithoutResponse || c.properties.write);
const bytes = new TextEncoder().encode(tsplJob);
const CHUNK = 180;
for (let i = 0; i < bytes.length; i += CHUNK) {
const part = bytes.slice(i, i + CHUNK);
if (out.properties.writeWithoutResponse) {
await out.writeValueWithoutResponse(part);
} else {
await out.writeValueWithResponse(part);
}
}
Why chunks. The Web Bluetooth specification rejects any single
write longer than 512 bytes, and the usable size per write is set by the
connection's negotiated MTU, which the browser negotiates and the page does
not see. A label job — especially one with a BITMAP — is far
longer, so it goes out in pieces. One report on a 4×6 BLE printer sent
179-byte chunks with writeValueWithoutResponse and an 8 ms gap
between them without losses; treat numbers like that as a starting point
for your device, not a constant.
The UUIDs are the printer's, not ours. Service and characteristic
IDs differ between vendors and even models — the reports below show
different short IDs on different printers — and we do not list them here.
Read them from the vendor's documentation or with a BLE scanner app. One
reporter found that optionalServices only worked with the full
128-bit UUID form.
A 50 × 30 mm label with a line of text and a Code 128 barcode, for a 203 dpi printer (8 dots per mm):
SIZE 50 mm, 30 mm
GAP 2 mm, 0 mm
CLS
TEXT 40,24,"3",0,1,1,"BOX 42"
BARCODE 40,80,"128",80,1,0,2,2,"12345678"
PRINT 1
SIZE and GAP describe the label stock in real
units, CLS clears the image buffer, TEXT and
BARCODE place content in dots from the top-left corner, and
PRINT 1 prints one copy. This job passes the
TSPL validator with no findings and fits
the label in the TSPL viewer; paste your
own job there before you send it over the air — a wrong SIZE
or a coordinate past the edge shows up on screen instead of on a strip of
labels. Command details are in the
TSPL command reference, and
the wider preview workflow in
TSPL preview and
debugging.
Check that you wrote to the characteristic that accepts print data, not
one used for notifications. Check the job ends with PRINT. If
some printers in a family answer to their own pocket-printer commands as
well, do not assume TSPL is the default: one report found TSPL on the same
characteristic gave a clean, edge-aligned print where the vendor's raster
commands came out cropped. A logo that prints as a black rectangle is a
TSPL bit-polarity issue — see
TSPL BITMAP prints a black
box. For Zebra printers and ZPL from phones, the routes are different;
see printing ZPL from
iPhone, iPad or Android.
Yes, if the printer uses Bluetooth Low Energy and the browser implements Web Bluetooth: Chrome on desktop and Android, Edge, Opera and Samsung Internet. Safari on iPhone, iPad and macOS and Firefox do not support it.
No. Web Bluetooth talks to BLE devices over GATT. For Classic serial-port printers Chrome points to the Web Serial API, which supports RFCOMM services such as SPP on paired devices in Chrome 117 on desktop.
The Web Bluetooth specification rejects a single write longer than 512 bytes, and the usable size per write depends on the connection's MTU. One report sent 179-byte chunks with an 8 ms gap without losses; tune the size for your printer.
A USB label printer has no port 9100. How the operating system's print queue carries raw ZPL, and how the Labelixa agent connects that queue to a cloud API.
Label Prints Too Large, Cropped or Blank After a CUPS UpdateAfter a Linux update the label is enlarged, cropped, blank or the barcode is a black block. Why driver queues do this, and the raw-queue fix.
Why did my label print on the wrong printer?Labels landing on the wrong printer are an identity problem, not bad luck: queue names, DHCP leases and shared drivers. How to diagnose and stop it.
Printing ZPL on macOS and Linux with a CUPS Raw QueueSet up a raw CUPS queue with lpadmin, send ZPL with lp, and avoid the classic trap: a filtering queue that prints your ZPL code as plain text.