4461035777
Zwischen Mock und echter GM4000 umschalten, ohne zu deployen und ohne Neustart:
mosquitto_pub -t grossanzeige/set/mount_host -m 192.168.1.115
mosquitto_pub -t grossanzeige/set/mount_host -m 192.168.178.26
mosquitto_pub -t grossanzeige/set/mount_host -m ''
Die Adresse liegt in settings.json auf dem Geraet und uebersteht damit
Stromausfall und Watchdog-Neustart. deploy.sh kopiert die Datei bewusst nicht
mit, die Einstellung ueberlebt also auch ein Deployen.
**Leer heisst: config.MOUNT_HOST gilt.** mount_client.resolve_host loest auf in
der Reihenfolge: ausdruecklich mitgegebener Host (REPL, run_display --host) ->
mount_host aus settings.json -> config.MOUNT_HOST. Ohne diesen Rueckweg wuerde
eine einmal per MQTT gesetzte Adresse die Zeile in config.py fuer immer
verdecken, auch nach einem Deployen mit geaendertem Code.
Pruefung: settings._pruefe unterscheidet jetzt Zahl und Text am Typ der Vorgabe
in DEFAULTS -- eine neue Einstellung wird weiterhin an genau einer Stelle
eingetragen. Fuer mount_host sind nur Buchstaben, Ziffern, Punkt und
Bindestrich erlaubt, hoechstens 63 Zeichen: Der Wert geht ungeprueft in
socket.getaddrinfo, und von MQTT kommt alles Moegliche. Ungueltiges wird
verworfen, settings.json bleibt unberuehrt.
Sofort wirksam: Der Rueckruf traegt die neue Adresse in den MountClient ein und
trennt nur die bestehende Verbindung -- neu verbindet die Poll-Schleife im
naechsten Durchlauf, mit der gewohnten Fehlerbehandlung. Antwortet dort niemand,
steht "KEIN LINK", und eine weitere Nachricht holt das Geraet zurueck.
grossanzeige/status/mount_host meldet, welche Adresse gerade abgefragt wird --
die Rueckmeldung steht direkt hinter bridge.check() und nicht im Erfolgspfad,
kommt also auch dann, wenn die Montierung schweigt.
Der on_change-Rueckruf ist als run_esp32.make_on_change aus main() herausgeloest.
test_mqtt.py prueft ihn bisher als Abschrift ("Dieselbe Logik wie in
run_esp32.main"); jetzt laeuft der echte Code durch die Tests.
201 Tests gruen. Neu, unter anderem:
- TestUmschaltenImLaufendenBetrieb faehrt die ganze Kette an echten Sockets:
erst "KEIN LINK" an einer toten Adresse, nach dem set/mount_host die
Koordinaten vom Mock.
- TestAdresswahl (test_lx200) haelt die Reihenfolge von resolve_host fest.
- test_esp32: main() ohne host nimmt nach einem Neustart, was gespeichert ist.
Nicht angefasst: run_display.py auf dem Mac behaelt config.MOUNT_HOST als
Vorgabe fuer --host. Dort gibt es keine MQTT-Bruecke, und settings.json ist
Zustand des Geraets, nicht des Macs.
Am Geraet bestaetigt (2026-07-29), am Broker mitgeschnitten. Umschalten im
laufenden Betrieb, mit dem Mock auf dem Mac als Ausgangslage:
0.0s status/mount_host 192.168.178.26 Ausgangslage, link = 1
-> set/mount_host = 192.168.178.99
5.1s status/mount_host 192.168.178.99 Rueckmeldung
5.9s status/link 0 dort antwortet niemand
-> set/mount_host = ''
17.8s status/mount_host 192.168.178.26 Vorgabe aus config.py
18.6s status/link 1 Koordinaten laufen wieder
Und ueber den Neustart hinweg -- der eigentliche Zweck des Speicherns:
-> set/mount_host = 192.168.178.99
1.6s status/mount_host 192.168.178.99
settings.json auf dem Geraet:
{"hysterese": 150, "mount_host": "192.168.178.99", "schwelle": 1800, ...}
-> Hardware-Reset
8.6s status/online 0 Last Will
15.5s status/online 1 Autostart durch
15.5s status/mount_host 192.168.178.99 aus der Datei, nicht aus config.py
Die alte settings.json ohne mount_host wird beim Laden einfach um die Vorgabe
ergaenzt; die eingestellten Helligkeiten bleiben unberuehrt. Eine Migration war
nicht noetig.
set/mount_host wird ohne Retain-Flag geschickt: Das ist ein Befehl, kein
Zustand. Retained laege er im Broker und wuerde bei jedem Reconnect erneut
zugestellt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
211 lines
10 KiB
Python
211 lines
10 KiB
Python
"""Zentrale Einstellungen fuer die Ansteuerung der Grossanzeige.
|
|
|
|
Laeuft unveraendert unter CPython (Mac) und MicroPython (XIAO ESP32-C3); was
|
|
sich zwischen beiden unterscheidet, haengt an MICROPYTHON.
|
|
"""
|
|
|
|
import sys
|
|
|
|
import migra
|
|
|
|
# True auf dem ESP32-C3, False auf dem Mac. Steuert, welchen Transport
|
|
# display.open_display waehlt und ob der LDR ueber den ADC gelesen werden kann.
|
|
MICROPYTHON = sys.implementation.name == "micropython"
|
|
|
|
# -- Serielle Schnittstelle -----------------------------------------------
|
|
# Auslieferzustand der Anzeige laut Handbuch (Kap. 4.1) und identisch zur
|
|
# UART-Init des alten MSP430-Programms: 19200 8E1.
|
|
BAUDRATE = 19200
|
|
PARITY = "E"
|
|
BYTESIZE = 8
|
|
STOPBITS = 1
|
|
|
|
# CPython: Geraetedatei des USB-RS232-Wandlers.
|
|
PORT = "/dev/cu.usbserial-120"
|
|
|
|
# ESP32-C3: UART1 mit frei zugeordneten Pins. Die Nummern stammen aus dem
|
|
# Schaltplan (Hardware/KiCad/Grossanzeige) -- am XIAO ist Pad 7 = D6 = GPIO21
|
|
# und Pad 8 = D7 = GPIO20. Von dort geht es an den MAX3232:
|
|
# GPIO21 (TX) -> T2IN, T2OUT -> J5 Pin 3 -> Anzeige RxD (Sub-D Pin 2)
|
|
# GPIO20 (RX) <- R2OUT, R2IN <- J5 Pin 2 <- Anzeige TxD (Sub-D Pin 3)
|
|
# UART0 bleibt frei, das ist die REPL-Konsole ueber USB.
|
|
UART_ID = 1
|
|
UART_TX = 21
|
|
UART_RX = 20
|
|
|
|
# -- Telegramm ------------------------------------------------------------
|
|
DISPLAY_ADDR = 1 # Zieladresse der Anzeige (DA = 0x81)
|
|
HOST_ADDR = 0 # unsere Quelladresse (SA = 0x80)
|
|
USE_CHECKSUM = False # das alte Programm fuhr ohne Pruefsumme
|
|
# Antworttelegramm als Flusskontrolle: Rueckweg verdrahtet und verifiziert
|
|
# (2026-07-16), Anzeige liefert 02 80 81 80 30 03. Nach Erhalt der Antwort darf
|
|
# sofort das naechste Telegramm folgen; der Fehlercode wird ausgewertet.
|
|
WANT_RESPONSE = True
|
|
RESPONSE_TIMEOUT = 1.0 # Sekunden (nur relevant, wenn WANT_RESPONSE = True)
|
|
QUIET_TIME = 0.4 # Pause zwischen Telegrammen, wenn ohne Antwort gefahren wird
|
|
|
|
# -- Darstellung ----------------------------------------------------------
|
|
# Zeichensatz 0, Zeile 1 ab Pixelzeile 0, Zeile 2 ab Pixelzeile 9 (aus dem alten
|
|
# C-Programm). Am echten Geraet ausgemessen (2026-07-15):
|
|
# CHARSET_SPACED=True -> ESC z (Sperrschrift, alle Zeichen gleich breit) -- vom
|
|
# Nutzer bevorzugt, weil sich sonst schmale Ziffern beruehren.
|
|
# CHARSET_SPACED=False -> ESC Z (proportional, schmaler, ~11 Zeichen, aber die "1"
|
|
# klebt an Nachbarn).
|
|
CHARSET = 0
|
|
CHARSET_SPACED = True
|
|
LINE_Y = (0, 9)
|
|
|
|
# 6 px pro Zeichenzelle -- mit "probe.py pitch 6" am Geraet gemessen (2026-07-27).
|
|
#
|
|
# Die Messung stellt den Auto-Vorschub der Anzeige gegen feste Pixelabstaende und
|
|
# hat beides zugleich gezeigt:
|
|
# - Auto-Vorschub der Sperrschrift ESC z ist 7 px. Eine Zeile aus einem Stueck
|
|
# bricht deshalb nach 9 Zeichen um (9*7 = 63 von 64 px). Frueher stand hier
|
|
# darum 9 -- das war der Vorschub, nicht die Zellenbreite.
|
|
# - Die Zeichenmatrix selbst ist nur 6 px breit: zehn Ziffern, einzeln auf
|
|
# x = i*6 gesetzt, stehen sauber und getrennt nebeneinander (60 von 64 px).
|
|
#
|
|
# Deshalb positioniert display._emit_line jedes Zeichen selbst, statt den
|
|
# Auto-Vorschub laufen zu lassen.
|
|
CELL_WIDTH = 6 # Pixel pro Zeichenzelle
|
|
DISPLAY_WIDTH = 64 # Pixelbreite der Anzeige
|
|
|
|
# Obergrenze in *Zeichen*. Die harte Grenze ist DISPLAY_WIDTH in Pixeln: Seit
|
|
# schmale Zeichen weniger Platz brauchen (CELL_NARROW), haengt die Breite einer
|
|
# Zeile davon ab, welche Zeichen darin stehen. 11 gehen nur auf, wenn mindestens
|
|
# eines schmal ist -- "RA:18h51.0m" braucht mit seinem Dezimalpunkt 63 der 64 px,
|
|
# "DE:+38°47'" waere als 11 volle Zellen dagegen 66 px und damit zu breit.
|
|
# display._aufbereiten schneidet deshalb nach Pixeln ab, nicht nach Zeichen.
|
|
CHARS_PER_LINE = 11
|
|
|
|
# Breite, die die Anzeige beim Schreiben eines Zeichens tatsaechlich bemalt --
|
|
# mehr als CELL_WIDTH. Daher auch der Auto-Vorschub von 7 px. Wichtig fuer das
|
|
# Nachfuehren einzelner Zellen: Ein neu geschriebenes Zeichen loescht die erste
|
|
# Pixelspalte seines rechten Nachbarn, der muss also mitgeschrieben werden
|
|
# (siehe display._zelle_faellig).
|
|
GLYPH_WIDTH = 7
|
|
|
|
# Zeichen, die weniger Platz brauchen als eine volle Zelle -- in der Sperrschrift
|
|
# stehen sie sonst sehr luftig zwischen den Ziffern. Je 3 px am Geraet
|
|
# ausprobiert (2026-07-27); weil jedes Zeichen ohnehin einzeln positioniert wird,
|
|
# kostet das nichts.
|
|
# "." Dezimalpunkt der Zehntelminuten (RA 18h37.1m)
|
|
# " " Abstand zwischen Beschriftung und Wert (RA 18h37.1m)
|
|
# Achtung: Das gilt auch fuer die Leerzeichen, mit denen eine Zeile aufgefuellt
|
|
# wird. display._aufbereiten rechnet beim Auffuellen darum bewusst mit der vollen
|
|
# Zellenbreite -- sonst reichte die letzte Zelle ueber den Rand hinaus.
|
|
CELL_NARROW = {".": 3, " ": 3}
|
|
|
|
# Alle Zeilen beginnen linksbuendig bei LINE_X. Bis 2026-07-27 gab es einen
|
|
# Schalter zum Zentrieren; er ist entfallen, weil die Zeilen seit den schmalen
|
|
# Zeichen (CELL_NARROW) unterschiedlich breit sind und zentriert um wenige Pixel
|
|
# gegeneinander versetzt standen. Mit der Beschriftung "RA:" / "DE:" sollen sie
|
|
# buendig untereinander beginnen.
|
|
#
|
|
# LINE_X = 0, ganz am Rand: Die RA-Zeile braucht mit der Minutenangabe am Ende
|
|
# ("RA:18h51.0m") 63 der 64 Pixel -- fuer einen Rand ist da kein Platz mehr.
|
|
LINE_X = 0
|
|
|
|
COLOR = migra.RED # einfarbig rote Anzeige
|
|
BACKGROUND = migra.BLACK
|
|
BRIGHTNESS = 100 # Prozent
|
|
|
|
# Gradzeichen: Im Standard-Zeichensatz 0 der Anzeige fehlt es. Der von der
|
|
# Sternwarte geladene Zeichensatz 1 hat an Position "/" (0x2F) ein hochgestelltes
|
|
# Gradzeichen -- verifiziert 2026-07-15, deckt sich mit dem alten Programm
|
|
# (asciitaf.h: Code 0xAF, per & 0x7F = 0x2F in Zeichensatz 1). Zur Ausgabe wird
|
|
# fuer dieses eine Zeichen kurz auf DEGREE_CHARSET umgeschaltet.
|
|
DEGREE_MARKER = "°" # Platzhalter im formatierten Text (siehe coords.py)
|
|
DEGREE_CHARSET = 1 # Zeichensatz mit dem Gradzeichen; None -> Fallback nutzen
|
|
DEGREE_CHAR = "/" # Zeichen, das in DEGREE_CHARSET als Grad erscheint
|
|
DEGREE_FALLBACK = "d" # Ersatz, falls DEGREE_CHARSET = None
|
|
|
|
# -- Montierung (Phase 2) -------------------------------------------------
|
|
# 10Micron GM4000 HPS II: LX200-Protokoll ueber TCP. Die IP der Montierung
|
|
# eintragen, sobald sie erreichbar ist; Port 3490 ist die Vorgabe von 10Micron.
|
|
#
|
|
# MOUNT_HOST ist nur die **Vorgabe**: Im Betrieb laesst sich die Adresse ueber
|
|
# MQTT umstellen (<PREFIX>/set/mount_host), sie liegt dann in settings.json und
|
|
# ueberlebt einen Stromausfall. Eine leere Nachricht an dieses Topic holt die
|
|
# Adresse von hier zurueck. Wer wissen will, was gerade gilt, liest
|
|
# <PREFIX>/status/mount_host oder ruft mount_client.resolve_host().
|
|
# Vorübergehend auf den Mock auf dem Mac gestellt (2026-07-27), weil die echte
|
|
# Montierung vom ESP32 aus nicht erreichbar ist: Sie steht in 192.168.1.115, der
|
|
# ESP32 im Heimnetz 192.168.178.x. Zum Umstellen die beiden Zeilen tauschen.
|
|
MOUNT_HOST = "192.168.178.26" # Mock auf dem Mac (mount_mock.py --lan --move --low)
|
|
# MOUNT_HOST = "192.168.1.115" # echte GM4000 (eingetragen 2026-07-18)
|
|
MOUNT_PORT = 3490
|
|
MOUNT_TIMEOUT = 3.0 # Sekunden fuer Verbindung/Antwort
|
|
POLL_INTERVAL = 1.0 # Sekunden zwischen zwei Koordinatenabfragen
|
|
|
|
# Koordinaten-Praezision der Montierung.
|
|
# False = Niedrigpraezision (10Micron-Standard): RA als HH:MM.T (Zehntel-Minuten),
|
|
# DEC als sDD MM (ganze Bogenminuten) -- die Montierung sendet keine Sekunden.
|
|
# Die Anzeige zeigt entsprechend RA mit Zehntel-Minute und DEC ohne Sekunden.
|
|
# True = Hochpraezision (Montierung mit ":U#" umgestellt): RA/DEC mit Sekunden.
|
|
HIGH_PRECISION = False
|
|
|
|
# Beschriftung der beiden Zeilen:
|
|
#
|
|
# RA 18h37.1m
|
|
# DE +38°47'
|
|
#
|
|
# Der Abstand ist ein Leerzeichen, das ueber CELL_NARROW nur 3 px breit gesetzt
|
|
# wird -- ein Doppelpunkt stand hier zwischenzeitlich, wirkte in Zeichensatz 0
|
|
# aber zu wuchtig (2026-07-27).
|
|
#
|
|
# Die Beschriftung gibt es nur bei Niedrigpraezision: Dort liefert die Montierung
|
|
# ohnehin nur Zehntelminuten und ganze Bogenminuten, das laesst den Platz. Bei
|
|
# Hochpraezision braucht die Deklination alle Zellen (+38°47'01") -- dann
|
|
# entfaellt sie.
|
|
LABEL_RA = "RA "
|
|
LABEL_DEC = "DE "
|
|
|
|
# Anzeige, wenn die Montierung (noch) nicht erreichbar ist.
|
|
# Achtung: hoechstens CHARS_PER_LINE Zeichen, sonst wird abgeschnitten --
|
|
# "MONTIERUNG" stand hier zuerst und erschien am Geraet als "MONTIERUN".
|
|
NOLINK_LINES = ("KEIN LINK", "TELESKOP")
|
|
|
|
# -- Dauerbetrieb (nur ESP32-C3) ------------------------------------------
|
|
# Watchdog: Kommt die Poll-Schleife so lange nicht mehr durch, startet die
|
|
# Hardware das Geraet neu. 120 s sind reichlich bemessen -- im Normalbetrieb
|
|
# wird jede Sekunde gefuettert. Der Wert muss ueber allem liegen, was regulaer
|
|
# blockieren darf: WLAN-Aufbau bis WIFI_TIMEOUT (20 s), Montierung
|
|
# MOUNT_TIMEOUT (3 s), dazu ein MQTT-Verbindungsversuch. Sonst startet das
|
|
# Geraet mitten im normalen Betrieb neu.
|
|
#
|
|
# Achtung: Ein einmal gestarteter Watchdog laesst sich nicht mehr abschalten.
|
|
# Deshalb legt ihn nur main.py an, nicht der Handbetrieb ueber die REPL.
|
|
WDT_TIMEOUT = 120.0 # Sekunden
|
|
|
|
# Wartezeit beim Autostart, bevor main.py die Schleife startet. In diesem
|
|
# Fenster bricht Ctrl-C ab und man kommt an die REPL -- ohne das waere ein
|
|
# Geraet mit fehlerhaftem main.py nur noch ueber ein Neuflashen erreichbar.
|
|
AUTOSTART_DELAY = 3.0 # Sekunden
|
|
|
|
# Nach einem Absturz in main.py: so lange warten, dann neu starten. Die Pause
|
|
# verhindert eine Neustart-Schleife im Sekundentakt, wenn etwas dauerhaft
|
|
# kaputt ist (und gibt Zeit, mit Ctrl-C einzugreifen).
|
|
RESTART_DELAY = 10.0 # Sekunden
|
|
|
|
# -- WLAN (nur ESP32-C3) --------------------------------------------------
|
|
# Zugangsdaten stehen in wifi_config.py (nicht im Git, siehe
|
|
# wifi_config_example.py). Hier nur das Zeitverhalten.
|
|
WIFI_TIMEOUT = 20.0 # Sekunden aufs Verbinden warten
|
|
WIFI_RETRY = 5.0 # Pause vor einem neuen Verbindungsversuch
|
|
|
|
# Anzeige, solange noch kein WLAN steht.
|
|
NOWIFI_LINES = ("KEIN WLAN", " ... ")
|
|
|
|
# -- Helligkeitssensor (LDR, nur ESP32-C3) --------------------------------
|
|
# LDR an Pad 1 (D0/A0) = GPIO2, auf dem C3 ein Kanal von ADC1.
|
|
LDR_PIN = 2
|
|
|
|
# Der Spannungsteiler auf der Platine ist 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 in brightness.py zugeschnitten sind. Deshalb wird der
|
|
# Rohwert gespiegelt (siehe brightness.raw_to_ldr).
|
|
LDR_INVERTED = True
|
|
LDR_MAX = 4095 # 12-bit-ADC, voller Ausschlag
|