dae7694a07
Seit der Einzelpositionierung blitzten bei jeder Aktualisierung willkuerliche Pixel auf, in beiden Zeilen. Ursache ist die Telegrammlaenge: Eine volle Zeile kostet mit einzeln gesetzten Zeichen rund 100 Bytes statt 20, bei 19200 Baud und 8E1 (11 Bit je Byte) dauerte ein kompletter Bildwechsel damit rund 300 ms. Die Anzeige stellt schon waehrend des Empfangs dar, also war der Aufbau zu sehen -- im Sekundentakt ein Drittel der Zeit. Von einer Sekunde zur naechsten aendert sich aber fast nichts: "18h36m56s" -> "18h36m57s" ist eine einzige Ziffer, die Deklination steht meist still. show_lines vergleicht deshalb gegen den zuletzt dargestellten Inhalt und schreibt nur die betroffenen Zellen; eine unveraenderte Zeile erzeugt gar kein Telegramm mehr. Am Geraet gemessen: 306 ms -> 66 ms je Bildwechsel. Dabei am Geraet aufgefallen und mitbehoben: Dem "s" hinter den Sekunden fehlten die linken Pixel, dem "m" hinter den Minuten ebenso -- beides Zeichen direkt rechts von einer Stelle, die sich gerade geaendert hatte. Die Anzeige malt je Zeichen 7 px breit (daher auch der 7-px-Auto-Vorschub), gesetzt wird aber auf 6-px-Raster: Ein neu geschriebenes Zeichen loescht die erste Pixelspalte seines rechten Nachbarn. Beim vollstaendigen Neuaufbau fiel das nie auf, weil der Nachbar gleich danach ohnehin neu gemalt wurde. _zelle_faellig zieht ihn jetzt mit. Weitere Details: - clear() verwirft den gemerkten Inhalt, sonst haelt der Vergleich Zellen faelschlich fuer vorhanden und die Anzeige bliebe teilweise leer. - Schlaegt ein Telegramm fehl, wird der gemerkte Inhalt der Zeile verworfen -- was angekommen ist, ist dann ungewiss. - Der Zeichensatz wird nur noch bei der ersten Ausgabe einer Zeile gesetzt. Das spart nicht nur Bytes: Jeder Wechsel beschaedigt die Home-Zelle. Findet doch einer statt (neues Gradzeichen), wird die oberste Zeile vollstaendig nachgezogen. Zu beiden Befunden gibt es Tests mit Gegenprobe (mit der alten Fassung schlagen sie fehl). 155 Tests gruen, am Geraet bestaetigt: ruhig, keine fehlenden Pixel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
578 lines
29 KiB
Markdown
578 lines
29 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
|
||
|
||
# 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. | ✓ |
|
||
| `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)
|
||
```
|
||
|
||
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 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.
|
||
- **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 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 `CHARS_PER_LINE` passt — bei Niedrigpräzision entsprechend
|
||
`+38°47'` ohne Sekunden.
|
||
|
||
- **Zeilen werden zentriert** (`CENTER_LINES = True`). Das wirkt auf zwei Ebenen:
|
||
`LINE_X` rü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.
|
||
|
||
- **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 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:
|
||
|
||
```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.
|