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