Files
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

254 lines
9.3 KiB
Bash
Executable File

#!/bin/bash
# ---------------------------------------------------------------------------
# Bondrucker-Bruecke einrichten - wird AUF DEM PI ausgefuehrt.
#
# Dieses Skript wird nicht von Hand gepflegt heruntergeladen, sondern von der
# Timer-App ausgeliefert: sie setzt ihre eigene Adresse und einen einmaligen
# Einrichtungscode ein und haengt den Druckdienst sowie die systemd-Unit
# gleich mit an. Heraus kommt eine einzige Datei, die alles enthaelt.
#
# Aufruf auf dem Pi:
# curl -fsSL "<Link aus der Timer-App>" -o bondrucker.sh
# sudo bash bondrucker.sh
#
# Der Link steht in der Timer-App unter Einstellungen -> Drucker einrichten.
#
# Dieses Skript installiert nur die Software. WELCHER Drucker bedient wird,
# wird anschliessend in der Timer-App ausgewaehlt - sie fragt den Pi, was
# angeschlossen ist. Hier ist dazu nichts einzustellen.
#
# Ohne Netz zur Timer-App laeuft das Skript trotzdem durch; es zeigt am Ende
# Adresse und Token an, die dann von Hand in die Einstellungen gehoeren.
#
# Schalter:
# --port 9100 Lausch-Port der Bruecke
# ---------------------------------------------------------------------------
set -euo pipefail
# Diese beiden Werte ersetzt die Timer-App beim Ausliefern.
APP_URL="@@APP_URL@@"
SETUP_CODE="@@SETUP_CODE@@"
ZIEL=/opt/bondrucker
PORT=9100
# --- Schalter --------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--port) PORT="${2:-9100}"; shift 2 ;;
-h|--help) sed -n '2,26p' "$0"; exit 0 ;;
*) echo "Unbekannter Schalter: $1" >&2; exit 1 ;;
esac
done
if [ "$(id -u)" -ne 0 ]; then
echo "Bitte mit sudo starten: sudo bash $0" >&2
exit 1
fi
# ---------------------------------------------------------------------------
# Hilfsmittel
# ---------------------------------------------------------------------------
titel() { printf '\n\033[1m%s\033[0m\n' "$1"; }
punkt() { printf ' %s\n' "$1"; }
warnung() { printf ' \033[33m%s\033[0m\n' "$1"; }
fehler() { printf '\n\033[31m%s\033[0m\n' "$1" >&2; exit 1; }
# Klartextname eines USB-Druckers aus dem sysfs. Nur zur Anzeige - ausgewaehlt
# wird spaeter in der Timer-App, die dieselbe Liste ueber /devices abfragt.
usb_name() {
local pfad
pfad=$(readlink -f "/sys/class/usbmisc/$(basename "$1")/device" 2>/dev/null) || return 1
while [ -n "$pfad" ] && [ "$pfad" != "/" ]; do
if [ -r "$pfad/product" ]; then
printf '%s %s' "$(cat "$pfad/manufacturer" 2>/dev/null || true)" "$(cat "$pfad/product")"
return 0
fi
pfad=$(dirname "$pfad")
done
return 1
}
# ---------------------------------------------------------------------------
# 1. System pruefen
# ---------------------------------------------------------------------------
titel "1. System"
command -v python3 >/dev/null || fehler "python3 fehlt. Bitte nachinstallieren: apt install python3"
command -v systemctl >/dev/null || fehler "systemd fehlt - dieses Skript richtet einen systemd-Dienst ein."
punkt "$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || uname -s) ($(uname -m))"
punkt "$(python3 --version)"
# ---------------------------------------------------------------------------
# 2. Angeschlossene Drucker anzeigen
# ---------------------------------------------------------------------------
titel "2. Angeschlossene Drucker"
gefunden=0
for d in /dev/usb/lp* /dev/lp*; do
[ -e "$d" ] || continue
gefunden=$((gefunden + 1))
printf ' %-16s %s\n' "$d" "$(usb_name "$d" || echo '')"
done
if [ "$gefunden" -eq 0 ]; then
warnung "Gerade haengt kein Drucker unter /dev/usb/lp* am Pi."
warnung "Das ist kein Hindernis: der Dienst findet ihn, sobald er angesteckt"
warnung "wird. Danach in der Timer-App die Liste neu abrufen."
else
echo
punkt "Welcher davon genommen wird, wird in der Timer-App ausgewaehlt."
fi
# ---------------------------------------------------------------------------
# 3. Dienstbenutzer
# ---------------------------------------------------------------------------
titel "3. Dienstbenutzer"
if ! id bondrucker >/dev/null 2>&1; then
useradd --system --no-create-home --shell /usr/sbin/nologin --groups lp bondrucker
punkt "Benutzer 'bondrucker' angelegt (Gruppe lp)."
else
usermod -aG lp bondrucker
punkt "Benutzer 'bondrucker' war schon da."
fi
# ---------------------------------------------------------------------------
# 4. Programmdateien
# ---------------------------------------------------------------------------
titel "4. Programmdateien"
install -d -m 755 "$ZIEL"
cat > "$ZIEL/print-server.py" <<'PRINT_SERVER_ENDE'
@@PRINT_SERVER@@
PRINT_SERVER_ENDE
chmod 755 "$ZIEL/print-server.py"
punkt "$ZIEL/print-server.py"
cat > /etc/systemd/system/bondrucker.service <<'SERVICE_ENDE'
@@SERVICE@@
SERVICE_ENDE
chmod 644 /etc/systemd/system/bondrucker.service
punkt "/etc/systemd/system/bondrucker.service"
# ---------------------------------------------------------------------------
# 5. Konfiguration
# ---------------------------------------------------------------------------
titel "5. Konfiguration"
TOKEN="$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 32)"
install -d -m 750 /etc/bondrucker
# Bewusst OHNE PRINT_DEVICE: welcher Drucker bedient wird, schickt die
# Timer-App bei jedem Auftrag mit. Stuende die Wahl auch hier, gaebe es zwei
# Stellen, die es zu wissen glauben - und eine davon waere irgendwann falsch.
{
printf 'PRINT_TOKEN=%s\n' "$TOKEN"
printf 'PRINT_PORT=%s\n' "$PORT"
} > /etc/bondrucker/env
chmod 640 /etc/bondrucker/env
chown root:bondrucker /etc/bondrucker/env
punkt "/etc/bondrucker/env geschrieben, neues Token erzeugt."
# Ohne diese Regel gehoert /dev/usb/lp0 nach jedem Einstecken wieder root
# allein - der Dienst laeuft aber nicht als root und koennte nicht drucken.
cat > /etc/udev/rules.d/99-bondrucker.rules <<'RULE'
# USB-Bondrucker: Gruppe lp und Schreibrecht, damit der Dienst 'bondrucker'
# ohne root drucken kann. Gilt fuer alle usblp-Geraete.
SUBSYSTEM=="usbmisc", KERNEL=="lp[0-9]*", GROUP="lp", MODE="0660"
SUBSYSTEM=="usb", ATTR{bInterfaceClass}=="07", GROUP="lp", MODE="0660"
RULE
udevadm control --reload-rules >/dev/null 2>&1 || true
udevadm trigger --subsystem-match=usbmisc >/dev/null 2>&1 || true
punkt "udev-Regel gesetzt (Geraet ueberlebt das Aus- und Einstecken)."
# ---------------------------------------------------------------------------
# 6. Dienst starten
# ---------------------------------------------------------------------------
titel "6. Dienst"
systemctl daemon-reload
systemctl enable bondrucker.service >/dev/null 2>&1
# Bewusst "restart" und nicht "enable --now": bei einer erneuten Einrichtung
# laeuft der Dienst schon, und "enable --now" wuerde ihn in Ruhe lassen. Er
# liefe dann mit dem ALTEN Token weiter und wuerde jeden Bon ablehnen.
systemctl restart bondrucker.service
sleep 1
if ! systemctl is-active --quiet bondrucker.service; then
systemctl --no-pager --lines=20 status bondrucker.service || true
fehler "Der Dienst laeuft nicht. Die Ausgabe oben sagt, woran es liegt."
fi
punkt "bondrucker.service laeuft."
# Gegenprobe von innen: antwortet die Bruecke, und nimmt sie das neue Token?
ANTWORT="$(curl -fsS -m 5 -H "X-Print-Token: $TOKEN" "http://127.0.0.1:$PORT/status" 2>/dev/null || true)"
case "$ANTWORT" in
*'"tokenAccepted": true'*|*'"tokenAccepted":true'*) punkt "Bruecke antwortet und nimmt das Token an." ;;
"") warnung "Die Bruecke antwortet auf 127.0.0.1:$PORT noch nicht." ;;
*) warnung "Die Bruecke antwortet, lehnt das Token aber ab: $ANTWORT" ;;
esac
# ---------------------------------------------------------------------------
# 7. Bei der Timer-App anmelden
# ---------------------------------------------------------------------------
IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
titel "7. Timer-App"
angemeldet=0
case "$APP_URL" in
@@*|"")
punkt "Kein Einrichtungscode enthalten - dieses Skript wurde nicht ueber"
punkt "die Timer-App geladen. Anmeldung wird uebersprungen."
;;
*)
if ! command -v curl >/dev/null; then
warnung "curl fehlt - die Anmeldung muss von Hand erfolgen."
else
RUMPF="$(printf '{"code":"%s","port":%s,"token":"%s","hostHint":"%s"}' \
"$SETUP_CODE" "$PORT" "$TOKEN" "$IP")"
if ERGEBNIS="$(curl -fsS -m 15 -X POST \
-H 'Content-Type: application/json' \
-d "$RUMPF" "$APP_URL/api/setup/register" 2>&1)"; then
punkt "Bei $APP_URL angemeldet."
punkt "Jetzt dort unter Einstellungen den Drucker auswaehlen."
angemeldet=1
else
warnung "Anmeldung fehlgeschlagen: $ERGEBNIS"
warnung "Meist ist der Code abgelaufen - in der Timer-App einen neuen erzeugen."
fi
fi
;;
esac
# ---------------------------------------------------------------------------
# Fertig
# ---------------------------------------------------------------------------
titel "Fertig"
punkt "Adresse: ${IP:-<IP des Pi>}"
punkt "Port: $PORT"
if [ "$angemeldet" -eq 0 ]; then
echo
punkt "Von Hand eintragen in der Timer-App unter Einstellungen:"
printf ' Adresse: %s\n Port: %s\n Token: %s\n' "${IP:-?}" "$PORT" "$TOKEN"
fi
echo
punkt "Zustand abfragen: curl -s http://${IP:-127.0.0.1}:$PORT/status"
punkt "Protokoll: journalctl -u bondrucker -f"
echo