feat(station): Panel und CLI auf Englisch, IP-Zeile im Off-Bild
Die deutschen Texte sahen auf dem 2,13"-Panel merkwuerdig aus - jetzt sind alle Texte, die der Bediener am Geraet oder auf der Konsole sieht, englisch: Modus-, Phasen-, Bildsatz- und Abschlussbeschriftungen in render.py, die Bedientexte des Tasterdienstes und saemtliche Ausgaben von ninlight-station inklusive des Hilfetexts. Die Testbeschreibungen der Suiten bleiben deutsch - die liest nur, wer die Tests laufen laesst. Neu im Off-Bild: die Adresse, unter der der Pi gerade erreichbar ist. Ist die Station aus, ist der Pi ein gewoehnlicher Rechner im Heimnetz, und die einzige Auskunft, die man dann am Panel braucht, ist die fuer SSH. Die Adresse kommt aus einem UDP-Socket, der nur verbunden und nie benutzt wird (kein Datenverkehr, kein DNS, kein Unterprozess in einer 5-s-Schleife) und liefert genau die Adresse der Standardroute. Ohne Netz sagt die Zeile ausdruecklich "no network": eine fehlende Zeile wuerde offen lassen, warum SSH gerade scheitert. Die Zeile gibt es NUR im Off-Bild - im Betrieb traegt wlan0 die Adresse des Zugangspunkts, die ohnehin auf jedem Statusbild steht.
This commit is contained in:
@@ -237,6 +237,35 @@ def _read_imageset(imageset_path: Path, conf: dict[str, str]) -> tuple[str, bool
|
||||
return name, False
|
||||
|
||||
|
||||
def local_ip() -> str:
|
||||
"""Die Adresse, unter der der Pi im Heimnetz erreichbar ist, oder "" wenn keine da ist.
|
||||
|
||||
Ueber einen UDP-Socket, der nur verbunden und nie benutzt wird: das erzeugt keinen
|
||||
Datenverkehr und fragt kein DNS, liefert aber genau die Adresse, die der Kernel fuer
|
||||
die Standardroute waehlen wuerde - also die, ueber die der Bediener tatsaechlich per
|
||||
SSH hereinkommt. Ein Aufruf von "hostname -I" waere die naheliegende Alternative,
|
||||
listet aber ALLE Adressen (inklusive der des Zugangspunkts und IPv6) und braucht einen
|
||||
Unterprozess in einer Schleife, die alle 5 s laeuft.
|
||||
|
||||
Wirft nie: ohne Netz gibt es keine Route, dann liefert der Aufruf "" und die Anzeige
|
||||
sagt das - eine Ausnahme aus der Adressermittlung darf das Panel nicht aufhalten."""
|
||||
import socket
|
||||
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
# Die Adresse wird nie angesprochen; sie waehlt nur die Route aus. Bewusst eine
|
||||
# dokumentierte Testadresse (RFC 5737) statt eines echten Ziels wie 8.8.8.8 -
|
||||
# es soll auch dann nichts Fremdes anklingen, wenn jemand mitliest.
|
||||
sock.connect(("192.0.2.1", 9))
|
||||
return str(sock.getsockname()[0])
|
||||
except OSError:
|
||||
return ""
|
||||
finally:
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
|
||||
|
||||
def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
||||
service_name: str = SERVICE_NAME,
|
||||
imageset_path: Path = IMAGESET_FILE) -> dict:
|
||||
@@ -262,7 +291,13 @@ def build_state(run_dir: Path = RUN_DIR, conf_path: Path = CONF_PATH,
|
||||
imageset, imageset_ok = _read_imageset(imageset_path, conf)
|
||||
|
||||
if not _service_active(service_name):
|
||||
return {"mode": "off", "imageset": imageset, "imageset_ok": imageset_ok}
|
||||
# Die IP-Zeile gibt es NUR hier: ist die Station aus, ist der Pi ein gewoehnlicher
|
||||
# Rechner im Heimnetz, und die einzige Auskunft, die man dann am Panel braucht,
|
||||
# ist die Adresse, unter der man per SSH drankommt. Im Betrieb waere sie sinnlos
|
||||
# (wlan0 traegt dann 192.168.8.1, das steht ohnehin auf jedem Statusbild) und
|
||||
# wuerde den knappen Platz fuer Betriebsauskuenfte wegnehmen.
|
||||
return {"mode": "off", "imageset": imageset, "imageset_ok": imageset_ok,
|
||||
"ip": local_ip()}
|
||||
|
||||
mode = _read_mode_file(run_dir)
|
||||
status = _read_json(run_dir / "status.json")
|
||||
|
||||
@@ -172,7 +172,7 @@ FONT_SEARCH_PATHS = (
|
||||
("/Library/Fonts/Arial.ttf", "/Library/Fonts/Arial Bold.ttf"),
|
||||
)
|
||||
|
||||
MODE_LABELS = {"recovery": "RETTUNG", "migration": "MIGRATION"}
|
||||
MODE_LABELS = {"recovery": "RECOVERY", "migration": "MIGRATION"}
|
||||
# "armed" wird gesondert behandelt (es zieht zusaetzlich eine Beiwerkzeile mit dem
|
||||
# Zielnamen nach sich), deshalb hier nicht gelistet.
|
||||
PHASE_LABELS = {"auto": "AUTO", "listen": "LISTEN"}
|
||||
@@ -186,8 +186,8 @@ PHASE_LABELS = {"auto": "AUTO", "listen": "LISTEN"}
|
||||
# breiteren Schrift hoechstens am Ende beschnitten wird, ohne dass dabei eine Auskunft
|
||||
# verloren geht - der Zustand "freigegeben" ist der einzige mit diesem Label) und der
|
||||
# Zielname als eigene Beiwerkzeile ueber die volle Panelbreite.
|
||||
ARMED_LABEL = "FREIGEGEBEN"
|
||||
ARMED_DETAIL_PREFIX = "Freigabe: "
|
||||
ARMED_LABEL = "ARMED"
|
||||
ARMED_DETAIL_PREFIX = "Armed: "
|
||||
|
||||
# Der Bildsatz-Umschalter in der Fusszeile. Bewusst als Aussage ueber die LAMPE formuliert
|
||||
# und nicht ueber das Verzeichnis: "migrate-wlan" sagt dem Bediener am Geraet nichts,
|
||||
@@ -201,16 +201,16 @@ ARMED_DETAIL_PREFIX = "Freigabe: "
|
||||
# mit 246,0 von 246 px haarscharf; deshalb hat es niemand bemerkt. Breitenbudget siehe
|
||||
# test_imageset_footer_fits_the_panel_with_room_for_a_wider_font.
|
||||
IMAGESET_LABELS = {
|
||||
"ohne-wlan": "ohne WLAN-Daten (Onboarding-AP)",
|
||||
"mit-wlan": "MIT WLAN-Daten (eigenes Netz)",
|
||||
"ohne-wlan": "no Wi-Fi data (onboarding AP)",
|
||||
"mit-wlan": "WITH Wi-Fi data (own network)",
|
||||
}
|
||||
# Fehlt das Verzeichnis des gewaehlten Satzes, ERSETZT die Warnung die Zeile, statt sie
|
||||
# zu verlaengern. Als angehaengtes Suffix (" - SATZ FEHLT", bis 2026-09-15) war sie in
|
||||
# BEIDEN Richtungen und in beiden Schriften ausserhalb der Panelbreite - die einzige
|
||||
# Zeile, die sagt, dass die Station gar nichts ausliefern kann, war damit nie zu sehen.
|
||||
IMAGESET_MISSING_LABELS = {
|
||||
"ohne-wlan": "SATZ FEHLT: ohne WLAN-Daten",
|
||||
"mit-wlan": "SATZ FEHLT: MIT WLAN-Daten",
|
||||
"ohne-wlan": "SET MISSING: no Wi-Fi data",
|
||||
"mit-wlan": "SET MISSING: WITH Wi-Fi data",
|
||||
}
|
||||
|
||||
# Der Abschlussbildschirm: was auf dem Panel stehen bleibt, wenn der Pi heruntergefaehrt
|
||||
@@ -228,10 +228,10 @@ FINAL_LABELS = {
|
||||
# Geraet als "Strom kann getrennt wer..." an - siehe die Fusszeile weiter oben, es
|
||||
# ist derselbe Fehler. Alle festen Beschriftungen haengen jetzt an
|
||||
# test_fixed_labels_fit_the_panel_with_room_for_a_wider_font.
|
||||
"poweroff": ("AUSGESCHALTET", "Strom trennen erlaubt", []),
|
||||
"reboot": ("NEUSTART", "Strom NICHT trennen!",
|
||||
["Der Pi kommt in rund 2 min wieder.",
|
||||
"Solange bleibt dieses Bild stehen."]),
|
||||
"poweroff": ("POWERED OFF", "Safe to unplug", []),
|
||||
"reboot": ("REBOOTING", "Do NOT unplug!",
|
||||
["The Pi is back in about 2 min.",
|
||||
"This screen stays until then."]),
|
||||
}
|
||||
|
||||
# Woraus die Station die Signiervariante abgeleitet hat (products.classify_variant):
|
||||
@@ -293,7 +293,7 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
||||
if label is None:
|
||||
# Unbekannte Spielart - lieber ehrlich nichts behaupten als "ausgeschaltet"
|
||||
# zu sagen, wenn gleich wieder gebootet wird.
|
||||
return [("Faehrt herunter", ROLE_BIG)]
|
||||
return [("Shutting down", ROLE_BIG)]
|
||||
title, hero, details = label
|
||||
return ([(title, ROLE_HEADER), (hero, ROLE_HERO)]
|
||||
+ [(d, ROLE_DETAIL) for d in details])
|
||||
@@ -308,14 +308,24 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
||||
# diesen Modus selbst, server.py kennt ihn nicht. Bewusst keine weiteren Zeilen
|
||||
# (insbesondere keine SSID - die waere aus einer alten Konfigurationsdatei und
|
||||
# gerade nicht aktiv). Genau deshalb ROLE_BIG: die Zeile ist der ganze Inhalt.
|
||||
out.append(("Station aus", ROLE_BIG))
|
||||
out.append(("Station off", ROLE_BIG))
|
||||
# Die Adresse, unter der der Pi gerade erreichbar ist - die einzige Auskunft, die
|
||||
# man bei ausgeschalteter Station am Geraet wirklich braucht (per SSH draufgehen,
|
||||
# ohne den Router zu befragen). display.py legt sie nur in DIESEN Zustand; fehlt
|
||||
# das Feld (aeltere display.py-Fassung, von Hand gebauter Zustand im Test), bleibt
|
||||
# die Zeile weg statt eine Adresse zu erfinden. Ist das Feld da, aber leer, sagt
|
||||
# die Zeile ausdruecklich, dass kein Netz da ist: "keine Zeile" und "kein Netz"
|
||||
# sind zwei verschiedene Auskuenfte, und die zweite erklaert, warum SSH scheitert.
|
||||
ip = state.get("ip")
|
||||
if isinstance(ip, str):
|
||||
out.append((f"IP {ip}" if ip else "no network", ROLE_DETAIL))
|
||||
return out + footer
|
||||
|
||||
if mode not in ("recovery", "migration"):
|
||||
# Unbekannter oder fehlender Modus, z.B. ein frisch aufgesetzter Pi, auf dem noch
|
||||
# nichts geschrieben wurde. Lieber ehrlich "unbekannt" zeigen als einen Modus zu
|
||||
# unterstellen.
|
||||
out.append((f"Status unbekannt ({mode})" if mode else "Status unbekannt", ROLE_BIG))
|
||||
out.append((f"Status unknown ({mode})" if mode else "Status unknown", ROLE_BIG))
|
||||
return out + footer
|
||||
|
||||
out.append((MODE_LABELS[mode], ROLE_HEADER))
|
||||
@@ -409,12 +419,12 @@ def blocks(state: dict) -> list[tuple[str, str]]:
|
||||
autooff_minutes = state.get("autooff_minutes")
|
||||
if isinstance(autooff_minutes, int) and not isinstance(autooff_minutes, bool):
|
||||
if autooff_minutes > 0:
|
||||
out.append(("Auto-Aus in {} min".format(autooff_minutes), ROLE_DETAIL))
|
||||
out.append(("Auto-off in {} min".format(autooff_minutes), ROLE_DETAIL))
|
||||
else:
|
||||
out.append(("Auto-Aus steht an", ROLE_DETAIL))
|
||||
out.append(("Auto-off pending", ROLE_DETAIL))
|
||||
|
||||
served = state.get("served")
|
||||
out.append((f"Ausgeliefert: {served if isinstance(served, int) else 0}", ROLE_DETAIL))
|
||||
out.append((f"Served: {served if isinstance(served, int) else 0}", ROLE_DETAIL))
|
||||
|
||||
recent = state.get("recent")
|
||||
if isinstance(recent, list) and recent:
|
||||
@@ -467,7 +477,7 @@ def _ui_blocks(ui: dict) -> list[tuple[str, str]]:
|
||||
# Es laeuft eine Bedienung, aber die Datei traegt keinen brauchbaren Text
|
||||
# (abgeschnitten, fremde Fassung). Lieber gross sagen, dass bedient wird, als
|
||||
# ein leeres Panel zu zeigen, das nach "haengt" aussieht.
|
||||
out.append(("Bedienung", ROLE_BIG))
|
||||
out.append(("Button menu", ROLE_BIG))
|
||||
return out
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user