Dauerbetrieb: Autostart (main.py) und Watchdog
MicroPython fuehrt main.py nach jedem Reset selbst aus -- nach Stromausfall oder Watchdog-Neustart laeuft die Anzeige damit ohne Notebook wieder an. Bisher gab es die Datei bewusst nicht, weil am Aufbau gearbeitet wurde. **Das Abbruchfenster ist kein Komfort, sondern die Notbremse.** Ohne es waere ein Geraet, dessen Programm beim Start haengt, nur noch durch Neuflashen erreichbar: Die Endlosschleife belegt die REPL sofort nach dem Reset. main.py wartet deshalb erst AUTOSTART_DELAY (3 s), in denen Ctrl-C zurueck an die REPL fuehrt -- dann laeuft auch kein Watchdog. Watchdog (WDT_TIMEOUT = 120 s): startet das Geraet neu, wenn die Poll-Schleife stehenbleibt. Gefuettert wird zu Beginn *jedes* Durchlaufs, auch nach einem Fehler -- "KEIN LINK" ist kein Stillstand, die Schleife laeuft ja weiter. Der Wert muss ueber allem liegen, was regulaer blockieren darf (WLAN 20 s, Montierung 3 s, dazu ein MQTT-Versuch); knapper waere er eine Neustart-Schleife. Ein gestarteter Watchdog laesst sich nicht mehr abschalten. Deshalb legt ihn nur main.py an -- im Handbetrieb ueber die REPL startete das Geraet sonst neu, waehrend man nachdenkt. Weiteres: - Nach einem Watchdog-Neustart meldet main.py das beim Start (machine.reset_cause()), sonst saehe er aus wie ein Stromausfall. - Eine Ausnahme faengt main.py ab und startet nach RESTART_DELAY (10 s) neu, statt in die REPL zu fallen und die Anzeige einzufrieren. Die Pause verhindert eine Neustart-Schleife im Sekundentakt. - Autostart faehrt mit Helligkeitsregelung (with_ldr=True, Nutzerentscheidung). Am Geraet bestaetigt: Autostart nach Reset laeuft durch (Watchdog aktiv, WLAN, MQTT), ein absichtlich herbeigefuehrter Stillstand loest den Watchdog aus und das Geraet kommt von allein wieder hoch -- mit der Meldung "Vorheriger Lauf wurde vom Watchdog beendet". Ctrl-C im Startfenster gibt die REPL frei. Beim Geraetetest aufgefallen: make_watchdog wurde ohne log aufgerufen, im Startprotokoll stand also nichts vom Watchdog. Behoben und durch einen Test festgehalten. 178 Tests gruen (neu: test_main.py). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+180
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests fuer den Autostart (main.py).
|
||||
|
||||
python3 -m unittest -v test_main
|
||||
|
||||
main.py startet beim Import die Schleife -- deshalb wird es hier nicht importiert,
|
||||
sondern die Quelle geladen und in einer eigenen Umgebung ausgefuehrt, in der
|
||||
time.sleep, run_esp32 und machine durch Attrappen ersetzt sind.
|
||||
|
||||
Der wichtigste Test ist das Abbruchfenster: Ohne es waere ein Geraet mit
|
||||
fehlerhaftem main.py nur noch durch Neuflashen erreichbar.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import config
|
||||
|
||||
|
||||
class FakeRunEsp32:
|
||||
def __init__(self, wirft=None):
|
||||
self.aufrufe = []
|
||||
self.watchdogs = 0
|
||||
self.log_beim_watchdog = None
|
||||
self._wirft = wirft
|
||||
|
||||
def make_watchdog(self, timeout=None, log=None):
|
||||
self.watchdogs += 1
|
||||
self.log_beim_watchdog = log
|
||||
return lambda: None
|
||||
|
||||
def main(self, **kwargs):
|
||||
self.aufrufe.append(kwargs)
|
||||
if self._wirft is not None:
|
||||
raise self._wirft
|
||||
|
||||
|
||||
class FakeMachine:
|
||||
WDT_RESET = 3
|
||||
PWRON_RESET = 1
|
||||
|
||||
def __init__(self, ursache=1):
|
||||
self.resets = 0
|
||||
self._ursache = ursache
|
||||
|
||||
def reset(self):
|
||||
self.resets += 1
|
||||
|
||||
def reset_cause(self):
|
||||
return self._ursache
|
||||
|
||||
|
||||
def lade_main(run_esp32, machine=None, sleep_wirft=None):
|
||||
"""main.py in einer kontrollierten Umgebung ausfuehren.
|
||||
|
||||
sleep_wirft: Ausnahme, die time.sleep beim n-ten Aufruf wirft -- damit
|
||||
laesst sich Ctrl-C im Abbruchfenster nachstellen.
|
||||
"""
|
||||
import sys
|
||||
import types
|
||||
|
||||
schlaefe = []
|
||||
|
||||
class FakeTime:
|
||||
@staticmethod
|
||||
def sleep(s):
|
||||
schlaefe.append(s)
|
||||
if sleep_wirft is not None and len(schlaefe) == 1:
|
||||
raise sleep_wirft
|
||||
|
||||
ausgaben = []
|
||||
umgebung = {
|
||||
"__name__": "main",
|
||||
"print": lambda *a: ausgaben.append(" ".join(str(x) for x in a)),
|
||||
}
|
||||
|
||||
gemerkt = {name: sys.modules.get(name)
|
||||
for name in ("time", "run_esp32", "machine")}
|
||||
try:
|
||||
sys.modules["time"] = FakeTime
|
||||
sys.modules["run_esp32"] = run_esp32
|
||||
if machine is not None:
|
||||
sys.modules["machine"] = machine
|
||||
else:
|
||||
sys.modules.pop("machine", None)
|
||||
with open("main.py", encoding="utf-8") as f:
|
||||
quelle = f.read()
|
||||
exec(compile(quelle, "main.py", "exec"), umgebung)
|
||||
finally:
|
||||
for name, modul in gemerkt.items():
|
||||
if modul is None:
|
||||
sys.modules.pop(name, None)
|
||||
else:
|
||||
sys.modules[name] = modul
|
||||
return ausgaben, schlaefe
|
||||
|
||||
|
||||
class TestAbbruchfenster(unittest.TestCase):
|
||||
"""Die Notbremse: Ctrl-C im Startfenster fuehrt zurueck an die REPL."""
|
||||
|
||||
def test_ohne_abbruch_startet_die_schleife(self):
|
||||
r = FakeRunEsp32()
|
||||
ausgaben, schlaefe = lade_main(r)
|
||||
self.assertEqual(len(r.aufrufe), 1)
|
||||
self.assertEqual(schlaefe[0], config.AUTOSTART_DELAY)
|
||||
|
||||
def test_ctrl_c_im_startfenster_bricht_ab(self):
|
||||
"""Ohne das waere ein Geraet mit kaputtem main.py nur noch flashbar."""
|
||||
r = FakeRunEsp32()
|
||||
ausgaben, _ = lade_main(r, sleep_wirft=KeyboardInterrupt())
|
||||
self.assertEqual(r.aufrufe, [])
|
||||
self.assertEqual(r.watchdogs, 0, "Watchdog darf dann nicht laufen")
|
||||
self.assertTrue(any("abgebrochen" in a for a in ausgaben))
|
||||
|
||||
def test_wartezeit_ist_lang_genug_zum_eingreifen(self):
|
||||
self.assertGreaterEqual(config.AUTOSTART_DELAY, 2)
|
||||
|
||||
|
||||
class TestStart(unittest.TestCase):
|
||||
|
||||
def test_watchdog_wird_angelegt_und_durchgereicht(self):
|
||||
r = FakeRunEsp32()
|
||||
lade_main(r)
|
||||
self.assertEqual(r.watchdogs, 1)
|
||||
self.assertIn("feed", r.aufrufe[0])
|
||||
self.assertIsNotNone(r.aufrufe[0]["feed"])
|
||||
|
||||
def test_watchdog_meldet_sich_im_startprotokoll(self):
|
||||
"""Sonst bliebe unsichtbar, ob er ueberhaupt laeuft (am Geraet aufgefallen)."""
|
||||
r = FakeRunEsp32()
|
||||
lade_main(r)
|
||||
self.assertIsNotNone(r.log_beim_watchdog,
|
||||
"make_watchdog wurde ohne log aufgerufen")
|
||||
|
||||
def test_helligkeitsregelung_ist_an(self):
|
||||
"""Nutzerentscheidung 2026-07-27: Autostart faehrt mit LDR."""
|
||||
r = FakeRunEsp32()
|
||||
lade_main(r)
|
||||
self.assertTrue(r.aufrufe[0]["with_ldr"])
|
||||
|
||||
def test_watchdog_neustart_wird_gemeldet(self):
|
||||
"""Sonst sieht ein Watchdog-Neustart aus wie ein Stromausfall."""
|
||||
r = FakeRunEsp32()
|
||||
m = FakeMachine(ursache=FakeMachine.WDT_RESET)
|
||||
ausgaben, _ = lade_main(r, machine=m)
|
||||
self.assertTrue(any("Watchdog" in a for a in ausgaben))
|
||||
|
||||
def test_gewoehnlicher_start_meldet_keinen_watchdog(self):
|
||||
r = FakeRunEsp32()
|
||||
m = FakeMachine(ursache=FakeMachine.PWRON_RESET)
|
||||
ausgaben, _ = lade_main(r, machine=m)
|
||||
self.assertFalse(any("Watchdog beendet" in a for a in ausgaben))
|
||||
|
||||
|
||||
class TestAbsturz(unittest.TestCase):
|
||||
"""Eine Ausnahme darf nicht in der REPL enden -- die Anzeige stuende still."""
|
||||
|
||||
def test_absturz_loest_neustart_aus(self):
|
||||
r = FakeRunEsp32(wirft=RuntimeError("kaputt"))
|
||||
m = FakeMachine()
|
||||
ausgaben, schlaefe = lade_main(r, machine=m)
|
||||
self.assertEqual(m.resets, 1)
|
||||
self.assertIn(config.RESTART_DELAY, schlaefe)
|
||||
self.assertTrue(any("Absturz" in a for a in ausgaben))
|
||||
|
||||
def test_neustartpause_verhindert_dauerschleife(self):
|
||||
"""Ist etwas dauerhaft kaputt, soll nicht im Sekundentakt neu gestartet
|
||||
werden -- die Pause gibt auch Zeit, mit Ctrl-C einzugreifen."""
|
||||
self.assertGreaterEqual(config.RESTART_DELAY, 5)
|
||||
|
||||
def test_ctrl_c_im_betrieb_startet_nicht_neu(self):
|
||||
"""Von Hand beendet heisst: die REPL soll frei werden."""
|
||||
r = FakeRunEsp32(wirft=KeyboardInterrupt())
|
||||
m = FakeMachine()
|
||||
ausgaben, _ = lade_main(r, machine=m)
|
||||
self.assertEqual(m.resets, 0)
|
||||
self.assertTrue(any("beendet" in a for a in ausgaben))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user