From e77d3da292cf69527b537446db5af6d38303dec4 Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Mon, 14 Sep 2026 14:13:56 +0200 Subject: [PATCH] fix(station): SPI in setup_pi.sh automatisch einschalten Auf frischem Raspberry Pi OS ist SPI aus, das E-Paper bleibt schwarz mit irrefuehrender "nicht verfuegbar"-Meldung. setup_pi.sh erkennt jetzt beide config.txt-Pfade, normalisiert dtparam=spi=on (fehlend/auskommentiert/off), sichert vorher nach config.txt.bak-ninlight und weist nur bei echter Aenderung auf den noetigen Neustart hin. --- tools/station/setup_pi.sh | 124 +++++++++++++++++++- tools/station/tests/test_setup_pi.sh | 169 ++++++++++++++++++++++++++- 2 files changed, 290 insertions(+), 3 deletions(-) diff --git a/tools/station/setup_pi.sh b/tools/station/setup_pi.sh index a71a600..a504572 100755 --- a/tools/station/setup_pi.sh +++ b/tools/station/setup_pi.sh @@ -210,6 +210,112 @@ else 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 @@ -353,10 +459,24 @@ write_profile "$AP_CONNECTION" "$SSID" "$PSK" update write_profile "$MIGRATE_AP_CONNECTION" "$MIGRATE_SSID" "$MIGRATE_PSK" keep REMOTE -cat <<'EOF' +# 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 diff --git a/tools/station/tests/test_setup_pi.sh b/tools/station/tests/test_setup_pi.sh index e921dc3..d644c28 100755 --- a/tools/station/tests/test_setup_pi.sh +++ b/tools/station/tests/test_setup_pi.sh @@ -169,7 +169,12 @@ setup_state() { local state=$1 rm -rf "$state" mkdir -p "$state/etc" "$state/opt" "$state/usr/local/bin" "$state/etc/systemd/system" \ - "$state/etc/NetworkManager/system-connections" "$state/tmp" + "$state/etc/NetworkManager/system-connections" "$state/tmp" \ + "$state/boot/firmware" "$state/dev" + # $state/boot/firmware/config.txt und $state/boot/config.txt existieren absichtlich + # NICHT von selbst (ein Szenario legt sie an, wenn es sie braucht) - so deckt der + # Standardzustand genau den Fall "kein Boot-Verfahren gefunden" ab, ohne dass jedes + # Szenario das extra anfordern muesste. } # Fuehrt setup_pi.sh mit gestubbtem PATH und auf die Sandbox umgebogenen @@ -198,6 +203,9 @@ run_setup() { NINLIGHT_STATION_UNIT="$state/etc/systemd/system/ninlight-station.service" \ NINLIGHT_DISPLAY_UNIT="$state/etc/systemd/system/ninlight-display.service" \ NINLIGHT_NM_DIR="$state/etc/NetworkManager/system-connections" \ + NINLIGHT_BOOT_CONFIG_NEW="$state/boot/firmware/config.txt" \ + NINLIGHT_BOOT_CONFIG_OLD="$state/boot/config.txt" \ + NINLIGHT_SPIDEV_PATH="$state/dev/spidev0.0" \ "$STATION_DIR/setup_pi.sh" pi-test } @@ -638,6 +646,10 @@ echo "=== Szenario 17 (Task 6b F2, N2): ein gescheitertes 'apt-get update' verhi STATE="$TMPROOT/s17-state"; STUBBIN="$TMPROOT/bin17"; LOG="$TMPROOT/s17.log" setup_state "$STATE" setup_stubbin "$STUBBIN" "$LOG" +# SPI schon aktiv: dieses Szenario prueft ausschliesslich den Paketschritt, eine +# fehlende Boot-Konfiguration wuerde eine zweite, hier voellig unbeteiligte Warnung +# erzeugen (Szenario 26 deckt genau die ab). +printf 'dtparam=spi=on\n' >"$STATE/boot/firmware/config.txt" : >"$LOG" set +e OUT=$(FAKE_PIL_MISSING=1 FAKE_APT_UPDATE_EXIT=100 run_setup "$STATE" "$STUBBIN" 2>"$TMPROOT/s17.err") @@ -739,6 +751,161 @@ else fail "ninlight-display.service wurde nicht installiert - Szenario 20 kann nichts pruefen" fi +echo +echo "=== Szenario 21 (Hardware-Abnahme, SPI): fehlende dtparam=spi-Zeile wird ergaenzt ===" +# Auf einem frischen Pi ist SPI aus, es gibt aber schon eine config.txt (mit anderem +# Inhalt) ohne jede dtparam=spi-Zeile - der haeufigste Fall auf einem frisch +# geflashten Image. +STATE="$TMPROOT/s21-state"; STUBBIN="$TMPROOT/bin21"; LOG="$TMPROOT/s21.log" +setup_state "$STATE" +setup_stubbin "$STUBBIN" "$LOG" +CONFIG21="$STATE/boot/firmware/config.txt" +cat >"$CONFIG21" <<'CFG' +# For more options and information see +# http://rptl.io/configtxt +dtparam=i2c_arm=on +dtparam=audio=on +CFG +BEFORE21=$(cat "$CONFIG21") +: >"$LOG" +set +e +OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1) +RC=$? +set -e +[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT" +grep -Fqx "dtparam=spi=on" "$CONFIG21" && pass "dtparam=spi=on wurde ergaenzt" || fail "dtparam=spi=on fehlt weiterhin: $(cat "$CONFIG21")" +grep -Fq "dtparam=i2c_arm=on" "$CONFIG21" && pass "der Rest der Datei blieb erhalten" || fail "vorhandener Inhalt ging verloren" +[ -f "${CONFIG21}.bak-ninlight" ] && pass "Sicherung angelegt" || fail "keine Sicherung unter ${CONFIG21}.bak-ninlight" +[ "$(cat "${CONFIG21}.bak-ninlight")" = "$BEFORE21" ] && pass "Sicherung entspricht dem Originalzustand" || fail "Sicherung weicht vom Original ab" +echo "$OUT" | grep -q "WICHTIG: SPI wurde soeben erst eingeschaltet" && pass "Neustart-Hinweis erscheint" || fail "kein Neustart-Hinweis in der Ausgabe: $OUT" +echo "$OUT" | grep -q "sudo reboot" && pass "der Hinweis nennt den Neustart-Befehl" || fail "kein 'sudo reboot' im Hinweis" + +echo +echo "=== Szenario 22 (Hardware-Abnahme, SPI): eine auskommentierte Zeile wird aktiviert ===" +STATE="$TMPROOT/s22-state"; STUBBIN="$TMPROOT/bin22"; LOG="$TMPROOT/s22.log" +setup_state "$STATE" +setup_stubbin "$STUBBIN" "$LOG" +CONFIG22="$STATE/boot/firmware/config.txt" +cat >"$CONFIG22" <<'CFG' +dtparam=audio=on +#dtparam=spi=on +dtparam=i2c_arm=on +CFG +: >"$LOG" +set +e +OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1) +RC=$? +set -e +[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT" +grep -Fqx "dtparam=spi=on" "$CONFIG22" && pass "die Zeile ist jetzt aktiv" || fail "dtparam=spi=on steht nicht aktiv in der Datei: $(cat "$CONFIG22")" +COUNT22=$(grep -c 'dtparam=spi=on' "$CONFIG22") +[ "$COUNT22" -eq 1 ] && pass "die Zeile wurde ersetzt, nicht verdoppelt" || fail "dtparam=spi=on steht $COUNT22 mal in der Datei" +[ -f "${CONFIG22}.bak-ninlight" ] && pass "Sicherung angelegt" || fail "keine Sicherung angelegt" +echo "$OUT" | grep -q "WICHTIG: SPI wurde soeben erst eingeschaltet" && pass "Neustart-Hinweis erscheint" || fail "kein Neustart-Hinweis: $OUT" + +echo +echo "=== Szenario 23 (Hardware-Abnahme, SPI): 'dtparam=spi=off' wird umgeschaltet ===" +# An eine explizit deaktivierte Zeile ist genauso zu denken wie an eine +# auskommentierte - beide sind "nicht an", beide muessen normalisiert werden. +STATE="$TMPROOT/s23-state"; STUBBIN="$TMPROOT/bin23"; LOG="$TMPROOT/s23.log" +setup_state "$STATE" +setup_stubbin "$STUBBIN" "$LOG" +CONFIG23="$STATE/boot/firmware/config.txt" +printf 'dtparam=spi=off\n' >"$CONFIG23" +: >"$LOG" +set +e +OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1) +RC=$? +set -e +[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT" +grep -Fqx "dtparam=spi=on" "$CONFIG23" && pass "'off' wurde auf 'on' umgeschaltet" || fail "Zeile weiterhin nicht aktiv: $(cat "$CONFIG23")" +echo "$OUT" | grep -q "WICHTIG: SPI wurde soeben erst eingeschaltet" && pass "Neustart-Hinweis erscheint" || fail "kein Neustart-Hinweis: $OUT" + +echo +echo "=== Szenario 24 (Hardware-Abnahme, SPI): schon aktiv -> echtes No-Op, kein Neustart-Hinweis ===" +STATE="$TMPROOT/s24-state"; STUBBIN="$TMPROOT/bin24"; LOG="$TMPROOT/s24.log" +setup_state "$STATE" +setup_stubbin "$STUBBIN" "$LOG" +CONFIG24="$STATE/boot/firmware/config.txt" +cat >"$CONFIG24" <<'CFG' +dtparam=audio=on +dtparam=spi=on +CFG +BEFORE24=$(cat "$CONFIG24") +: >"$LOG" +set +e +OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1) +RC=$? +set -e +[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT" +[ "$(cat "$CONFIG24")" = "$BEFORE24" ] && pass "die Datei blieb byteweise unveraendert" || fail "die Datei wurde trotz bereits aktivem SPI angefasst" +[ ! -f "${CONFIG24}.bak-ninlight" ] && pass "keine Sicherung angelegt (es gab nichts zu sichern)" || fail "eine Sicherung wurde angelegt, obwohl nichts geaendert wurde" +echo "$OUT" | grep -q "bereits eingeschaltet" && pass "meldet ruhig, dass SPI schon an ist" || fail "keine Bestaetigung in der Ausgabe: $OUT" +echo "$OUT" | grep -q "WICHTIG: SPI wurde soeben erst eingeschaltet" && fail "Neustart-Hinweis erscheint, obwohl nichts geaendert wurde" || pass "kein Neustart-Hinweis" + +echo +echo "=== Szenario 25 (Hardware-Abnahme, SPI): /dev/spidev0.0 existiert schon -> SPI gilt als an, egal was in config.txt steht ===" +# Kann per Overlay oder einem anderen Mechanismus aktiviert worden sein - die Datei +# ist dann nicht die Wahrheit. +STATE="$TMPROOT/s25-state"; STUBBIN="$TMPROOT/bin25"; LOG="$TMPROOT/s25.log" +setup_state "$STATE" +setup_stubbin "$STUBBIN" "$LOG" +mkdir -p "$(dirname "$STATE/dev/spidev0.0")" +: >"$STATE/dev/spidev0.0" +CONFIG25="$STATE/boot/firmware/config.txt" +printf '#dtparam=spi=on\n' >"$CONFIG25" +: >"$LOG" +set +e +OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1) +RC=$? +set -e +[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT" +grep -Fqx "#dtparam=spi=on" "$CONFIG25" && pass "config.txt blieb unangetastet (spidev0.0 war entscheidend, nicht die Datei)" || fail "config.txt wurde trotz vorhandenem spidev0.0 geaendert: $(cat "$CONFIG25")" +[ ! -f "${CONFIG25}.bak-ninlight" ] && pass "keine Sicherung angelegt" || fail "eine Sicherung wurde angelegt, obwohl spidev0.0 schon da war" +echo "$OUT" | grep -q "SPI ist bereits aktiv" && pass "meldet, dass SPI ueber das Geraet schon aktiv ist" || fail "keine passende Meldung: $OUT" +echo "$OUT" | grep -q "WICHTIG: SPI wurde soeben erst eingeschaltet" && fail "Neustart-Hinweis erscheint, obwohl nichts geaendert wurde" || pass "kein Neustart-Hinweis" + +echo +echo "=== Szenario 26 (Hardware-Abnahme, SPI): weder config.txt noch config.txt (alt) vorhanden -> Warnung, kein Abbruch ===" +# Kein Raspberry Pi OS oder ein anderes Boot-Verfahren - die Anzeige ist nirgends +# Voraussetzung fuer eine funktionierende Station, dasselbe Prinzip wie beim +# apt-Schritt. +STATE="$TMPROOT/s26-state"; STUBBIN="$TMPROOT/bin26"; LOG="$TMPROOT/s26.log" +setup_state "$STATE" +setup_stubbin "$STUBBIN" "$LOG" +: >"$LOG" +set +e +OUT=$(run_setup "$STATE" "$STUBBIN" 2>"$TMPROOT/s26.err") +RC=$? +set -e +ERR=$(cat "$TMPROOT/s26.err") +[ "$RC" -eq 0 ] && pass "Exit-Code 0 trotz fehlender Boot-Konfiguration" || fail "Exit-Code $RC: $ERR" +echo "$ERR" | grep -qi "Warnung" && pass "Warnung auf stderr" || fail "keine Warnung: $ERR" +echo "$ERR" | grep -q "weder" && pass "Warnung nennt beide gesuchten Pfade" || fail "Warnung erwaehnt die Pfade nicht: $ERR" +[ -x "$STATE/opt/ninlight-station/server.py" ] && pass "die Station wurde trotzdem vollstaendig eingerichtet" || fail "server.py fehlt - ein fehlendes Boot-Verfahren haette die Einrichtung nicht verhindern duerfen" +echo "$OUT" | grep -q "WICHTIG: SPI wurde soeben erst eingeschaltet" && fail "Neustart-Hinweis erscheint, obwohl SPI gar nicht geprueft werden konnte" || pass "kein Neustart-Hinweis" + +echo +echo "=== Szenario 27 (Hardware-Abnahme, SPI): eine bestehende Sicherung wird nicht ueberschrieben ===" +# Ein zweiter Lauf, nachdem SPI zwischendurch von Hand wieder ausgeschaltet wurde - +# ohne diese Absicherung waere der allererste Originalzustand nach dem zweiten Lauf +# weg. +STATE="$TMPROOT/s27-state"; STUBBIN="$TMPROOT/bin27"; LOG="$TMPROOT/s27.log" +setup_state "$STATE" +setup_stubbin "$STUBBIN" "$LOG" +CONFIG27="$STATE/boot/firmware/config.txt" +printf '#dtparam=spi=on\n' >"$CONFIG27" +printf 'urspruenglicher-originalzustand\n' >"${CONFIG27}.bak-ninlight" +: >"$LOG" +set +e +OUT=$(run_setup "$STATE" "$STUBBIN" 2>&1) +RC=$? +set -e +[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC, Ausgabe: $OUT" +grep -Fqx "dtparam=spi=on" "$CONFIG27" && pass "die Zeile wurde trotzdem aktiviert" || fail "dtparam=spi=on fehlt: $(cat "$CONFIG27")" +[ "$(cat "${CONFIG27}.bak-ninlight")" = "urspruenglicher-originalzustand" ] && pass "die bestehende Sicherung blieb unangetastet" || fail "die Sicherung wurde ueberschrieben: $(cat "${CONFIG27}.bak-ninlight")" +echo "$OUT" | grep -q "existiert bereits - wird nicht ueberschrieben" && pass "Ausgabe erwaehnt, dass die Sicherung nicht ueberschrieben wurde" || fail "keine entsprechende Meldung: $OUT" + echo if [ "$FAILED" -eq 0 ]; then echo "Alle Tests bestanden."