feat(station): Umschalter fuer Migrationsimages mit/ohne WLAN-Daten

Die WLAN-Zugangsdaten stecken zur Bauzeit in der Firmware, es gibt also
zwei Abzuege je Ziel. Die Station haelt jetzt beide vor - images/
migrate-nocreds (aus tools/build_nocreds.sh, Lampe startet im
Onboarding-AP) und images/migrate-wlan (aus build/, Lampe haengt sofort
im eigenen WLAN) - und der Bediener waehlt per Umschalter, welcher
ausgeliefert wird: "ninlight-station bildsatz" oder der Menueeintrag am
Taster.

Der Zustand liegt in /var/lib/ninlight-station/imageset, ueberlebt
Neustarts und steht dauerhaft in der Fusszeile des Panels - in jedem
Modus, auch bei ausgeschalteter Station: man soll vor dem Einschalten
sehen, was gilt.

Vorgabe und Rueckfallwert in JEDEM Zweifelsfall ist "ohne-wlan". Im
Automatikmodus wird jede Klyqa-Lampe in Reichweite migriert, auch
fremde; ein Abzug ohne Zugangsdaten ist dann ein harmloser Fehlgriff,
einer mit Zugangsdaten traegt das eigene WLAN-Passwort in ein fremdes
Geraet.

Fehlt der gewaehlte Satz (Verzeichnis weg oder leer), wird NICHTS
ausgeliefert - kein stilles Ausweichen auf den anderen Satz. Der Server
antwortet 404 mit Status "noimageset", CLI und Panel sagen "FEHLT".

server.py und die Stations-Unit muessen mit: die Auswahl der
ausgelieferten Datei ist genau die Aufgabe des Servers, und ein
Symlink-Tausch haette "gewaehlt, aber nicht vorhanden" von "dieses
Modell fehlt" nicht mehr unterscheidbar gemacht.
This commit is contained in:
Michael Sauer
2026-09-15 02:17:36 +02:00
parent 3642672c71
commit 300edafc3e
15 changed files with 1381 additions and 200 deletions
+149 -3
View File
@@ -22,6 +22,31 @@ CONF=${NINLIGHT_STATION_CONF:-/etc/ninlight-station.conf}
RUN_DIR=${NINLIGHT_RUN_DIR:-/run/ninlight-station}
STATION_LIB=${NINLIGHT_STATION_LIB:-/opt/ninlight-station}
# --- Umschalter: Migrationsimages mit oder ohne WLAN-Zugangsdaten --------------------
#
# Die WLAN-Zugangsdaten stecken zur BAUZEIT in der Firmware. Es liegen deshalb zwei
# Saetze Migrationsimages auf dem Pi:
#
# ohne-wlan -> $IMAGE_DIR/migrate-nocreds (tools/build_nocreds.sh): die migrierte
# Lampe startet im eigenen Onboarding-Zugangspunkt und wartet auf ein WLAN
# mit-wlan -> $IMAGE_DIR/migrate-wlan (build/): die Lampe haengt sofort im WLAN
# des Bedieners
#
# Der Umschalter ist ein ZUSTAND, keine Abfrage je Migration: er bleibt, bis jemand ihn
# aendert, und er steht dauerhaft auf dem Panel (display.py liest dieselbe Datei).
# Deshalb liegt sie NICHT in $RUN_DIR (tmpfs, weg nach jedem Neustart) und nicht in
# $CONF (0600 root:root, und setup_pi.sh traegt dort Schluessel nach) - sondern in einer
# eigenen, dauerhaften Datei mit 0644, die auch der Anzeigedienst lesen kann.
IMAGESET_FILE=${NINLIGHT_IMAGESET_FILE:-/var/lib/ninlight-station/imageset}
# Die sichere Vorgabe, und zwar in JEDEM Zweifelsfall (Datei fehlt, unlesbar, unbekannter
# Inhalt). Begruendung: im Automatikmodus wird jede alte Klyqa-Lampe in Funkreichweite
# migriert, auch fremde. Mit dem Abzug OHNE Zugangsdaten ist ein Fehlgriff aergerlich,
# aber harmlos - die Lampe steht im Onboarding-AP und wartet. Mit dem Abzug MIT
# Zugangsdaten steht das eigene WLAN-Passwort unwiderruflich in einem fremden Geraet.
# Der teurere Fehler darf nie der sein, der ohne Zutun passiert. server.py:read_image_set()
# haelt dieselbe Vorgabe unabhaengig ein.
IMAGESET_DEFAULT=ohne-wlan
# --- Zeitschalter ("Auto-Aus") -----------------------------------------------------
#
# Rein OPTIONAL: nur "--for <minuten>" legt einen an. Ohne die Angabe passiert hier
@@ -109,8 +134,111 @@ images() {
find "$IMAGE_DIR" -maxdepth 1 -name 'ninlight_*_ota.img' 2>/dev/null | sort
}
# --- Bildsatz lesen, schreiben, benennen --------------------------------------------
# Der rohe Inhalt der Umschalterdatei (ohne Leerraum), oder leer. Nur fuer die Meldung
# unten gedacht - entschieden wird ausschliesslich mit imageset_get().
imageset_raw() {
cat "$IMAGESET_FILE" 2>/dev/null | tr -d ' \t\r\n' || true
}
# Der eingestellte Bildsatz, oder $IMAGESET_DEFAULT bei fehlender/unlesbarer Datei bzw.
# unbekanntem Inhalt. Gibt IMMER einen gueltigen Namen aus und scheitert nie - ein
# kaputter Umschalter darf weder "off" noch das Panel aufhalten.
imageset_get() {
_value=$(imageset_raw)
case "$_value" in
ohne-wlan|mit-wlan) printf '%s' "$_value" ;;
*) printf '%s' "$IMAGESET_DEFAULT" ;;
esac
}
# Das Verzeichnis zum Namen des Bildsatzes ($1).
imageset_dir() {
case "$1" in
mit-wlan) printf '%s' "$IMAGE_DIR/migrate-wlan" ;;
*) printf '%s' "$IMAGE_DIR/migrate-nocreds" ;;
esac
}
imageset_label() {
case "$1" in
mit-wlan) printf 'mit-wlan (Lampe haengt sofort im eigenen WLAN)' ;;
*) printf 'ohne-wlan (Lampe startet im Onboarding-Zugangspunkt)' ;;
esac
}
# Schreibt den Umschalter atomar mit 0644 - dasselbe Muster wie write_mode()/
# write_control(), aber in ein DAUERHAFTES Verzeichnis: der Zustand soll einen Neustart
# ueberleben (das ist der Sinn eines Umschalters), und der Anzeigedienst muss ihn lesen
# koennen.
imageset_set() {
sudo mkdir -p "$(dirname "$IMAGESET_FILE")"
_tmp="$(dirname "$IMAGESET_FILE")/.imageset-$$"
printf '%s\n' "$1" | sudo tee "$_tmp" >/dev/null && sudo chmod 0644 "$_tmp" || return 1
sudo mv -f "$_tmp" "$IMAGESET_FILE"
}
migrate_images() {
find "$IMAGE_DIR/migrate" -maxdepth 1 -name '*.img' 2>/dev/null | sort
find "$(imageset_dir "$(imageset_get)")" -maxdepth 1 -name '*.img' 2>/dev/null | sort
}
# "bildsatz" ohne Argument: nur berichten. Mit Argument: umschalten.
bildsatz_cmd() {
_want=${1:-}
_now=$(imageset_get)
if [ -z "$_want" ]; then
imageset_status_lines
return 0
fi
case "$_want" in
ohne-wlan|mit-wlan) ;;
*)
echo "Fehler: unbekannter Bildsatz '$_want' - erlaubt sind 'ohne-wlan' und 'mit-wlan'" >&2
exit 2
;;
esac
imageset_set "$_want" || {
echo "Fehler: $IMAGESET_FILE konnte nicht geschrieben werden" >&2
exit 1
}
echo "Bildsatz: $(imageset_label "$_want")"
[ "$_now" = "$_want" ] && echo " (war schon eingestellt)"
_dir=$(imageset_dir "$_want")
if [ "$(find "$_dir" -maxdepth 1 -name '*.img' 2>/dev/null | wc -l | tr -d ' ')" -eq 0 ]; then
# Kein stiller Rueckfall auf den anderen Satz - server.py liefert dann gar nichts
# aus (Status "noimageset"), und das Panel sagt "FEHLT". Hier trotzdem sofort
# warnen, damit niemand erst an der Lampe merkt, dass nichts kommt.
echo "Warnung: $_dir fehlt oder ist leer - in diesem Bildsatz wird NICHTS ausgeliefert. tools/station/deploy.sh erneut laufen lassen." >&2
fi
if [ "$_want" = "mit-wlan" ]; then
echo "Achtung: migrierte Lampen haengen damit sofort im WLAN aus config.local.cmake - auch fremde Lampen, die im Automatikmodus mitgenommen werden." >&2
fi
}
# Die zwei Zeilen, die "status" und "bildsatz" ohne Argument gemeinsam ausgeben.
imageset_status_lines() {
_set=$(imageset_get)
_dir=$(imageset_dir "$_set")
# "Vorhanden" heisst: mindestens ein Image darin - dieselbe Regel wie
# server.py:migrate_set_present() und display.py:_read_imageset(). Ein leeres
# Verzeichnis liefert genauso nichts aus wie ein fehlendes.
_n=$(migrate_images | wc -l | tr -d ' ')
if [ "$_n" -gt 0 ]; then
_state="$_n Image(s)"
else
_state="FEHLT - es wird nichts ausgeliefert"
fi
printf 'Bildsatz: %s\n' "$(imageset_label "$_set")"
printf ' %s (%s)\n' "$_dir" "$_state"
# Steht in der Datei etwas, das kein Bildsatzname ist, gilt stillschweigend die
# Vorgabe - das ist richtig so (sicher), aber der Bediener muss es erfahren: sonst
# glaubt er, sein Eintrag von Hand haette gewirkt.
_raw=$(imageset_raw)
if [ -n "$_raw" ] && [ "$_raw" != "$_set" ]; then
printf " (unbekannte Einstellung '%s' in %s - Vorgabe gilt)\n" \
"$_raw" "$IMAGESET_FILE"
fi
}
# Faehrt erst jedes andere Stationsprofil herunter, dann (falls $1 nicht leer ist)
@@ -518,8 +646,10 @@ migrate_on() {
done
[ "$_for_seen" -eq 1 ] || _for_minutes=""
need_profile "$MIGRATE_AP_CONNECTION"
_set=$(imageset_get)
_setdir=$(imageset_dir "$_set")
n=$(migrate_images | wc -l | tr -d ' ')
[ "$n" -gt 0 ] || echo "Warnung: $IMAGE_DIR/migrate enthaelt keine Migrationsimages - die Station antwortet auf alles mit 404" >&2
[ "$n" -gt 0 ] || echo "Warnung: $_setdir enthaelt keine Migrationsimages - die Station antwortet auf alles mit 404" >&2
# Faehrt ninlight-recovery (falls es laeuft) herunter, BEVOR qconnex-firmware
# hochkommt, und prueft danach nach, dass wirklich nur eines aktiv ist.
ap_switch "$MIGRATE_AP_CONNECTION"
@@ -530,6 +660,10 @@ migrate_on() {
}
start_and_verify "$MIGRATE_AP_CONNECTION" "migration" "$_phase"
echo "Migration an: SSID $MIGRATE_SSID auf 192.168.8.1, Phase $_phase, $n Migrationsimage(s)"
# Der Bildsatz gehoert in JEDE Einschaltmeldung: er entscheidet, ob die migrierte
# Lampe das WLAN-Passwort des Bedieners mitbekommt, und er kann von vorletzter Woche
# stehen.
echo "Bildsatz: $(imageset_label "$_set")"
echo "Achtung: das Passwort dieses Zugangspunkts ist aus der alten Firmware oeffentlich bekannt - siehe README.md." >&2
echo "Nicht vergessen: 'ninlight-station off', wenn du fertig bist."
autooff_arm_if_requested
@@ -861,6 +995,7 @@ PY
fi
printf 'Modus : %s\n' "$_modus"
printf 'Phase : %s\n' "$_phase"
imageset_status_lines
printf 'Erkannt: %s\n' "$_erkannt"
printf 'Dienst : %s\n' "$(systemctl is-active ninlight-station 2>/dev/null || true)"
autooff_status_line
@@ -898,8 +1033,16 @@ display_cmd() {
}
usage() {
echo "Aufruf: ninlight-station on [--for <minuten>]|recovery {on [--for <minuten>]|off}|migrate {on [--listen] [--for <minuten>]|off}|arm [<target>]|listen|renew [<minuten>]|hold|display {test|clear|off}|off|status" >&2
echo "Aufruf: ninlight-station on [--for <minuten>]|recovery {on [--for <minuten>]|off}|migrate {on [--listen] [--for <minuten>]|off}|arm [<target>]|listen|bildsatz [ohne-wlan|mit-wlan]|renew [<minuten>]|hold|display {test|clear|off}|off|status" >&2
echo "" >&2
echo " bildsatz Umschalter: welcher Satz Migrationsimages ausgeliefert wird." >&2
echo " Ohne Argument nur berichten. 'ohne-wlan' (Vorgabe) liefert die" >&2
echo " Abzuege aus tools/build_nocreds.sh - die migrierte Lampe startet" >&2
echo " im eigenen Onboarding-Zugangspunkt. 'mit-wlan' liefert die" >&2
echo " Abzuege aus build/ - die Lampe haengt sofort im WLAN aus" >&2
echo " config.local.cmake, auch wenn es eine fremde Lampe ist, die im" >&2
echo " Automatikmodus mitgenommen wurde. Der Zustand bleibt, bis ihn" >&2
echo " jemand aendert, und steht dauerhaft auf dem Panel." >&2
echo " --for <minuten> Zeitschalter: die Station schaltet sich nach <minuten> selbst" >&2
echo " wieder aus (zurueck ins Heim-WLAN). OPTIONAL - ohne '--for'" >&2
echo " bleibt sie an, bis jemand 'off' sagt. Nuetzlich, wenn du ueber" >&2
@@ -956,6 +1099,9 @@ case "$_cmd" in
listen)
listen_cmd
;;
bildsatz)
bildsatz_cmd "${1:-}"
;;
renew)
renew_cmd "${1:-}"
;;