Files
grossanzeige/displayloop.py
T
admin 142a621066 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>
2026-07-27 18:41:33 +02:00

163 lines
6.9 KiB
Python

"""Die Poll-Schleife: Koordinaten holen, formatieren, anzeigen.
Herausgeloest aus run_display.py, damit Mac und ESP32-C3 wirklich denselben
Ablauf fahren. Auf dem Mac kommt drumherum die Kommandozeile (run_display.py),
auf dem ESP32 das WLAN-Setup (run_esp32.py) -- die Schleife selbst ist gleich.
Reines Python ohne Abhaengigkeiten ausser den Projektmodulen; argparse und
sys.stderr bleiben bewusst draussen, beides gibt es unter MicroPython nicht.
"""
import time
import config
import coords
import migra
from mount_client import MountError
class LinkError(Exception):
"""Die Netzverbindung fehlt (auf dem ESP32: kein WLAN)."""
def format_coordinates(ra_raw, dec_raw):
"""Rohstrings der Montierung fuer die Anzeige aufbereiten.
Bei **Niedrigpraezision** bekommen die Zeilen eine Beschriftung:
RA:18h36.9
DE:+38°47'
Das geht genau auf, weil die Montierung in diesem Modus ohnehin nur
Zehntelminuten (7 Zeichen) und ganze Bogenminuten (7 Zeichen) liefert -- in
zehn Zellen bleiben also drei fuer "RA:"/"DE:" uebrig. Die Zeilen sind
linksbuendig gesetzt (config.CENTER_LINES), damit beide Beschriftungen
buendig untereinander beginnen.
Bei **Hochpraezision** entfaellt die Beschriftung: Dort braucht die
Deklination mit +38°47'01" alle zehn Zeichen.
"""
if config.HIGH_PRECISION:
return coords.format_ra(ra_raw), coords.format_dec(dec_raw)
breite_ra = config.CHARS_PER_LINE - len(config.LABEL_RA)
breite_dec = config.CHARS_PER_LINE - len(config.LABEL_DEC)
return (config.LABEL_RA + coords.format_ra(ra_raw, width=breite_ra),
config.LABEL_DEC + coords.format_dec(dec_raw, width=breite_dec))
def poll_loop(display, client, interval=None, once=False,
read_ldr=None, dimmer=None, ensure_link=None, log=None,
bridge=None, feed=None):
"""Zyklisch die Montierung abfragen und die Anzeige nachfuehren.
display offenes Display-Objekt
client MountClient (wird bei Bedarf selbst verbunden)
interval Sekunden zwischen zwei Abfragen (Vorgabe config.POLL_INTERVAL)
once nur ein Durchlauf, dann zurueck
read_ldr optional: Funktion ohne Argumente, liefert einen LDR-Rohwert
in der Zaehlrichtung "hoch = hell" (siehe brightness.raw_to_ldr)
dimmer optional: BrightnessController; noetig, wenn read_ldr gesetzt ist
ensure_link optional: Funktion, die vor jeder Abfrage die Netzverbindung
sicherstellt (auf dem ESP32 das WLAN)
log optional: Ausgabefunktion, Vorgabe print; None-freies Logging
erlaubt es, auf dem ESP32 still zu laufen (log=lambda *a: None)
bridge optional: mqtt.MqttBridge -- empfaengt Einstellungen und
veroeffentlicht den Zustand. Faellt der Broker aus, laeuft die
Anzeige unveraendert weiter (mqtt.py faengt seine Fehler selbst).
feed optional: Funktion ohne Argumente, die den Watchdog fuettert.
Wird zu Beginn **jedes** Durchlaufs gerufen -- auch nach einem
Fehler, denn eine gemeldete Stoerung ist kein Stillstand: Die
Schleife laeuft ja weiter und zeigt "KEIN LINK". Der Watchdog
soll nur greifen, wenn gar nichts mehr geht.
Laeuft bis KeyboardInterrupt oder -- bei once=True -- genau einmal. Fehler
von Anzeige und Montierung werden abgefangen: die Schleife meldet sie und
versucht es beim naechsten Durchlauf erneut, statt abzustuerzen.
"""
if interval is None:
interval = config.POLL_INTERVAL
if log is None:
log = print
last_shown = None
while True:
# Ganz am Anfang und ausserhalb des try: Der Watchdog misst, ob die
# Schleife noch laeuft -- nicht, ob sie fehlerfrei laeuft.
if feed is not None:
feed()
try:
# Zuerst MQTT abholen: So wirkt eine gerade gesetzte Helligkeit noch
# in diesem Durchlauf, statt eine Runde hinterherzuhinken.
if bridge is not None:
bridge.check()
if ensure_link is not None:
ensure_link()
if not client.connected:
client.connect()
# Helligkeit nachfuehren -- nur senden, wenn sich die Stufe aendert.
if read_ldr is not None and dimmer is not None:
raw = read_ldr()
level, percent, changed = dimmer.update(raw)
if changed:
display.set_brightness(percent)
log("LDR %d -> %s = %d %%"
% (raw, "HELL" if level else "DUNKEL", percent))
if bridge is not None:
bridge.publish("ldr", raw)
bridge.publish("helligkeit", percent)
ra_raw, dec_raw = client.get_coordinates()
ra_line, dec_line = format_coordinates(ra_raw, dec_raw)
if (ra_line, dec_line) != last_shown:
# Bei jeder Aenderung beide Zeilen neu zeichnen. Kein Vollbild-
# Loeschen (kein Flimmern); die umgekehrte Zeilenreihenfolge in
# show_lines schuetzt die Home-Zelle vor dem Gradzeichen-
# Zeichensatzwechsel, auch bei staendiger Aktualisierung.
first = last_shown is None or last_shown in (config.NOLINK_LINES,
config.NOWIFI_LINES)
display.show_lines(ra_line, dec_line, clear=first)
last_shown = (ra_line, dec_line)
log("RA %-11s DEC %-12s -> %s | %s"
% (ra_raw, dec_raw, ra_line, dec_line))
if bridge is not None:
bridge.publish("ra", ra_line)
bridge.publish("dec", dec_line)
if bridge is not None:
bridge.publish("link", 1)
except LinkError as e:
# Kein Netz (WLAN): die Montierung gar nicht erst versuchen.
log("Netz: %s" % e)
client.close()
last_shown = _show_notice(display, config.NOWIFI_LINES, last_shown, log)
except migra.MigraError as e:
# Anzeige-/Antwortfehler (z.B. gestoerte Antwort): nicht abstuerzen,
# nur melden und beim naechsten Durchlauf neu zeichnen.
log("Anzeige: %s" % e)
last_shown = None
except (MountError, ValueError) as e:
log("Montierung: %s" % e)
client.close()
last_shown = _show_notice(display, config.NOLINK_LINES, last_shown, log)
if bridge is not None:
bridge.publish("link", 0)
if once:
return last_shown
time.sleep(interval)
def _show_notice(display, lines, last_shown, log):
"""Hinweiszeilen anzeigen, wenn sie nicht schon stehen."""
if lines == last_shown:
return last_shown
try:
display.show_lines(lines[0], lines[1], clear=True)
except migra.MigraError as e:
log("Anzeige: %s" % e)
return None
return lines