A generic Brother P-touch IPP Everywhere print server, built on PAPPL. It exposes a P-touch label printer as a driverless IPP Everywhere / AirPrint device so any client can discover it and print without a vendor driver.
The Brother raster protocol and device table are reused from hannesweisbach/ptouch-print (GPL). See LICENSE.
Working and deployed. Only the Brother PT-2730 is hardware-certified; the print path emits PT-2730 protocol for every device.
| Milestone | Scope | State |
|---|---|---|
| M0 | Hardware spike: confirm the raster bytes and the usb:// hang on real hardware |
Done |
| M1 | Pure C core: device table, protocol command builders, raster packing, status parse | Done |
| M2 | PAPPL adapter and read path: custom ptouch:// libusb scheme, media/status callback |
Done |
| M3 | Print path: 1:1 geometry, roll media, per-tape imageable width | Done |
| M4 | Cutter modes, chaining, and the strict width guard | Done |
| M5 | Packaging, GHCR publishing, container deployment | Done |
What it does today:
- Prints 1:1 at the tape's imageable width, with the label length taken from the submitted image rather than a fixed page.
- Advertises nominal tape sizes with real margins (
om_24x100mm), so clients match the tape they actually loaded. - Pre-cuts once, cuts labels apart, and chains consecutive queued jobs so a pool of jobs costs one leader rather than one per job.
- Rejects a job whose width does not match the loaded tape, before anything is sent to the printer.
- Reports
offlinewhen the printer is switched off instead of disappearing, and recovers on its own when it comes back - queued jobs then print.
Known gaps are tracked as issues: cross-tape centring is uncalibrated, and
PTOUCH_CUT_MODE=end cuts every label when pre-cut is enabled.
Three layers:
-
Pure C core (
src/tables.c,src/protocol.c,src/raster.c,src/status.c,src/cutter.c). Libc only, no hardware, no PAPPL. All Brother byte sequences live inprotocol.c; raster packing centers a scanline into the 16-byte (128-dot) raster line inraster.c; the device table and per-model flags live intables.c. This layer builds and unit-tests on any machine. -
PAPPL driver (
src/driver.c). Registers the IPP printer, fills the driver data (media, resolution, finishings), reads loaded-tape status, and drives the raster print callbacks (rstartjob/rwriteline/rendjob). -
Custom
ptouch://libusb scheme (src/device_usb.c). PAPPL's built-inusb://scheme issues an IEEE-1284 device-ID probe that hangs on the PT-2730 (confirmed in M0). This project registers its ownptouch://PAPPL_DEVTYPE_CUSTOM_LOCALscheme over raw libusb with bounded timeouts, and never auto-adds the built-inusb://device.
src/tables.c recognizes the P-touch USB ids below (transcribed from upstream
ptdevs[]). Only the PT-2730 is print-supported and hardware-certified. Every
other entry is recognized and discoverable, but is not print-supported: the
raster path emits PT-2730 protocol (uncompressed, 128-dot head) regardless of the
matched model, and per-model flag dispatch (PackBits, P700 init) does not exist
yet. All known models are 180 dpi / 128 px max.
| Model | USB id | flags | Print-supported |
|---|---|---|---|
| PT-2420PC | 04f9:2007 | RASTER_PACKBITS |
recognized only |
| PT-1230PC | 04f9:202c | NONE |
recognized only |
| PT-2430PC | 04f9:202d | NONE |
recognized only |
| PT-1230PC (PLite) | 04f9:2030 | PLITE |
recognized only |
| PT-2430PC (PLite) | 04f9:2031 | PLITE |
recognized only |
| PT-2730 | 04f9:2041 | NONE |
certified |
| PT-E500 | 04f9:205f | RASTER_PACKBITS |
recognized only |
| PT-H500 | 04f9:205e | RASTER_PACKBITS | P700_INIT |
recognized only |
| PT-P700 | 04f9:2061 | RASTER_PACKBITS | P700_INIT |
recognized only |
| PT-P700 (PLite) | 04f9:2064 | PLITE |
recognized only |
| PT-P750W | 04f9:2062 | RASTER_PACKBITS | P700_INIT |
recognized only |
| PT-P750W (PLite) | 04f9:2065 | PLITE |
recognized only |
| PT-D450 | 04f9:2073 | RASTER_PACKBITS |
recognized only |
| PT-D600 | 04f9:2074 | RASTER_PACKBITS |
recognized only |
To add a model, see docs/ADDING-A-MODEL.md.
The pure C core builds and unit-tests anywhere with CMake and a C11 compiler. No hardware or PAPPL needed:
cmake -S . -B build && cmake --build build && ctest --test-dir buildThis builds ptcore and runs the five core test suites (tables, protocol,
raster, status, cutter).
The full ptouch-app executable builds only where PAPPL and libusb-1.0 are
present (pkg-config finds pappl and libusb-1.0). On a machine without them,
CMake skips the app target and still builds the core and tests. The app is built
and exercised against PT-2730 hardware in a container; see Dockerfile.
The printer advertises continuous-roll media (tape-width-fixed, length-variable
roll_min/roll_max range) and uses print-scaling=none, so a label prints
1:1. For a label to come out at its true size, a client must:
- send the image at the printer resolution, 180 dpi (a PNG with no DPI metadata is read as 180 dpi), and
- match the loaded tape across its width. The printer reports the tape via
media-readyat its nominal width, with the unprintable edge expressed as left/right margins (a 24 mm tape images ~18 mm, the 128-dot head); a job whose rendered width does not match the loaded tape is refused.
Label length follows the PNG. A submitted image/png prints at the physical
length of the image itself (its along-the-tape pixel count divided by its DPI),
so no media-col length is needed and there are no blank runs before and after
the content. Lengths below the 5 mm roll minimum are padded up to it; anything
over the 300 mm roll maximum is refused rather than cropped. JPEG is unaffected:
it still goes through PAPPL's built-in filter and prints on the fixed media
length.
The print path is configured entirely through environment variables (no config file or volume). The driver reads them per job.
| Variable | Values | Meaning |
|---|---|---|
PTOUCH_CUT_MODE |
each (default) / end / none |
Cut policy for a batch: cut after every label, only after the last, or never |
PTOUCH_PRECUT |
1 (default) / 0 |
Feed and cut a leader before the first label of a batch |
PTOUCH_WIDTH_GUARD |
strict (default) |
Refuse to print when the rendered width does not match the loaded tape. Only strict is implemented; any other value falls back to strict |
PTOUCH_DEVICE_URI overrides device discovery with an explicit URI (e.g.
socket://127.0.0.1:9100) for testing without a PT-2730; the printer registers
but only connects on an actual print.
The deploy image is published to GHCR as
ghcr.io/pfa230/ptouch-print-server:edge (the moving tag) and a per-commit
sha-<commit> tag for rollback. It is built and pushed by
.github/workflows/deploy.yml on every push to main; pull requests build and
run an IPP smoke test but do not push.
The image is a multi-stage build (Dockerfile.deploy): PAPPL 1.4.11 and the app
compile in a Debian build stage, and only the runtime libraries, avahi-daemon,
and dbus ship in the debian-bookworm-slim runtime stage. The entrypoint
starts a system dbus and Avahi, then runs ptouch-app server on port 8000. The
container has no persistent state, so no volume is needed.
services:
ptouch:
image: ghcr.io/pfa230/ptouch-print-server:edge
container_name: ptouch
restart: unless-stopped
hostname: ptouch
ulimits:
nofile: # dbus needs 65536; below that it dies,
soft: 65536 # avahi dies with it, and DNS-SD
hard: 65536 # advertises nothing at all
environment:
PTOUCH_CUT_MODE: each
PTOUCH_PRECUT: "1"
PTOUCH_WIDTH_GUARD: strict
volumes:
- /dev/bus/usb:/dev/bus/usb # BIND MOUNT, not `devices:` - see below
device_cgroup_rules:
- 'c 189:* rwm' # allow libusb to claim USB char devices
networks:
some_shared_net: # a network the client also joins
aliases:
- ptouch.local # PAPPL accepts *.local as a Host headerUse a bind mount for /dev/bus/usb, not compose devices:. devices: maps
only the device nodes that exist when the container starts. Power-cycling the
printer creates a new node, which the container can then never see: it retries
the stale one indefinitely and only a container restart recovers it. A bind
mount propagates new nodes; the cgroup rule grants access to USB char devices.
Addressing it. PAPPL rejects any HTTP Host that is not an IP address,
localhost, or a *.local name, with 400 Bad Request. So address it by IP, by
a .local name, or, behind a reverse proxy, rewrite the header:
@ptouch host ptouch.example.com
reverse_proxy @ptouch http://ptouch:8000 {
header_up Host ptouch.local:8000
}IPP is HTTP over TCP, so it proxies normally; keep response buffering off so raster streams.
Discovery. The container advertises DNS-SD over mDNS when it is on the LAN
segment itself. Behind a proxy, or on a bridge network, mDNS will not reach LAN
clients: publish unicast DNS-SD records (RFC 6763 _ipp._tcp PTR/SRV/TXT) in
your zone instead, pointing at wherever clients should connect.
The PT-2730 USB interface can be held by only one process. A dev/spike container holding the device must be stopped before this container starts; both cannot own the device at once.
The reasoning behind the architecture, including several corrections, is recorded
as ADRs in docs/adr/ - why this is its own PAPPL
application, why the USB scheme is custom, why media is advertised at nominal tape
width, why the app owns the PNG filter, how offline and job chaining work, and why
the licence is GPL-3.0.
This project is licensed under the GNU General Public License v3.0 only
(GPL-3.0-only). The full text is in LICENSE.
It is GPL because it reuses Brother P-touch raster protocol and device-table code from hannesweisbach/ptouch-print (GPLv3, by Dominic Radermacher). Credit and thanks to that upstream project.