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:
Michael Sauer
2026-09-20 00:28:33 +02:00
parent 71a579c0f9
commit 021089a68b
7 changed files with 373 additions and 274 deletions
+35 -35
View File
@@ -139,7 +139,7 @@ class Action:
diesem Moment gar nicht vor Augen hat."""
def __init__(self, label: str, argv=(), kind: str = "cli", confirm: bool = True,
done_text: str | None = None, confirm_title: str = "RUECKFRAGE",
done_text: str | None = None, confirm_title: str = "CONFIRM",
confirm_details=None) -> None:
self.label = label
self.argv = tuple(argv)
@@ -154,16 +154,16 @@ class Action:
def action_shutdown() -> Action:
return Action("Herunterfahren", kind="shutdown", confirm=True,
done_text="Faehrt herunter")
return Action("Shut down", kind="shutdown", confirm=True,
done_text="Shutting down")
def action_migrate_listen() -> Action:
"""Der naheliegende Weg in die Migration: Zugangspunkt an, aber es geht NICHTS raus,
bis der Bediener eine erkannte Lampe freigibt ("arm"). Deshalb steht dieser Eintrag
vor dem Automatikmodus und traegt die uebliche Rueckfrage."""
return Action("Migration: horchen", ("migrate", "on", "--listen"),
done_text="Migration: horcht")
return Action("Migration: listen", ("migrate", "on", "--listen"),
done_text="Migration: listening")
def action_migrate_auto() -> Action:
@@ -179,11 +179,11 @@ def action_migrate_auto() -> Action:
danach nicht mehr mit seiner App spricht. Ein "Wirklich?" fragt nach etwas, das der
Bediener in diesem Moment gar nicht vor Augen hat; deshalb steht die Folge in der
Rueckfrage selbst."""
return Action("Migration: AUTOMATIK", ("migrate", "on"),
done_text="Migration: automatisch",
confirm_title="ACHTUNG",
confirm_details=["Flasht JEDE alte Klyqa-Lampe",
"in Reichweite - auch fremde"])
return Action("Migration: AUTOMATIC", ("migrate", "on"),
done_text="Migration: automatic",
confirm_title="WARNING",
confirm_details=["Flashes EVERY old Klyqa lamp",
"in range - other people's too"])
def action_imageset(current: str) -> Action:
@@ -196,19 +196,19 @@ def action_imageset(current: str) -> Action:
Lampe, die sie erwischt."""
target = "mit-wlan" if current != "mit-wlan" else "ohne-wlan"
if target == "mit-wlan":
return Action("Umschalten: mit WLAN", ("bildsatz", "mit-wlan"),
done_text="Bildsatz: mit WLAN",
confirm_title="ACHTUNG",
confirm_details=["Jede migrierte Lampe bekommt",
"dein WLAN-Passwort"])
return Action("Umschalten: ohne WLAN", ("bildsatz", "ohne-wlan"),
done_text="Bildsatz: ohne WLAN")
return Action("Switch to: with Wi-Fi", ("bildsatz", "mit-wlan"),
done_text="Image set: with Wi-Fi",
confirm_title="WARNING",
confirm_details=["Every migrated lamp receives",
"your Wi-Fi password"])
return Action("Switch to: no Wi-Fi", ("bildsatz", "ohne-wlan"),
done_text="Image set: no Wi-Fi")
def action_cancel() -> Action:
"""Die Haltegeste in Ruhe: Station aus. Bewusst ohne Rueckfrage - sie IST die
Rueckfrage (3 s halten), und sie ist der Notausgang, der ohne Umweg wirken muss."""
return Action("Station aus", ("off",), confirm=False, done_text="Station aus")
return Action("Station off", ("off",), confirm=False, done_text="Station off")
def menu_actions(state: dict) -> list[Action]:
@@ -221,7 +221,7 @@ def menu_actions(state: dict) -> list[Action]:
if not running:
return [
Action("Rettung starten", ("recovery", "on"), done_text="Rettung laeuft"),
Action("Start recovery", ("recovery", "on"), done_text="Recovery running"),
# Reihenfolge ist Absicht: der horchende Modus zuerst, weil er der
# naheliegende und der harmlose ist - wer nur blaettert und irgendwo
# loslaesst, landet nicht im Automatikmodus.
@@ -237,13 +237,13 @@ def menu_actions(state: dict) -> list[Action]:
if mode == "migration" and target:
# Nur wenn last_seen wirklich ein Ziel kennt - ohne erkannte Lampe gibt es
# nichts freizugeben, und "arm" ohne Ziel wuerde das CLI ohnehin abweisen.
items.append(Action("Freigabe: {}".format(target), ("arm", str(target)),
done_text="Freigegeben: {}".format(target)))
items.append(Action("Station aus", ("off",), done_text="Station aus"))
items.append(Action("Arm: {}".format(target), ("arm", str(target)),
done_text="Armed: {}".format(target)))
items.append(Action("Station off", ("off",), done_text="Station off"))
items.append(action_imageset(imageset))
# Veraendert nichts am Betrieb - einzige Auswahl ohne Rueckfrage.
items.append(Action("Anzeige auffrischen", ("display", "test"), confirm=False,
done_text="Anzeige aufgefrischt"))
items.append(Action("Refresh display", ("display", "test"), confirm=False,
done_text="Display refreshed"))
items.append(action_shutdown())
return items
@@ -561,32 +561,32 @@ class ButtonUI:
and self.screen in ("idle", "busy")):
held = now - self._press_t
if self._hold_stage == 1:
return _view("hold", "STATION AUS",
"Herunterfahren in {} s".format(
return _view("hold", "STATION OFF",
"Shutting down in {} s".format(
_countdown(SHUTDOWN_HOLD_S, held)),
["Weiter halten - Loslassen bricht ab"])
["Keep holding - release to cancel"])
if held >= SHORT_PRESS_MAX_S:
return _view("hold", "HALTEN",
"Abbruch in {} s".format(_countdown(CANCEL_HOLD_S, held)),
["Weiter halten - Loslassen bricht ab"])
return _view("hold", "HOLD",
"Cancel in {} s".format(_countdown(CANCEL_HOLD_S, held)),
["Keep holding - release to cancel"])
if self.screen == "menu" and self.items:
selected = self.items[self.index]
rest = self.items[self.index + 1:] + self.items[:self.index]
return _view("menu", "MENUE", "> " + selected.label,
[a.label for a in rest] + ["kurz: weiter, halten: waehlen"])
return _view("menu", "MENU", "> " + selected.label,
[a.label for a in rest] + ["tap: next, hold: select"])
if self.screen == "confirm" and self.pending is not None:
# Die Bedienhinweise stehen IMMER und IMMER ZULETZT - eine Rueckfrage, bei
# der man raten muss, wie man ablehnt, ist keine. Eine eigene Warnung des
# Eintrags (confirm_details) kommt davor, damit sie zuerst gelesen wird.
details = list(self.pending.confirm_details or [])
details += ["halten (1,5 s): ja", "kurz: nein"]
details += ["hold (1.5 s): yes", "tap: no"]
return _view("confirm", self.pending.confirm_title, self.pending.label,
details)
if self.screen == "busy" and self.busy is not None:
return _view("busy", "BITTE WARTEN", self.busy.label, [])
return _view("busy", "PLEASE WAIT", self.busy.label, [])
if self.result is not None:
text, ok = self.result
return _view("result", "FERTIG" if ok else "FEHLER", text, [])
return _view("result", "DONE" if ok else "ERROR", text, [])
return {"view": "idle", "active": False}