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>
631 lines
31 KiB
Markdown
631 lines
31 KiB
Markdown
# 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
|
||
|
||
```bash
|
||
# 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
|
||
python3 mount_mock.py --move --low # ... im Auslieferformat der GM4000
|
||
# (RA als HH:MM.T, DEC ohne Sekunden)
|
||
|
||
# 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](#esp32-c3)):
|
||
|
||
```bash
|
||
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. | ✓ |
|
||
| `main.py` | Autostart nach jedem Reset, mit Watchdog und Abbruchfenster. | ✓ |
|
||
| `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/L` und `CHK-H/L` **nicht** mitgesendet werden.
|
||
- Prüfsumme = Low-Byte der Summe aus `DA, SA, FC, LEN-H, LEN-L` und 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 **Pixel**position 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](https://micropython.org/download/ESP32_GENERIC_C3/)):
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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)
|
||
```
|
||
|
||
### Dauerbetrieb: Autostart und Watchdog
|
||
|
||
`main.py` startet die Anzeige nach jedem Reset von selbst — nach Stromausfall
|
||
oder Watchdog-Neustart läuft sie also ohne Notebook wieder an.
|
||
|
||
**Das Abbruchfenster ist die Notbremse.** Ohne es wäre ein Gerät, dessen Programm
|
||
beim Start hängt, nur noch durch Neuflashen erreichbar: Die Endlosschleife belegt
|
||
die REPL sofort nach dem Reset. Deshalb wartet `main.py` erst
|
||
`config.AUTOSTART_DELAY` Sekunden:
|
||
|
||
```
|
||
Grossanzeige startet in 3 s -- Ctrl-C bricht ab.
|
||
Watchdog: aktiv, 120 s
|
||
WLAN: verbinde mit Mizar ...
|
||
```
|
||
|
||
In diesen drei Sekunden führt Ctrl-C zurück an die REPL (`Autostart abgebrochen,
|
||
REPL ist frei`) — dann läuft auch kein Watchdog. Wer das Gerät ganz ohne
|
||
Autostart will: `mpremote rm :main.py`.
|
||
|
||
Der **Watchdog** (`config.WDT_TIMEOUT`, 120 s) startet das Gerät neu, wenn die
|
||
Poll-Schleife stehenbleibt. Gefüttert wird zu Beginn jedes Durchlaufs — auch nach
|
||
einem Fehler, denn „KEIN LINK" ist kein Stillstand: Die Schleife läuft ja weiter.
|
||
Der Watchdog greift nur, wenn gar nichts mehr geht.
|
||
|
||
Die 120 s müssen über allem liegen, was regulär blockieren darf: WLAN-Aufbau bis
|
||
20 s, Montierung 3 s, dazu ein MQTT-Verbindungsversuch. Ein knapper Wert wäre
|
||
eine Neustart-Schleife. **Ein gestarteter Watchdog lässt sich nicht mehr
|
||
abschalten** — deshalb legt ihn nur `main.py` an, nicht der Handbetrieb über die
|
||
REPL; sonst startete das Gerät neu, während man an der REPL nachdenkt.
|
||
|
||
Nach einem Watchdog-Neustart meldet `main.py` das beim Start
|
||
(`Vorheriger Lauf wurde vom Watchdog beendet`, über `machine.reset_cause()`) —
|
||
sonst sähe der Neustart aus wie ein gewöhnlicher Stromausfall. Eine **Ausnahme**
|
||
fängt `main.py` ab und startet nach `config.RESTART_DELAY` neu, statt in die REPL
|
||
zu fallen und die Anzeige einzufrieren.
|
||
|
||
Am Gerät bestätigt (2026-07-27): Autostart nach Reset, Watchdog löst bei
|
||
absichtlichem Stillstand aus und das Gerät kommt von allein wieder hoch, Ctrl-C
|
||
im Startfenster gibt die REPL frei.
|
||
|
||
### 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 kein `rjust`/`center`/`zfill`).
|
||
Das fiel erst am Gerät auf — auf dem Mac gibt es die Methode, alle Tests
|
||
waren grün. `display._emit_line` füllt jetzt von Hand auf.
|
||
`TestMicropythonVertraeglichkeit` sucht 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.py`
|
||
rechnet deshalb über `ticks_diff`, das den Überlauf richtig behandelt.
|
||
- **Der UART braucht denselben Puffer-Flush wie pyserial.** `UartTransport.write`
|
||
leert den Empfangspuffer vor jedem Senden, sonst sieht `read_response` alte
|
||
Störbytes statt der frischen Antwort.
|
||
- **Feste Anzeigetexte müssen in `CHARS_PER_LINE` passen.** `NOLINK_LINES` hieß
|
||
zuerst „MONTIERUNG" (10 Zeichen) und erschien als „MONTIERUN"; `_emit_line`
|
||
schneidet stillschweigend ab. `test_display.py` prüft die Breite jetzt.
|
||
(Seit `CHARS_PER_LINE = 10` wü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:
|
||
|
||
```bash
|
||
python3 mount_mock.py --lan --move # gibt die IP aus, die einzutragen ist
|
||
```
|
||
|
||
Auf dem ESP32 dann:
|
||
|
||
```python
|
||
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, `selftest` geht 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.json` bestätigen. Bis dahin läuft die Anzeige mit fester Helligkeit
|
||
(`run_esp32.main()` ohne `with_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 ist `192.168.178.x` — der Mac erreicht sie per VPN, der
|
||
ESP32 so nicht. Muss geklärt werden, bevor es an die echte Montierung geht.
|
||
|
||
## 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 mit `probe.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 pitch` stellt den Auto-Vorschub
|
||
der Anzeige gegen feste Pixelpositionen `x = i·cell` und trennt damit beides:
|
||
|
||
- Der **Auto-Vorschub** der Sperrschrift `ESC z` ist **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·6` gesetzt, stehen sauber getrennt nebeneinander (60 von 64 px).
|
||
|
||
Deshalb positioniert `display._emit_line` jedes 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.
|
||
|
||
```bash
|
||
python3 probe.py pitch 6 # stehen beide Zeilen untereinander -> 6 px/Zelle
|
||
python3 probe.py pitch 7 # ... -> 7 px/Zelle
|
||
```
|
||
|
||
- **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_dec` wählt automatisch die breiteste
|
||
Form, die in die Zeile passt — bei Niedrigpräzision entsprechend `+38°47'`
|
||
ohne Sekunden.
|
||
|
||
- **Schmale Zeichen bekommen weniger Platz** (`config.CELL_NARROW`). Der
|
||
Dezimalpunkt der Zehntelminuten und der Abstand zwischen Beschriftung und Wert
|
||
belegen statt 6 nur 3 px — in der Sperrschrift standen sie sonst sehr luftig.
|
||
Möglich ist das nur, weil ohnehin jedes Zeichen einzeln positioniert wird.
|
||
Für die Leerzeichen am Zeilenende gilt es *nicht*: Die sollen den Rest der
|
||
Zeile überschreiben und behalten die volle Breite.
|
||
|
||
Deshalb entscheidet die **Pixelbreite** über das Abschneiden, nicht die
|
||
Zeichenzahl: `RA 18h36.9m` sind 11 Zeichen und trotzdem nur 60 px, `DE +38°47'`
|
||
wären als 11 volle Zellen dagegen 66 px und damit zu breit.
|
||
|
||
- **Zeilen stehen linksbündig** ab `LINE_X = 0`. Zentriert waren sie
|
||
zwischenzeitlich, standen dann aber um wenige Pixel gegeneinander versetzt, weil
|
||
die RA-Zeile mit ihrem schmalen Dezimalpunkt kürzer ist als die DEC-Zeile.
|
||
|
||
- **Es wird nur geschrieben, was sich geändert hat.** `show_lines` vergleicht gegen
|
||
den zuletzt dargestellten Inhalt und schickt nur die betroffenen Zellen; eine
|
||
unveränderte Zeile erzeugt gar kein Telegramm. `clear=True` nur beim ersten Bild
|
||
und nach einem Verbindungsabriss.
|
||
|
||
Das ist nicht nur Sparsamkeit, sondern der Grund für eine ruhige Anzeige. Mit
|
||
Einzelpositionierung kostet eine volle Zeile rund 100 Bytes statt 20; bei 19200
|
||
Baud mit 8E1 (11 Bit je Byte) dauerte ein kompletter Bildwechsel damit **rund
|
||
300 ms**. Die Anzeige stellt schon während des Empfangs dar, also war der Aufbau
|
||
als Flackern zu sehen — bei Aktualisierung im Sekundentakt ein Drittel der Zeit.
|
||
Von einer Sekunde zur nächsten ändert sich aber fast nichts: `18h36m56s` →
|
||
`18h36m57s` ist eine einzige Ziffer. Am Gerät gemessen (2026-07-27):
|
||
**306 ms → 66 ms** pro Bildwechsel.
|
||
|
||
- **Der rechte Nachbar wird mitgeschrieben.** Die Anzeige malt je Zeichen 7 px
|
||
breit (daher der 7-px-Auto-Vorschub), gesetzt wird aber auf 6-px-Raster. Ein neu
|
||
geschriebenes Zeichen löscht deshalb die erste Pixelspalte des Zeichens rechts
|
||
daneben. Beim vollständigen Neuaufbau fiel das nie auf, weil der Nachbar gleich
|
||
danach ohnehin neu gemalt wurde — beim Schreiben einzelner Zellen blieb er
|
||
beschädigt stehen: dem `s` hinter den Sekunden fehlten die linken Pixel, dem `m`
|
||
hinter den Minuten ebenso. `_zelle_faellig` zieht den rechten Nachbarn deshalb mit.
|
||
|
||
- Wichtig bleibt: Zeilen dürfen **nicht breiter** als `CHARS_PER_LINE` aufgefü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 ganze Darstellung — sie folgt dem, was die
|
||
Montierung wirklich liefert:
|
||
|
||
| | Niedrigpräzision (Vorgabe) | Hochpräzision (nach `:U#`) |
|
||
|---|---|---|
|
||
| Zeile 1 | `RA 18h36.9m` | `18h36m56s` |
|
||
| Zeile 2 | `DE +38°47'` | `+38°47'01"` |
|
||
|
||
Bei Niedrigpräzision zeigt die **RA Zehntelminuten** statt Sekunden — genau die
|
||
Auflösung der Rohdaten. Bis 2026-07-27 wurde hier in Sekunden umgerechnet
|
||
(`11h36m54s`); die sprangen aber in 6-Sekunden-Schritten, weil die Montierung sie
|
||
gar nicht hergibt. Die kürzere Form lässt zugleich Platz für die **Beschriftung**
|
||
`RA` / `DE` (`config.LABEL_RA`, `LABEL_DEC`).
|
||
|
||
Bei Hochpräzision entfällt die Beschriftung: Dort braucht die Deklination mit
|
||
`+38°47'01"` alle Zellen. `:U#` ist ein schreibendes Kommando und wird hier
|
||
bewusst nicht automatisch gesendet.
|
||
|
||
`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:
|
||
|
||
```python
|
||
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`):
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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.json`
|
||
bleibt unberührt. Genau dafür prüft `settings.py` jeden Wert auf Typ und Bereich.
|
||
- **Gerät fällt aus** → Der **Last Will** sorgt dafür, dass der Broker von sich aus
|
||
`online = 0` meldet. Ohne das bliebe `online = 1` stehen, 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-*Sender*ausgang 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 schmale `1` wirkt etwas luftig — Font-Eigenschaft, nur per eigenem
|
||
Font (microSYST-PC-Software) änderbar.
|