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:
+118
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user