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:
2026-07-27 18:41:33 +02:00
parent 433c53aabd
commit 142a621066
8 changed files with 489 additions and 7 deletions
+118
View File
@@ -80,6 +80,20 @@ class FakeADC:
return self.value
class FakeWDT:
"""machine.WDT-Attrappe: merkt sich Timeout und feed-Aufrufe."""
last = None
def __init__(self, timeout=None):
self.timeout = timeout
self.feeds = 0
FakeWDT.last = self
def feed(self):
self.feeds += 1
class FakeWLAN:
"""network.WLAN-Attrappe. connect() gelingt, wenn succeed gesetzt ist."""
@@ -120,6 +134,9 @@ def _install_machine():
m.UART = FakeUART
m.Pin = FakePin
m.ADC = FakeADC
m.WDT = FakeWDT
m.reset_cause = lambda: 1
m.PWRON_RESET, m.WDT_RESET, m.SOFT_RESET, m.DEEPSLEEP_RESET = 1, 3, 5, 4
sys.modules["machine"] = m
return m
@@ -343,6 +360,106 @@ class TestLdrKennlinie(unittest.TestCase):
self.assertEqual(c.update(dunkel)[0], brightness.DUNKEL)
class TestWatchdog(unittest.TestCase):
"""Dauerbetrieb: Der Watchdog holt das Geraet aus einem Stillstand zurueck."""
def setUp(self):
_install_machine()
import transport
importlib.reload(transport)
_remove("run_esp32")
import run_esp32
self.run_esp32 = run_esp32
def tearDown(self):
_remove("machine", "run_esp32")
import transport
importlib.reload(transport)
def test_timeout_kommt_aus_config_und_ist_in_millisekunden(self):
import config
self.run_esp32.make_watchdog()
self.assertEqual(FakeWDT.last.timeout, int(config.WDT_TIMEOUT * 1000))
def test_gelieferte_funktion_fuettert(self):
feed = self.run_esp32.make_watchdog()
feed()
feed()
self.assertEqual(FakeWDT.last.feeds, 2)
def test_timeout_liegt_ueber_den_regulaeren_wartezeiten(self):
"""Sonst startet das Geraet mitten im normalen Betrieb neu.
Der WLAN-Aufbau darf allein WIFI_TIMEOUT Sekunden dauern; kommt die
Montierung dazu, waere ein knapper Watchdog eine Neustart-Schleife.
"""
import config
self.assertGreater(config.WDT_TIMEOUT,
config.WIFI_TIMEOUT + config.MOUNT_TIMEOUT + 10)
def test_ohne_machine_gibt_es_keinen_watchdog(self):
"""Auf dem Mac laeuft dieselbe Schleife -- ohne Watchdog, ohne Fehler."""
_remove("machine", "run_esp32")
import run_esp32
meldungen = []
self.assertIsNone(run_esp32.make_watchdog(log=meldungen.append))
self.assertTrue(any("WDT" in m for m in meldungen))
class TestWatchdogInDerSchleife(unittest.TestCase):
"""poll_loop muss den Watchdog jede Runde fuettern -- auch bei Stoerung."""
def _lauf(self, client, once=True):
import displayloop
from display import Display
from transport import FakeTransport
gefuettert = []
d = Display(FakeTransport(echo=False), response=True)
displayloop.poll_loop(d, client, once=once,
feed=lambda: gefuettert.append(1),
log=lambda *a: None)
return gefuettert
def test_normaler_durchlauf_fuettert(self):
class Client:
connected = True
def connect(self):
pass
def close(self):
pass
def get_coordinates(self):
return "18:36:56", "+38*47:01"
self.assertEqual(len(self._lauf(Client())), 1)
def test_auch_bei_ausgefallener_montierung_wird_gefuettert(self):
"""Der wichtige Fall: "KEIN LINK" ist kein Stillstand.
Die Schleife laeuft weiter und zeigt den Hinweis -- der Watchdog darf
deshalb nicht zuschlagen, sonst startet das Geraet neu, solange die
Montierung aus ist.
"""
from mount_client import MountError
class Client:
connected = False
def connect(self):
raise MountError("keine Verbindung")
def close(self):
pass
def get_coordinates(self):
raise MountError("keine Verbindung")
self.assertEqual(len(self._lauf(Client())), 1)
class TestLdrLeser(unittest.TestCase):
"""run_esp32.make_ldr_reader: ADC lesen, auf 12 bit bringen, spiegeln."""
@@ -441,6 +558,7 @@ DEPLOYED = (
"ticks.py", "migra.py", "config.py", "settings.py", "transport.py",
"display.py", "coords.py", "lx200.py", "mount_client.py", "brightness.py",
"displayloop.py", "wifi.py", "mqtt.py", "run_esp32.py", "probe.py",
"main.py",
)