# 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=` 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=/' /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) |