142a621066
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>
181 lines
5.8 KiB
Python
181 lines
5.8 KiB
Python
#!/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)
|