Files
timer-app/deploy/bondrucker/README.md
T
Kerim (portable/claudecode)andClaude Opus 5 0e8d826bce
Deploy auf den Timer-Server / deploy (push) Successful in 20s
Drucker wird in der App gewaehlt, nicht auf dem Pi
Das Einrichtungsskript fragte bisher am Pi, welcher Drucker bedient werden
soll. Damit stand die Einstellung an einem Ort, an den man nur mit einem
Terminal kommt - und ein zweiter Drucker am Pi bedeutete: Skript nochmal.

Jetzt fragt die App den Pi, was angeschlossen ist, und der Bediener waehlt in
den Einstellungen. Die Bruecke bekam dafuer /devices; sie liest die
Klartextnamen aus dem sysfs, damit dort "EPSON TM-T88IV" steht und nicht
"lp0" - bei zwei Druckern der Unterschied zwischen Wissen und Raten.

Der gewaehlte Pfad geht bei jedem Auftrag im Kopf X-Print-Device mit. Die
Bruecke haelt dazu bewusst keinen Zustand und das Skript schreibt kein
PRINT_DEVICE mehr: es soll genau eine Stelle geben, die weiss, welcher Drucker
gemeint ist, naemlich die, an der es bedient wird. Leer bleibt "automatisch" -
das erste gefundene Geraet, zugleich der Rueckfall, wenn aus lp0 nach dem
Umstecken ein lp1 geworden ist.

Der Pfad kommt damit von aussen und wird an beiden Enden geprueft, in
PUT /api/settings und noch einmal in der Bruecke. Eine Schranke, die nur an
einem Ende steht, ist keine: ohne die Pruefung auf dem Pi liesse sich der
Dienst dazu bringen, in eine beliebige Datei zu schreiben.

Nebenbei: ein nicht bereiter Drucker meldete sich in der Oberflaeche als
"HTTP 503". Das verriet niemandem, dass nur ein Stecker lose ist. Die 503 der
Bruecke ist kein Protokollfehler, und ihr Rumpf sagt, was fehlt - der wird
jetzt ausgewertet.

Geprueft am echten Pi (Debian 13, TM-T88IV): Einrichtung ohne Rueckfrage,
Geraeteliste mit Namen, ausdrueckliche Wahl, Druck darauf, Rueckkehr auf
automatisch, Druck darauf. Dazu die Abweise: /etc/passwd und ein Pfad mit
angehaengtem Befehl werden zurueckgewiesen, ein erlaubtes aber fehlendes
/dev/usb/lp7 ergibt eine Meldung im Klartext.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 13:46:50 +02:00

166 lines
7.2 KiB
Markdown

# 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 "<Link aus der Timer-App>" -o bondrucker.sh
sudo bash bondrucker.sh
```
Das Skript richtet alles ein und meldet den Pi anschließend selbst bei der
Timer-App an — ohne Rückfrage. In der Oberfläche springt die Anzeige von
„wartet auf den Pi" auf den fertigen Drucker um. In der `.env` ist **nichts**
einzutragen.
**Welcher Drucker bedient wird, wird danach in der Timer-App ausgewählt**
(Einstellungen → *Drucker am Pi* → **🔄 Suchen**). Die App fragt den Pi über
`/devices`, was angeschlossen ist, und schickt den gewählten Pfad bei jedem
Auftrag im Kopf `X-Print-Device` mit. Dieser Dienst hält dazu keinen Zustand:
es gibt genau eine Stelle, die weiß, welcher Drucker gemeint ist.
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 Port | `/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.
Abweichender Port der Brücke:
```bash
sudo bash bondrucker.sh --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=`. 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://<PI-IP>: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 |
| `GET /devices` | alle `/dev/usb/lp*` mit Klartextnamen. Token nötig |
| `POST /print` | Rumpf = rohe ESC/POS-Bytes, Kopfzeile `X-Print-Token`. Antwortet `{"ok":true,"bytes":N}` |
`/status` und `/print` beachten den Kopf **`X-Print-Device`**. Fehlt er, nimmt
die Brücke das eingestellte Gerät und sonst das erste gefundene. Der Pfad wird
gegen `^/dev/(usb/)?lp[0-9]{1,3}$` geprüft — er kommt von außen, und ohne diese
Schranke ließe sich der Dienst dazu bringen, in eine beliebige Datei zu
schreiben. Ein unerlaubter Pfad ergibt HTTP 400.
`/devices` verlangt ein Token: welche Geräte am Pi hängen, ist zwar kein
Geheimnis, aber auch keine Auskunft für jeden im Netz.
`/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` |