Am echten Broker aufgefallen: "hell_prozent = 100 uebernommen" stand im Log, die Anzeige blieb aber auf 50 %. Der Wert landete korrekt in settings.json und der Dimmer las ihn per reload() auch nach -- nur ging kein Helligkeitstelegramm raus. Grund: Ein Telegramm wird nur beim *Wechsel* der Stufe gesendet (so war es schon im alten MSP430-Programm gedacht, und fuer die LDR-Regelung ist das richtig). Aendert man die Helligkeit von aussen, waehrend die Stufe dieselbe bleibt, gibt es keinen Wechsel -- die Aenderung waere erst beim naechsten zufaelligen Stufenwechsel sichtbar geworden. run_esp32.main setzt nach dem reload() jetzt die Helligkeit der aktuellen Stufe neu. Ohne Dimmer (kein LDR) gilt weiterhin direkt hell_prozent. test_mqtt.TestHelligkeitWirdSofortSichtbar haelt das fest. Gegenprobe gemacht: mit der alten Fassung schlaegt der Test fehl (keine Helligkeit gesendet), mit der neuen geht genau ein Telegramm mit dem gesetzten Wert raus. Am Geraet bestaetigt, Broker "nuccy": Verbindung steht, Status (ra/dec/ldr/ helligkeit/link/online) kommt retained an, Einstellungen werden uebernommen, ungueltige Werte (500, Text) abgelehnt ohne settings.json anzufassen, und die Anzeige reagiert jetzt sichtbar auf 100 % -> 15 % -> 50 %. 145 Tests gruen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Großanzeige
Ansteuerung einer microSYST migra SC/MC Seriell LED-Großanzeige mit den Koordinaten (Rektaszension / Deklination) einer 10Micron GM4000 HPS II.
Zielgerät ist ein XIAO ESP32-C3, der die Montierung per WLAN abfragt und die Koordinaten über UART/RS232 auf die Anzeige schreibt. Entwickelt und getestet wird auf dem Mac (Koordinaten von Hand bzw. von der Montierung, Ausgabe über einen USB-RS232-Wandler); dieselben Module laufen auf dem ESP32.
Benutzung
# Phase 1 -- Koordinaten von Hand:
python3 show_coords.py 12h34m56s +45d12m30s # Koordinaten anzeigen
python3 show_coords.py 12:34:56 -07:15:00 # Trennzeichen sind egal
python3 show_coords.py --clear # Anzeige löschen
python3 show_coords.py 12h34m56s +45d12m30s --dry-run # nur Telegramm als Hex
# Phase 2 -- Koordinaten von der Montierung (10Micron GM4000):
python3 run_display.py # Montierung aus config.py pollen
python3 run_display.py --host 10.0.0.42 # andere IP
python3 run_display.py --mock --move # ohne Teleskop: lokaler Mock, RA läuft
python3 run_display.py --mock --move --ldr 2000 # zusätzlich mit Helligkeitsregelung
python3 mount_mock.py --move # Mock separat starten
# Helligkeit aus einem LDR-Wert setzen:
python3 brightness.py --ldr 1500 # LDR-Rohwert -> Helligkeit -> senden
python3 -m unittest discover -p 'test_*.py' # alle Tests
python3 probe.py ruler # Geometrie der Anzeige ausmessen
Auf dem ESP32-C3 (siehe Abschnitt ESP32-C3):
cp wifi_config_example.py wifi_config.py # WLAN-Zugangsdaten eintragen
./deploy.sh # Dateien auf den ESP32 kopieren
mpremote repl # dann: import run_esp32
# run_esp32.main()
Einzige Abhängigkeit auf dem Mac ist pyserial, fürs Übertragen zusätzlich
mpremote. Auf dem ESP32 laufen die Module ohne jede Fremdbibliothek.
Aufbau
Alles, was auf beiden Plattformen läuft, ist bewusst abhängigkeitsfrei
gehalten. Was sich unterscheidet, steckt in transport.py (UART) und in den
beiden Startprogrammen.
| Datei | Zweck | ESP32 |
|---|---|---|
migra.py |
Protokoll: Telegrammrahmen, Prüfsumme, ESC-Sequenzen. | ✓ |
transport.py |
UART: pyserial (Mac), machine.UART (ESP32-C3), FakeTransport (Trockenlauf). |
✓ |
ticks.py |
Timeouts, die unter CPython und MicroPython gleich funktionieren. | ✓ |
display.py |
Treiber: Telegramm senden, Antwort auswerten. | ✓ |
coords.py |
Koordinaten einlesen und auf die Zeilenbreite formatieren. | ✓ |
config.py |
Port/Pins, Adresse, Geometrie, WLAN — fest, ändert sich nur beim Deployen. | ✓ |
settings.py |
Zur Laufzeit änderbare Werte (Helligkeiten, Schwelle) in settings.json. |
✓ |
displayloop.py |
Die Poll-Schleife: abfragen, formatieren, anzeigen, Fehler abfangen. | ✓ |
brightness.py |
Helligkeit: LDR-Wert → dunkel/hell, Schwelle aus settings.json. |
✓ |
mount_client.py |
TCP-Client zur GM4000 (LX200). | ✓ |
lx200.py |
LX200-Kommandorahmen. | ✓ |
wifi.py |
WLAN-Anbindung des ESP32 (verbinden, Verbindung halten). | ✓ |
mqtt.py |
MQTT: Einstellungen empfangen, Zustand veröffentlichen. | ✓ |
run_esp32.py |
Startprogramm auf dem ESP32: WLAN, Anzeige, Schleife. | ✓ |
wifi_config.py |
WLAN-Zugangsdaten, nicht im Git (Vorlage wifi_config_example.py). |
✓ |
mqtt_config.py |
MQTT-Zugangsdaten, nicht im Git (Vorlage mqtt_config_example.py). |
✓ |
run_display.py |
Startprogramm auf dem Mac: dieselbe Schleife mit Kommandozeile. | |
show_coords.py |
Testprogramm (Koordinaten von Hand). | |
mount_mock.py |
Simulator der Montierung (LX200) fürs Testen ohne Teleskop. | |
probe.py |
Testmuster zum Ausmessen von Zeilenbreite und Zeilenposition. | |
deploy.sh |
Kopiert die ESP32-Dateien per mpremote auf das Gerät. |
|
test_*.py |
Tests (migra, lx200, brightness, settings, display, esp32). |
Protokoll (Kurzfassung)
Referenz: Benutzerhandbuch X-M31-9AXX5X-006 (V4.20), Kapitel 3.5–3.9.
Schnittstelle: 19200 Baud, 8 Datenbits, Even Parity, 1 Stopbit. 9-pol. Sub-D: Pin 2 = RxD, Pin 3 = TxD, Pin 5 = GND.
Telegramm:
STX | DA | SA | FC | [LEN-H LEN-L] | Data-Unit | [CHK-H CHK-L] | ETX
02 81 80 81 03
DA/SA: Adressen mit gesetztem Bit 7 → Anzeige = 1 (0x81), Host = 0 (0x80).FC: Bit 7 fest 1, Bit 1 = Prüfsumme, Bit 0 = Antwort anfordern.0x81= ohne Prüfsumme, mit Antwort (so fuhr auch das alte MSP430-Programm).- Ohne Prüfsumme dürfen
LEN-H/LundCHK-H/Lnicht mitgesendet werden. - Prüfsumme = Low-Byte der Summe aus
DA, SA, FC, LEN-H, LEN-Lund allen Datenbytes, als zwei Nibbles mit gesetztem High-Nibble (0xF0 | nibble).
Antwort: 02 80 81 80 <Fehlercode> 03, Fehlercode '0' = kein Fehler.
Nach Erhalt der Antwort darf sofort das nächste Telegramm folgen — das ist die
Flusskontrolle. Ohne Antwort muss zwischen den Telegrammen gewartet werden
(Empfangstimeout + ca. 150 ms).
ESC-Sequenzen (alle Zahlen ASCII-codiert, Position 123 → '1' '2' '3'):
| Sequenz | Bedeutung |
|---|---|
ESC z NN / ESC Z NN |
Zeichensatz NN (z = Sperrschrift, gleiche Zeichenbreite) |
ESC C xxxyyy |
Cursor auf Pixelposition x/y, 0/0 ist links oben |
ESC A <fg><bg><blink> |
Attribute; Farbe 0 = schwarz, 1 = grün, 2 = rot, 3 = gelb |
ESC F <Farbe> |
Anzeige füllen; ESC F 0 = löschen |
ESC H <Farbe><ppp> |
Helligkeit 0–100 % |
ESC T <+/-><nnn> |
gespeicherten Text ein-/ausblenden |
Folgt auf eine ESC-Sequenz ein Online-Text, muss 0x1F als Trennzeichen
dazwischen. Data-Unit max. 230 Zeichen. migra.DataUnit setzt das Trennzeichen
selbst.
ESP32-C3
Verdrahtung
Die Pins stammen aus dem Schaltplan (Hardware/KiCad/Grossanzeige) und stehen
in config.py. Am XIAO ESP32-C3 ist UART1 frei zuordenbar; UART0 bleibt für die
REPL-Konsole über USB reserviert.
| XIAO | GPIO | Netz | weiter zu |
|---|---|---|---|
| Pad 7 (D6) | 21 | TX | MAX3232 T2IN → T2OUT → J5.3 → Anzeige RxD (Sub-D Pin 2) |
| Pad 8 (D7) | 20 | RX | MAX3232 R2OUT ← R2IN ← J5.2 ← Anzeige TxD (Sub-D Pin 3) |
| Pad 1 (D0/A0) | 2 | LDR | Spannungsteiler J2, siehe unten |
| Pad 12 | — | 3V3 | MAX3232 VCC |
| Pad 13 | — | GND | gemeinsame Masse |
Die Anzeige will RS232-Pegel, der ESP32-C3 kann nur TTL — der MAX3232 dazwischen ist zwingend.
Einrichten
MicroPython für den ESP32-C3 flashen (einmalig; Firmware von micropython.org/download/ESP32_GENERIC_C3):
pip install esptool mpremote
esptool.py --chip esp32c3 --port /dev/cu.usbmodem1101 erase_flash
esptool.py --chip esp32c3 --port /dev/cu.usbmodem1101 --baud 460800 \
write_flash -z 0x0 ESP32_GENERIC_C3-*.bin
Dann die Zugangsdaten anlegen und alles übertragen:
cp wifi_config_example.py wifi_config.py # SSID und Passwort eintragen
./deploy.sh # oder ./deploy.sh /dev/cu.usbmodem1101
wifi_config.py steht in .gitignore — das WLAN-Passwort gehört nicht ins
Repository.
Starten
mpremote repl
>>> import run_esp32
>>> run_esp32.main() # ohne Helligkeitsregelung
>>> run_esp32.main(with_ldr=True) # mit LDR über den ADC
>>> run_esp32.main(with_mqtt=False) # ohne MQTT (sonst an, sofern konfiguriert)
Bewusst kein main.py: MicroPython startet eine Datei dieses Namens nach
jedem Reset von selbst, und solange am Aufbau gearbeitet wird, ist der manuelle
Start angenehmer. Für den Dauerbetrieb in der Sternwarte reicht später eine
main.py mit den zwei Zeilen von oben.
Was auf beiden Seiten gleich läuft
displayloop.poll_loop ist die gemeinsame Hauptschleife — Mac und ESP32 fahren
denselben Ablauf, drumherum liegt nur die Kommandozeile (run_display.py) bzw.
das WLAN-Setup (run_esp32.py). Fällt das WLAN aus, zeigt die Anzeige
NOWIFI_LINES, fällt die Montierung aus, NOLINK_LINES; in beiden Fällen läuft
die Schleife weiter und verbindet sich neu.
Stolpersteine, die beim Portieren aufgefallen sind und in den Tests
festgehalten sind (test_esp32.py, test_display.py):
- MicroPython hat kein
str.ljust(auch keinrjust/center/zfill). Das fiel erst am Gerät auf — auf dem Mac gibt es die Methode, alle Tests waren grün.display._emit_linefüllt jetzt von Hand auf.TestMicropythonVertraeglichkeitsucht solche Aufrufe seitdem statisch. time.ticks_ms()läuft nach ~12 Tagen über. Ein einfaches „jetzt + Timeout" ließe eine Warteschleife danach tagelang hängen.ticks.pyrechnet deshalb überticks_diff, das den Überlauf richtig behandelt.- Der UART braucht denselben Puffer-Flush wie pyserial.
UartTransport.writeleert den Empfangspuffer vor jedem Senden, sonst siehtread_responsealte Störbytes statt der frischen Antwort. - Feste Anzeigetexte müssen in
CHARS_PER_LINEpassen.NOLINK_LINEShieß zuerst „MONTIERUNG" (10 Zeichen) und erschien als „MONTIERUN";_emit_lineschneidet stillschweigend ab.test_display.pyprüft die Breite jetzt. (SeitCHARS_PER_LINE = 10würde „MONTIERUNG" wieder passen — die Texte sind aber bei „KEIN LINK"/„TELESKOP" geblieben.)
Was MicroPython 1.28 entgegen der Erwartung doch kann (am Gerät nachgeprüft):
%0*d mit dynamischer Feldbreite, %-11s, bytes.hex(), str.strip/lstrip,
str.isdigit.
Ohne Hardware lässt sich der ESP32-Teil trotzdem weitgehend prüfen:
test_esp32.py hängt Attrappen für machine und network ein und fährt die
ganze Kette gegen einen echten Mock — UART-Transport, WLAN, ADC-Auswertung,
Zeitrechnung und die Poll-Schleife samt Ausfallverhalten.
Gegen den Mock auf dem Mac testen
Solange die Montierung nicht erreichbar ist, spielt der Mock ihre Rolle. Er muss dafür auf allen Schnittstellen lauschen, sonst kommt der ESP32 nicht heran:
python3 mount_mock.py --lan --move # gibt die IP aus, die einzutragen ist
Auf dem ESP32 dann:
import run_esp32
run_esp32.selftest(host="192.168.178.26") # Bring-up prüfen
run_esp32.main(host="192.168.178.26", fake_display=True) # Dauerschleife
selftest geht die drei Stufen einzeln durch — WLAN, Montierung, Telegrammbau —
und sagt, an welcher es klemmt. fake_display=True schreibt die Telegramme nur
als Hex in die REPL, es braucht also weder Pegelwandler noch Anzeige.
Wenn keine Verbindung zustande kommt, obwohl der Mock lauscht: macOS fragt beim
ersten Mal, ob python3 eingehende Verbindungen annehmen darf — das muss
erlaubt sein.
Am Gerät bestätigt (2026-07-26)
XIAO ESP32-C3 mit MicroPython 1.28.0 (ESP32_GENERIC_C3), gegen den Mock auf dem
Mac:
- Alle zwölf Module importieren auf dem Gerät.
- Telegrammbau und Koordinaten-Formatierung sind byte-identisch zum Mac, inklusive Gradzeichen über den Zeichensatzwechsel.
- WLAN verbindet sich,
selftestgeht durch alle drei Stufen. - Die Poll-Schleife lief 20 s im Sekundentakt, 22 Abfragen ohne Aussetzer.
- Ausfall und Erholung: Mock gestoppt → „KEIN LINK / TELESKOP" erscheint, die Schleife läuft weiter; Mock zurück → automatischer Reconnect, und der erste Wiedereinstieg löscht einmal das Vollbild, sodass kein Rest stehen bleibt.
- UART1 öffnet auf GPIO21/20 mit 19200 8E1, sendet, und der Lese-Timeout greift sauber (kein Hänger) — allerdings noch ohne Gegenstelle.
- ADC auf GPIO2 mit 11 dB Dämpfung liefert Werte.
Offene Punkte
- Umschaltschwelle in der Sternwarte gegenprüfen. Die Schwelle (1800) ist am
realen Aufbau gemessen (2026-07-27, siehe Helligkeitsregelung), aber am
Schreibtisch. Vor Ort bei Tag und Nacht die Rohwerte ablesen und die Schwelle in
settings.jsonbestätigen. Bis dahin läuft die Anzeige mit fester Helligkeit (run_esp32.main()ohnewith_ldr). - MQTT im Dauerbetrieb beobachten. Die Strecke ist am echten Broker in beide Richtungen bestätigt (2026-07-27); offen ist nur, wie sie sich über Tage verhält (Reconnect nach Broker-Neustart, WLAN-Aussetzer).
- Netzwerk zur echten Montierung. Die GM4000 steht in
192.168.1.115, das Heimnetz des ESP32 ist192.168.178.x— der Mac erreicht sie per VPN, der ESP32 so nicht. Muss geklärt werden, bevor es an die echte Montierung geht. - Dauerbetrieb: Autostart (
main.py) und ein Watchdog fehlen noch bewusst.
Gradzeichen
Der Standard-Zeichensatz 0 der Anzeige hat kein °. Der von der Sternwarte
geladene Zeichensatz 1 enthält an Position / (0x2F) ein hochgestelltes
Gradzeichen (deckt sich mit dem alten Programm, asciitaf.h: Code 0xAF → per
& 0x7F = 0x2F in Zeichensatz 1). Zur Ausgabe wird für dieses eine Zeichen kurz
auf Zeichensatz 1 umgeschaltet und wieder zurück (config.DEGREE_*, display._emit_degree).
Fallstrick: Ein Zeichensatzwechsel beschädigt die Home-Zelle (0,0) — das erste
Zeichen der obersten Zeile wird zerstört. Deshalb schreibt show_lines die Zeilen
von unten nach oben (oberste Zeile zuletzt), damit sie diese Zelle überschreibt.
Das gilt weiterhin, seit jede Zeile ihr eigenes Telegramm bekommt: dann entscheidet
die Reihenfolge der Telegramme statt der Reihenfolge innerhalb eines Telegramms.
Geometrie und Darstellung
-
10 Zeichen pro Zeile zu 6 px (
CHARS_PER_LINE = 10,CELL_WIDTH = 6), am Gerät entschieden mitprobe.py pitch 6(2026-07-27).Vorher stand hier 9 — das war am Zeilenumbruch abgezählt und maß etwas anderes, als es zu messen schien.
probe.py pitchstellt den Auto-Vorschub der Anzeige gegen feste Pixelpositionenx = i·cellund trennt damit beides:- Der Auto-Vorschub der Sperrschrift
ESC zist 7 px. Eine Zeile in einem Stück geschrieben bricht deshalb nach 9 Zeichen um (9·7 = 63 von 64 px). - Die Zeichenmatrix ist aber nur 6 px breit. Zehn Ziffern, einzeln auf
x = i·6gesetzt, stehen sauber getrennt nebeneinander (60 von 64 px).
Deshalb positioniert
display._emit_linejedes Zeichen selbst, statt den Auto-Vorschub laufen zu lassen. Der Preis sind längere Telegramme (rund 10 Byte je Zeichen statt 1), der Gewinn ein volles Zeichen mehr pro Zeile.python3 probe.py pitch 6 # stehen beide Zeilen untereinander -> 6 px/Zelle python3 probe.py pitch 7 # ... -> 7 px/Zelle - Der Auto-Vorschub der Sperrschrift
-
Ein Telegramm je Zeile. Früher gingen beide Zeilen in einem Telegramm raus. Mit Einzelpositionierung kämen sie mit Gradzeichen und Vollbild-Löschen auf 224 der erlaubten 230 Bytes (
migra.MAX_DATA_UNIT) — zu wenig Reserve für ein Gerät, das monatelang durchläuft. Getrennte Telegramme halten jedes bei rund der Hälfte; die Flusskontrolle über das Antworttelegramm macht die zweite Sendung unproblematisch. Die Reihenfolge bleibt unten vor oben (Home-Zelle). -
Die Deklination passt damit in voller Form mit abschließendem
"(+38°47'01", 10 Zeichen).coords.format_decwählt automatisch die breiteste Form, die inCHARS_PER_LINEpasst — bei Niedrigpräzision entsprechend+38°47'ohne Sekunden. -
Zeilen werden zentriert (
CENTER_LINES = True). Das wirkt auf zwei Ebenen:LINE_Xrückt den Zeichenblock in die Mitte (10·6 = 60 px lassen je 2 px Rand), und kürzere Texte werden beidseitig mit Leerzeichen aufgefüllt. Der Block bleibt dabei immer gleich breit und an derselben Stelle — so überschreibt er bei jedem Durchlauf denselben Bereich und lässt keine Reste stehen. Ein pixelgenau mitwandernder Offset könnte das nicht; dafür ist die Zentrierung nur auf eine halbe Zelle (3 px) genau, wenn die Restbreite ungerade ist. -
Zyklische Updates flimmerfrei:
show_lineslöscht nicht bei jeder Aktualisierung, sondern überschreibt die Zeilen an Ort und Stelle (auf volle Breite aufgefüllt).clear=Truenur beim ersten Bild. Wichtig: Zeilen dürfen nicht breiter alsCHARS_PER_LINEaufgefüllt werden, sonst bricht das Füll-Leerzeichen um und beschädigt die andere Zeile.
Präzision (Low / High)
Die GM4000 läuft standardmäßig in Niedrigpräzision und sendet keine Sekunden:
RA als HH:MM.T (Zehntel-Minuten), DEC als sDD°MM (ganze Bogenminuten). Das
Gradzeichen kommt als Byte 0xDF — auf der Anzeige korrekt als Gradring dargestellt;
in der Terminal-Konsole erscheint der Rohwert dieses Bytes als ß (rein kosmetisch).
config.HIGH_PRECISION steuert die DEC-Darstellung:
False(Vorgabe): DEC ohne Sekunden (-00°55').True: DEC mit Sekunden (+38°47'01) — erst sinnvoll, wenn die Montierung per:U#auf Hochpräzision gestellt ist (schreibendes Kommando, hier bewusst nicht automatisch gesendet).
Die RA wird immer mit Sekunden gezeigt (11h36m54s); bei Niedrigpräzision
kommen die Sekunden aus den Zehntel-Minuten und springen daher in 6-Sekunden-Schritten
(kein Dezimalpunkt — der wirkt in der Sperrschrift zu luftig).
coords.format_ra/format_dec nehmen high_precision= als Parameter (Vorgabe aus
config). Die Montierungs-Rohwerte werden im coords-Parser unverändert korrekt
zerlegt (Trennzeichen :, *, 0xDF, ' sind alle egal).
Helligkeitsregelung
Zwei Helligkeiten, ein Umschaltpunkt. Unterhalb der Schwelle läuft die Anzeige
mit dunkel_prozent, oberhalb mit hell_prozent. Die vier abgestuften Helligkeiten
des alten MSP430-Programms (5/15/50/100 %) sind bewusst aufgegeben — für die
Sternwarte genügt der Wechsel Tag/Nacht.
Ein LDR-Rohwert (12-Bit-ADC, 0–4095) wird über raw_to_ldr in die Zählrichtung
„groß = hell" gebracht, über einen gleitenden Mittelwert (8 Werte) geglättet und
gegen die Schwelle gehalten:
| Bereich | Helligkeit | Umschaltpunkt |
|---|---|---|
| unter der Schwelle | dunkel_prozent = 5 % |
herunter ab < 1650 |
| über der Schwelle | hell_prozent = 50 % |
hoch ab ≥ 1950 |
Schwelle 1800, Hysterese ±150. Beim allerersten Messwert entscheidet die
blanke Schwelle (1800) statt der Kante — sonst startete die Anzeige im Bereich
dazwischen fälschlich dunkel und wachte erst später auf.
Die Werte stehen in settings.json, nicht im Code
config.py ist Programmcode und ändert sich nur beim Deployen. Die Helligkeiten
und die Schwelle sollen dagegen im Betrieb verstellbar sein — später über MQTT.
Sie liegen deshalb als JSON auf dem Gerät und werden über settings.py gelesen und
geschrieben:
import settings
settings.load() # mit Vorgaben aufgefüllt
settings.update({"hell_prozent": 60}) # ändern und speichern
Ein laufender BrightnessController übernimmt Änderungen mit reload().
Jeder Wert wird beim Laden und beim Schreiben geprüft (Typ und Bereich:
Prozente 0–100, Schwelle 0–4095). Das ist der Grund, warum die Prüfung überhaupt
existiert: Sobald MQTT dazukommt, kommen die Werte von außen, und ein
Helligkeitswert von 500 % oder eine Schwelle von −1 würde die Anzeige sonst
unbrauchbar machen. Fehlt die Datei oder ist sie beschädigt, gelten die Vorgaben
aus settings.DEFAULTS — ein Gerät in der Sternwarte soll sich nicht an einer
kaputten Konfigurationsdatei aufhängen. Ein einzelner unbrauchbarer Wert verwirft
nur diesen, nicht die ganze Datei.
settings.json wird von deploy.sh nicht mitkopiert und steht in .gitignore:
Es ist Laufzeitzustand des Geräts, kein Quellcode. Würde es mitkopiert, wären die
eingestellten Werte nach jedem Deployen weg.
Die Schwelle stammt aus einer Messung
Am realen Aufbau gemessen (2026-07-27), XIAO ESP32-C3 mit LDR an GPIO2,
je 200 Messwerte, Median — Werte nach raw_to_ldr:
| Lichtsituation | roh | nach raw_to_ldr |
Stufe |
|---|---|---|---|
| abgedeckt | 4095 (ADC-Anschlag) | 0 | dunkel → 5 % |
| Raumlicht | 1642 | 2453 | hell → 50 % |
| angeleuchtet (Taschenlampe) | 543 | 3552 | hell → 50 % |
Die Schwelle 1800 liegt mit Abstand zwischen dem Dunkelwert und dem Raumlicht.
Die Hysterese ist nicht optional. Der Rohwert streut bei Raumlicht um ±270,
nach der Mittelung bleiben rund ±32 (1σ). Ohne Hysterese würde die Anzeige am
Umschaltpunkt zwischen den beiden Helligkeiten hin- und herspringen; ±150 liegt
sicher darüber (≈4,7σ). Ein Helligkeitstelegramm wird ohnehin nur bei
Stufenwechsel gesendet (ESC H 2 <ppp>), wie im Original.
Im Dunkeln steht der ADC am Anschlag (4095). Der Teiler treibt A0 über den
Messbereich, „Dämmerung" und „stockdunkel" sind daher nicht unterscheidbar. Bei
nur zwei Helligkeiten ist das ohne Belang — unterschieden werden muss nur hell
gegen dunkel, und dafür reicht die Auflösung weit. Eine feinere
Nachtabstufung ginge nur über den Vorwiderstand: sättigungsfrei wäre
R2 ≥ R_LDR(dunkel) × (3,3 V − U_max)/U_max, also grob R2 ≥ 0,32 × R_LDR(dunkel).
Bei einem typischen LDR (0,5–1 MΩ im Dunkeln) liefe das auf 160–330 kΩ hinaus — dann
lägen Tag und Raumlicht aber bei ADC-Werten unter 15 und damit in der Totzone. Der
LDR überstreicht rund Faktor 4500; ein einzelner Teiler bildet davon nur etwa zwei
Dekaden ab. 10–22 kΩ wäre der Kompromiss, wenn mehr Nachtauflösung gewünscht ist.
Auf dem Mac kommt der LDR-Wert per Kommandozeile (run_display.py --ldr <wert>),
auf dem ESP32-C3 vom ADC an GPIO2 (run_esp32.main(with_ldr=True)).
Die Kennlinie ist auf der Platine umgekehrt. Der Teiler lautet
3V3 —[R2 2k]— A0 —[LDR]— GND; der LDR wird bei mehr Licht niederohmiger, die
Spannung an A0 sinkt also mit steigender Helligkeit — genau andersherum als beim
alten MSP430-Aufbau, auf den die Schwellen oben zugeschnitten sind. Der Rohwert
wird deshalb gespiegelt: config.LDR_INVERTED = True, angewendet in
brightness.raw_to_ldr. Wer die Hardware lieber umbaut (LDR nach 3V3, R2 nach
GND), setzt das Flag auf False.
Die Schwellen sind am Aufbau gemessen, aber am Schreibtisch, nicht in der
Sternwarte. Vor dem Echteinsatz dort bei Tag und Nacht gegenprüfen — deshalb
bleibt with_ldr in run_esp32.main vorerst auf False.
MQTT
Der ESP32 nimmt Einstellungen über MQTT entgegen und veröffentlicht seinen
Zustand. Zugangsdaten kommen aus mqtt_config.py (gitignored, Vorlage
mqtt_config_example.py):
cp mqtt_config_example.py mqtt_config.py # Broker, Port, ggf. Benutzer/Passwort
./deploy.sh
Fehlt die Datei, läuft die Anzeige einfach ohne MQTT — deploy.sh weist nur
darauf hin und kopiert sie nicht.
Topics
Empfangen (<PREFIX>/set/<name>, Nutzlast als Zahl):
| Topic | Beispiel | Wirkung |
|---|---|---|
grossanzeige/set/hell_prozent |
60 |
Helligkeit über der Schwelle |
grossanzeige/set/dunkel_prozent |
5 |
Helligkeit unter der Schwelle |
grossanzeige/set/schwelle |
1800 |
Umschaltpunkt (LDR-Wert) |
grossanzeige/set/hysterese |
150 |
Breite um den Umschaltpunkt |
Gesendet (<PREFIX>/status/<name>, alle retained):
| Topic | Beispiel |
|---|---|
grossanzeige/status/ra |
18h36m56s |
grossanzeige/status/dec |
+38°47'01" |
grossanzeige/status/ldr |
2453 |
grossanzeige/status/helligkeit |
50 |
grossanzeige/status/link |
1 (Montierung erreichbar) |
grossanzeige/status/online |
1 — Last Will setzt es auf 0 |
Zum Ausprobieren:
mosquitto_sub -h <broker> -t 'grossanzeige/#' -v
mosquitto_pub -h <broker> -t grossanzeige/set/hell_prozent -m 60
Eine empfangene Einstellung wird sofort wirksam: Sie geht durch dieselbe Prüfung
wie jede andere (settings.update), landet in settings.json und der laufende
BrightnessController lädt sie über reload() nach — ein Neustart ist nicht nötig.
Wichtig dabei: Nach dem Nachladen muss die Helligkeit auch neu an die Anzeige
gesendet werden. Ein Helligkeitstelegramm geht sonst nur beim Wechsel der Stufe
raus — eine per MQTT gesetzte Helligkeit bliebe unsichtbar, solange die Stufe
dieselbe bleibt. Das ist am Gerät aufgefallen (2026-07-27) und wird von
test_mqtt.py (TestHelligkeitWirdSofortSichtbar) festgehalten.
Was dabei schiefgehen kann, und warum es nichts ausmacht
Grundsatz: MQTT darf die Anzeige nie aufhalten. Die Anzeige ist der Zweck des
Geräts, MQTT ist Beiwerk. Deshalb fängt mqtt.py seine Fehler selbst ab:
- Broker nicht erreichbar → Der Verbindungsversuch schlägt fehl, wird notiert, und die Schleife läuft weiter. Wiederholt wird mit wachsendem Abstand (5 s bis 120 s) — sonst würde ein dauerhaft toter Broker in jedem Schleifendurchlauf Zeit kosten.
- Verbindung bricht mitten im Betrieb ab → Wird beim nächsten Senden oder Empfangen bemerkt, die Brücke verwirft die Verbindung und baut sie später neu auf. Nach dem Reconnect wird der gesamte Status erneut gesendet, damit der Broker nicht auf veralteten Werten sitzenbleibt.
- Unsinniger Wert von außen (
hell_prozent = 500,schwelle = -1, Text statt Zahl, unbekanntes Topic) → wird verworfen und protokolliert;settings.jsonbleibt unberührt. Genau dafür prüftsettings.pyjeden Wert auf Typ und Bereich. - Gerät fällt aus → Der Last Will sorgt dafür, dass der Broker von sich aus
online = 0meldet. Ohne das bliebeonline = 1stehen, obwohl niemand mehr da ist.
test_mqtt.py prüft all diese Fälle gegen einen eingehängten Fake-Broker — auch
die, die sich mit einem echten Server nur schwer herbeiführen lassen. Der wichtigste
Test ist test_toter_broker_haelt_die_anzeige_nicht_auf.
Der Empfang blockiert nicht: check_msg() kehrt sofort zurück, wenn nichts anliegt.
Statuswerte werden nur bei Änderung gesendet — die Koordinaten ändern sich ständig,
LDR und Helligkeit kaum.
Antwortleitung (RX)
Der Rückkanal ist verdrahtet und verifiziert (2026-07-16): Anzeige Pin 3 (TxD) →
MAX232-Empfängereingang (R_IN) → R_OUT → Adapter RxD. Die Anzeige quittiert jedes
Telegramm mit 02 80 81 80 30 03 (Fehlercode 0 = kein Fehler). WANT_RESPONSE = True
ist damit aktiv: Nach der Antwort darf sofort das nächste Telegramm folgen, der
Fehlercode wird ausgewertet. resp_test.py prüft den Rückweg (sendet mit Antwort-
Anforderung und zeigt die empfangenen Antworten).
Wichtig: SerialTransport.write leert vor jedem Senden den Empfangspuffer, damit
read_response nur die frische Antwort sieht (sonst können Störbytes zwischen den
Telegrammen das Parsen stören). run_display fängt Anzeige-Fehler ab und zeichnet
beim nächsten Durchlauf neu, statt abzustürzen.
Diagnose-Historie: Der Rückweg war anfangs falsch verdrahtet — Pin 3 lag an einem
MAX232-Senderausgang statt einem Empfängereingang; zwei Ausgänge trieben
gegeneinander (verschliffener ~3-V-Pegel am Oszi). loopback.py grenzt so etwas ein.
Erledigt
- RS232-Strecke ESP32 → MAX3232 → Anzeige bestätigt (2026-07-27). Das war das letzte ungetestete Kettenglied. Die Anzeige quittiert jedes Telegramm mit Fehlercode 0, die volle Kette (Mock → WLAN → ESP32 → UART1 → MAX3232 → Anzeige) läuft im Sekundentakt ohne Aussetzer. Damit ist die Portierung funktional komplett; offen sind nur noch Betriebsthemen (LDR, Netzwerk, Autostart).
- Phase 1 + 2 am Gerät bestätigt (2026-07-15/16): Koordinaten erscheinen korrekt,
inkl. echtem Gradzeichen; im
run_display-Betrieb aktualisieren beide Zeilen laufend, sauber und ohne Flimmern, mit Antwort-Handshake der Anzeige. Protokoll, 19200 8E1, Zeichensatz und Zeilengeometrie stimmen. - Schrift: Sperrschrift (
ESC z,CHARSET_SPACED = True), 10 Zeichen/Zeile bei fester Positionierung auf 6 px (gemessen 27.07., siehe Geometrie und Darstellung). Zwischenzeitlich stand hier 9 — das war am Umbruch abgezählt und traf den Auto-Vorschub (7 px), nicht die Zeichenbreite. Die schmale1wirkt etwas luftig — Font-Eigenschaft, nur per eigenem Font (microSYST-PC-Software) änderbar.