A free, open-source CUPS driver for cheap USB thermal label printers that speak TSPL/TSPL2.
Turn a USB-tethered label printer into a shared network printer for every device —
Mac, iPhone, Windows, Linux — driverless.
Runs on Linux & Raspberry Pi (arm64 / armhf), the platforms the vendor drivers leave out.
💙 Maintained for free by Run The Wall. We don't want your money — we give away tools like this to introduce you to Constly, our genuinely great free Markdown editor → constly.com. If it saved you an afternoon, that's the whole payment we wanted. (The driver logs one thank-you line to your CUPS log per job — no tracking, no phone-home.)
Cheap 4×6 USB label printers mostly speak the same TSPL/TSPL2 language and mostly lack an ARM/Linux driver — so one driver covers them. We can only physically test the printers we own, so support is graded honestly:
✅ Confirmed working (verified on real hardware): HZD950-PRO / HERO (our bench unit), plus community-verified HPRT SK41, FlashLabel Y43BT, Xprinter XP-420B and 123inkt LW650XL PRO — thank you, reporters!
Everything else is 🟢 TSPL-confirmed (vendor/community docs say it speaks TSPL and our command set should drive it) or 🟡 community-reported — not yet verified by us. Got one working? Tell us and we'll move it to Confirmed and add its USB id to auto-detect.
| Printer | dpi | USB id | Status |
|---|---|---|---|
| HZD950-PRO / HERO | 300 | 0fe6:811e |
✅ Tested |
| Munbyn ITPP941 / 941B (941P: see note) | 203 | 09c6:0426 / generic |
🟢 TSPL |
| iDPRT SP410 / SP420 | 203 | 20d1:7008 |
🟢 TSPL |
| HPRT SK41 | 203 | 20d1:7008 |
✅ Tested |
| HPRT N41 / SL42 | 203 | 20d1 family (inferred) |
🟢 TSPL |
| Beeprt BY-426 (shared OEM engine) | 203 | 09c6:0426 |
🟢 TSPL |
| JADENS JD-168 / JD-268BT | 203 | 09c6:0426 |
🟢 TSPL |
| Polono PL420 | 203 | unknown | 🟡 community |
| Xprinter XP-420B | 203 | 2d37:83d7 |
✅ Tested |
| Xprinter XP-460B / 470B | 203 | 2d84:b528 (460B) / varies |
🟢 TSPL |
| Phomemo PM-241 / D520 | 203 | (unverified) | 🟡 community |
| FlashLabel Y43BT | 203 | 5958:0041 |
✅ Tested |
| 123inkt LW650XL PRO ("QIN LabelPrinter") | 300 | 2e3c:5757 |
✅ Tested |
Munbyn's vendor specs list the 941 / 941B as 203 dpi TSPL; the 300 dpi "941P 3.0" has no public spec confirming TSPL yet, and the AirPrint "941AP" speaks OPL (excluded below). Polono is grouped with the TSPL clone family by community reports, but the oft-repeated "HPRT rebadge" claim has no public evidence (different FCC grantees), so it stays 🟡 until someone reports one.
The Y43BT is a 203 dpi head but the PPD defaults to 300 dpi, so add
-o Resolution=203dpi to the lpadmin line below or labels print oversized
and clipped. 5958 is the Yxwl OEM engine, so other Y4xBT rebadges should
match the same id. Xprinter units are built by Zhuhai Poskey, who are registered
under two vendor ids, 2d84 and 2d37; we've seen a 420B report 2d37
and a 460B report 2d84, so check yours rather than assuming. Both ids are
auto-detected. The LW650XL PRO shows up as 2e3c, which is the Artery AT32
microcontroller vendor id rather than a printer maker's, so it is matched on
the exact 2e3c:5757 pair only.
Check yours in 10 seconds (prints nothing): cat /sys/class/usbmisc/lp0/device/ieee1284_id —
many TSPL printers self-describe with TSPL in the CMD: / COMMAND SET: field (the HZD950-PRO
reports COMMAND SET:TSPL), no driver needed. A missing TSPL there doesn't mean no: the
XP-420B advertises CMD:CEZD, the LW650XL PRO CMD:XPP,XL, and both speak TSPL perfectly well, so if
the model is on the list above, just try it. You can also
ask the printer itself: printf '~!T\r\n' | sudo tee /dev/usb/lp0 >/dev/null; sudo timeout 2 head -c 32 /dev/usb/lp0 — but many clones are write-only over USB, so no reply proves nothing; go by the id
string or just try a print.
Not this driver (different language): Munbyn AirPrint / "OPL" models (use AirPrint directly), Phomemo M110 / M120 / D30 / M02 & mini printers (ESC/POS), Brother QL / DYMO (proprietary raster), Zebra (ZPL/EPL — already well-supported), and Rollo / OFFNOVA (TSPL, but they ship their own arm64 drivers).
Add it once, then install and upgrade by name like any system package. Signed, hosted free on GitHub Pages.
# Debian / Ubuntu / Raspberry Pi OS
curl -fsSL https://runthewall.github.io/tspl-cups-driver/apt/KEY.gpg | sudo tee /usr/share/keyrings/tspl.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/tspl.gpg] https://runthewall.github.io/tspl-cups-driver/apt ./" | sudo tee /etc/apt/sources.list.d/tspl.list
sudo apt update && sudo apt install tspl-cups-driver# Fedora / RHEL / openSUSE
sudo curl -fsSL https://runthewall.github.io/tspl-cups-driver/rpm/tspl.repo -o /etc/yum.repos.d/tspl.repo
sudo dnf install tspl-cups-drivergit clone https://github.com/RunTheWall/tspl-cups-driver
cd tspl-cups-driver && sudo ./install.shBuilds from source if you have a compiler; otherwise downloads a prebuilt binary for your CPU (no build tools needed). Turns on network sharing + AirPrint on the way.
Packages install the driver files only; create a queue once (the script does this for you). Name it
anything — HZD950 is just the default:
sudo lpadmin -p HZD950 -E -v tspl://auto -P /usr/share/ppd/tspl/tspl-label.ppd \
-o printer-is-shared=true -o media=na_index-4x6_4x6in
# 203 dpi printer? add: -o Resolution=203dpitspl://auto finds a known TSPL printer. If yours has an unlisted USB id, the backend prints the id it
sees — pin it with -v tspl://<vid>-<pid> (e.g. tspl://2d37-83d7), -v tspl://<usb-serial> or
-v tspl:///dev/usb/lp0 (and please
open an issue with the id so we auto-detect it).
Write the USB id with a dash, not a colon. CUPS reads the
:pidoftspl://<vid>:<pid>as a port number and rejects the URI outright (lpadmin: Bad device-uri) unless the pid happens to be all digits —20d1:7008is accepted while0fe6:811e,2d84:b528and2d37:83d7are refused. The dash form always works; the colon form still works for the ids CUPS accepts, so existing queues are fine.
Also: one-off .deb/.rpm download · Arch (AUR / PKGBUILD) · NixOS
Every release is built by GitHub Actions for amd64 / arm64 / armhf — grab one from Releases:
sudo apt install ./tspl-cups-driver_*_arm64.deb # Debian/Ubuntu/Pi (or _amd64 / _armhf)
sudo dnf install ./tspl-cups-driver-*.x86_64.rpm # Fedora/RHEL (or .aarch64)
# Arch — build the shipped PKGBUILD (AUR listing pending):
( cd tspl-cups-driver/packaging/aur && makepkg -si )
# NixOS — add to your CUPS drivers:
# services.printing.drivers = [ inputs.tspl.packages.${pkgs.system}.default ];
# flake input: github:RunTheWall/tspl-cups-driverPackaging sources live in packaging/ and flake.nix.
The Pi renders, so clients never install a driver — they just add the shared queue.
- Mac / iPhone / iPad — it appears in Add Printer as AirPrint; pick it, done. (
install.shapplies the one server-side fix that makes this work:BrowseDNSSDSubTypes _print,_universalincupsd.conf, so macOS offers driverless AirPrint instead of a wrong "Generic PostScript" driver.) - Windows 10/11 · Linux — auto-discovered as an IPP Everywhere / Mopria printer; add it driverless.
- Helpers —
client/add-printer.command(Mac; auto-discovers over Bonjour) and a downloadable.mobileconfigprofile.
| Option | Values | → TSPL |
|---|---|---|
| Print Mode (halftone) | Default (threshold — crisp text/barcodes) · Gathering (dither — greys/photos) · None · Diffusion · Error Diffusion | — (rendered into the bitmap) |
| Darkness | 0–15 (default 8) |
DENSITY |
| Print Speed | 1–6 in/sec (default 4) · Printer default (sends nothing) |
SPEED |
| Media tracking | Die-cut (gap) · Black-mark · Continuous · Fixed pitch (die-cut, sensor off) · Printer setting | GAP / BLINE |
| Gap / black-mark length | 1.5–5 mm (default 3 mm) |
the length in GAP/BLINE; on Fixed pitch, added to SIZE |
| Resolution | 203 / 300 dpi |
— |
Loaded black-mark or continuous stock instead of die-cut labels? Set it per queue —
-o MediaTracking=BlackMark or Continuous — sending the default gap-sensor command to gapless
media makes the printer hunt for a gap and error out. And after any media change, run the printer's
hold-the-feed-button calibration and make sure the queue's page size matches the physical labels:
TSPL firmwares skip or garble labels when SIZE disagrees with the stock. Note PrinterDefault
sends no boundary command at all — and GAP/BLINE persist in printer memory, so such a queue
inherits whatever the last job set (e.g. GAP 0 from a Continuous queue sharing the printer).
Gap length. The 3 mm in GAP 3 mm,0 mm is the TSC factory default and what every other TSPL
driver sends, but small die-cut labels are often cut with a 2 mm gap, the minimum TSC rates its
sensors for. If labels lose register, measure the gap and set it: -o GapLength=20 (tenths of a
millimetre; a bare -o GapLength=2 or -o GapLength=2.5 is read as millimetres). Values under 1 mm
are refused on the sensor modes: TSPL takes GAP 0 as "continuous", and the printer remembers it.
Fixed pitch. Some clone sensors cannot hold a very short pitch (a 12 mm label with a 2 mm gap, say):
the first label prints, the next feed faults, whatever GAP says. -o MediaTracking=FixedPitch
takes the sensor out of the loop: it sends GAP 0 like Continuous but puts the full label + gap
pitch in SIZE (from GapLength), so the printer feeds blind and stays in register. Plain
Continuous is untouched: on a continuous roll the page height is the feed length.
Two things come with running blind. The last label stops short of the tear bar — positioning to
tear-off needs the gap sensor, so it sits half out until the next job pushes it through; that is the
printer, not a lost job. And the pitch is only as good as your measurement: GapLength has to match
the real gap or the image walks a little further along the roll with every label. Measure it, print a
few, and check the last one still lands where the first did. Set the queue back to Die-cut (gap)
and the printer re-syncs on the next job.
Two queues: crisp labels + a "photo" (Gathering) queue
Print Mode is a halftone choice and the best one depends on content — Default (threshold) for crisp text/barcodes/QR, Gathering (clustered-dot dither) for greys/photos/watermarks. The filter honours each queue's PPD default, so run two queues on the same printer and pick per job:
# crisp labels (Default / threshold)
sudo lpadmin -p HZD950 -E -v tspl://auto -P /usr/share/ppd/tspl/tspl-label.ppd \
-o printer-is-shared=true -o PrintMode=5 -o Darkness=8 -o PrintSpeed=50
# photo / Gathering (greys, watermarks)
sudo lpadmin -p HZD950-Photo -E -v tspl://auto -P /usr/share/ppd/tspl/tspl-label.ppd \
-o printer-is-shared=true -o PrintMode=3 -o Darkness=7 -o PrintSpeed=20Baked into the queue default, this works even for driverless clients (AirPrint/IPP-Everywhere) that can't
show the option menus — they just pick the right queue. Values: PrintMode 5=Default 3=Gathering
0=None 2=Diffusion 4=ErrorDiffusion · Darkness 0–15 · PrintSpeed = in/sec ×10
(0 = leave it to the printer) · MediaTracking Gap/BlackMark/Continuous/FixedPitch/PrinterDefault
· GapLength = mm ×10 (20 = 2 mm). lpadmin -o GapLength= only keeps values the PPD lists
(15 20 25 30 40 50); for any other value use -o GapLength-default=17, which CUPS then applies to
every job on the queue.
How it works · build from source · multiple printers · notes
your app ─► CUPS ─► gstoraster ─► rastertotspl ─► TSPL ─► tspl backend ─► printer
(this repo) (this repo)
rastertotspl(C filter) reads the CUPS raster and emits TSPL —SIZE / GAP|BLINE / DENSITY / SPEED / DIRECTION / REFERENCE / CLS / BITMAP … / PRINT. The 8-bit page is flattened to 1-bit dots with the selected Print Mode dither (bitmaps, not printer fonts — so no per-model font quirks).tspl(shell backend) writes the TSPL straight to the printer'susblpdevice, located by USB id/serial so it survives reboots and coexists with other USB printers. It spools the whole job, then writes it in one burst: byte-capture testing on the HZD950-PRO proved these firmwares silently drop data that trickles in while the printer is already printing — drivers that stream at render speed lose every page after the first on multi-copy and multi-page jobs.
Build from source: needs gcc, make, CUPS dev headers (sudo apt install build-essential libcups2-dev,
or the dnf/pacman/zypper equivalent), then make.
Multiple USB printers? tspl://auto only picks a printer whose USB id is on our known list, so it
won't grab your laser or inkjet. Two of those entries (2d84, 2d37) match a whole vendor, Poskey,
whose range is label printers — if you own some other Poskey device, pin the queues explicitly rather
than relying on auto. Pin by vid-pid/node. Drop in
udev/99-tspl-label.rules for a stable
/dev/usb/tspl-label symlink — and with two label printers, pin each queue to its per-unit
/dev/usb/tspl-label-<serial> link (the bare symlink points at whichever enumerated last).
Notes: CUPS 2.4 prints a "printer drivers are deprecated" warning — harmless; classic PPD+filter drivers work for years yet. Reverse-engineered cleanly from the printer's own TSPL output; no vendor code is redistributed.
Coming from another driver where 2 copies printed 1 label, or a multi-page PDF stopped after page one? That's the mid-print data loss described above — cheap TSPL firmwares eat pages that arrive while the printer is busy, and most drivers (including vendor ones) stream. This driver spools since v1.3.1, so all pages print. If you still see missing pages here, that's a bug we want: report it with your model.
It's free, but we do want it to actually work for you — and your report is how the Supported printers list grows. No hoops, just GitHub Issues:
- Your printer works — or doesn't? → Open a printer report.
It asks for your model,
lsusbUSB id, the IEEE-1284 id (or~!Treply), and your distro/arch. Confirmed printers move to ✅ and get added to auto-detect. - Hit a bug? → Open a bug report
with your CUPS error log (
/var/log/cups/error_log— please redact hostnames/IPs). - Just a question? → open an issue anyway; we read them all.
If the driver saved you time, the nicest thank-you is trying Constly 💙.
MIT © Run The Wall. See LICENSE. Built and maintained for free — support us by trying Constly, our free Markdown editor → constly.com.
