feat(station): ausgelieferte Firmware-Version auf dem Panel
Welche Version die Station gerade ausliefert, war am Geraet nur per SSH zu beantworten. Jetzt sagt es die Anzeige selbst, je nach Bild an der Stelle, wo Platz ist: - Aus-Zustand: rechtsbuendig neben der Adresse (neue Rolle ROLE_DETAIL_RIGHT, nach dem Muster der Phase neben dem Modus) - Rettung: rechtsbuendig in der Kopfzeile, dort steht keine Phase im Weg - Migration: als beschriftete Zeile "liefert: <version>" direkt hinter "FW:", NICHT in der Kopfzeile - daneben bleiben nur rund 112 px, daran ist schon "FREIGEGEBEN: <ziel>" gescheitert, und zwei nackte Versionsnummern nebeneinander waeren eine Verwechslungsfalle (FW: ist die der Lampe) Quelle ist der GEWAEHLTE Bildsatz: im Migrationsmodus manifest.json (die Migrationsabzuege tragen keinen ninlight-Anhaenger), im Rettungsmodus der Anhaenger der _ota.img. Weichen die Eintraege voneinander ab, steht da "gemischt" - ein zusammengefuehrter Deploy kann Alt-Eintraege mit anderer Version enthalten, und eine davon herauszugreifen waere geraten. Fehlt die Quelle, bleibt die Angabe weg statt eine Version zu erfinden.
This commit is contained in:
@@ -319,6 +319,77 @@ def _read_imageset(imageset_path: Path, conf: dict[str, str], mode: str) -> tupl
|
||||
return name, False
|
||||
|
||||
|
||||
def _trailer_version(path: Path) -> str:
|
||||
"""Firmware-Version aus dem NINL-Anhaenger eines ninlight-Abzugs, sonst "".
|
||||
|
||||
Nur die letzten 512 Byte werden gelesen: die Rettungsabzuege liegen als
|
||||
_ota.img vor und tragen ihre Version im Anhaenger (Geraetetyp bei Offset 5,
|
||||
Version bei 21) - das Entpacken des gzip-Rumpfs waere dafuer unnoetige Arbeit auf
|
||||
einem Pi, der alle paar Sekunden neu zeichnet.
|
||||
"""
|
||||
try:
|
||||
with path.open("rb") as f:
|
||||
f.seek(-512, 2)
|
||||
anhaenger = f.read(512)
|
||||
except OSError:
|
||||
return ""
|
||||
if len(anhaenger) != 512 or anhaenger[0:4] != b"NINL":
|
||||
return ""
|
||||
return anhaenger[21:37].split(b"\x00", 1)[0].decode("ascii", errors="replace")
|
||||
|
||||
|
||||
def delivered_version(conf: dict, imageset: str, mode: str) -> str:
|
||||
"""Die Firmware-Version, die die Station gerade ausliefern wuerde, sonst "".
|
||||
|
||||
Quelle ist der GEWAEHLTE Bildsatz, nicht irgendein Verzeichnis: wer am Taster
|
||||
zwischen "mit WLAN" und "ohne WLAN" umschaltet, bekommt sonst eine Zahl angezeigt,
|
||||
die zum anderen Satz gehoert.
|
||||
|
||||
Im Migrationsmodus steht die Version in `manifest.json` - die Migrationsabzuege
|
||||
selbst tragen keinen ninlight-Anhaenger (siehe server.py:image_version). Weichen
|
||||
die Eintraege voneinander ab, ist das Ergebnis "gemischt": ein zusammengefuehrter
|
||||
Deploy kann Alt-Eintraege mit anderer Version enthalten, und eine davon
|
||||
herauszugreifen waere geraten. Im Rettungsmodus liegen gewoehnliche _ota.img, deren
|
||||
Anhaenger die Version tragen.
|
||||
|
||||
Fehlt die Quelle oder ist sie unlesbar, bleibt das Ergebnis leer - dann zeigt die
|
||||
Anzeige gar nichts, statt eine Version zu erfinden.
|
||||
"""
|
||||
image_dir = conf.get("IMAGE_DIR")
|
||||
if not image_dir:
|
||||
return ""
|
||||
if mode == "recovery":
|
||||
verzeichnis = Path(image_dir) / RESCUE_IMAGESET_DIRS.get(imageset, "")
|
||||
try:
|
||||
abzuege = sorted(p for p in verzeichnis.iterdir() if p.name.endswith(".img"))
|
||||
except OSError:
|
||||
return ""
|
||||
versionen = {v for v in (_trailer_version(p) for p in abzuege) if v}
|
||||
else:
|
||||
pfad = Path(image_dir) / IMAGESET_DIRS.get(imageset, "") / "manifest.json"
|
||||
try:
|
||||
daten = json.loads(pfad.read_text())
|
||||
except (OSError, ValueError):
|
||||
return ""
|
||||
if not isinstance(daten, dict):
|
||||
return ""
|
||||
versionen = set()
|
||||
eintraege = daten.get("images")
|
||||
if isinstance(eintraege, list):
|
||||
versionen = {
|
||||
e["ninlight_version"]
|
||||
for e in eintraege
|
||||
if isinstance(e, dict) and isinstance(e.get("ninlight_version"), str)
|
||||
and e["ninlight_version"]
|
||||
}
|
||||
if not versionen:
|
||||
oben = daten.get("ninlight_version")
|
||||
versionen = {oben} if isinstance(oben, str) and oben else set()
|
||||
if not versionen:
|
||||
return ""
|
||||
return versionen.pop() if len(versionen) == 1 else "gemischt"
|
||||
|
||||
|
||||
def _route_lookup() -> str:
|
||||
"""Die Rohauskunft von "ip -4 route get" zur Standardroute, oder "" wenn es keine gibt.
|
||||
|
||||
@@ -416,13 +487,17 @@ def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
||||
# (wlan0 traegt dann 192.168.8.1, das steht ohnehin auf jedem Statusbild) und
|
||||
# wuerde den knappen Platz fuer Betriebsauskuenfte wegnehmen.
|
||||
geraet, adresse = local_address()
|
||||
# Auch im Aus-Zustand: welche Version die Station ausliefern WUERDE, ist genau
|
||||
# dann interessant, wenn man sie noch nicht eingeschaltet hat.
|
||||
return {"mode": "off", "imageset": imageset, "imageset_ok": imageset_ok,
|
||||
"ip": adresse, "iface": geraet}
|
||||
"ip": adresse, "iface": geraet,
|
||||
"image_version": delivered_version(conf, imageset, "migration")}
|
||||
|
||||
status = _read_json(run_dir / "status.json")
|
||||
state: dict = {"mode": mode, "served": status.get("served"), "recent": status.get("recent"),
|
||||
"imageset": imageset, "imageset_ok": imageset_ok,
|
||||
"activity": _recent_request(status)}
|
||||
"activity": _recent_request(status),
|
||||
"image_version": delivered_version(conf, imageset, mode)}
|
||||
|
||||
if mode == "recovery":
|
||||
state["ssid"] = conf.get("SSID")
|
||||
|
||||
Reference in New Issue
Block a user