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>
201 lines
7.7 KiB
Python
201 lines
7.7 KiB
Python
"""Hauptprogramm auf dem XIAO ESP32-C3 (MicroPython).
|
|
|
|
Das Gegenstueck zu run_display.py: dort die Kommandozeile auf dem Mac, hier das
|
|
WLAN-Setup auf dem Geraet. Die Poll-Schleife selbst ist dieselbe (displayloop).
|
|
|
|
Am REPL starten:
|
|
|
|
import run_esp32
|
|
run_esp32.selftest(host="192.168.178.26") # Bring-up pruefen
|
|
run_esp32.main(host="192.168.178.26", fake_display=True) # gegen den Mock
|
|
run_esp32.main() # echte Montierung, echte Anzeige
|
|
|
|
Absichtlich *nicht* main.py genannt: MicroPython startet eine Datei dieses
|
|
Namens nach jedem Reset von selbst. Solange wir am Aufbau arbeiten, ist der
|
|
manuelle Start angenehmer -- der Autostart ist spaeter eine main.py mit den
|
|
zwei Zeilen von oben.
|
|
|
|
Verdrahtung (aus dem Schaltplan, siehe Hardware/KiCad/Grossanzeige):
|
|
|
|
XIAO Pad 7 (D6/GPIO21) --> MAX3232 T2IN, T2OUT --> Anzeige RxD (Sub-D 2)
|
|
XIAO Pad 8 (D7/GPIO20) <-- MAX3232 R2OUT, R2IN <-- Anzeige TxD (Sub-D 3)
|
|
XIAO Pad 1 (D0/GPIO2) <-- LDR-Teiler (3V3 --[2k]-- A0 --[LDR]-- GND)
|
|
MAX3232 VCC = 3V3 vom XIAO, GND gemeinsam
|
|
"""
|
|
|
|
import config
|
|
import displayloop
|
|
import wifi
|
|
from brightness import BrightnessController, raw_to_ldr
|
|
from display import open_display
|
|
from mount_client import MountClient
|
|
|
|
try: # MicroPython
|
|
from machine import ADC, Pin
|
|
except ImportError:
|
|
ADC = Pin = None
|
|
|
|
|
|
def make_ldr_reader():
|
|
"""Funktion liefern, die den LDR als Wert in Zaehlrichtung "hoch = hell" gibt.
|
|
|
|
Gibt None zurueck, wenn kein ADC verfuegbar ist -- dann laeuft die Anzeige
|
|
einfach mit fester Helligkeit weiter.
|
|
"""
|
|
if ADC is None:
|
|
return None
|
|
adc = ADC(Pin(config.LDR_PIN))
|
|
# Voller Eingangsbereich bis ~3,3 V; ohne Daempfung endet der ADC bei ~1 V.
|
|
try:
|
|
adc.atten(ADC.ATTN_11DB)
|
|
except AttributeError:
|
|
pass
|
|
|
|
def read():
|
|
# read_u16() gibt es auf allen Ports; auf 12 bit zurueckskalieren, weil
|
|
# die Schwellen in brightness.py aus dem 12-bit-ADC des MSP430 stammen.
|
|
raw = adc.read_u16() * config.LDR_MAX // 65535
|
|
return raw_to_ldr(raw)
|
|
|
|
return read
|
|
|
|
|
|
def selftest(host=None, port=None, log=print):
|
|
"""Bring-up in einem Rutsch pruefen, ohne die Dauerschleife zu starten.
|
|
|
|
WLAN aufbauen, IP zeigen, einmal die Montierung (oder den Mock auf dem Mac)
|
|
abfragen und ein Anzeige-Telegramm bauen -- letzteres nur als Hex, ohne
|
|
serielle Hardware. Damit laesst sich Stufe fuer Stufe eingrenzen, wo es
|
|
klemmt, bevor Anzeige und Pegelwandler ueberhaupt dranhaengen.
|
|
|
|
import run_esp32
|
|
run_esp32.selftest(host="192.168.178.26")
|
|
"""
|
|
log("1) WLAN ...")
|
|
cfg = wifi.connect(log=log)
|
|
log(" IP %s, Gateway %s" % (cfg[0], cfg[2]))
|
|
|
|
host = config.MOUNT_HOST if host is None else host
|
|
port = config.MOUNT_PORT if port is None else port
|
|
log("2) Montierung %s:%d ..." % (host, port))
|
|
client = MountClient(host, port, timeout=config.MOUNT_TIMEOUT)
|
|
try:
|
|
client.connect()
|
|
ra_raw, dec_raw = client.get_coordinates()
|
|
log(" RA %s DEC %s" % (ra_raw, dec_raw))
|
|
finally:
|
|
client.close()
|
|
|
|
log("3) Anzeige-Telegramm (nur Hex, keine Hardware) ...")
|
|
d = open_display(fake=True)
|
|
try:
|
|
d.show_lines(*displayloop.format_coordinates(ra_raw, dec_raw), clear=True)
|
|
finally:
|
|
d.close()
|
|
log("Selbsttest bestanden.")
|
|
|
|
|
|
def make_watchdog(timeout=None, log=None):
|
|
"""Watchdog starten und seine feed-Funktion liefern.
|
|
|
|
Liefert None, wenn es keinen gibt (CPython). **Ein gestarteter Watchdog
|
|
laesst sich nicht mehr abschalten** -- deshalb ruft nur main.py das hier
|
|
auf, nicht der Handbetrieb ueber die REPL: Sonst startet das Geraet mitten
|
|
im Entwickeln neu, sobald man an der REPL nachdenkt.
|
|
"""
|
|
log = log or (lambda *a: None)
|
|
try:
|
|
from machine import WDT
|
|
except ImportError:
|
|
log("Watchdog: machine.WDT nicht verfuegbar")
|
|
return None
|
|
sekunden = config.WDT_TIMEOUT if timeout is None else timeout
|
|
wdt = WDT(timeout=int(sekunden * 1000))
|
|
log("Watchdog: aktiv, %d s" % sekunden)
|
|
return wdt.feed
|
|
|
|
|
|
def main(host=None, port=None, with_ldr=False, fake_display=False,
|
|
once=False, with_mqtt=True, feed=None, log=print):
|
|
"""WLAN aufbauen, Anzeige oeffnen und die Poll-Schleife fahren.
|
|
|
|
host/port Montierung; Vorgabe aus config.py. Zum Testen gegen den Mock
|
|
auf dem Mac dessen LAN-Adresse angeben.
|
|
with_ldr Helligkeitsregelung ueber den ADC dazuschalten. Die Schwellen
|
|
sind am realen LDR gemessen (2026-07-27, siehe brightness.py),
|
|
aber am Schreibtisch, nicht in der Sternwarte -- deshalb bleibt
|
|
die Vorgabe aus, bis sie vor Ort bestaetigt sind.
|
|
fake_display Anzeige nur als Hex in die REPL schreiben, statt ueber den
|
|
UART zu senden -- fuer den Test ohne Pegelwandler und Anzeige.
|
|
once nur ein Durchlauf, dann zurueck.
|
|
with_mqtt MQTT-Anbindung versuchen. Ohne mqtt_config.py passiert nichts,
|
|
deshalb ist die Vorgabe an: fehlt die Datei, laeuft alles wie
|
|
bisher weiter.
|
|
feed Watchdog-Funktion aus make_watchdog(). Vorgabe None: Von Hand
|
|
gestartet laeuft die Schleife ohne Watchdog, sonst wuerde das
|
|
Geraet beim Nachdenken an der REPL neu starten. main.py legt
|
|
ihn an.
|
|
"""
|
|
d = open_display(fake=fake_display)
|
|
client = MountClient(config.MOUNT_HOST if host is None else host,
|
|
config.MOUNT_PORT if port is None else port,
|
|
timeout=config.MOUNT_TIMEOUT)
|
|
|
|
dimmer = read_ldr = None
|
|
if with_ldr:
|
|
read_ldr = make_ldr_reader()
|
|
if read_ldr is None:
|
|
log("LDR: kein ADC verfuegbar, Helligkeitsregelung bleibt aus")
|
|
else:
|
|
dimmer = BrightnessController()
|
|
|
|
bridge = None
|
|
try:
|
|
# Einmal vorab verbinden, damit ein Fehler gleich sichtbar wird; danach
|
|
# haelt ensure_link die Verbindung in der Schleife.
|
|
try:
|
|
wifi.connect(log=log)
|
|
except wifi.WifiError as e:
|
|
log("WLAN: %s" % e)
|
|
|
|
if with_mqtt:
|
|
import mqtt
|
|
|
|
def uebernehmen(werte):
|
|
"""Per MQTT geaenderte Einstellungen sofort sichtbar machen.
|
|
|
|
Der Dimmer muss die neuen Werte nicht nur lesen -- die Helligkeit
|
|
muss auch neu an die Anzeige. Sonst bliebe eine gesetzte
|
|
Helligkeit unsichtbar, solange die Stufe dieselbe bleibt: ein
|
|
Telegramm geht sonst nur beim *Wechsel* raus. (Am Geraet
|
|
aufgefallen, 2026-07-27.)
|
|
"""
|
|
try:
|
|
if dimmer is not None:
|
|
dimmer.reload()
|
|
if dimmer.level is not None:
|
|
d.set_brightness(dimmer.percent(dimmer.level))
|
|
else:
|
|
# Ohne LDR gibt es keine Stufen -- dann gilt "hell".
|
|
d.set_brightness(werte["hell_prozent"])
|
|
except Exception as e:
|
|
log("Anzeige: %s" % e)
|
|
|
|
bridge = mqtt.connect_from_config(log=log, on_change=uebernehmen)
|
|
|
|
displayloop.poll_loop(d, client,
|
|
once=once,
|
|
read_ldr=read_ldr,
|
|
dimmer=dimmer,
|
|
ensure_link=lambda: wifi.ensure(log=log),
|
|
bridge=bridge,
|
|
feed=feed,
|
|
log=log)
|
|
except KeyboardInterrupt:
|
|
log("\nbeendet")
|
|
finally:
|
|
if bridge is not None:
|
|
bridge.close()
|
|
client.close()
|
|
d.close()
|