#!/usr/bin/env bash # Richtet einen Raspberry Pi als ninlight-Station ein. Idempotent: mehrfaches # Ausfuehren aendert nichts und schadet nicht - auch nicht auf einem Pi, auf dem # schon eine aeltere Version dieses Skripts gelaufen ist (siehe "Konfiguration" # unten: fehlende Schluessel werden ergaenzt, vorhandene bleiben unangetastet). # # Aufruf: tools/station/setup_pi.sh set -euo pipefail HOST=${1:?Aufruf: tools/station/setup_pi.sh } HERE=$(cd "$(dirname "$0")" && pwd) # /usr/bin/ssh und /usr/bin/scp statt Varianten im PATH, die sich anders verhalten koennen. # Ueberschreibbar (wie NINLIGHT_STATION_ROOT unten und in deploy.sh), damit ein Test gegen # Stubs statt einen echten Pi laufen kann. SSH_BIN=${NINLIGHT_SSH_BIN:-/usr/bin/ssh} SCP_BIN=${NINLIGHT_SCP_BIN:-/usr/bin/scp} # Wo die per scp uebertragenen Dateien auf dem Zielsystem zwischenlanden, bevor # sie an ihren endgueltigen Platz installiert werden. Ueberschreibbar wie die # anderen NINLIGHT_*-Pfade: auf dem Pi bleibt es bei /tmp, ein Test kann hier in # eine Sandbox statt in das echte /tmp des Entwicklungsrechners umleiten # (Fixrunde 2, N4) - sowohl fuer die lokale scp-Zielangabe unten als auch fuer # die REMOTE-Bloecke, die dieselbe Variable (mit demselben Default) referenzieren. STAGE_DIR=${NINLIGHT_STAGE_DIR:-/tmp} echo "== Voraussetzungen auf $HOST pruefen ==" "$SSH_BIN" "$HOST" bash -s <<'REMOTE' set -e systemctl is-active --quiet NetworkManager || { echo "NetworkManager laeuft nicht - dieser Plan setzt ihn voraus" >&2; exit 1; } nmcli -t -f DEVICE,STATE device | grep -q "^wlan0:" || { echo "wlan0 wird nicht von NetworkManager verwaltet" >&2; exit 1; } python3 -c "import sys; sys.exit(0 if sys.version_info >= (3,11) else 1)" || { echo "Python 3.11+ noetig" >&2; exit 1; } echo " ok" REMOTE echo "== Konfiguration ==" "$SCP_BIN" -q "$HERE/station.conf.example" "$HOST:$STAGE_DIR/ninlight-station.conf.example" # CONF ueberschreibbar wie ROOT_DIR/BIN_PATH/UNIT_PATH unten: auf dem Pi bleibt es # beim Default, ein Test kann hier gegen eine Sandbox-Datei statt gegen echte # Systempfade laufen. "$SSH_BIN" "$HOST" bash -s <<'REMOTE' set -e CONF=${NINLIGHT_STATION_CONF:-/etc/ninlight-station.conf} TEMPLATE=${NINLIGHT_STAGE_DIR:-/tmp}/ninlight-station.conf.example if [ -f "$CONF" ]; then # Eine bestehende Konfiguration bleibt bei ihren Werten - nur Schluessel, die # eine neuere Version des CLI zusaetzlich verlangt (z.B. die Migrations- und # Anzeige-Schluessel aus Task 4), werden aus der Vorlage ergaenzt. Ohne das # wuerde das Ausrollen eines neuen ninlight-station auf einen bereits # eingerichteten Pi jeden Unterbefehl - "off" eingeschlossen - mit "Schluessel # ... fehlt" blockieren, und ein bereits laufender Zugangspunkt bliebe an, bis # jemand die 0600-Datei von Hand ergaenzt (Fixrunde 1, I4). ADDED="" for KEY in SSID PSK IMAGE_DIR HOME_CONNECTION AP_CONNECTION \ MIGRATE_AP_CONNECTION MIGRATE_SSID MIGRATE_PSK EPD_REVISION; do # Nicht nur "^KEY=": eine vorhandene Zuweisung mit fuehrendem Leerraum oder # einem "export" davor (beides gueltiges ". datei"-Shell) wuerde sonst als # fehlend gelten, die Vorlagenzeile wird angehaengt, und da das CLI die # Datei per ". $CONF" sourct, gewinnt die LETZTE Zuweisung - ein eigenes # Passwort wuerde so still durch den oeffentlichen Vorlagenwert ersetzt # (Fixrunde 2, N3: genau das "vorhandene Werte nicht ueberschreiben" aus I4, # nur eine Formatierungsvariante weiter). if ! sudo grep -Eq "^[[:space:]]*(export[[:space:]]+)?${KEY}=" "$CONF"; then LINE=$(grep -E "^${KEY}=" "$TEMPLATE" || true) if [ -n "$LINE" ]; then printf '%s\n' "$LINE" | sudo tee -a "$CONF" >/dev/null ADDED="$ADDED $KEY" fi fi done if [ -n "$ADDED" ]; then echo " $CONF: fehlende Schluessel ergaenzt:$ADDED (vorhandene Werte unveraendert)" else echo " $CONF existiert bereits, alle Schluessel vorhanden" fi else sudo install -m 0600 "$TEMPLATE" "$CONF" echo " $CONF aus der Vorlage angelegt" fi rm -f "$TEMPLATE" REMOTE echo "== Server, CLI und Units installieren ==" # Reststand der Ablage wegraeumen, BEVOR das scp laeuft (Task 6b F1, I1). "scp -r quelle # ziel" kopiert das Quellverzeichnis IN ein bereits vorhandenes Ziel hinein - aus # einem liegengebliebenen "…/ninlight-display/" wird dann "…/ninlight-display/display/", # die Installationsschleife unten ueberspringt Verzeichnisse und findet gar nichts mehr. # Ergebnis waere ein Lauf, der Erfolg meldet, den Anzeigedienst aktiviert und ein leeres # $ROOT_DIR/display hinterlaesst: 203/EXEC im 5-Sekunden-Takt, Panel schwarz. Genau so # ein Reststand entsteht, wenn ein frueherer Lauf nach dem scp abbricht (sudo-Timeout, # Platte voll, Strg-C) - die Aufraeumzeile unten laeuft dann nicht mehr. Idempotent # heisst, dass auch dieser Fall glattgeht (siehe Kopf dieser Datei). "$SSH_BIN" "$HOST" bash -s <<'REMOTE' set -e STAGE_DIR=${NINLIGHT_STAGE_DIR:-/tmp} rm -rf "$STAGE_DIR/ninlight-display" "$STAGE_DIR/ninlight-button" REMOTE "$SCP_BIN" -q "$HERE/server.py" "$HERE/products.py" "$HERE/ninlight-station" \ "$HERE/ninlight-station.service" "$HERE/ninlight-display.service" "$HERE/ninlight-button.service" \ "$HOST:$STAGE_DIR/" # Das Anzeigeverzeichnis als Ganzes: render.py, display.py, epdconfig.py, die beiden # uebernommenen Panel-Treiber und HERKUNFT.md (der Herkunftsnachweis gehoert mit auf den # Pi). "scp -r" statt rsync, weil setup_pi.sh - anders als deploy.sh - ausdruecklich nur # ssh/scp voraussetzt; __pycache__ wird auf der Gegenseite weggeraeumt (unten). "$SCP_BIN" -q -r "$HERE/display" "$HOST:$STAGE_DIR/ninlight-display" # Das Tasterverzeichnis genauso: als Ganzes, mit demselben "vorher aufraeumen"-Schutz # gegen ein "scp -r" auf ein bereits vorhandenes Ziel (siehe Kommentar oben zu # ninlight-display). "$SCP_BIN" -q -r "$HERE/button" "$HOST:$STAGE_DIR/ninlight-button" # Pfade als Variablen mit Default (wie in deploy.sh): so kann ein Test die # Installation gegen ein Sandbox-Verzeichnis statt gegen echte Systempfade laufen # lassen. "$SSH_BIN" "$HOST" bash -s <<'REMOTE' set -e STAGE_DIR=${NINLIGHT_STAGE_DIR:-/tmp} ROOT_DIR=${NINLIGHT_STATION_ROOT:-/opt/ninlight-station} BIN_PATH=${NINLIGHT_STATION_BIN:-/usr/local/bin/ninlight-station} UNIT_PATH=${NINLIGHT_STATION_UNIT:-/etc/systemd/system/ninlight-station.service} DISPLAY_UNIT_PATH=${NINLIGHT_DISPLAY_UNIT:-/etc/systemd/system/ninlight-display.service} BUTTON_UNIT_PATH=${NINLIGHT_BUTTON_UNIT:-/etc/systemd/system/ninlight-button.service} # Die Ablage wird in JEDEM Fall abgeraeumt, auch wenn dieser Block vorzeitig aussteigt # (Task 6b F2, N1). Vorher stand das "rm -rf" nur am Blockende und wurde beim Abbruch # uebersprungen - die uebertragenen Dateien blieben unter /tmp liegen und waren beim # naechsten Lauf genau der Reststand, gegen den der Block eigentlich absichert. cleanup_stage() { rm -rf "$STAGE_DIR/server.py" "$STAGE_DIR/products.py" "$STAGE_DIR/ninlight-station" \ "$STAGE_DIR/ninlight-station.service" "$STAGE_DIR/ninlight-display.service" \ "$STAGE_DIR/ninlight-display" "$STAGE_DIR/ninlight-button.service" \ "$STAGE_DIR/ninlight-button" } trap cleanup_stage EXIT rm -rf "$STAGE_DIR/ninlight-display/__pycache__" "$STAGE_DIR/ninlight-button/__pycache__" # Harte Vorbedingung, und zwar VOR dem ersten "sudo install" (Task 6b F2, N1): ohne # display.py in der Ablage hat das scp nicht geliefert, was es sollte - abgebrochen, # falsch verschachtelt oder gar nicht gelaufen. Die Installationsschleife weiter unten # wuerde das stillschweigend als "nichts zu tun" verbuchen, der Lauf meldete Erfolg und # der aktivierte Anzeigedienst liefe gegen eine nicht existierende Datei. # # Die Pruefung stand zuerst NACH den vier "sudo install" darunter - die Meldung "Nichts # installiert" war damit schlicht unwahr: server.py, products.py, das CLI und die # Stations-Unit lagen dann bereits auf dem Pi. Ausgerechnet an dem Werkbank-Moment, fuer # den diese Pruefung gedacht ist, log das Skript den Bediener an. Jetzt stimmt der Satz. if [ ! -f "$STAGE_DIR/ninlight-display/display.py" ]; then echo "Fehler: $STAGE_DIR/ninlight-display/display.py fehlt - die Uebertragung des Anzeigeverzeichnisses ist fehlgeschlagen." >&2 echo " Nichts installiert: $ROOT_DIR, $BIN_PATH und alle drei Units sind unveraendert. Ablage aufgeraeumt, Lauf einfach wiederholen." >&2 exit 1 fi # Dieselbe harte Vorbedingung wie bei display.py, aus demselben Grund: ohne button.py in # der Ablage hat das scp nicht geliefert, was es sollte, und die Installationsschleife # unten wuerde das stillschweigend als "nichts zu tun" verbuchen. if [ ! -f "$STAGE_DIR/ninlight-button/button.py" ]; then echo "Fehler: $STAGE_DIR/ninlight-button/button.py fehlt - die Uebertragung des Tasterverzeichnisses ist fehlgeschlagen." >&2 echo " Nichts installiert: $ROOT_DIR, $BIN_PATH und alle drei Units sind unveraendert. Ablage aufgeraeumt, Lauf einfach wiederholen." >&2 exit 1 fi sudo install -d -m 0755 "$ROOT_DIR" "$ROOT_DIR/images" "$ROOT_DIR/display" "$ROOT_DIR/button" sudo install -m 0755 "$STAGE_DIR/server.py" "$ROOT_DIR/server.py" sudo install -m 0644 "$STAGE_DIR/products.py" "$ROOT_DIR/products.py" sudo install -m 0755 "$STAGE_DIR/ninlight-station" "$BIN_PATH" sudo install -m 0644 "$STAGE_DIR/ninlight-station.service" "$UNIT_PATH" # Anzeige: display.py ist ausfuehrbar (die Unit ruft es zwar ueber /usr/bin/python3 auf, # "ninlight-station display test" aber auch von Hand), der Rest ist importierter Code. for f in "$STAGE_DIR"/ninlight-display/*; do [ -f "$f" ] || continue case "$(basename "$f")" in display.py) sudo install -m 0755 "$f" "$ROOT_DIR/display/$(basename "$f")" ;; *) sudo install -m 0644 "$f" "$ROOT_DIR/display/$(basename "$f")" ;; esac done # Was im Repo geloescht wurde, muss auch hier verschwinden - dasselbe Argument wie in # deploy.sh (dort per "rsync --delete"): display/ ist reiner Code aus dem Repo, es gibt # keinen Bestand, der einen Lauf ueberleben muesste, und ein zurueckgebliebenes altes # Modul wuerde weiter mit importiert. rsync steht hier nicht zur Verfuegung (setup_pi.sh # setzt ausdruecklich nur ssh/scp voraus), also von Hand. for f in "$ROOT_DIR"/display/*; do [ -e "$f" ] || continue if [ ! -e "$STAGE_DIR/ninlight-display/$(basename "$f")" ]; then sudo rm -rf "$f" echo " veraltete Datei entfernt: display/$(basename "$f")" fi done sudo install -m 0644 "$STAGE_DIR/ninlight-display.service" "$DISPLAY_UNIT_PATH" # Taster: dieselbe Behandlung wie die Anzeige - button.py ist ausfuehrbar (die Unit ruft # es zwar ueber /usr/bin/python3 auf, von Hand pruefen laesst es sich trotzdem), der Rest # (aktuell nichts weiter) waere importierter Code. for f in "$STAGE_DIR"/ninlight-button/*; do [ -f "$f" ] || continue case "$(basename "$f")" in button.py) sudo install -m 0755 "$f" "$ROOT_DIR/button/$(basename "$f")" ;; *) sudo install -m 0644 "$f" "$ROOT_DIR/button/$(basename "$f")" ;; esac done # Dasselbe Argument wie bei display/: button/ ist reiner Code aus dem Repo. for f in "$ROOT_DIR"/button/*; do [ -e "$f" ] || continue if [ ! -e "$STAGE_DIR/ninlight-button/$(basename "$f")" ]; then sudo rm -rf "$f" echo " veraltete Datei entfernt: button/$(basename "$f")" fi done sudo install -m 0644 "$STAGE_DIR/ninlight-button.service" "$BUTTON_UNIT_PATH" # Das Aufraeumen der Ablage macht der EXIT-Trap oben - auf beiden Wegen aus diesem Block. sudo systemctl daemon-reload echo " installiert" REMOTE echo "== Pakete ==" # Pillow kommt auf dem Pi bewusst aus dem Distributionspaket (python3-pil), nicht per # pip: tools/station/ laeuft dort unter dem System-Python 3.13, und Debian 13 laesst ein # "pip install" ins System-Python gar nicht mehr zu (PEP 668). Ohne Pillow zeichnet die # Anzeige nicht - display.py faengt den ImportError ab und beendet sich still mit 0 # (Absicht, damit ein fehlendes Panel nie eine Migration verhindert). Genau deshalb muss # das Paket ueberhaupt hier kommen: sonst faellt es nirgends auf. # # Aber NACH der Installation und nur als Warnung (Task 6b F1, I4): eine Migrationsstation # steht typischerweise nicht am Netz, und mit veralteten Paketlisten oder ohne Internet # scheitert apt. Unter "set -e" vor der Installation haette das den Lauf abgebrochen, # bevor server.py, das CLI, beide Units und beide NetworkManager-Profile installiert sind # - man haette dann GAR KEINE Station statt einer Station ohne Panel. Das waere die # einzige harte Stelle in einer sonst durchgehend weichen Kette gewesen (display.py # beendet sich mit 0, deploy.sh warnt nur, enable/restart unten warnen nur). "$SSH_BIN" "$HOST" bash -s <<'REMOTE' set -e if python3 -c 'import PIL' 2>/dev/null; then echo " python3-pil ist bereits vorhanden" else # "update" zuerst: auf einem laenger nicht gepflegten Pi zeigen die Paketlisten auf # Versionen, die es im Archiv nicht mehr gibt, und "install" scheitert mit 404. # # Aber "|| true", und "install" laeuft UNABHAENGIG davon (Task 6b F2, N2): "apt-get # update" liefert schon dann einen Fehler, wenn eine einzige Quelle nicht erreichbar # ist - eine veraltete Drittanbieter-Liste, teilweises Netz. Mit "update && install" # bekaeme ein Pi, auf dem "install" aus dem Paketcache oder den erreichbaren Quellen # muehelos durchgelaufen waere, still kein Pillow. Das Auffrischen ist eine # Verbesserung der Chancen, keine Voraussetzung. sudo apt-get update >/dev/null 2>&1 || true if sudo apt-get install -y python3-pil; then echo " python3-pil installiert" else echo " Warnung: python3-pil liess sich nicht installieren (kein Netz? veraltete Paketlisten?)." >&2 echo " Die Station ist vollstaendig eingerichtet, das E-Paper bleibt aber leer," >&2 echo " bis 'sudo apt install python3-pil' durchlaeuft und der Anzeigedienst neu startet." >&2 fi fi REMOTE echo "== SPI aktivieren ==" # Auf einem frischen Raspberry Pi OS ist SPI ausgeschaltet: dtparam=spi=on steht # auskommentiert in der Boot-Konfiguration, /dev/spidev0.0 fehlt, und der Anzeigedienst # meldet "E-Paper-Anzeige nicht verfuegbar" - das sieht nach fehlender Hardware aus, # ist aber nur ein abgeschalteter Bus. Ohne diesen Schritt haette jede frische Station # ein schwarzes Panel, bis jemand von Hand nachhilft (wie hier gestern geschehen). # # Die Aenderung wirkt erst nach einem Neustart der Boot-Konfiguration - das Skript # startet den Pi NICHT selbst neu (ein unerwarteter Reboot mitten in einer Fernsitzung # waere ueberraschender als ein Hinweis am Ende), meldet ueber den Exit-Code des # Remote-Blocks aber, ob einer noetig war (10 = geaendert, 0 = schon an, 20 = keine # Boot-Konfiguration gefunden). $SPI_REBOOT_NEEDED steuert unten, ob der Hinweis in den # naechsten Schritten erscheint - nur dann, wenn wirklich etwas geaendert wurde. SPI_REBOOT_NEEDED=0 set +e "$SSH_BIN" "$HOST" bash -s <<'REMOTE' set -e # Ueberschreibbar wie die anderen NINLIGHT_*-Pfade oben: auf dem Pi bleibt es bei den # echten Pfaden, ein Test kann hier gegen Sandbox-Dateien statt gegen echte # Systempfade laufen. SPIDEV=${NINLIGHT_SPIDEV_PATH:-/dev/spidev0.0} CONFIG_NEW=${NINLIGHT_BOOT_CONFIG_NEW:-/boot/firmware/config.txt} CONFIG_OLD=${NINLIGHT_BOOT_CONFIG_OLD:-/boot/config.txt} # Existiert das Geraeteknoten schon, ist SPI aktiv - unabhaengig davon, was in der # Boot-Konfiguration steht (es kann auch per Overlay oder einem anderen Mechanismus # kommen). Dann kein Eingriff und kein Neustart-Hinweis. if [ -e "$SPIDEV" ]; then echo " SPI ist bereits aktiv ($SPIDEV vorhanden)" exit 0 fi # Aktuelle Raspberry-Pi-OS-Images nutzen /boot/firmware/config.txt, aeltere # /boot/config.txt - beide werden geprueft, die neuere zuerst. Gibt es keinen von # beiden, ist das kein Grund abzubrechen: ein anderes Boot-Verfahren (oder gar kein # Raspberry Pi OS) ist moeglich, und die Anzeige ist nirgends Voraussetzung fuer eine # funktionierende Station (dasselbe Prinzip wie beim apt-Schritt und beim # Anzeigedienst oben). CONFIG="" if sudo test -f "$CONFIG_NEW"; then CONFIG="$CONFIG_NEW" elif sudo test -f "$CONFIG_OLD"; then CONFIG="$CONFIG_OLD" fi if [ -z "$CONFIG" ]; then echo "Warnung: weder $CONFIG_NEW noch $CONFIG_OLD gefunden - vermutlich kein" >&2 echo " Raspberry Pi OS oder ein anderes Boot-Verfahren. SPI konnte nicht" >&2 echo " geprueft werden - bleibt das E-Paper schwarz, ist das der" >&2 echo " wahrscheinlichste Grund." >&2 exit 20 fi # Eine aktive (nicht auskommentierte) "dtparam=spi=on"-Zeile heisst: schon an - dann # nichts tun und keinen Neustart verlangen, den es nicht braucht. if sudo grep -Eq '^[[:space:]]*dtparam=spi=on[[:space:]]*$' "$CONFIG"; then echo " SPI ist in $CONFIG bereits eingeschaltet" exit 0 fi BACKUP="${CONFIG}.bak-ninlight" if sudo test -f "$BACKUP"; then # Nicht ueberschreiben: sonst waere beim zweiten Lauf (z.B. weil zwischendurch von # Hand wieder auf "off" gestellt wurde) der urspruengliche Originalzustand weg. echo " Sicherung $BACKUP existiert bereits - wird nicht ueberschrieben" else sudo cp -p "$CONFIG" "$BACKUP" echo " Sicherung angelegt: $BACKUP" fi # Drei Faelle bleiben: die Zeile fehlt ganz, sie steht auskommentiert da, oder sie # steht aktiv auf "off" (z.B. von Hand deaktiviert). In allen dreien wird auf # "dtparam=spi=on" normalisiert - eine vorhandene Zeile wird ERSETZT statt eine # zweite anzuhaengen, sonst stuenden am Ende zwei widerspruechliche # dtparam=spi-Zeilen in der Datei und es waere Zufall, welche NetworkManager beim # naechsten Boot zuletzt liest. # # Kein "sed -i": dieser Block laeuft zwar auf dem Pi (GNU) unter "REMOTE", aber der # Stub-Test fuehrt ihn woertlich auf dem Entwicklungsrechner (macOS/BSD) aus - "sed # -i" braucht dort ein Suffix-Argument, GNU-sed keins, und die falsche Reihenfolge # war in diesem Projekt schon zweimal ein Fehler. Deshalb sed nur als Filter # (Eingabe->Ausgabe), das Ergebnis geht ueber eine Temp-Datei und "sudo install" an # den Zielpfad - das funktioniert auf beiden Plattformen identisch. if sudo grep -Eq '^[[:space:]]*#?[[:space:]]*dtparam=spi=(on|off)[[:space:]]*$' "$CONFIG"; then TMP_CONFIG=$(mktemp "${TMPDIR:-/tmp}/ninlight-spi-config.XXXXXX") sudo sed -E 's/^[[:space:]]*#?[[:space:]]*dtparam=spi=(on|off)[[:space:]]*$/dtparam=spi=on/' "$CONFIG" >"$TMP_CONFIG" sudo install -m 0644 -o root -g root "$TMP_CONFIG" "$CONFIG" rm -f "$TMP_CONFIG" echo " $CONFIG: dtparam=spi=on gesetzt (vorhandene Zeile aktiviert)" else printf '%s\n' 'dtparam=spi=on' | sudo tee -a "$CONFIG" >/dev/null echo " $CONFIG: dtparam=spi=on ergaenzt" fi # Signalisiert dem Aufrufer, dass ein Neustart noetig ist (siehe oben). exit 10 REMOTE SPI_RC=$? set -e case "$SPI_RC" in 0) ;; 10) SPI_REBOOT_NEEDED=1 ;; 20) ;; *) echo "Warnung: SPI-Aktivierung ergab einen unerwarteten Fehler (rc=$SPI_RC) - /boot/firmware/config.txt bzw. /boot/config.txt von Hand pruefen." >&2 ;; esac echo "== Anzeigedienst aktivieren ==" # Die Anzeige wird aktiviert (startet also beim Booten mit) UND jetzt gestartet - anders # als die Station selbst, die absichtlich aus bleibt: die Anzeige sendet nichts, sie # zeigt nur an, und "Station aus" ist genau das, was nach dem Einrichten auf dem Panel # stehen soll. Erst hier, nach dem Paketschritt: so startet der Dienst bereits mit # vorhandenem Pillow, statt sich beim ersten Zeichnen still mit 0 zu beenden und bis zum # naechsten manuellen Neustart nichts anzuzeigen. # # Fehlt Panel oder Pillow, beendet sich der Dienst still mit 0 (siehe display.py) - # deshalb nur Warnungen: ein fehlendes Panel darf setup_pi.sh nicht abbrechen. "$SSH_BIN" "$HOST" bash -s <<'REMOTE' set -e sudo systemctl enable ninlight-display.service >/dev/null 2>&1 || \ echo " Warnung: 'systemctl enable ninlight-display' fehlgeschlagen - die Anzeige startet nach einem Neustart des Pi nicht von allein mit" >&2 sudo systemctl restart ninlight-display.service >/dev/null 2>&1 || \ echo " Warnung: 'systemctl restart ninlight-display' fehlgeschlagen - journalctl -u ninlight-display" >&2 echo " Anzeigedienst aktiviert" REMOTE echo "== Tasterdienst aktivieren ==" # Dieselbe weiche Behandlung wie die Anzeige, aus demselben Grund: der Taster ist # nirgends Voraussetzung. Fehlt gpiozero, fehlt der Taster oder ist GPIO23 belegt, # beendet sich button.py still mit 0 (siehe dessen Modul-Docstring) - ein # fehlgeschlagenes enable/start darf setup_pi.sh trotzdem nicht abbrechen, denn eine # Station ohne Taster muss vollstaendig benutzbar bleiben. "$SSH_BIN" "$HOST" bash -s <<'REMOTE' set -e sudo systemctl enable ninlight-button.service >/dev/null 2>&1 || \ echo " Warnung: 'systemctl enable ninlight-button' fehlgeschlagen - der Taster startet nach einem Neustart des Pi nicht von allein mit" >&2 sudo systemctl restart ninlight-button.service >/dev/null 2>&1 || \ echo " Warnung: 'systemctl restart ninlight-button' fehlgeschlagen - journalctl -u ninlight-button" >&2 echo " Tasterdienst aktiviert" REMOTE echo "== NetworkManager-Profile ==" # Zwei Dinge, die man an einer echten Lampenstation nicht falsch machen darf: # - /etc/ninlight-station.conf ist 0600 root:root (das Passwort soll nicht # jeder lesen koennen). Der SSH-Benutzer ist aber nicht root, direktes # ". /etc/ninlight-station.conf" wuerde also mit "Permission denied" # abbrechen. "sudo cat" + eval liest sie dennoch ein, ohne die Rechte der # Datei aufzuweichen. # - "nmcli ... wifi-sec.psk $PSK" wuerde das Passwort als Kommandozeilen- # argument an einen Prozess uebergeben - fuer jeden lokalen Benutzer per # "ps"/"/proc//cmdline" sichtbar. Deshalb wird das Verbindungsprofil # stattdessen direkt als NetworkManager-Keyfile (0600 root:root) # geschrieben; das Passwort landet nur in einer Root-Datei, nie in einer # Prozessliste. "$SSH_BIN" "$HOST" bash -s <<'REMOTE' set -e CONF=${NINLIGHT_STATION_CONF:-/etc/ninlight-station.conf} NM_DIR=${NINLIGHT_NM_DIR:-/etc/NetworkManager/system-connections} eval "$(sudo cat "$CONF")" # Die Keyfiles werden erst in einer Temp-Datei aufgebaut und dann per "install" # an ihren Platz gelegt - dazwischen liegt das WLAN-Passwort in einer Datei, die # noch nicht 0600 root:root ist. Ein Abbruch (set -e) genau dort darf sie nicht # liegen lassen. Ein EXIT-Trap in write_profile() selbst waere dafuer untauglich: # die Funktion laeuft zweimal, der zweite trap ueberschriebe den ersten, und # "$tmp" ist "local" - beim Auswerten des Traps am Skriptende gaebe es die # Variable gar nicht mehr. Deshalb ein gemeinsames Verzeichnis mit genau einem # Trap darum herum (0700, mktemp -d legt es so an). # # Zur Reichweite des Traps (Task 6b F2, N3 - die Begruendung hier war vorher falsch): # der EXIT-Trap allein WUERDE bei einem Strg-C schon reichen, bash fuehrt ihn auch beim # Abbruch durch ein nicht abgefangenes SIGINT aus (Exit 130). Der Grund fuer die # Signalhandler ist ein anderer: sie sollen den Exit-Code ehrlich halten. Ein Handler # fuer INT/TERM, der nicht selbst beendet, laesst bash naemlich NACH dem Handler # weiterlaufen - ein Strg-C mitten im Schreiben der Profile koennte dann in "fertig, # Exit 0" muenden, obwohl der Lauf abgebrochen wurde. Deshalb raeumt jeder Handler auf # und beendet danach ausdruecklich mit dem ueblichen Signal-Code (128+n). # # Mit ausdruecklicher Vorlage unter ${TMPDIR:-/tmp}, nicht als blankes "mktemp -d": # BSD-mktemp (macOS, wo diese Suite auch laeuft) beachtet TMPDIR ohne Vorlage gar nicht # und legt unabhaengig davon unter /var/folders/... an, GNU-mktemp (der Pi) dagegen # schon. Ohne Vorlage landet das Verzeichnis also je nach Plattform woanders - ein Test, # der nachsieht, ob wirklich nichts liegengeblieben ist, sucht dann auf macOS an der # falschen Stelle und ist gruen, ohne etwas geprueft zu haben. PROFILE_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/ninlight-profile.XXXXXX") trap 'rm -rf "$PROFILE_TMPDIR"' EXIT trap 'rm -rf "$PROFILE_TMPDIR"; exit 130' INT trap 'rm -rf "$PROFILE_TMPDIR"; exit 143' TERM trap 'rm -rf "$PROFILE_TMPDIR"; exit 129' HUP # $1 = Profilname, $2 = SSID, $3 = PSK, $4 = "update" (bestehendes Profil neu schreiben, # UUID behalten) oder "keep" (bestehendes Profil unangetastet lassen). # # Beide Profile bekommen dieselbe Adresse 192.168.8.1/24 auf wlan0 - genau deshalb # schliessen sie sich gegenseitig aus, und genau deshalb prueft ninlight-station's # ap_switch() nach jedem Umschalten nach, dass wirklich nur eines aktiv ist. write_profile() { local id=$1 ssid=$2 psk=$3 mode=$4 # Ein leerer Profilname wuerde eine Datei ".nmconnection" anlegen, eine leere SSID # oder ein leeres PSK ein Profil, das NetworkManager beim Hochfahren stumm ablehnt - # in beiden Faellen faellt es erst auf, wenn jemand mit einer toten Lampe vor der # Station steht. Der Konfigurationsschritt oben ergaenzt fehlende Schluessel aus der # Vorlage; leer sein koennen sie trotzdem (von Hand geleert). if [ -z "$id" ] || [ -z "$ssid" ] || [ -z "$psk" ]; then echo "Fehler: Profilname, SSID oder PSK ist leer - $CONF pruefen (Profil '$id')" >&2 return 1 fi local conn_file="$NM_DIR/${id}.nmconnection" local uuid action tmp if sudo test -f "$conn_file"; then if [ "$mode" = "keep" ]; then # Der Migrations-AP ist auf manchen Pis von Hand angelegt worden (SSID/PSK sind # von der alten Klyqa-Firmware fest vorgegeben, es gibt also nichts zu variieren). # Ein bestehendes Profil wird deshalb nicht ueberschrieben - es koennte # handjustierte Felder (Kanal, Band, Regulierungsdomaene) tragen, die dieses # Skript gar nicht kennt. echo " Profil $id existiert bereits - unveraendert gelassen" return 0 fi uuid=$(sudo sed -n 's/^uuid=//p' "$conn_file" | head -n1) action="aktualisiert" else # /proc/sys/kernel/random/uuid gibt es nur unter Linux (dem Pi) - der Fallback # auf uuidgen ist es, was einen Testlauf gegen den lokalen Stub (macOS) ueberhaupt # ermoeglicht, aendert aber nichts am Verhalten auf dem Pi selbst. uuid=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen | tr '[:upper:]' '[:lower:]') action="angelegt" fi tmp=$(mktemp "$PROFILE_TMPDIR/profile.XXXXXX") { printf '%s\n' '[connection]' printf 'id=%s\n' "$id" printf 'uuid=%s\n' "$uuid" printf '%s\n' 'type=wifi' printf '%s\n' 'interface-name=wlan0' printf '%s\n' 'autoconnect=false' printf '\n%s\n' '[wifi]' printf '%s\n' 'mode=ap' printf 'ssid=%s\n' "$ssid" printf '%s\n' 'band=bg' printf '%s\n' 'channel=6' printf '\n%s\n' '[wifi-security]' printf '%s\n' 'key-mgmt=wpa-psk' printf '%s\n' 'proto=rsn' printf '%s\n' 'pairwise=ccmp;' printf '%s\n' 'group=ccmp;' printf 'psk=%s\n' "$psk" printf '\n%s\n' '[ipv4]' printf '%s\n' 'method=shared' printf '%s\n' 'address1=192.168.8.1/24' printf '\n%s\n' '[ipv6]' printf '%s\n' 'method=disabled' } >"$tmp" sudo install -o root -g root -m 0600 "$tmp" "$conn_file" rm -f "$tmp" sudo nmcli connection load "$conn_file" >/dev/null echo " Profil $id $action" } write_profile "$AP_CONNECTION" "$SSID" "$PSK" update # Ohne dieses zweite Profil scheitert "ninlight-station migrate on" auf einem frisch # eingerichteten Pi an need_profile "$MIGRATE_AP_CONNECTION". write_profile "$MIGRATE_AP_CONNECTION" "$MIGRATE_SSID" "$MIGRATE_PSK" keep REMOTE # Der Neustart-Hinweis steht an eigener, auffaelliger Stelle VOR den "Naechsten # Schritten" - nicht dazwischengemischt - und nur, wenn oben wirklich etwas an der # Boot-Konfiguration geaendert wurde (SPI_REBOOT_NEEDED). Ein Hinweis bei jedem Lauf, # auch wenn SPI laengst an ist, wuerde nach ein paar Durchlaeufen ignoriert. SPI_NOTE="" if [ "$SPI_REBOOT_NEEDED" -eq 1 ]; then SPI_NOTE=' WICHTIG: SPI wurde soeben erst eingeschaltet (Aenderung an der Boot-Konfiguration) - das wirkt erst nach einem Neustart. Bis dahin bleibt das E-Paper schwarz, auch wenn sonst alles richtig eingerichtet ist. ssh sudo reboot ' fi cat < ninlight-station display test Testbild aufs E-Paper - bleibt es leer oder verzerrt, steht EPD_REVISION in /etc/ninlight-station.conf auf der falschen Panel-Revision (V2 statt V3 oder umgekehrt) tools/station/deploy.sh Images ausliefern ssh ninlight-station on Station einschalten ssh ninlight-station off wieder ausschalten (nicht vergessen) Der Taster auf GPIO23 ist mit eingerichtet: kurzer Druck oeffnet das Menue am Geraet, 3 s gehalten brechen eine laufende Migration/Rettung ab. EOF