feat(station): Automatikmodus als eigener Menueeintrag am Taster
Das Tastermenue bot bisher nur "Migration starten" an, fest verdrahtet
auf die Horchphase; den Automatikmodus gab es nur ueber die
Kommandozeile - also ausgerechnet dort nicht, wo man ihn braucht, denn
"on" macht wlan0 zum Zugangspunkt und reisst jede SSH-Sitzung ab.
Jetzt zwei getrennte, namentlich unterscheidbare Eintraege:
"Migration: horchen" (--listen) zuerst, danach "Migration: AUTOMATIK"
(ohne --listen). Der Automatikeintrag bekommt eine andere, deutlichere
Rueckfrage ("ACHTUNG" statt "RUECKFRAGE", plus zwei Warnzeilen), weil
der Migrations-Zugangspunkt ein oeffentlich bekanntes Passwort hat und
im Automatikmodus jede alte Klyqa-Lampe in Funkreichweite ungefragt
migriert wird, auch fremde. Die Bedienhinweise stehen weiterhin und
zuletzt.
This commit is contained in:
@@ -43,11 +43,14 @@ Countdown bis zum Herunterfahren, den Loslassen jederzeit abbricht. Das Menü sc
|
|||||||
Zustand ab (Station aus / Rettung läuft / Migration läuft); jede verändernde Auswahl
|
Zustand ab (Station aus / Rettung läuft / Migration läuft); jede verändernde Auswahl
|
||||||
bekommt eine Rückfrage.
|
bekommt eine Rückfrage.
|
||||||
|
|
||||||
**„Migration starten" startet über das Menü immer in der Horchphase** (`--listen`), nie
|
**Die beiden Migrationsarten sind getrennte Einträge.** „Migration: horchen"
|
||||||
automatisch: der Migrations-Zugangspunkt hat ein öffentlich bekanntes Passwort, und ein
|
(`migrate on --listen`) steht zuerst und ist der naheliegende: der Zugangspunkt steht,
|
||||||
Tastendruck, der ihn im Automatikmodus hochfährt, würde jede alte Klyqa-Lampe in
|
aber es geht nichts raus, bis du eine erkannte Lampe freigibst. „Migration: AUTOMATIK"
|
||||||
Funkreichweite ungefragt migrieren, auch fremde. Wer den Automatikmodus will, nimmt die
|
(`migrate on`) folgt danach und bekommt eine **andere, deutlichere Rückfrage** — der
|
||||||
Kommandozeile.
|
Migrations-Zugangspunkt hat ein öffentlich bekanntes Passwort (es steht in der alten
|
||||||
|
Klyqa-Firmware), und im Automatikmodus wird jede alte Klyqa-Lampe in Funkreichweite
|
||||||
|
ungefragt migriert, auch fremde. Die Rückfrage benennt genau das, statt nur „Wirklich?"
|
||||||
|
zu fragen.
|
||||||
|
|
||||||
Der Taster ist nirgends Voraussetzung: fehlt `gpiozero` oder der Taster, protokolliert der
|
Der Taster ist nirgends Voraussetzung: fehlt `gpiozero` oder der Taster, protokolliert der
|
||||||
Dienst eine Zeile und beendet sich ohne Fehler. Er fasst den SPI-Bus **nicht** an (zwei
|
Dienst eine Zeile und beendet sich ohne Fehler. Er fasst den SPI-Bus **nicht** an (zwei
|
||||||
|
|||||||
@@ -32,12 +32,17 @@ ninlight-station), nie ueber nachgebaute Logik: nur das CLI kennt die Profilumsc
|
|||||||
die Nachpruefungen ("es darf nie mehr als ein Stationsprofil aktiv sein") und den
|
die Nachpruefungen ("es darf nie mehr als ein Stationsprofil aktiv sein") und den
|
||||||
Zeitschalter.
|
Zeitschalter.
|
||||||
|
|
||||||
SICHERHEITSREGEL: "Migration starten" startet ueber das Menue IMMER in der Horchphase
|
SICHERHEITSREGEL: die beiden Migrationseintraege sind GETRENNT und namentlich
|
||||||
("migrate on --listen"), nie automatisch. Der Migrations-Zugangspunkt hat ein oeffentlich
|
unterscheidbar. "Migration: horchen" ruft "migrate on --listen" - der Zugangspunkt steht,
|
||||||
bekanntes Passwort (es steht in der alten Klyqa-Firmware); ein Tastendruck, der ihn im
|
aber es geht nichts raus, bis der Bediener eine erkannte Lampe freigibt; das ist der
|
||||||
Automatikmodus hochfaehrt, wuerde jede alte Klyqa-Lampe in Funkreichweite ungefragt
|
erste und naheliegende Eintrag. "Migration: AUTOMATIK" ruft "migrate on" ohne "--listen"
|
||||||
migrieren, auch fremde. Die bequeme Geste muss die sichere sein - wer den Automatikmodus
|
und bekommt eine ANDERE, deutlichere Rueckfrage (siehe action_migrate_auto()): der
|
||||||
will, nimmt die Kommandozeile, dort ist die Absicht eindeutig.
|
Migrations-Zugangspunkt hat ein oeffentlich bekanntes Passwort (es steht in der alten
|
||||||
|
Klyqa-Firmware), und im Automatikmodus wird jede alte Klyqa-Lampe in Funkreichweite
|
||||||
|
ungefragt migriert, auch fremde. Die bequeme Geste bleibt damit die sichere, ohne dass
|
||||||
|
der Automatikmodus - fuer eine ganze Kiste eigener Lampen am Stueck - nur noch ueber die
|
||||||
|
Kommandozeile erreichbar waere, die im Zugangspunktbetrieb gerade nicht zur Verfuegung
|
||||||
|
steht.
|
||||||
|
|
||||||
Der Taster ist NIRGENDS Voraussetzung: fehlt gpiozero, fehlt der Taster oder ist GPIO23
|
Der Taster ist NIRGENDS Voraussetzung: fehlt gpiozero, fehlt der Taster oder ist GPIO23
|
||||||
belegt, schreibt main() GENAU EINE Protokollzeile und beendet sich mit sys.exit(0) -
|
belegt, schreibt main() GENAU EINE Protokollzeile und beendet sich mit sys.exit(0) -
|
||||||
@@ -115,15 +120,26 @@ class Action:
|
|||||||
"""Was ein Menueeintrag bzw. die Haltegeste ausloest.
|
"""Was ein Menueeintrag bzw. die Haltegeste ausloest.
|
||||||
|
|
||||||
`confirm` = braucht eine Rueckfrage. Zusage: JEDE veraendernde Auswahl bekommt eine
|
`confirm` = braucht eine Rueckfrage. Zusage: JEDE veraendernde Auswahl bekommt eine
|
||||||
("Anzeige auffrischen" veraendert nichts und braucht keine)."""
|
("Anzeige auffrischen" veraendert nichts und braucht keine).
|
||||||
|
|
||||||
|
`confirm_title`/`confirm_details` machen die Rueckfrage bei Bedarf DEUTLICHER als die
|
||||||
|
uebliche. Die Vorgabe ("RUECKFRAGE" + die zwei Bedienhinweise) reicht fuer eine
|
||||||
|
Auswahl, deren Tragweite im Eintragsnamen steht ("Station aus", "Herunterfahren").
|
||||||
|
Sie reicht NICHT fuer den Automatikmodus: was dort passiert - jede alte Klyqa-Lampe
|
||||||
|
in Funkreichweite wird ungefragt migriert, auch fremde - steht in keinem
|
||||||
|
Eintragsnamen, und ein blosses "Wirklich?" fragt nach etwas, das der Bediener in
|
||||||
|
diesem Moment gar nicht vor Augen hat."""
|
||||||
|
|
||||||
def __init__(self, label: str, argv=(), kind: str = "cli", confirm: bool = True,
|
def __init__(self, label: str, argv=(), kind: str = "cli", confirm: bool = True,
|
||||||
done_text: str | None = None) -> None:
|
done_text: str | None = None, confirm_title: str = "RUECKFRAGE",
|
||||||
|
confirm_details=None) -> None:
|
||||||
self.label = label
|
self.label = label
|
||||||
self.argv = tuple(argv)
|
self.argv = tuple(argv)
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.confirm = confirm
|
self.confirm = confirm
|
||||||
self.done_text = done_text or label
|
self.done_text = done_text or label
|
||||||
|
self.confirm_title = confirm_title
|
||||||
|
self.confirm_details = list(confirm_details) if confirm_details else None
|
||||||
|
|
||||||
def __repr__(self) -> str: # nur fuer Testausgaben
|
def __repr__(self) -> str: # nur fuer Testausgaben
|
||||||
return "Action({!r}, {!r}, kind={!r})".format(self.label, self.argv, self.kind)
|
return "Action({!r}, {!r}, kind={!r})".format(self.label, self.argv, self.kind)
|
||||||
@@ -134,6 +150,34 @@ def action_shutdown() -> Action:
|
|||||||
done_text="Faehrt herunter")
|
done_text="Faehrt herunter")
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
def action_migrate_auto() -> Action:
|
||||||
|
"""Der Automatikmodus - fuer die eigene Kiste Lampen am Stueck, ohne jede einzeln
|
||||||
|
freizugeben. Er steht hier, weil das Verabreden ueber die Kommandozeile genau dann
|
||||||
|
nicht geht, wenn man ihn braucht: "on" macht wlan0 zum Zugangspunkt und reisst jede
|
||||||
|
SSH-Sitzung ueber WLAN ab.
|
||||||
|
|
||||||
|
Seine Rueckfrage ist ABSICHTLICH eine andere als die der uebrigen Eintraege. Der
|
||||||
|
Migrations-Zugangspunkt hat ein oeffentlich bekanntes Passwort (es steht in der alten
|
||||||
|
Klyqa-Firmware), und im Automatikmodus wird jede alte Klyqa-Lampe in Funkreichweite
|
||||||
|
ungefragt migriert - auch die des Nachbarn, der davon nichts weiss und dessen Lampe
|
||||||
|
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"])
|
||||||
|
|
||||||
|
|
||||||
def action_cancel() -> Action:
|
def action_cancel() -> Action:
|
||||||
"""Die Haltegeste in Ruhe: Station aus. Bewusst ohne Rueckfrage - sie IST die
|
"""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."""
|
Rueckfrage (3 s halten), und sie ist der Notausgang, der ohne Umweg wirken muss."""
|
||||||
@@ -150,10 +194,11 @@ def menu_actions(state: dict) -> list[Action]:
|
|||||||
if not running:
|
if not running:
|
||||||
return [
|
return [
|
||||||
Action("Rettung starten", ("recovery", "on"), done_text="Rettung laeuft"),
|
Action("Rettung starten", ("recovery", "on"), done_text="Rettung laeuft"),
|
||||||
# IMMER --listen (siehe Modul-Docstring, Sicherheitsregel). Wer den
|
# Reihenfolge ist Absicht: der horchende Modus zuerst, weil er der
|
||||||
# Automatikmodus will, nimmt die Kommandozeile.
|
# naheliegende und der harmlose ist - wer nur blaettert und irgendwo
|
||||||
Action("Migration starten", ("migrate", "on", "--listen"),
|
# loslaesst, landet nicht im Automatikmodus.
|
||||||
done_text="Migration: horcht"),
|
action_migrate_listen(),
|
||||||
|
action_migrate_auto(),
|
||||||
action_shutdown(),
|
action_shutdown(),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -486,8 +531,13 @@ class ButtonUI:
|
|||||||
return _view("menu", "MENUE", "> " + selected.label,
|
return _view("menu", "MENUE", "> " + selected.label,
|
||||||
[a.label for a in rest] + ["kurz: weiter, halten: waehlen"])
|
[a.label for a in rest] + ["kurz: weiter, halten: waehlen"])
|
||||||
if self.screen == "confirm" and self.pending is not None:
|
if self.screen == "confirm" and self.pending is not None:
|
||||||
return _view("confirm", "RUECKFRAGE", self.pending.label,
|
# Die Bedienhinweise stehen IMMER und IMMER ZULETZT - eine Rueckfrage, bei
|
||||||
["halten (1,5 s): ja", "kurz: nein"])
|
# 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"]
|
||||||
|
return _view("confirm", self.pending.confirm_title, self.pending.label,
|
||||||
|
details)
|
||||||
if self.screen == "busy" and self.busy is not None:
|
if self.screen == "busy" and self.busy is not None:
|
||||||
return _view("busy", "BITTE WARTEN", self.busy.label, [])
|
return _view("busy", "BITTE WARTEN", self.busy.label, [])
|
||||||
if self.result is not None:
|
if self.result is not None:
|
||||||
|
|||||||
@@ -163,7 +163,15 @@ def _labels(state):
|
|||||||
|
|
||||||
|
|
||||||
def test_menu_off_offers_starting_and_shutdown():
|
def test_menu_off_offers_starting_and_shutdown():
|
||||||
assert _labels(STATE_OFF) == ["Rettung starten", "Migration starten", "Herunterfahren"]
|
assert _labels(STATE_OFF) == ["Rettung starten", "Migration: horchen",
|
||||||
|
"Migration: AUTOMATIK", "Herunterfahren"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_listening_migration_comes_before_the_automatic_one():
|
||||||
|
"""Der horchende Modus ist der erste und naheliegende Eintrag - wer nur blaettert und
|
||||||
|
irgendwo loslaesst, soll nicht im Automatikmodus landen."""
|
||||||
|
labels = _labels(STATE_OFF)
|
||||||
|
assert labels.index("Migration: horchen") < labels.index("Migration: AUTOMATIK")
|
||||||
|
|
||||||
|
|
||||||
def test_menu_in_recovery_offers_stopping_refreshing_and_shutdown():
|
def test_menu_in_recovery_offers_stopping_refreshing_and_shutdown():
|
||||||
@@ -219,11 +227,11 @@ def test_a_short_press_opens_the_menu():
|
|||||||
def test_a_short_press_steps_to_the_next_entry():
|
def test_a_short_press_steps_to_the_next_entry():
|
||||||
rec = _drive([_short(1.0), _short(2.0)], 4.0)
|
rec = _drive([_short(1.0), _short(2.0)], 4.0)
|
||||||
assert rec.argvs() == []
|
assert rec.argvs() == []
|
||||||
assert rec.views()[-1][1]["hero"] == "> Migration starten"
|
assert rec.views()[-1][1]["hero"] == "> Migration: horchen"
|
||||||
|
|
||||||
|
|
||||||
def test_a_short_press_wraps_around_the_last_entry():
|
def test_a_short_press_wraps_around_the_last_entry():
|
||||||
rec = _drive([_short(1.0), _short(2.0), _short(3.0), _short(4.0)], 6.0)
|
rec = _drive([_short(1.0), _short(2.0), _short(3.0), _short(4.0), _short(5.0)], 7.0)
|
||||||
assert rec.views()[-1][1]["hero"] == "> Rettung starten"
|
assert rec.views()[-1][1]["hero"] == "> Rettung starten"
|
||||||
|
|
||||||
|
|
||||||
@@ -254,15 +262,41 @@ def test_the_refresh_entry_runs_without_a_confirmation():
|
|||||||
assert "confirm" not in rec.kinds()
|
assert "confirm" not in rec.kinds()
|
||||||
|
|
||||||
|
|
||||||
def test_starting_the_migration_always_listens():
|
def test_the_listening_entry_calls_the_cli_with_listen():
|
||||||
"""Sicherheitsregel, nicht verhandelbar: der Migrations-Zugangspunkt hat ein
|
"""Der zweite Eintrag ("Migration: horchen") ruft das CLI MIT --listen: der
|
||||||
oeffentlich bekanntes Passwort - ein Tastendruck, der ihn im Automatikmodus
|
Zugangspunkt steht, es geht aber nichts raus, bis jemand eine erkannte Lampe
|
||||||
hochfaehrt, wuerde jede alte Klyqa-Lampe in Funkreichweite ungefragt migrieren, auch
|
freigibt."""
|
||||||
fremde."""
|
|
||||||
rec = _drive([_short(1.0), _short(2.0), (3.0, 5.0), (6.0, 8.0)], 10.0)
|
rec = _drive([_short(1.0), _short(2.0), (3.0, 5.0), (6.0, 8.0)], 10.0)
|
||||||
assert rec.argvs() == [["/usr/local/bin/ninlight-station", "migrate", "on",
|
assert rec.argvs() == [["/usr/local/bin/ninlight-station", "migrate", "on",
|
||||||
"--listen"]]
|
"--listen"]]
|
||||||
assert "--listen" in rec.argvs()[0]
|
|
||||||
|
|
||||||
|
def test_the_automatic_entry_calls_the_cli_without_listen():
|
||||||
|
"""Der dritte Eintrag ("Migration: AUTOMATIK") ruft dasselbe CLI OHNE --listen - das
|
||||||
|
ist der ganze Unterschied zwischen den beiden Eintraegen, und er entscheidet, ob
|
||||||
|
fremde Lampen ungefragt migriert werden."""
|
||||||
|
rec = _drive([_short(1.0), _short(2.0), _short(3.0), (4.0, 6.0), (7.0, 9.0)], 11.0)
|
||||||
|
assert rec.argvs() == [["/usr/local/bin/ninlight-station", "migrate", "on"]]
|
||||||
|
assert "--listen" not in rec.argvs()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_automatic_entry_asks_a_louder_question_than_the_others():
|
||||||
|
"""Zusage: die Rueckfrage des Automatikmodus benennt die Folge, statt nur "Wirklich?"
|
||||||
|
zu fragen - und sie unterscheidet sich sichtbar von der der uebrigen Eintraege."""
|
||||||
|
rec = _drive([_short(1.0), _short(2.0), _short(3.0), (4.0, 6.0)], 8.0)
|
||||||
|
_t, view = rec.first_view("confirm")
|
||||||
|
assert view["title"] == "ACHTUNG"
|
||||||
|
assert view["hero"] == "Migration: AUTOMATIK"
|
||||||
|
text = " ".join(view["details"]).lower()
|
||||||
|
assert "jede" in text and "fremde" in text
|
||||||
|
# Die Bedienhinweise bleiben trotzdem stehen, und zwar zuletzt.
|
||||||
|
assert view["details"][-2:] == ["halten (1,5 s): ja", "kurz: nein"]
|
||||||
|
|
||||||
|
# Gegenprobe am horchenden Eintrag: uebliche Rueckfrage, keine Warnung.
|
||||||
|
rec = _drive([_short(1.0), _short(2.0), (3.0, 5.0)], 7.0)
|
||||||
|
_t, plain = rec.first_view("confirm")
|
||||||
|
assert plain["title"] == "RUECKFRAGE"
|
||||||
|
assert plain["details"] == ["halten (1,5 s): ja", "kurz: nein"]
|
||||||
|
|
||||||
|
|
||||||
def test_the_cancel_fires_at_three_seconds_while_the_button_is_still_held():
|
def test_the_cancel_fires_at_three_seconds_while_the_button_is_still_held():
|
||||||
|
|||||||
Reference in New Issue
Block a user