feat(station): Rettung folgt dem Bildsatz-Umschalter, Server meldet Lebenszeichen
server.py: image_path() liefert jetzt aus rescue_dir() (nach dem Umschalter ohne-wlan/mit-wlan), genau wie migrate_dir() es fuer die Migration schon tut. Bewusst kein Fallback auf den alten flachen Bestand in images/ - ein Pi mit altem Bestand muss einmal per deploy.sh neu bestueckt werden, sonst wuerde "ohne-wlan" den alten Zustand (immer mit Zugangsdaten) stillschweigend fortsetzen. Station.last_activity/"last_request_ts" in status.json: Zeitpunkt jeder Anfrage (auch erfolgloser), damit die Anzeige ein Lebenszeichen zeigen kann, statt nur am Servierzaehler abzulesen, ob ueberhaupt etwas ankommt.
This commit is contained in:
+64
-3
@@ -86,6 +86,21 @@ def trailer_version(path: Path) -> str:
|
||||
IMAGE_SET_DIRS = {"ohne-wlan": "migrate-nocreds", "mit-wlan": "migrate-wlan"}
|
||||
DEFAULT_IMAGE_SET = "ohne-wlan"
|
||||
|
||||
# Derselbe Umschalter gilt jetzt auch fuer die RETTUNG (nicht nur die Migration): der
|
||||
# Eigentuemer hat das ausdruecklich verlangt, denn bisher bekam eine Rettung IMMER ein
|
||||
# Abbild MIT Zugangsdaten (die flachen Abzuege in images/ stammten aus build/). Die
|
||||
# Rettungsabzuege OHNE Zugangsdaten kommen aus tools/build_nocreds.sh, genau wie ihr
|
||||
# Migrations-Gegenstueck - deshalb dasselbe Namensschema, nur mit "rescue-" statt
|
||||
# "migrate-" (deploy.sh legt sie entsprechend ab, siehe dort).
|
||||
#
|
||||
# Zwei getrennte Verzeichnispaare (migrate-*/rescue-*) statt einem gemeinsamen: die
|
||||
# Migrationsabzuege sind signiert und tragen ein eigenes manifest.json, die
|
||||
# Rettungsabzuege sind die gewoehnlichen _ota.img mit ninlight-Trailer. Sie
|
||||
# unterscheiden sich in Form und Herstellung; ein gemeinsames Verzeichnis wuerde beide
|
||||
# Herstellungswege ununterscheidbar ineinander mischen (siehe auch: "migrate-wlan/ und
|
||||
# migrate-nocreds/ werden bei der Rettung nie angefasst" im Auftrag).
|
||||
RESCUE_SET_DIRS = {"ohne-wlan": "rescue-nocreds", "mit-wlan": "rescue-wlan"}
|
||||
|
||||
|
||||
def read_image_set(path: Path | None) -> str:
|
||||
"""Der eingestellte Bildsatz, oder DEFAULT_IMAGE_SET, wenn die Datei fehlt, unlesbar
|
||||
@@ -117,6 +132,13 @@ class Station:
|
||||
self.since = now_iso()
|
||||
self.served = 0
|
||||
self.recent: list[dict] = []
|
||||
# Zeitpunkt der letzten Anfrage JEDER Art (erfolgreich oder nicht) - das
|
||||
# Lebenszeichen fuer die Anzeige (display.py:_recent_request()). Anders als
|
||||
# "served" zaehlt hier auch eine erfolglose Anfrage: der Eigentuemer wollte
|
||||
# gerade nicht erst am Servierzaehler raten muessen, ob ueberhaupt etwas
|
||||
# anklopft (Bedienerwunsch 2026-09-23). None, solange seit dem Start der
|
||||
# Station noch keine einzige Anfrage einging.
|
||||
self.last_activity: str | None = None
|
||||
# Zuletzt erkanntes Modell im Migrationsmodus, auch wenn (noch) nichts ausgeliefert
|
||||
# wurde - das ist es, was die Anzeige gross darstellt (Spec 5.2).
|
||||
self.last_seen: dict | None = None
|
||||
@@ -148,8 +170,36 @@ class Station:
|
||||
self.lock = threading.RLock()
|
||||
self.write_state()
|
||||
|
||||
def rescue_dir(self) -> Path:
|
||||
"""Das Verzeichnis des EINGESTELLTEN Bildsatzes fuer die RETTUNG. Dieselbe Regel
|
||||
wie migrate_dir() (dort steht die ausfuehrliche Begruendung): der Umschalter
|
||||
entscheidet, nicht eine Abfrage je Rettung, und es wird nie stillschweigend auf
|
||||
den jeweils anderen Satz ausgewichen.
|
||||
|
||||
Bestandskompatibilitaet bewusst NICHT gegeben: vor diesem Umschalter lagen
|
||||
Rettungsabzuege FLACH in image_dir (immer MIT Zugangsdaten, aus build/). Ein
|
||||
Pi mit diesem alten Bestand wuerde, faende image_path() dort noch etwas, bei der
|
||||
Vorgabe "ohne-wlan" weiterhin klaglos ein Abbild MIT Zugangsdaten ausliefern -
|
||||
also genau die alte, jetzt unerwuenschte Grundhaltung stillschweigend
|
||||
fortsetzen, die dieser Umschalter gerade abstellen soll. Ein Pi mit altem
|
||||
Bestand muss deshalb einmal per deploy.sh neu bestueckt werden (das raeumt den
|
||||
flachen Altbestand auch auf); bis dahin liefert die Rettung in der Vorgabe
|
||||
"ohne-wlan" sichtbar nichts aus (Status "notfound"), statt einen Fehlgriff zu
|
||||
verstecken."""
|
||||
return self.image_dir / RESCUE_SET_DIRS[self.image_set()]
|
||||
|
||||
def rescue_set_present(self) -> bool:
|
||||
"""Liegt der eingestellte Rettungs-Bildsatz ueberhaupt auf dem Pi? Dieselbe Regel
|
||||
wie migrate_set_present() (Verzeichnis vorhanden UND mindestens ein Image
|
||||
darin) - Panel und CLI sollen fuer die Rettung nach demselben Massstab
|
||||
urteilen wie fuer die Migration."""
|
||||
try:
|
||||
return any(p.name.endswith(".img") for p in self.rescue_dir().iterdir())
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def image_path(self, target: str) -> Path:
|
||||
return self.image_dir / f"ninlight_{target}_ota.img"
|
||||
return self.rescue_dir() / f"ninlight_{target}_ota.img"
|
||||
|
||||
def image_set(self) -> str:
|
||||
"""Der gerade eingestellte Bildsatz ("ohne-wlan"/"mit-wlan")."""
|
||||
@@ -326,7 +376,11 @@ class Station:
|
||||
with self.lock:
|
||||
if status == "ok":
|
||||
self.served += 1
|
||||
self.recent.append({"ts": now_iso(), "ip": ip, "target": target,
|
||||
ts = now_iso()
|
||||
# Jede Anfrage zaehlt als Lebenszeichen, nicht nur eine erfolgreiche - siehe
|
||||
# self.last_activity oben.
|
||||
self.last_activity = ts
|
||||
self.recent.append({"ts": ts, "ip": ip, "target": target,
|
||||
"version": version, "bytes": nbytes, "status": status})
|
||||
del self.recent[:-RECENT_MAX]
|
||||
self.write_state()
|
||||
@@ -347,6 +401,9 @@ class Station:
|
||||
"variant_source": variant_source,
|
||||
"product_id": product_id, "lamp_version": lamp_version,
|
||||
"version": version, "bytes": nbytes, "status": status}
|
||||
# Jede Anfrage zaehlt als Lebenszeichen, auch "listen"/"unknown"/"notfound"/
|
||||
# "noimageset" - siehe self.last_activity oben und record()'s Gegenstueck.
|
||||
self.last_activity = entry["ts"]
|
||||
self.recent.append(entry)
|
||||
del self.recent[:-RECENT_MAX]
|
||||
if target is not None:
|
||||
@@ -359,7 +416,11 @@ class Station:
|
||||
def write_state(self) -> None:
|
||||
with self.lock:
|
||||
payload = {"since": self.since, "mode": self.mode, "served": self.served,
|
||||
"recent": list(self.recent)}
|
||||
"recent": list(self.recent),
|
||||
# In BEIDEN Modi (nicht nur migration, siehe unten) - das
|
||||
# Lebenszeichen auf dem Panel gilt fuer die Rettung genauso wie
|
||||
# fuer die Migration (display.py:_recent_request()).
|
||||
"last_request_ts": self.last_activity}
|
||||
if self.mode == "migration":
|
||||
phase, armed_target = self.read_control()
|
||||
payload["phase"] = phase
|
||||
|
||||
Reference in New Issue
Block a user