feat: Aufsetz- und Ausliefer-Skripte der Recovery-Station
setup_pi.sh ist idempotent und laesst eine vorhandene Konfiguration in Ruhe; deploy.sh prueft jedes Image mit nl_mkimage --verify, bevor es etwas uebertraegt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
df8e796f1e
commit
fda732d6c4
@@ -0,0 +1,42 @@
|
||||
# ninlight-Recovery-Station
|
||||
|
||||
Ein Raspberry Pi spannt den Zugangspunkt `ninlight-recovery` auf und liefert unter
|
||||
`http://192.168.8.1/ninlight_<target>_ota.img` Firmware-Images aus.
|
||||
|
||||
Jede ninlight-Lampe sucht beim Booten bis zu drei Sekunden nach dieser Station. Findet sie
|
||||
sie, lädt sie das Image für ihren Gerätetyp und flasht es — ohne dass die Lampe im WLAN
|
||||
erreichbar sein muss. Das ist der Rettungsweg für Lampen, an die man sonst nur noch mit dem
|
||||
Lötkolben käme.
|
||||
|
||||
Trägt das angebotene Image dieselbe Version, die bereits läuft, lehnt die Lampe es ab und
|
||||
bootet normal weiter. Eine angelassene Station flasht also nicht in einer Endlosschleife.
|
||||
|
||||
## Bedienung
|
||||
|
||||
tools/station/setup_pi.sh <pi-host> einmalig einrichten
|
||||
tools/station/deploy.sh <pi-host> [target] Images ausliefern
|
||||
ssh <pi-host> ninlight-station on einschalten
|
||||
ssh <pi-host> ninlight-station status Zustand und letzte Auslieferungen
|
||||
ssh <pi-host> ninlight-station off ausschalten
|
||||
|
||||
SSID und Passwort stehen in `/etc/ninlight-station.conf` auf dem Pi und müssen zu den
|
||||
Build-Parametern `NINLIGHT_RECOVERY_SSID` und `NINLIGHT_RECOVERY_PASSWORD` aus
|
||||
`config.local.cmake` passen. Weicht eines ab, findet die Lampe die Station nicht.
|
||||
|
||||
## Vertrauensgrenze
|
||||
|
||||
Der Trailer eines Images prüft Gerätetyp und SHA256. Er ist **keine** kryptografische
|
||||
Signatur, und der Download läuft über `http://` ohne TLS. Die Station schützt allein ein
|
||||
WPA2-Passwort, dessen Default im Repo steht. Wer in Funkreichweite ist und es kennt, kann
|
||||
jeder Lampe beim nächsten Boot beliebige Firmware unterschieben.
|
||||
|
||||
Das ist gewollt: Die Signaturprüfung der alten Firmware auszuhebeln ist der Zweck dieses
|
||||
Projekts. Daraus folgen zwei Regeln.
|
||||
|
||||
**Setz ein eigenes Passwort.** In `config.local.cmake` und in
|
||||
`/etc/ninlight-station.conf` — an beiden Stellen denselben Wert.
|
||||
|
||||
**Lass die Station nur an, solange du sie brauchst.** Nicht nur wegen des Fremdzugriffs:
|
||||
Die Firmware löscht den Power-Cycle-Zähler, bevor sie einen Recovery-Versuch startet.
|
||||
Solange eine Station in Reichweite ist, funktioniert der Werksreset per fünf Stromzyklen
|
||||
bei **keiner** Lampe in Reichweite mehr.
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# Liefert Firmware-Images und den Stationscode an den Pi aus.
|
||||
#
|
||||
# Aufruf: tools/station/deploy.sh <pi-host> [target ...]
|
||||
# Ohne target-Argumente werden alle in build/*/ gefundenen _ota.img uebertragen.
|
||||
set -euo pipefail
|
||||
HOST=${1:?Aufruf: tools/station/deploy.sh <pi-host> [target ...]}
|
||||
shift || true
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
ROOT=$(cd "$HERE/../.." && pwd)
|
||||
MKIMAGE="$ROOT/tools/mkimage/nl_mkimage.py"
|
||||
|
||||
declare -a FILES=()
|
||||
if [ $# -gt 0 ]; then
|
||||
for t in "$@"; do
|
||||
f="$ROOT/build/$t/ninlight_${t}_ota.img"
|
||||
[ -f "$f" ] || { echo "Fehler: $f fehlt - erst bauen" >&2; exit 1; }
|
||||
FILES+=("$f")
|
||||
done
|
||||
else
|
||||
while IFS= read -r f; do FILES+=("$f"); done < <(find "$ROOT/build" -maxdepth 2 -name 'ninlight_*_ota.img' | sort)
|
||||
[ ${#FILES[@]} -gt 0 ] || { echo "Fehler: keine Images unter $ROOT/build gefunden" >&2; exit 1; }
|
||||
fi
|
||||
|
||||
echo "== Images pruefen =="
|
||||
STAGE=$(mktemp -d)
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
printf '%-26s %-10s %s\n' GERAET VERSION BYTES
|
||||
for f in "${FILES[@]}"; do
|
||||
base=$(basename "$f")
|
||||
target=${base#ninlight_}; target=${target%_ota.img}
|
||||
# Ein Image, das die Pruefung nicht besteht, wuerde jede Lampe ablehnen -
|
||||
# das faellt sonst erst am Geraet auf.
|
||||
out=$(python3 "$MKIMAGE" --verify "$f" --expect-device "$target") || {
|
||||
echo "Fehler: $base besteht die Trailer-Pruefung nicht, nichts uebertragen" >&2; exit 1; }
|
||||
# shellcheck disable=SC2086
|
||||
set -- $out
|
||||
printf '%-26s %-10s %s\n' "$1" "$2" "$3"
|
||||
cp "$f" "$STAGE/$base"
|
||||
done
|
||||
|
||||
echo "== uebertragen =="
|
||||
rsync -a --delete "$STAGE/" "$HOST:/tmp/ninlight-images/"
|
||||
rsync -a "$HERE/server.py" "$HERE/ninlight-station" "$HERE/ninlight-station.service" "$HOST:/tmp/"
|
||||
ssh "$HOST" 'set -e
|
||||
sudo install -d -m 0755 /opt/ninlight-station/images
|
||||
sudo rsync -a --delete /tmp/ninlight-images/ /opt/ninlight-station/images/
|
||||
sudo install -m 0755 /tmp/server.py /opt/ninlight-station/server.py
|
||||
sudo install -m 0755 /tmp/ninlight-station /usr/local/bin/ninlight-station
|
||||
sudo install -m 0644 /tmp/ninlight-station.service /etc/systemd/system/ninlight-station.service
|
||||
rm -rf /tmp/ninlight-images /tmp/server.py /tmp/ninlight-station /tmp/ninlight-station.service
|
||||
sudo systemctl daemon-reload
|
||||
systemctl is-active --quiet ninlight-station && { sudo systemctl restart ninlight-station; echo " Dienst neu gestartet"; } || echo " Dienst laeuft nicht (Station ist aus)"'
|
||||
|
||||
echo "fertig."
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# Richtet einen Raspberry Pi als ninlight-Recovery-Station ein. Idempotent:
|
||||
# mehrfaches Ausfuehren aendert nichts und schadet nicht.
|
||||
#
|
||||
# Aufruf: tools/station/setup_pi.sh <pi-host>
|
||||
set -euo pipefail
|
||||
HOST=${1:?Aufruf: tools/station/setup_pi.sh <pi-host>}
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
echo "== Voraussetzungen auf $HOST pruefen =="
|
||||
ssh "$HOST" '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"'
|
||||
|
||||
echo "== Konfiguration =="
|
||||
scp -q "$HERE/station.conf.example" "$HOST:/tmp/ninlight-station.conf.example"
|
||||
ssh "$HOST" 'set -e
|
||||
if [ -f /etc/ninlight-station.conf ]; then
|
||||
echo " /etc/ninlight-station.conf existiert bereits, bleibt unveraendert"
|
||||
else
|
||||
sudo install -m 0600 /tmp/ninlight-station.conf.example /etc/ninlight-station.conf
|
||||
echo " /etc/ninlight-station.conf aus der Vorlage angelegt"
|
||||
fi
|
||||
rm -f /tmp/ninlight-station.conf.example'
|
||||
|
||||
echo "== Server, CLI und Unit installieren =="
|
||||
scp -q "$HERE/server.py" "$HERE/ninlight-station" "$HERE/ninlight-station.service" "$HOST:/tmp/"
|
||||
ssh "$HOST" 'set -e
|
||||
sudo install -d -m 0755 /opt/ninlight-station /opt/ninlight-station/images
|
||||
sudo install -m 0755 /tmp/server.py /opt/ninlight-station/server.py
|
||||
sudo install -m 0755 /tmp/ninlight-station /usr/local/bin/ninlight-station
|
||||
sudo install -m 0644 /tmp/ninlight-station.service /etc/systemd/system/ninlight-station.service
|
||||
rm -f /tmp/server.py /tmp/ninlight-station /tmp/ninlight-station.service
|
||||
sudo systemctl daemon-reload
|
||||
echo " installiert"'
|
||||
|
||||
echo "== NetworkManager-Profil =="
|
||||
ssh "$HOST" 'set -e
|
||||
. /etc/ninlight-station.conf
|
||||
if nmcli -t -f NAME con show | grep -qx "$AP_CONNECTION"; then
|
||||
sudo nmcli con mod "$AP_CONNECTION" \
|
||||
802-11-wireless.ssid "$SSID" wifi-sec.psk "$PSK" \
|
||||
ipv4.addresses 192.168.8.1/24 ipv4.method shared \
|
||||
connection.autoconnect no
|
||||
echo " Profil $AP_CONNECTION aktualisiert"
|
||||
else
|
||||
sudo nmcli con add type wifi ifname wlan0 con-name "$AP_CONNECTION" ssid "$SSID" \
|
||||
802-11-wireless.mode ap 802-11-wireless.band bg 802-11-wireless.channel 6 \
|
||||
wifi-sec.key-mgmt wpa-psk wifi-sec.proto rsn wifi-sec.pairwise ccmp wifi-sec.group ccmp \
|
||||
wifi-sec.psk "$PSK" \
|
||||
ipv4.method shared ipv4.addresses 192.168.8.1/24 ipv6.method disabled \
|
||||
connection.autoconnect no >/dev/null
|
||||
echo " Profil $AP_CONNECTION angelegt"
|
||||
fi'
|
||||
|
||||
cat <<'EOF'
|
||||
|
||||
Fertig. Die Station ist absichtlich noch AUS.
|
||||
|
||||
Naechste Schritte:
|
||||
tools/station/deploy.sh <pi-host> Images ausliefern
|
||||
ssh <pi-host> ninlight-station on Station einschalten
|
||||
ssh <pi-host> ninlight-station off wieder ausschalten (nicht vergessen)
|
||||
EOF
|
||||
Reference in New Issue
Block a user