RA in Zehntelminuten mit Beschriftung: "RA 18h36.9m" / "DE +38°47'"

Die GM4000 liefert im Auslieferzustand keine Sekunden, sondern Zehntelminuten.
Bisher wurde daraus eine Sekundenanzeige gerechnet -- die sprang in
6-Sekunden-Schritten, weil die Rohdaten sie gar nicht hergeben. Die Zehntelform
ist ehrlicher und kuerzer, und der gewonnene Platz traegt die Beschriftung:

    RA 18h36.9m
    DE +38°47'

Bei Hochpraezision entfaellt die Beschriftung, dort braucht die Deklination mit
+38°47'01" alle Zellen.

Der Mock konnte das gar nicht nachstellen -- er sendete immer das
Hochpraezisionsformat. Neu: "mount_mock.py --low" bildet den Auslieferzustand ab
(RA als HH:MM.T, DEC ohne Bogensekunden). Vorgabe bleibt hoch, damit die
bestehenden Tests mit ihren festen Erwartungen unveraendert durchlaufen.

Zur Darstellung, alles am Geraet ausprobiert:

- **Schmale Zeichen** (config.CELL_NARROW): Dezimalpunkt und der Abstand
  zwischen Beschriftung und Wert bekommen 3 statt 6 px -- in der Sperrschrift
  standen sie sonst sehr luftig. Fuer die Leerzeichen am Zeilenende gilt das
  nicht, die sollen den Rest der Zeile ueberschreiben.
- Damit entscheidet die **Pixelbreite** ueber das Abschneiden, nicht mehr die
  Zeichenzahl: "RA 18h36.9m" sind 11 Zeichen und trotzdem nur 60 px.
- **Linksbuendig ab LINE_X = 0**, der Schalter zum Zentrieren ist entfallen.
  Zentriert standen die Zeilen um wenige Pixel gegeneinander versetzt, weil die
  RA-Zeile mit ihrem schmalen Punkt kuerzer ist als die DEC-Zeile.

Ein Zwischenschritt, der wieder verworfen wurde und als Warnung im Code steht:
Ein Doppelpunkt als Trenner ("RA:18h36.9m") sah in Zeichensatz 0 zu wuchtig aus;
der aus Zeichensatz 1 ist schlanker, steht aber mitten in der obersten Zeile --
und jeder Zeichensatzwechsel zerstoert die Home-Zelle. Das erste Zeichen danach
noch einmal zu schreiben machte es schlimmer: Das nachgezogene Zeichen bemalt
GLYPH_WIDTH Pixel und frisst die linke Spalte seines rechten Nachbarn. Deshalb
steht in der obersten Zeile jetzt bewusst kein Sonderzeichen.

161 Tests gruen, am Geraet bestaetigt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-27 18:23:07 +02:00
parent dae7694a07
commit 433c53aabd
10 changed files with 468 additions and 140 deletions
+34 -17
View File
@@ -23,6 +23,8 @@ 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 # ohne Teleskop: lokaler Mock, RA läuft
python3 run_display.py --mock --move --ldr 2000 # zusätzlich mit Helligkeitsregelung 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 # 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: # Helligkeit aus einem LDR-Wert setzen:
python3 brightness.py --ldr 1500 # LDR-Rohwert -> Helligkeit -> senden python3 brightness.py --ldr 1500 # LDR-Rohwert -> Helligkeit -> senden
@@ -310,16 +312,23 @@ die Reihenfolge der Telegramme statt der Reihenfolge innerhalb eines Telegramms.
- Die Deklination passt damit in **voller Form mit abschließendem `"`** - Die Deklination passt damit in **voller Form mit abschließendem `"`**
(`+38°47'01"`, 10 Zeichen). `coords.format_dec` wählt automatisch die breiteste (`+38°47'01"`, 10 Zeichen). `coords.format_dec` wählt automatisch die breiteste
Form, die in `CHARS_PER_LINE` passt — bei Niedrigpräzision entsprechend Form, die in die Zeile passt — bei Niedrigpräzision entsprechend `+38°47'`
`+38°47'` ohne Sekunden. ohne Sekunden.
- **Zeilen werden zentriert** (`CENTER_LINES = True`). Das wirkt auf zwei Ebenen: - **Schmale Zeichen bekommen weniger Platz** (`config.CELL_NARROW`). Der
`LINE_X` rückt den Zeichenblock in die Mitte (10·6 = 60 px lassen je 2 px Rand), Dezimalpunkt der Zehntelminuten und der Abstand zwischen Beschriftung und Wert
und kürzere Texte werden beidseitig mit Leerzeichen aufgefüllt. Der Block bleibt belegen statt 6 nur 3 px — in der Sperrschrift standen sie sonst sehr luftig.
dabei immer gleich breit und an derselben Stelle — so überschreibt er bei jedem glich ist das nur, weil ohnehin jedes Zeichen einzeln positioniert wird.
Durchlauf denselben Bereich und lässt keine Reste stehen. Ein pixelgenau Für die Leerzeichen am Zeilenende gilt es *nicht*: Die sollen den Rest der
mitwandernder Offset könnte das nicht; dafür ist die Zentrierung nur auf eine Zeile überschreiben und behalten die volle Breite.
halbe Zelle (3 px) genau, wenn die Restbreite ungerade ist.
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 - **Es wird nur geschrieben, was sich geändert hat.** `show_lines` vergleicht gegen
den zuletzt dargestellten Inhalt und schickt nur die betroffenen Zellen; eine den zuletzt dargestellten Inhalt und schickt nur die betroffenen Zellen; eine
@@ -353,15 +362,23 @@ 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; Gradzeichen kommt als Byte `0xDF` — auf der Anzeige korrekt als Gradring dargestellt;
in der Terminal-Konsole erscheint der **Rohwert** dieses Bytes als `ß` (rein kosmetisch). in der Terminal-Konsole erscheint der **Rohwert** dieses Bytes als `ß` (rein kosmetisch).
`config.HIGH_PRECISION` steuert die DEC-Darstellung: `config.HIGH_PRECISION` steuert die ganze Darstellung — sie folgt dem, was die
- `False` (Vorgabe): DEC ohne Sekunden (`-00°55'`). Montierung wirklich liefert:
- `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 | | Niedrigpräzision (Vorgabe) | Hochpräzision (nach `:U#`) |
kommen die Sekunden aus den Zehntel-Minuten und springen daher in 6-Sekunden-Schritten |---|---|---|
(kein Dezimalpunkt — der wirkt in der Sperrschrift zu luftig). | 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 `coords.format_ra`/`format_dec` nehmen `high_precision=` als Parameter (Vorgabe aus
`config`). Die Montierungs-Rohwerte werden im `coords`-Parser unverändert korrekt `config`). Die Montierungs-Rohwerte werden im `coords`-Parser unverändert korrekt
+57 -18
View File
@@ -55,33 +55,56 @@ CHARSET = 0
CHARSET_SPACED = True CHARSET_SPACED = True
LINE_Y = (0, 9) LINE_Y = (0, 9)
# 10 Zeichen zu 6 px -- mit "probe.py pitch 6" am Geraet entschieden (2026-07-27). # 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 # Die Messung stellt den Auto-Vorschub der Anzeige gegen feste Pixelabstaende und
# hat beides zugleich gezeigt: # hat beides zugleich gezeigt:
# - Auto-Vorschub der Sperrschrift ESC z ist 7 px. Eine Zeile aus einem Stueck # - 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). Daher stand hier # bricht deshalb nach 9 Zeichen um (9*7 = 63 von 64 px). Frueher stand hier
# frueher 9 -- das war der Vorschub, nicht die Zellenbreite. # darum 9 -- das war der Vorschub, nicht die Zellenbreite.
# - Die Zeichenmatrix selbst ist aber nur 6 px breit: zehn Ziffern, einzeln auf # - 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). # x = i*6 gesetzt, stehen sauber und getrennt nebeneinander (60 von 64 px).
# #
# Deshalb positioniert display._emit_line jedes Zeichen selbst, statt den # Deshalb positioniert display._emit_line jedes Zeichen selbst, statt den
# Auto-Vorschub laufen zu lassen. So passen 10 Zeichen, und die DEC-Form bekommt # Auto-Vorschub laufen zu lassen.
# ihr abschliessendes " (siehe coords.format_dec). CELL_WIDTH = 6 # Pixel pro Zeichenzelle
CHARS_PER_LINE = 10
CELL_WIDTH = 6 # Pixel pro Zeichenzelle bei fester Positionierung
DISPLAY_WIDTH = 64 # Pixelbreite der Anzeige DISPLAY_WIDTH = 64 # Pixelbreite der Anzeige
# Zeilen mittig setzen statt linksbuendig. Wirkt auf zwei Ebenen: # Obergrenze in *Zeichen*. Die harte Grenze ist DISPLAY_WIDTH in Pixeln: Seit
# - LINE_X rueckt den ganzen Zeichenblock in die Mitte: 10*6 = 60 px belegen # schmale Zeichen weniger Platz brauchen (CELL_NARROW), haengt die Breite einer
# nicht die vollen 64, das laesst je 2 px Rand. # Zeile davon ab, welche Zeichen darin stehen. 11 gehen nur auf, wenn mindestens
# - Kuerzere Texte werden in _emit_line beidseitig mit Leerzeichen aufgefuellt, # eines schmal ist -- "RA:18h51.0m" braucht mit seinem Dezimalpunkt 63 der 64 px,
# z.B. die 7 Zeichen von "+38°47'" bei Niedrigpraezision. # "DE:+38°47'" waere als 11 volle Zellen dagegen 66 px und damit zu breit.
# Der Block bleibt dabei immer gleich breit und an derselben Stelle -- er # display._aufbereiten schneidet deshalb nach Pixeln ab, nicht nach Zeichen.
# ueberschreibt so bei jedem Durchlauf denselben Bereich, ohne Reste der vorigen CHARS_PER_LINE = 11
# Anzeige stehen zu lassen. Ein pixelgenau mitwandernder Offset koennte das nicht.
CENTER_LINES = True # Breite, die die Anzeige beim Schreiben eines Zeichens tatsaechlich bemalt --
LINE_X = (DISPLAY_WIDTH - CHARS_PER_LINE * CELL_WIDTH) // 2 # 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 COLOR = migra.RED # einfarbig rote Anzeige
BACKGROUND = migra.BLACK BACKGROUND = migra.BLACK
@@ -112,6 +135,22 @@ POLL_INTERVAL = 1.0 # Sekunden zwischen zwei Koordinatenabfragen
# True = Hochpraezision (Montierung mit ":U#" umgestellt): RA/DEC mit Sekunden. # True = Hochpraezision (Montierung mit ":U#" umgestellt): RA/DEC mit Sekunden.
HIGH_PRECISION = False 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. # Anzeige, wenn die Montierung (noch) nicht erreichbar ist.
# Achtung: hoechstens CHARS_PER_LINE Zeichen, sonst wird abgeschnitten -- # Achtung: hoechstens CHARS_PER_LINE Zeichen, sonst wird abgeschnitten --
# "MONTIERUNG" stand hier zuerst und erschien am Geraet als "MONTIERUN". # "MONTIERUNG" stand hier zuerst und erschien am Geraet als "MONTIERUN".
+40 -9
View File
@@ -75,22 +75,53 @@ def _fit(candidates, width):
return candidates[-1][:width] return candidates[-1][:width]
def format_ra(value, width=None, high_precision=None): def _to_tenths(h, m, s):
"""Rektaszension formatieren, immer mit Sekunden (HHhMMmSSs). """(h, m, s) in (Stunden, Minuten, Zehntelminuten) umrechnen.
Bei Niedrigpraezision liefert die Montierung nur Zehntel-Minuten; die Ganzzahlig gerechnet, damit es unter MicroPython keine Float-Ueberraschungen
Sekunden kommen dann in 6-Sekunden-Schritten. Das Format ist dasselbe wie gibt. Der Uebertrag ist wichtig: 18:36:57 sind 36,95 Minuten und werden zu
bei Hochpraezision -- der Unterschied steckt allein in der Aufloesung der 18:37.0, nicht zu 18:36.10.
Rohdaten, nicht in der Darstellung. high_precision wird daher fuer RA nicht """
ausgewertet (nur bei DEC relevant, siehe format_dec). zehntel = ((h * 3600 + m * 60 + s) * 10 + 30) // 60 # kaufmaennisch runden
minuten, t = divmod(zehntel, 10)
h, m = divmod(minuten, 60)
return h % 24, m, t
def format_ra(value, width=None, high_precision=None):
"""Rektaszension formatieren.
Die Darstellung folgt dem, was die Montierung wirklich liefert:
Niedrigpraezision (Vorgabe): **HHhMM.T** mit Zehntelminuten -- genau die
Aufloesung, die die GM4000 in diesem Modus sendet. Sieben Zeichen, das laesst
Platz fuer die Beschriftung "RA:" auf der Anzeige (siehe
displayloop.format_coordinates). Das "h" statt eines Doppelpunkts ist
Nutzerwunsch -- der Doppelpunkt wirkt in der Sperrschrift zu wuchtig.
Hochpraezision: HHhMMmSSs mit echten Sekunden.
Bis 2026-07-27 wurde auch bei Niedrigpraezision mit Sekunden angezeigt. Die
sprangen dort in 6-Sekunden-Schritten, weil die Rohdaten sie gar nicht
hergeben -- die Zehntelform ist ehrlicher und kuerzer.
""" """
width = config.CHARS_PER_LINE if width is None else width width = config.CHARS_PER_LINE if width is None else width
high = config.HIGH_PRECISION if high_precision is None else high_precision
h, m, s = parse_ra(value) h, m, s = parse_ra(value)
return _fit([ if high:
candidates = [
"%02dh%02dm%02ds" % (h, m, s), "%02dh%02dm%02ds" % (h, m, s),
"%02d:%02d:%02d" % (h, m, s), "%02d:%02d:%02d" % (h, m, s),
"%02dh%02dm" % (h, m), "%02dh%02dm" % (h, m),
], width) ]
else:
h, m, t = _to_tenths(h, m, s)
candidates = [
"%02dh%02d.%dm" % (h, m, t), # 8: 18h36.9m
"%02dh%02d.%d" % (h, m, t), # 7: ohne Minutenzeichen
"%02dh%02dm" % (h, m), # 6: ohne Zehntel
]
return _fit(candidates, width)
def format_dec(value, width=None, high_precision=None): def format_dec(value, width=None, high_precision=None):
+111 -26
View File
@@ -115,13 +115,51 @@ class Display:
self._gezeigt = {} self._gezeigt = {}
self._zeile_schreiben(index, text) self._zeile_schreiben(index, text)
def _aufbereiten(self, text): @staticmethod
"""Auf Zeilenbreite bringen: abschneiden und auffuellen (ggf. mittig).""" def _breiten(text):
"""Breite jeder einzelnen Zelle in Pixeln.
Zeichen aus config.CELL_NARROW brauchen weniger Platz. Fuer Leerzeichen
gilt das aber nur *innerhalb* des Textes -- dort trennen sie Beschriftung
und Wert ("RA 18h36.9m"). Die Leerzeichen, mit denen die Zeile rechts
aufgefuellt wird, behalten die volle Breite: Sie sollen den Rest der
Zeile ueberschreiben, und je schmaler sie waeren, desto mehr Zellen
muesste man dafuer schreiben.
"""
ohne_fuellung = len(text.rstrip())
return [config.CELL_NARROW.get(ch, config.CELL_WIDTH)
if i < ohne_fuellung else config.CELL_WIDTH
for i, ch in enumerate(text)]
@staticmethod
def _breite(text):
"""Pixelbreite eines Textes."""
return sum(Display._breiten(text))
@staticmethod
def _aufbereiten(text):
"""Auf die Zeilenbreite bringen: abschneiden und auffuellen.
Massgeblich sind die **Pixel**, nicht die Zeichenzahl: Seit schmale
Zeichen weniger Platz brauchen (config.CELL_NARROW), passen mal 10 und
mal 11 Zeichen in dieselbe Zeile. Ein Text wird deshalb so lange
gekuerzt, bis er in DISPLAY_WIDTH passt, und nur so weit aufgefuellt, wie
eine weitere Zelle noch hineingeht. Ohne das ragte die letzte Zelle ueber
den Rand und die Anzeige quittierte mit Fehlercode 4.
"""
# Ohne str.ljust/center: die kennt MicroPython nicht (am Geraet aufgefallen). # Ohne str.ljust/center: die kennt MicroPython nicht (am Geraet aufgefallen).
platz = config.DISPLAY_WIDTH - config.LINE_X
text = text[:config.CHARS_PER_LINE] text = text[:config.CHARS_PER_LINE]
rest = config.CHARS_PER_LINE - len(text) while text and Display._breite(text) > platz:
links = rest // 2 if config.CENTER_LINES else 0 text = text[:-1]
return " " * links + text + " " * (rest - links) # Beim Auffuellen bewusst mit der *vollen* Zellenbreite rechnen, obwohl
# das Leerzeichen schmal gesetzt wird: Die Anzeige bemalt je Zeichen
# GLYPH_WIDTH Pixel, ein Leerzeichen am rechten Rand reichte sonst
# darueber hinaus.
while (len(text) < config.CHARS_PER_LINE
and Display._breite(text) + config.CELL_WIDTH <= platz):
text += " "
return text
def _zeile_schreiben(self, index, text, voll=False): def _zeile_schreiben(self, index, text, voll=False):
"""Eine Zeile nachfuehren. Liefert True, wenn der Zeichensatz gewechselt wurde. """Eine Zeile nachfuehren. Liefert True, wenn der Zeichensatz gewechselt wurde.
@@ -129,19 +167,27 @@ class Display:
voll=True schreibt alle Zellen, auch die unveraenderten. voll=True schreibt alle Zellen, auch die unveraenderten.
""" """
neu = self._aufbereiten(text) neu = self._aufbereiten(text)
alt = None if voll else self._gezeigt.get(index) pos = self._positionen(neu)
if alt == neu: alt, alt_pos = self._gezeigt.get(index, (None, None))
if voll:
alt = None
elif alt_pos != pos:
# Die Zeile hat eine andere Form als vorher (z.B. ein Dezimalpunkt
# kommt dazu, der weniger Platz braucht). Dann sitzen alle Zellen
# woanders und der Vergleich waere sinnlos -- vollstaendig neu.
alt = None
elif alt == neu:
return False # nichts zu tun, kein Telegramm return False # nichts zu tun, kein Telegramm
du = migra.DataUnit() du = migra.DataUnit()
du.attributes(fg=config.COLOR, bg=config.BACKGROUND, blink=migra.STATIC) du.attributes(fg=config.COLOR, bg=config.BACKGROUND, blink=migra.STATIC)
if alt is None: if alt is None:
# Erste Ausgabe dieser Zeile: Zeichensatz sicherheitshalber setzen. # Erste Ausgabe dieser Zeile: Zeichensatz sicherheitshalber setzen.
# Danach behaelt die Anzeige ihn, und _emit_degree schaltet nach # Danach behaelt die Anzeige ihn, und _emit_fremd schaltet nach
# jedem Gradzeichen wieder zurueck -- ein Wechsel je Telegramm waere # jedem Sonderzeichen wieder zurueck -- ein Wechsel je Telegramm
# nur unnoetiger Ballast und wuerde die Home-Zelle beschaedigen. # waere nur Ballast und wuerde die Home-Zelle beschaedigen.
du.charset(config.CHARSET, spaced=config.CHARSET_SPACED) du.charset(config.CHARSET, spaced=config.CHARSET_SPACED)
wechsel = self._emit_line(du, config.LINE_Y[index], neu, alt) wechsel = self._emit_line(du, config.LINE_Y[index], neu, alt, pos)
try: try:
self.send(du) self.send(du)
except Exception: except Exception:
@@ -151,23 +197,43 @@ class Display:
# faelschlich fuer aktuell hielten. # faelschlich fuer aktuell hielten.
self._gezeigt.pop(index, None) self._gezeigt.pop(index, None)
raise raise
self._gezeigt[index] = neu self._gezeigt[index] = (neu, pos)
return wechsel or alt is None return wechsel or alt is None
@staticmethod @staticmethod
def _zelle_faellig(neu, alt, i): def _positionen(text):
"""x-Position jeder Zelle einer Zeile.
Normalerweise ein festes Raster aus CELL_WIDTH. Zeichen aus
config.CELL_NARROW bekommen weniger Platz -- der Dezimalpunkt der
Zehntelminuten stuende sonst sehr luftig zwischen den Ziffern.
"""
breiten = Display._breiten(text)
x = config.LINE_X
aus = []
for b in breiten:
aus.append(x)
x += b
return aus
@staticmethod
def _zelle_faellig(neu, alt, i, pos):
"""Muss Zelle i neu geschrieben werden? """Muss Zelle i neu geschrieben werden?
Ja, wenn sie sich selbst geaendert hat -- oder wenn ihr **linker** Ja, wenn sie sich selbst geaendert hat -- oder wenn ihr **linker**
Nachbar neu geschrieben wird: Der reicht 1 px in diese Zelle hinein und Nachbar neu geschrieben wird und bis in diese Zelle hineinragt: Die
loescht deren erste Pixelspalte (siehe _emit_line). Anzeige bemalt je Zeichen GLYPH_WIDTH Pixel, mehr als eine Zelle breit
ist, und loescht damit die erste Pixelspalte hier (siehe _emit_line).
""" """
def anders(k): def anders(k):
return k >= len(alt) or alt[k] != neu[k] return k >= len(alt) or alt[k] != neu[k]
return anders(i) or (i > 0 and anders(i - 1)) if anders(i):
return True
return (i > 0 and anders(i - 1)
and pos[i] < pos[i - 1] + config.GLYPH_WIDTH)
def _emit_line(self, du, y, text, alt=None): def _emit_line(self, du, y, text, alt=None, pos=None):
"""Zellen einer Zeile ausgeben. Liefert True bei Zeichensatzwechsel. """Zellen einer Zeile ausgeben. Liefert True bei Zeichensatzwechsel.
Jedes Zeichen wird einzeln auf x = LINE_X + i*CELL_WIDTH gesetzt, statt Jedes Zeichen wird einzeln auf x = LINE_X + i*CELL_WIDTH gesetzt, statt
@@ -189,26 +255,45 @@ class Display:
Am Geraet aufgefallen (2026-07-27): Beim Sekundenwechsel fehlten dem "s" Am Geraet aufgefallen (2026-07-27): Beim Sekundenwechsel fehlten dem "s"
die linken Pixel, beim Minutenwechsel dem "m". die linken Pixel, beim Minutenwechsel dem "m".
""" """
if pos is None:
pos = self._positionen(text)
wechsel = False wechsel = False
for i in range(len(text)): for i in range(len(text)):
if alt is not None and not self._zelle_faellig(text, alt, i): if alt is not None and not self._zelle_faellig(text, alt, i, pos):
continue continue
du.cursor(config.LINE_X + i * config.CELL_WIDTH, y) du.cursor(pos[i], y)
if text[i] == config.DEGREE_MARKER: if self._emit_zeichen(du, text[i]):
self._emit_degree(du)
wechsel = True wechsel = True
else:
du.text(text[i])
return wechsel return wechsel
def _emit_degree(self, du): def _emit_zeichen(self, du, ch):
"""Das Gradzeichen einfuegen -- ueber Zeichensatzwechsel oder als Fallback.""" """Ein Zeichen ausgeben. Liefert True, wenn der Zeichensatz wechseln musste.
Nur das Gradzeichen kommt aus einem anderen Zeichensatz -- in
Zeichensatz 0 gibt es keines.
Ein Zeichensatzwechsel beschaedigt die Home-Zelle (0,0), also den Anfang
der obersten Zeile. Solange alle Wechsel in der *unteren* Zeile
stattfinden, genuegt die Reihenfolge in show_lines (unten vor oben).
**Ein Wechsel mitten in der obersten Zeile laesst sich dagegen nicht
sauber reparieren.** Das erste Zeichen danach noch einmal zu schreiben,
wurde am Geraet ausprobiert (2026-07-27) und machte es schlimmer: Das
nachgezogene Zeichen bemalt GLYPH_WIDTH Pixel und frisst dabei die linke
Spalte seines rechten Nachbarn. Deshalb steht in der obersten Zeile
bewusst kein Sonderzeichen -- die Beschriftung nutzt ein schmales
Leerzeichen statt eines Doppelpunkts aus Zeichensatz 1.
"""
if ch == config.DEGREE_MARKER:
if config.DEGREE_CHARSET is None: if config.DEGREE_CHARSET is None:
du.text(config.DEGREE_FALLBACK) du.text(config.DEGREE_FALLBACK)
return return False
du.charset(config.DEGREE_CHARSET, spaced=config.CHARSET_SPACED) du.charset(config.DEGREE_CHARSET, spaced=config.CHARSET_SPACED)
du.text(config.DEGREE_CHAR) du.text(config.DEGREE_CHAR)
du.charset(config.CHARSET, spaced=config.CHARSET_SPACED) du.charset(config.CHARSET, spaced=config.CHARSET_SPACED)
return True
du.text(ch)
return False
def close(self): def close(self):
self._t.close() self._t.close()
+22 -1
View File
@@ -21,9 +21,30 @@ class LinkError(Exception):
def format_coordinates(ra_raw, dec_raw): def format_coordinates(ra_raw, dec_raw):
"""Rohstrings der Montierung fuer die Anzeige aufbereiten.""" """Rohstrings der Montierung fuer die Anzeige aufbereiten.
Bei **Niedrigpraezision** bekommen die Zeilen eine Beschriftung:
RA:18h36.9
DE:+38°47'
Das geht genau auf, weil die Montierung in diesem Modus ohnehin nur
Zehntelminuten (7 Zeichen) und ganze Bogenminuten (7 Zeichen) liefert -- in
zehn Zellen bleiben also drei fuer "RA:"/"DE:" uebrig. Die Zeilen sind
linksbuendig gesetzt (config.CENTER_LINES), damit beide Beschriftungen
buendig untereinander beginnen.
Bei **Hochpraezision** entfaellt die Beschriftung: Dort braucht die
Deklination mit +38°47'01" alle zehn Zeichen.
"""
if config.HIGH_PRECISION:
return coords.format_ra(ra_raw), coords.format_dec(dec_raw) return coords.format_ra(ra_raw), coords.format_dec(dec_raw)
breite_ra = config.CHARS_PER_LINE - len(config.LABEL_RA)
breite_dec = config.CHARS_PER_LINE - len(config.LABEL_DEC)
return (config.LABEL_RA + coords.format_ra(ra_raw, width=breite_ra),
config.LABEL_DEC + coords.format_dec(dec_raw, width=breite_dec))
def poll_loop(display, client, interval=None, once=False, def poll_loop(display, client, interval=None, once=False,
read_ldr=None, dimmer=None, ensure_link=None, log=None, read_ldr=None, dimmer=None, ensure_link=None, log=None,
+38 -10
View File
@@ -58,31 +58,54 @@ def _numbers(text):
return [int(x) for x in out] return [int(x) for x in out]
def _fmt_ra(total_s): def _fmt_ra(total_s, high=True):
"""Rektaszension wie die GM4000.
high=True -> HH:MM:SS (Hochpraezision, nach ":U#")
high=False -> HH:MM.T (Zehntelminuten -- so liefert die Montierung im
Auslieferzustand, verifiziert 2026-07-18)
"""
total_s %= 86400 total_s %= 86400
h, rest = divmod(total_s, 3600) h, rest = divmod(total_s, 3600)
m, s = divmod(rest, 60) m, s = divmod(rest, 60)
if high:
return "%02d:%02d:%02d" % (h, m, s) return "%02d:%02d:%02d" % (h, m, s)
zehntel = (total_s * 10 + 30) // 60 # in Zehntelminuten, gerundet
minuten, t = divmod(zehntel, 10)
h, m = divmod(minuten, 60)
return "%02d:%02d.%d" % (h % 24, m, t)
def _fmt_dec(total_arcsec): def _fmt_dec(total_arcsec, high=True):
"""Deklination wie die GM4000.
high=True -> sDD*MM:SS (Hochpraezision)
high=False -> sDD*MM (ganze Bogenminuten, Auslieferzustand)
Gradzeichen als '*', Minuten/Sekunden mit ':' -- der coords-Parser kommt mit
beiden Trennzeichen klar. (Die echte Montierung sendet 0xDF als Gradzeichen.)
"""
sign = "-" if total_arcsec < 0 else "+" sign = "-" if total_arcsec < 0 else "+"
total_arcsec = abs(total_arcsec) total_arcsec = abs(total_arcsec)
d, rest = divmod(total_arcsec, 3600) d, rest = divmod(total_arcsec, 3600)
m, s = divmod(rest, 60) m, s = divmod(rest, 60)
# Gradzeichen als '*', Minuten/Sekunden mit ':' -- der coords-Parser if high:
# kommt mit beiden Trennzeichen klar.
return "%s%02d*%02d:%02d" % (sign, d, m, s) return "%s%02d*%02d:%02d" % (sign, d, m, s)
return "%s%02d*%02d" % (sign, d, m)
class MountMock: class MountMock:
"""Simuliert die Montierung auf TCP-Ebene.""" """Simuliert die Montierung auf TCP-Ebene."""
def __init__(self, host="127.0.0.1", port=3490, def __init__(self, host="127.0.0.1", port=3490,
ra="18:36:56", dec="+38:47:01", move=False): ra="18:36:56", dec="+38:47:01", move=False, high=True):
self._ra0 = _parse_ra(ra) self._ra0 = _parse_ra(ra)
self._dec = _parse_dec(dec) self._dec = _parse_dec(dec)
self._move = move self._move = move
# high=False bildet den Auslieferzustand der GM4000 nach: keine Sekunden,
# RA als Zehntelminuten. Vorgabe bleibt True, damit bestehende Tests mit
# ihren festen Erwartungen unveraendert durchlaufen.
self._high = high
self._tick = 0 # zaehlt bei --move pro RA-Abfrage hoch self._tick = 0 # zaehlt bei --move pro RA-Abfrage hoch
self._dectick = 0 # zaehlt bei --move pro DEC-Abfrage hoch self._dectick = 0 # zaehlt bei --move pro DEC-Abfrage hoch
self._lock = threading.Lock() self._lock = threading.Lock()
@@ -116,9 +139,9 @@ class MountMock:
def _reply_for(self, cmd): def _reply_for(self, cmd):
"""Antwort (inkl. '#') fuer ein Kommando ohne ':' und ohne '#'.""" """Antwort (inkl. '#') fuer ein Kommando ohne ':' und ohne '#'."""
if cmd == "GR": if cmd == "GR":
return _fmt_ra(self._current_ra()) + "#" return _fmt_ra(self._current_ra(), self._high) + "#"
if cmd == "GD": if cmd == "GD":
return _fmt_dec(self._current_dec()) + "#" return _fmt_dec(self._current_dec(), self._high) + "#"
if cmd == "GVP": if cmd == "GVP":
return PRODUCT + "#" return PRODUCT + "#"
if cmd == "GVN": if cmd == "GVN":
@@ -204,13 +227,18 @@ def main(argv=None):
p.add_argument("--ra", default="18:36:56") p.add_argument("--ra", default="18:36:56")
p.add_argument("--dec", default="+38:47:01") p.add_argument("--dec", default="+38:47:01")
p.add_argument("--move", action="store_true", help="RA laeuft mit der Zeit hoch") p.add_argument("--move", action="store_true", help="RA laeuft mit der Zeit hoch")
p.add_argument("--low", action="store_true",
help="Niedrigpraezision wie im Auslieferzustand der GM4000: "
"RA als HH:MM.T, DEC ohne Bogensekunden")
args = p.parse_args(argv) args = p.parse_args(argv)
host = "0.0.0.0" if args.lan else args.host host = "0.0.0.0" if args.lan else args.host
mock = MountMock(host=host, port=args.port, ra=args.ra, dec=args.dec, mock = MountMock(host=host, port=args.port, ra=args.ra, dec=args.dec,
move=args.move) move=args.move, high=not args.low)
print("Mock-Montierung auf %s:%d (RA=%s DEC=%s%s)" print("Mock-Montierung auf %s:%d (RA=%s DEC=%s%s, %s)"
% (mock.host, mock.port, args.ra, args.dec, ", move" if args.move else "")) % (mock.host, mock.port, args.ra, args.dec,
", move" if args.move else "",
"Niedrigpraezision" if args.low else "Hochpraezision"))
if host == "0.0.0.0": if host == "0.0.0.0":
ip = lan_address() ip = lan_address()
if ip: if ip:
+110 -33
View File
@@ -38,7 +38,14 @@ def zellen(frames):
y = int(frame[i + 5:i + 8]) y = int(frame[i + 5:i + 8])
j = frame.find(bytes([migra.SEP]), i + 8) j = frame.find(bytes([migra.SEP]), i + 8)
if j >= 0 and j + 1 < len(frame): if j >= 0 and j + 1 < len(frame):
out[(x, y)] = chr(frame[j + 1]) ch = chr(frame[j + 1])
# Das Gradzeichen entsteht ueber einen Zeichensatzwechsel und
# steht als DEGREE_CHAR ("/") im Telegramm. Hier wieder in das
# zurueckuebersetzen, was auf der Anzeige zu sehen ist.
if (frame[i + 8:j].startswith(b"\x1bz")
and ch == config.DEGREE_CHAR):
ch = config.DEGREE_MARKER
out[(x, y)] = ch
i += 8 i += 8
else: else:
i += 1 i += 1
@@ -52,12 +59,22 @@ def zeile(frames, index):
return "".join(ch for _, ch in sorted(treffer)) return "".join(ch for _, ch in sorted(treffer))
def zentriert(text): # So viele *volle* Zellen passen in eine Zeile. Das ist weniger als
"""Erwartungswert: Text so, wie _emit_line ihn auf die Zeile legt.""" # config.CHARS_PER_LINE: Dessen 11 Zeichen gehen nur auf, wenn mindestens ein
text = text[:config.CHARS_PER_LINE] # schmales dabei ist (config.CELL_NARROW). Fuer Tests mit normalen Zeichen ist
rest = config.CHARS_PER_LINE - len(text) # das hier die Zeilenbreite.
links = rest // 2 if config.CENTER_LINES else 0 VOLLE_ZELLEN = (config.DISPLAY_WIDTH - config.LINE_X) // config.CELL_WIDTH
return " " * links + text + " " * (rest - links)
def aufbereitet(text):
"""Erwartungswert: Text so, wie er auf der Zeile landet.
Nutzt bewusst dieselbe Funktion wie der Treiber. Wie breit eine Zeile ist,
haengt seit config.CELL_NARROW davon ab, welche Zeichen darin stehen -- eine
zweite Nachbildung hier waere nur eine Fehlerquelle. Was der Wert *bedeutet*,
pruefen die Tests darunter mit eigenen Zusicherungen.
"""
return Display._aufbereiten(text)
class TestFesteTexte(unittest.TestCase): class TestFesteTexte(unittest.TestCase):
@@ -174,8 +191,8 @@ class TestNurGeaenderteZellen(unittest.TestCase):
frames = self._neu() frames = self._neu()
# Loeschtelegramm + beide Zeilen vollstaendig # Loeschtelegramm + beide Zeilen vollstaendig
self.assertEqual(len(frames), 3) self.assertEqual(len(frames), 3)
self.assertEqual(len(zeile(frames, 0)), config.CHARS_PER_LINE) self.assertEqual(len(zeile(frames, 0)), VOLLE_ZELLEN)
self.assertEqual(len(zeile(frames, 1)), config.CHARS_PER_LINE) self.assertEqual(len(zeile(frames, 1)), VOLLE_ZELLEN)
def test_nach_clear_kein_rest_der_alten_anzeige(self): def test_nach_clear_kein_rest_der_alten_anzeige(self):
"""clear muss den gemerkten Inhalt verwerfen. """clear muss den gemerkten Inhalt verwerfen.
@@ -211,7 +228,7 @@ class TestNurGeaenderteZellen(unittest.TestCase):
# Dieselbe Zeile noch einmal: muss vollstaendig geschrieben werden. # Dieselbe Zeile noch einmal: muss vollstaendig geschrieben werden.
self.d.show_lines("18h36m57s", "+38°47'01\"") self.d.show_lines("18h36m57s", "+38°47'01\"")
self.assertEqual(len(zeile(self._neu(), 0)), config.CHARS_PER_LINE) self.assertEqual(len(zeile(self._neu(), 0)), VOLLE_ZELLEN)
def test_gradzeichen_wird_nicht_staendig_neu_gesetzt(self): def test_gradzeichen_wird_nicht_staendig_neu_gesetzt(self):
"""Spart nicht nur Bytes: Jeder Zeichensatzwechsel beschaedigt (0,0).""" """Spart nicht nur Bytes: Jeder Zeichensatzwechsel beschaedigt (0,0)."""
@@ -221,6 +238,8 @@ class TestNurGeaenderteZellen(unittest.TestCase):
gesendet = b"".join(self._neu()) gesendet = b"".join(self._neu())
self.assertNotIn(b"\x1bz01", gesendet) self.assertNotIn(b"\x1bz01", gesendet)
def test_neues_gradzeichen_zieht_die_oberste_zeile_nach(self): def test_neues_gradzeichen_zieht_die_oberste_zeile_nach(self):
"""Wird der Zeichensatz doch gewechselt, ist die Home-Zelle hin. """Wird der Zeichensatz doch gewechselt, ist die Home-Zelle hin.
@@ -233,7 +252,66 @@ class TestNurGeaenderteZellen(unittest.TestCase):
frames = self._neu() frames = self._neu()
self.assertIn(b"\x1bz01", b"".join(frames)) # Wechsel fand statt self.assertIn(b"\x1bz01", b"".join(frames)) # Wechsel fand statt
# Obere Zeile trotz unveraendertem Text komplett neu geschrieben. # Obere Zeile trotz unveraendertem Text komplett neu geschrieben.
self.assertEqual(len(zeile(frames, 0)), config.CHARS_PER_LINE) self.assertEqual(len(zeile(frames, 0)), VOLLE_ZELLEN)
class TestSchmaleZeichen(unittest.TestCase):
"""Der Dezimalpunkt der Zehntelminuten braucht weniger als eine volle Zelle.
In der Sperrschrift stand "RA 18:37.1" sonst sehr luftig -- der Punkt belegt
6 px, obwohl er nur ein, zwei Pixel breit ist. 3 px am Geraet ausprobiert
(2026-07-27). Moeglich ist das nur, weil jedes Zeichen ohnehin einzeln
positioniert wird.
"""
def setUp(self):
self.t = FakeTransport(echo=False)
self.d = Display(self.t, response=True)
def _neu(self):
frames = self.t.frames[:]
self.t.frames.clear()
return frames
def test_punkt_bekommt_weniger_platz(self):
pos = Display._positionen("1.2")
self.assertEqual(pos[1] - pos[0], config.CELL_WIDTH) # vor dem Punkt
self.assertEqual(pos[2] - pos[1], config.CELL_NARROW["."]) # nach dem Punkt
def test_zeile_mit_punkt_wird_schmaler_und_bleibt_mittig(self):
def breite(positionen):
return positionen[-1] + config.CELL_WIDTH - positionen[0]
ohne = Display._positionen("RA 18:3701") # gleiche Laenge, kein Punkt
mit = Display._positionen("RA 18:37.1")
self.assertEqual(breite(ohne) - breite(mit),
config.CELL_WIDTH - config.CELL_NARROW["."])
# Weiterhin vollstaendig innerhalb der Anzeige.
self.assertGreaterEqual(mit[0], 0)
self.assertLessEqual(mit[-1] + config.CELL_WIDTH, config.DISPLAY_WIDTH)
def test_zehntel_wechsel_schreibt_weiter_nur_wenige_zellen(self):
"""Der Alltagsfall bei Niedrigpraezision -- das Nachfuehren muss greifen."""
self.d.show_lines("RA 18:37.1", "DE +38°47'")
self._neu()
self.d.show_lines("RA 18:37.2", "DE +38°47'")
frames = self._neu()
self.assertEqual(len(frames), 1) # DEC unveraendert
self.assertLessEqual(len(zellen(frames)), 2) # nur die Zehntelziffer
def test_strukturwechsel_schreibt_die_zeile_vollstaendig_neu(self):
"""Kommt ein Punkt dazu, sitzen alle folgenden Zellen woanders.
Ein Vergleich Zeichen fuer Zeichen waere dann sinnlos -- es muss alles
neu geschrieben werden.
"""
self.d.show_lines("RA 18:3701") # ohne Punkt: 10 volle Zellen
self._neu()
self.d.show_lines("RA 18:37.1") # mit Punkt: andere Positionen
gesetzt = zellen(self._neu())
# Alles neu -- und dank des schmalen Punktes passt sogar ein Zeichen mehr.
self.assertEqual(len(gesetzt), len(Display._aufbereiten("RA 18:37.1")))
self.assertGreater(len(gesetzt), VOLLE_ZELLEN)
class TestZeilenaufbau(unittest.TestCase): class TestZeilenaufbau(unittest.TestCase):
@@ -252,18 +330,28 @@ class TestZeilenaufbau(unittest.TestCase):
nicht kennt (am Geraet aufgefallen). nicht kennt (am Geraet aufgefallen).
""" """
self.d.show_lines("AB", "CD") self.d.show_lines("AB", "CD")
self.assertEqual(zeile(self._alle(), 0), zentriert("AB")) self.assertEqual(zeile(self._alle(), 0), aufbereitet("AB"))
self.assertEqual(zeile(self._alle(), 1), zentriert("CD")) self.assertEqual(zeile(self._alle(), 1), aufbereitet("CD"))
# Es wird immer die volle Breite geschrieben, damit nichts stehen bleibt. # Es wird immer die volle Breite geschrieben, damit nichts stehen bleibt.
self.assertEqual(len(zeile(self._alle(), 0)), config.CHARS_PER_LINE) self.assertEqual(len(zeile(self._alle(), 0)), VOLLE_ZELLEN)
def test_kuerzere_zeile_wird_mittig_gesetzt(self): def test_kuerzere_zeile_steht_linksbuendig(self):
"""Zentriert heisst: links und rechts gleich viel Luft (Nutzerwunsch).""" """Nutzerwunsch 2026-07-27: beide Zeilen buendig untereinander.
Zentriert standen sie um wenige Pixel gegeneinander versetzt, weil die
RA-Zeile mit ihrem schmalen Dezimalpunkt kuerzer ist als die DEC-Zeile.
"""
self.d.show_lines("+38°47'") # 7 Zeichen in 10 Zellen self.d.show_lines("+38°47'") # 7 Zeichen in 10 Zellen
text = zeile(self._alle(), 0) text = zeile(self._alle(), 0)
links = len(text) - len(text.lstrip()) self.assertEqual(text, "+38°47'" + " " * (VOLLE_ZELLEN - 7))
rechts = len(text) - len(text.rstrip())
self.assertLessEqual(abs(links - rechts), 1) def test_beide_zeilen_beginnen_an_derselben_stelle(self):
"""Der eigentliche Punkt: RA und DE muessen buendig starten."""
self.d.show_lines("RA:18h37.1", "DE:+38°47'")
gesetzt = zellen(self._alle())
erste_oben = min(x for (x, y) in gesetzt if y == config.LINE_Y[0])
erste_unten = min(x for (x, y) in gesetzt if y == config.LINE_Y[1])
self.assertEqual(erste_oben, erste_unten)
def test_jedes_zeichen_sitzt_auf_seiner_zelle(self): def test_jedes_zeichen_sitzt_auf_seiner_zelle(self):
"""Der Auto-Vorschub der Sperrschrift ist 7 px und liesse nur 9 Zeichen """Der Auto-Vorschub der Sperrschrift ist 7 px und liesse nur 9 Zeichen
@@ -272,13 +360,13 @@ class TestZeilenaufbau(unittest.TestCase):
self.d.show_lines("0123456789") self.d.show_lines("0123456789")
gesetzt = zellen(self._alle()) gesetzt = zellen(self._alle())
y = config.LINE_Y[0] y = config.LINE_Y[0]
for i in range(config.CHARS_PER_LINE): for i in range(VOLLE_ZELLEN):
self.assertIn((config.LINE_X + i * config.CELL_WIDTH, y), gesetzt) self.assertIn((config.LINE_X + i * config.CELL_WIDTH, y), gesetzt)
def test_zeile_passt_in_die_anzeigenbreite(self): def test_zeile_passt_in_die_anzeigenbreite(self):
"""Auch die letzte Zelle darf nicht ueber den rechten Rand hinausragen -- """Auch die letzte Zelle darf nicht ueber den rechten Rand hinausragen --
sonst quittiert die Anzeige mit Fehlercode 4 (ausserhalb).""" sonst quittiert die Anzeige mit Fehlercode 4 (ausserhalb)."""
rechts = config.LINE_X + config.CHARS_PER_LINE * config.CELL_WIDTH rechts = config.LINE_X + VOLLE_ZELLEN * config.CELL_WIDTH
self.assertLessEqual(rechts, config.DISPLAY_WIDTH) self.assertLessEqual(rechts, config.DISPLAY_WIDTH)
self.assertGreaterEqual(config.LINE_X, 0) self.assertGreaterEqual(config.LINE_X, 0)
@@ -287,18 +375,7 @@ class TestZeilenaufbau(unittest.TestCase):
beschaedigt die andere Zeile -- deshalb hart abschneiden.""" beschaedigt die andere Zeile -- deshalb hart abschneiden."""
lang = "X" * (config.CHARS_PER_LINE + 5) lang = "X" * (config.CHARS_PER_LINE + 5)
self.d.show_lines(lang) self.d.show_lines(lang)
self.assertEqual(zeile(self._alle(), 0), "X" * config.CHARS_PER_LINE) self.assertEqual(zeile(self._alle(), 0), "X" * VOLLE_ZELLEN)
def test_ohne_zentrierung_steht_der_text_links(self):
"""CENTER_LINES = False muss wieder linksbuendig setzen."""
alt = config.CENTER_LINES
try:
config.CENTER_LINES = False
self.d.show_lines("AB")
self.assertEqual(zeile(self._alle(), 0),
"AB" + " " * (config.CHARS_PER_LINE - 2))
finally:
config.CENTER_LINES = alt
def test_oberste_zeile_wird_zuletzt_gezeichnet(self): def test_oberste_zeile_wird_zuletzt_gezeichnet(self):
"""Schutz der Home-Zelle: ein Zeichensatzwechsel zerstoert (0,0), """Schutz der Home-Zelle: ein Zeichensatzwechsel zerstoert (0,0),
+12 -6
View File
@@ -556,13 +556,17 @@ class TestEsp32GegenMock(unittest.TestCase):
self.assertIn("Selbsttest bestanden", text) self.assertIn("Selbsttest bestanden", text)
def test_main_once_zeigt_die_koordinaten(self): def test_main_once_zeigt_die_koordinaten(self):
self._run(self.run_esp32.main, fake_display=True, once=True) """Der Mock sendet 18:36:56; bei Niedrigpraezision (config-Vorgabe) wird
daraus die Zehntelform mit Beschriftung."""
self._run(self.run_esp32.main, fake_display=True, once=True,
with_mqtt=False)
text = "\n".join(self.log) text = "\n".join(self.log)
self.assertIn("18h36m56s", text) # RA, fuer die Anzeige formatiert self.assertIn("RA 18h36.9m", text)
self.assertIn("+38", text) # DEC self.assertIn("DE +38", text)
def test_main_once_baut_ein_gueltiges_telegramm(self): def test_main_once_baut_ein_gueltiges_telegramm(self):
hexdump = self._run(self.run_esp32.main, fake_display=True, once=True) hexdump = self._run(self.run_esp32.main, fake_display=True, once=True,
with_mqtt=False)
self.assertIn("TX: 02 81 80 81", hexdump) # STX, DA, SA, FC self.assertIn("TX: 02 81 80 81", hexdump) # STX, DA, SA, FC
self.assertTrue(hexdump.strip().endswith("03")) # ETX self.assertTrue(hexdump.strip().endswith("03")) # ETX
@@ -574,7 +578,8 @@ class TestEsp32GegenMock(unittest.TestCase):
self.mock.stop() self.mock.stop()
with contextlib.redirect_stdout(io.StringIO()) as out: with contextlib.redirect_stdout(io.StringIO()) as out:
self.run_esp32.main(host="127.0.0.1", port=self.mock.port, self.run_esp32.main(host="127.0.0.1", port=self.mock.port,
fake_display=True, once=True, log=self.log.append) fake_display=True, once=True, with_mqtt=False,
log=self.log.append)
self.assertTrue(any("Montierung" in line for line in self.log)) self.assertTrue(any("Montierung" in line for line in self.log))
# Die Hinweiszeilen stehen wirklich im Telegramm -- ohne den Text hier # Die Hinweiszeilen stehen wirklich im Telegramm -- ohne den Text hier
# zu wiederholen, sonst bricht der Test bei jeder Umformulierung. # zu wiederholen, sonst bricht der Test bei jeder Umformulierung.
@@ -598,7 +603,8 @@ class TestEsp32GegenMock(unittest.TestCase):
import io import io
with contextlib.redirect_stdout(io.StringIO()): with contextlib.redirect_stdout(io.StringIO()):
self.run_esp32.main(host=self.mock.host, port=self.mock.port, self.run_esp32.main(host=self.mock.host, port=self.mock.port,
fake_display=True, once=True, log=self.log.append) fake_display=True, once=True, with_mqtt=False,
log=self.log.append)
finally: finally:
config.WIFI_TIMEOUT = orig config.WIFI_TIMEOUT = orig
text = "\n".join(self.log) text = "\n".join(self.log)
+31 -8
View File
@@ -136,14 +136,38 @@ class TestKoordinaten(unittest.TestCase):
self.assertEqual(coords.format_dec("+45d12m30s", width=7, **hp), "+45°12'") self.assertEqual(coords.format_dec("+45d12m30s", width=7, **hp), "+45°12'")
def test_formatierung_niedrigpraezision(self): def test_formatierung_niedrigpraezision(self):
"""RA in Zehntelminuten -- so, wie die Montierung sie liefert.
Bis 2026-07-27 wurde hier in Sekunden umgerechnet (11h36m54s). Die
sprangen aber in 6-Sekunden-Schritten, weil die Rohdaten sie nicht
hergeben. Die Zehntelform ist ehrlicher und laesst mit sieben Zeichen
Platz fuer die Beschriftung "RA:".
"""
lp = dict(high_precision=False) lp = dict(high_precision=False)
# RA: Zehntel-Minute (0.9 min) kommt als Sekunden (54 s) heraus # Breiteste passende Form: mit Minutenzeichen am Ende, sonst ohne.
self.assertEqual(coords.format_ra("11:36.9", width=9, **lp), "11h36m54s") self.assertEqual(coords.format_ra("11:36.9", width=9, **lp), "11h36.9m")
self.assertEqual(coords.format_ra("11:36.9", width=8, **lp), "11h36.9m")
self.assertEqual(coords.format_ra("11:36.9", width=7, **lp), "11h36.9")
self.assertEqual(coords.format_ra("11:36.9", width=6, **lp), "11h36m") self.assertEqual(coords.format_ra("11:36.9", width=6, **lp), "11h36m")
# DEC: ohne Sekunden # DEC: ohne Sekunden
self.assertEqual(coords.format_dec("-00°55", width=9, **lp), "-00°55'") self.assertEqual(coords.format_dec("-00°55", width=9, **lp), "-00°55'")
self.assertEqual(coords.format_dec("-00°55", width=6, **lp), "-00:55") self.assertEqual(coords.format_dec("-00°55", width=6, **lp), "-00:55")
def test_zehntelminuten_mit_uebertrag(self):
"""18:36:57 sind 36,95 Minuten -- das muss auf 18:37.0 aufgehen."""
lp = dict(high_precision=False, width=7)
self.assertEqual(coords.format_ra("18:36:57", **lp), "18h37.0")
self.assertEqual(coords.format_ra("18:36:00", **lp), "18h36.0")
self.assertEqual(coords.format_ra("18:36:54", **lp), "18h36.9")
# Auch ueber die Stunde und ueber Mitternacht hinweg.
self.assertEqual(coords.format_ra("18:59:58", **lp), "19h00.0")
self.assertEqual(coords.format_ra("23:59:58", **lp), "00h00.0")
def test_hochpraezision_behaelt_die_sekundenform(self):
hp = dict(high_precision=True, width=10)
self.assertEqual(coords.format_ra("18:36:56", **hp), "18h36m56s")
self.assertEqual(coords.format_dec("+38*47:01", **hp), "+38°47'01\"")
class TestDisplay(unittest.TestCase): class TestDisplay(unittest.TestCase):
@@ -173,20 +197,19 @@ class TestDisplay(unittest.TestCase):
def zelle(i, y): def zelle(i, y):
return b"\x1bC%03d%03d" % (config.LINE_X + i * config.CELL_WIDTH, y) return b"\x1bC%03d%03d" % (config.LINE_X + i * config.CELL_WIDTH, y)
# "12:34:56" hat 8 Zeichen und wird in 10 Zellen mittig gesetzt (ab Zelle 1), # Linksbuendig (config.CENTER_LINES): beide Zeilen beginnen bei Zelle 0.
# "+45:12:30" hat 9 und beginnt daher schon bei Zelle 0. self.assertIn(zelle(0, 0) + b"\x1f1", oben)
self.assertIn(zelle(1, 0) + b"\x1f1", oben) self.assertIn(zelle(1, 0) + b"\x1f2", oben)
self.assertIn(zelle(2, 0) + b"\x1f2", oben)
self.assertIn(zelle(0, 9) + b"\x1f+", unten) self.assertIn(zelle(0, 9) + b"\x1f+", unten)
def test_ohne_clear_kein_fill_aber_aufgefuellt(self): def test_ohne_clear_kein_fill_aber_aufgefuellt(self):
"""Standard (clear=False): kein Vollbild-Loeschen, Zeilen auf volle Breite.""" """Standard (clear=False): kein Vollbild-Loeschen, Zeilen auf volle Breite."""
from test_display import zeile, zentriert from test_display import zeile, aufbereitet
t = FakeTransport(echo=False) t = FakeTransport(echo=False)
Display(t, response=True).show_lines("12:34:56", clear=False) Display(t, response=True).show_lines("12:34:56", clear=False)
self.assertNotIn(b"\x1bF0", t.frames[0]) self.assertNotIn(b"\x1bF0", t.frames[0])
# auf CHARS_PER_LINE mit Leerzeichen aufgefuellt (mittig, siehe CENTER_LINES) # auf CHARS_PER_LINE mit Leerzeichen aufgefuellt (mittig, siehe CENTER_LINES)
self.assertEqual(zeile(t.frames, 0), zentriert("12:34:56")) self.assertEqual(zeile(t.frames, 0), aufbereitet("12:34:56"))
def test_gradzeichen_via_zeichensatz(self): def test_gradzeichen_via_zeichensatz(self):
"""Gradzeichen wird durch kurzen Wechsel auf Zeichensatz 1 erzeugt.""" """Gradzeichen wird durch kurzen Wechsel auf Zeichensatz 1 erzeugt."""
+2 -1
View File
@@ -398,7 +398,8 @@ class TestSchleifenanbindung(MqttTest):
self.client.fail_check = True self.client.fail_check = True
gezeigt = self._loop_einmal(self.bridge) gezeigt = self._loop_einmal(self.bridge)
self.assertIsNotNone(gezeigt) # Anzeige wurde trotzdem bedient self.assertIsNotNone(gezeigt) # Anzeige wurde trotzdem bedient
self.assertEqual(gezeigt[0], "18h36m56s") # Bei Niedrigpraezision (config-Vorgabe) die beschriftete Zehntelform.
self.assertEqual(gezeigt[0], "RA 18h36.9m")
if __name__ == "__main__": if __name__ == "__main__":