# Bondrucker-Bruecke
Kleiner Dienst für den Raspberry Pi, an dem der Kassendrucker per USB hängt.
## Warum es ihn gibt
Der Bondrucker ist ein USB-Gerät (`/dev/usb/lp0`) und damit nur für den Rechner
sichtbar, an dem er steckt. Der Timer-Server läuft woanders. Diese Brücke nimmt
fertige ESC/POS-Daten per HTTP entgegen und schiebt sie in das Gerät — mehr
nicht. Der Bon selbst wird in der Timer-App gebaut (`backend/receipt.js`), damit
Layout-Änderungen nicht auf dem Pi angefasst werden müssen.
Nur Python-Standardbibliothek, kein pip, kein CUPS, kein node.
## Installation — der übliche Weg
**In der Timer-App:** ⚙️ Einstellungen → **🔧 Drucker einrichten** →
**🔗 Einrichtungslink erzeugen**. Die App zeigt einen Befehl an. Der wird auf
dem Pi in ein Terminal getippt:
```bash
curl -fsSL "" -o bondrucker.sh
sudo bash bondrucker.sh
```
Das Skript fragt, welcher Drucker verwendet werden soll, richtet alles ein und
meldet den Pi anschließend selbst bei der Timer-App an. In der Oberfläche
springt die Anzeige von „wartet auf den Pi" auf den fertigen Drucker um. In der
`.env` ist **nichts** einzutragen.
Der Pi braucht dafür nur Python 3, systemd, `curl` und den Drucker am
USB-Anschluss. Ein frisch aufgesetztes Raspberry Pi OS erfüllt das ab Werk.
### Warum der Pi sich meldet und nicht umgekehrt
Früher hat die App den Pi per SSH eingerichtet. Das verlangte ein SSH-Passwort
in einem Webformular und eine Verbindung vom Server zum Pi. Beides ist jetzt
weg: kein Passwort verlässt den Laden, und die Verbindung muss nur in eine
Richtung funktionieren. Wer einen Drucker anschließt, steht ohnehin am Pi.
Der Link enthält einen Einrichtungscode. Er gilt **einmal** und **30 Minuten**,
liegt nur im Arbeitsspeicher der App und ist nach einem Neustart der App
ungültig. Wer den Code hat, kann den Drucker der App umbiegen — er gehört
deshalb nicht in einen Chatverlauf.
## Was das Skript anlegt
| Was | Wo |
|---|---|
| Dienstbenutzer `bondrucker` (Gruppe `lp`) | — |
| Programm | `/opt/bondrucker/print-server.py` |
| Token und Gerät | `/etc/bondrucker/env` (`0640`, root:bondrucker) |
| udev-Regel | `/etc/udev/rules.d/99-bondrucker.rules` |
| Dienst | `bondrucker.service`, aktiviert und gestartet |
Das Skript ist wiederholbar: ein erneuter Aufruf überschreibt Programm und
Token und **startet den Dienst neu**. Ohne den Neustart liefe er mit dem alten
Token weiter und würde jeden Druckauftrag mit HTTP 403 ablehnen.
Schalter für den Fall, dass nicht gefragt werden soll:
```bash
sudo bash bondrucker.sh --device /dev/usb/lp1 --port 9100
```
## Installation ohne die Timer-App
`einrichten.sh` im Quelltext ist eine Vorlage: die Stellen `@@APP_URL@@`,
`@@SETUP_CODE@@`, `@@PRINT_SERVER@@` und `@@SERVICE@@` füllt die App beim
Ausliefern. Direkt ausführbar ist sie deshalb nicht.
Ohne die App werden die Dateien von Hand abgelegt — `print-server.py` nach
`/opt/bondrucker/`, `bondrucker.service` nach `/etc/systemd/system/`, dazu
`/etc/bondrucker/env` mit `PRINT_TOKEN=` und `PRINT_DEVICE=`. Adresse, Port und
Token kommen anschließend von Hand in die Einstellungen der Timer-App.
## Timer-Server konfigurieren
Nach der Einrichtung über den Link: nichts zu tun.
Nach der Einrichtung von Hand: in ⚙️ Einstellungen unter „Drucker einrichten"
lassen sich Adresse, Port und Token auch direkt eintragen. Der Druckerzugang
liegt in der Datenbank, nicht in der `.env` — ein Neustart des Dienstes ist für
einen Druckerwechsel nicht nötig.
Die Werte `PRINTER_HOST`, `PRINTER_PORT`, `PRINTER_TOKEN` und `PRINTER_WIDTH`
in der `.env` sind nur die **Startwerte beim allerersten Start**. Sind sie leer,
ist die Druckfunktion aus und die App verhält sich wie vorher — ohne Bon-Knöpfe,
bis ein Drucker eingerichtet wird.
## Prüfen
```bash
# Zustand des Druckers
curl -s http://:9100/status
# Aus der App heraus: Einstellungen -> "Probedruck"
```
Antwortet `/status` mit `"ok": true`, ist das Gerät da und beschreibbar.
## Schnittstelle
| Aufruf | Wirkung |
|---|---|
| `GET /status` | JSON mit Gerät, Beschreibbarkeit, letztem Fehler. HTTP 503, wenn kein Drucker da ist |
| `POST /print` | Rumpf = rohe ESC/POS-Bytes, Kopfzeile `X-Print-Token`. Antwortet `{"ok":true,"bytes":N}` |
`/status` verlangt **kein** Token — der Zustand des Druckers ist nicht geheim
und soll für Health-Checks abfragbar bleiben. Wird trotzdem eines mitgeschickt,
meldet die Antwort in `tokenAccepted`, ob es passt. Die App nutzt das nach der
Einrichtung als Gegenprobe: erreichbar allein genügt nicht, das Token muss auch
angenommen werden.
Fehler kommen als HTTP 4xx/5xx mit `{"error": "..."}` zurück — die Timer-App
zeigt diesen Text unverändert an. „Papier leer" und „Deckel offen" landen als
`OSError` beim Schreiben und damit als HTTP 502 in der Anzeige.
## Betriebshinweise
* **Feste IP vergeben.** Der Pi hängt per WLAN und DHCP im Netz. Wechselt die
Adresse, druckt die App nicht mehr, bis `PRINTER_HOST` angepasst ist. Eine
DHCP-Reservierung im Router ist der einfachste Weg.
* **USB-Kabel neu gesteckt?** Aus `lp0` kann `lp1` werden. Der Dienst sucht
deshalb selbst nach `/dev/usb/lp*` — es ist nichts zu tun.
* **Ein Auftrag nach dem anderen.** Der Dienst serialisiert Druckaufträge, zwei
gleichzeitige Bons können sich nicht ineinanderschieben.
* **Netzgrenze.** Port 9100 sollte nur aus dem eigenen Netz erreichbar sein.
Das Token schützt gegen versehentliche, nicht gegen entschlossene Zugriffe.
## Fehlersuche
```bash
systemctl status bondrucker
journalctl -u bondrucker -f # jeder Auftrag wird protokolliert
# Am Pi direkt drucken, ohne App und ohne Dienst:
printf 'Test\n\n\n\n\x1dVB\x00' | sudo tee /dev/usb/lp0 > /dev/null
```
| Symptom | Ursache |
|---|---|
| `/status` sagt `deviceExists: false` | Drucker aus oder USB-Kabel ab |
| `/status` sagt `writable: false` | udev-Regel nicht gegriffen — `sudo udevadm trigger`, Dienst neu starten |
| HTTP 403 | `PRINTER_TOKEN` in der `.env` weicht von `/etc/bondrucker/env` ab |
| App meldet „nimmt keine Verbindung an" | Dienst läuft nicht, oder falsche IP/Port |
| Einrichtung bricht bei „Dienst eingerichtet" ab | sudo-Passwort falsch, oder der Benutzer darf kein sudo |
| Einrichtung meldet „lehnt das neue Token ab" | auf dem Pi läuft noch eine Brücke vor 1.1 — Einrichtung wiederholen |
| Umlaute als Kringel | Drucker beherrscht Codepage 16 (WPC1252) nicht — siehe `backend/escpos.js` |