A Pi 3B+ (stickah.local) shares a Phomemo PM246 to the LAN as a plain CUPS queue, so any machine can print 4x6 labels -- fulfillr-site's shipping labels in particular -- without installing the vendor driver, which is x86-64 only. The role builds the TSPL CUPS driver from source instead. It is Debian, not Fedora, so it lives in its own inventory and playbook (make deploy-labelprint / check-labelprint) and the home.debyl.io roles can never run against it. make bootfs renders its cloud-init first-boot files onto a freshly imaged SD card from the same templates the role uses. The Wi-Fi credentials for the home and rescue networks are in the vault. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
163 lines
7.0 KiB
Markdown
163 lines
7.0 KiB
Markdown
# labelprint — 4x6 label print proxy
|
|
|
|
A Raspberry Pi 3B+ (`stickah.local`) with a **Phomemo PM246** on USB, shared to
|
|
the LAN so any Mac, Windows or Linux machine can print 4x6 labels without
|
|
installing a printer driver. Built for
|
|
[`fulfillr-site`](../../../../debyltech/fulfillr-site), which prints shipping
|
|
labels straight out of the browser, but the queue is a plain 4x6 label printer
|
|
and anything can use it.
|
|
|
|
Deployed on its own:
|
|
|
|
```
|
|
make deploy-labelprint
|
|
make check-labelprint # dry run
|
|
make deploy-labelprint TAGS=cups # just the queue
|
|
```
|
|
|
|
## Why not the Phomemo driver
|
|
|
|
The vendor driver that runs on `yoga` installs
|
|
`/usr/lib/cups/filter/rastertolabeltspl`, and that file is an **x86-64 ELF**.
|
|
There is no ARM build, so none of it can be reused on the Pi.
|
|
|
|
The PM246 speaks TSPL over USB, so this role builds
|
|
[RunTheWall/tspl-cups-driver](https://github.com/RunTheWall/tspl-cups-driver)
|
|
(MIT) from a pinned commit instead: a CUPS raster filter, a backend and a PPD,
|
|
about a minute of compiling on the Pi. Pinned to a commit rather than installed
|
|
from the project's apt repo so that a version bump is a reviewed change in this
|
|
repo, and so the Pi carries no third-party signing key.
|
|
|
|
The PM246's USB id is not in the driver's auto-detect list. `tspl://auto` is
|
|
tried first; if the queue cannot find the printer, read the id off the Pi and
|
|
pin it — see `labelprint_device_uri` in [defaults/main.yml](defaults/main.yml).
|
|
|
|
## The image
|
|
|
|
The SD card image is built by hand, but the cloud-init files on it are not:
|
|
`make bootfs` renders them from the same templates the role uses, so the Pi
|
|
boots with its Wi-Fi profiles, its rescue access point and its login already in
|
|
place. Nothing here needs the printer driver — that is Ansible's job.
|
|
|
|
1. **Write the image** with rpi-imager: **Raspberry Pi OS (64-bit)**, Bookworm or
|
|
newer, so NetworkManager is the network stack. Verified against the
|
|
`2026-06-18` pi-gen build. Skip the customisation screen entirely — anything
|
|
set there is about to be overwritten.
|
|
2. **Render the boot files** onto the mounted boot partition:
|
|
|
|
```
|
|
make bootfs # defaults to /Volumes/bootfs
|
|
make bootfs BOOTFS=/path/to/boot
|
|
```
|
|
|
|
It refuses to write anywhere without a `config.txt`, and keeps whatever
|
|
rpi-imager wrote as `*.rpi-imager.bak`.
|
|
3. **Eject**, boot the Pi, and give cloud-init a couple of minutes.
|
|
4. `make deploy-labelprint`.
|
|
|
|
`make bootfs` is safe to re-run. The instance id is a hash of the rendered
|
|
`user-data`, so an unchanged render leaves the card alone, and a changed one
|
|
makes the Pi re-apply it on the next boot.
|
|
|
|
That id is written in **two** places, and they have to agree: `meta-data`, and
|
|
the `ds=nocloud;i=<id>` token rpi-imager puts on the kernel command line in
|
|
`cmdline.txt`. The command line wins. Leave rpi-imager's id there and a card
|
|
that has booted even once looks like the same instance to cloud-init forever —
|
|
it skips `users`, `write_files` and `runcmd` without a word, and the only
|
|
symptom is a Pi that came up with none of this applied. `make bootfs` rewrites
|
|
both.
|
|
|
|
To force a re-apply on a Pi that is already running, without pulling the card:
|
|
|
|
```
|
|
sudo sed -i 's/;i=[^ ]*/;i=<new-id>/' /boot/firmware/cmdline.txt
|
|
sudo cloud-init clean --logs
|
|
sudo reboot
|
|
```
|
|
|
|
### Getting in
|
|
|
|
- `ssh stickah@stickah.local` — your `~/.ssh/id_ed25519.pub` is installed, and
|
|
that is what Ansible uses.
|
|
- Password auth is **on**, with the password `stickah`. It is deliberately
|
|
trivial and deliberately not hashed: this is the fallback for a Pi that will
|
|
not take the key — reflashed card, someone else's laptop, standing at the
|
|
bench — and port 22 is reachable only from the LAN or from the rescue AP,
|
|
which has a real WPA2 password of its own. If that trade stops being the right
|
|
one, `labelprint_user_password` in [defaults/main.yml](defaults/main.yml) is
|
|
the only thing to change.
|
|
|
|
## Wi-Fi and the rescue AP
|
|
|
|
Normally the Pi is a station on the home SSID. When that network is
|
|
unreachable — the password changed, the router died, the Pi moved — the
|
|
`wifi-rescue` watchdog brings up an access point so there is still a way in:
|
|
|
|
- Join the rescue SSID (both it and its password are in the vault).
|
|
- The Pi is at **192.168.4.1**: `ssh pi@192.168.4.1`, or print directly to
|
|
`ipp://192.168.4.1:631/printers/labels`.
|
|
- Deploy to it there with
|
|
`make deploy-labelprint EXTRA_VARS="ansible_host=192.168.4.1"`.
|
|
|
|
The Pi also keeps the Wi-Fi profile netplan rendered from the image's original
|
|
`network-config`, at a lower autoconnect priority than `home-wifi`. It is a
|
|
deliberate fallback: if `home-wifi` is ever rendered wrong, the Pi still comes
|
|
back on the LAN rather than stranding itself on the rescue AP.
|
|
|
|
The 3B+ has one radio and cannot hold an AP and a station link at the same
|
|
time, so the watchdog cannot listen for the home SSID while the AP is up.
|
|
Instead the AP drops every five minutes to scan -- about seven seconds if the
|
|
home SSID is plainly gone, up to half a minute if it is worth a join attempt --
|
|
and comes back immediately if the home network is still missing. When the home SSID
|
|
does return, the Pi rejoins it and shuts the AP down by itself.
|
|
|
|
If you are working over the rescue AP and do not want your session cut:
|
|
|
|
```
|
|
touch /run/wifi-rescue.hold
|
|
```
|
|
|
|
The hold expires after 30 minutes, so a forgotten hold file cannot strand the
|
|
Pi. Deploys take the hold automatically for as long as they run.
|
|
|
|
Watch it work with `journalctl -t wifi-rescue -f`.
|
|
|
|
## Adding the printer from a client
|
|
|
|
Nothing to install anywhere — the Pi renders.
|
|
|
|
- **macOS** — System Settings → Printers → `+`. It appears under its own name
|
|
as an **AirPrint** printer. (`BrowseDNSSDSubTypes _print,_universal` in
|
|
cupsd.conf is what makes macOS offer the driverless add instead of guessing at
|
|
Generic PostScript.)
|
|
- **Windows 10/11** — Add printer; it is discovered as IPP Everywhere / Mopria.
|
|
- **Linux** — discovered by `cups-browsed`, or add
|
|
`ipp://stickah.local:631/printers/labels` by hand.
|
|
|
|
## Access
|
|
|
|
LAN only, `192.168.1.0/24`, plus the rescue AP subnet. Enforced twice on
|
|
purpose: at nftables ([templates/nftables.conf.j2](templates/nftables.conf.j2))
|
|
and again in cupsd's own `Location` blocks
|
|
([templates/cupsd.conf.j2](templates/cupsd.conf.j2)). CUPS is explicitly set
|
|
`--no-remote-any`; the upstream driver's `install.sh` turns that on, which would
|
|
offer this printer to anything that can route to the Pi.
|
|
|
|
The Pi patches itself: `unattended-upgrades` is on, with the Raspberry Pi
|
|
archives added to the origins allowlist (Debian's default covers only the Debian
|
|
security origin, which would leave the kernel and firmware — the packages most
|
|
specific to this hardware — unpatched). Kernel updates reboot at 04:00. Nothing
|
|
here holds state across a reboot; a queued job is spooled to disk and resumes.
|
|
|
|
## Secrets
|
|
|
|
In `ansible/vars/vault.yml` (`make vault`), no `vault_` prefix, per repo
|
|
convention:
|
|
|
|
| Key | What |
|
|
| --- | --- |
|
|
| `stickah_ssid` | Home SSID |
|
|
| `stickah_psk` | Home passphrase, or the 64-hex precomputed PSK |
|
|
| `stickah_ssid_rescue` | Rescue AP SSID |
|
|
| `stickah_psk_rescue` | Rescue AP passphrase (WPA2, 8+ characters) |
|