From 4f7375add3866d66cf40938761586109ba0d9cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Mon, 27 Jul 2026 15:56:15 +0200 Subject: [PATCH] 10 Zeichen pro Zeile: Zellenbreite am Geraet gemessen, Zeilen zentriert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die RS232-Strecke ESP32 -> MAX3232 -> Anzeige ist am Geraet bestaetigt; damit war "probe.py pitch" moeglich und hat die offene Zeichenbreiten-Frage geklaert. Das Ergebnis korrigiert eine Annahme: Bisher stand CHARS_PER_LINE = 9, abgezaehlt am Zeilenumbruch. Gemessen wurde damit aber der Auto-Vorschub der Sperrschrift (7 px), nicht die Zeichenbreite. Die Zeichenmatrix ist 6 px breit -- zehn Ziffern, einzeln auf x = i*6 gesetzt, stehen sauber getrennt nebeneinander. - display._emit_line positioniert jedes Zeichen selbst, statt den Auto-Vorschub laufen zu lassen. CHARS_PER_LINE = 10, CELL_WIDTH = 6. - Die Deklination passt dadurch in voller Form: +38°47'01" statt +38°47'01. - show_lines sendet ein Telegramm je Zeile. Mit Einzelpositionierung kaemen beide zusammen auf 224 der erlaubten 230 Bytes -- zu wenig Reserve fuer den Dauerbetrieb. Die Reihenfolge bleibt unten vor oben (Home-Zelle). - Zeilen werden zentriert (CENTER_LINES, LINE_X). Der Zeichenblock bleibt dabei gleich breit und an derselben Stelle, damit keine Reste stehen bleiben. - probe.py kommt mit aufs Geraet: Die Anzeige haengt jetzt am ESP32, nur von dort laesst sich die Geometrie noch ausmessen. Die Tests pruefen nicht mehr auf zusammenhaengenden Text im Telegramm -- den gibt es nicht mehr -- sondern rechnen ueber die Cursor-Sequenzen zurueck, was wirklich auf welcher Zelle landet. 97 Tests gruen, Kette am Geraet im Sekundentakt bestaetigt. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 84 ++++++++++++++++++------------ config.py | 35 ++++++++++--- deploy.sh | 11 +++- display.py | 49 ++++++++++++------ test_display.py | 133 +++++++++++++++++++++++++++++++++++++++++------- test_esp32.py | 15 +++--- test_lx200.py | 3 +- test_migra.py | 68 +++++++++++++++++-------- 8 files changed, 292 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 9bfb17f..00e4e51 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,8 @@ festgehalten sind (`test_esp32.py`, `test_display.py`): - **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`, @@ -245,9 +247,6 @@ Mac: ## Offene Punkte -- **RS232-Strecke zur Anzeige.** Als einziges Stück der Kette noch nicht am - ESP32 erprobt: Der Aufbau mit MAX3232 und Anzeige fehlt. Alles davor ist - bestätigt, die Telegramme stimmen byteweise — es geht nur noch um die Pegel. - **LDR-Schwellen nachmessen.** Die Grenzen in `brightness.py` stammen vom alten MSP430-Aufbau. Sobald der LDR am ESP32 hängt, die Rohwerte bei Tag und Nacht ablesen und `HELL_LOW`/`HELL_HIGH`/`HELL_MAX` anpassen. Bis dahin läuft die @@ -265,40 +264,55 @@ 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 mitten im Telegramm beschädigt die Home-Zelle -(0,0) — das erste Zeichen der obersten Zeile wird zerstört. Deshalb zeichnet -`show_lines` die Zeilen **von unten nach oben** (oberste Zeile zuletzt), damit sie -diese Zelle überschreibt. +**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 -- **9 Zeichen pro Zeile** in Sperrschrift (`CHARSET_SPACED = True`, am Gerät - ausgemessen 2026-07-15). +- **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). - Das widerspricht der naheliegenden Rechnung: Die Anzeige ist 64 px breit, und - eine 5×7-Matrix mit 1 px Lücke wäre 6 px pro Zelle — das gäbe 10 Zeichen - (60 px). Die Erklärung ist vermutlich, dass die Sperrschrift eben sperrt: - bei 2 px Lücke sind es 7 px pro Zelle und damit 9 Zeichen (63 von 64 px). - Dazu passt auch die Beobachtung, dass die *proportionale* Schrift `ESC Z` - rund 11 Zeichen schafft (≈ 5,8 px/Zelle). + 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: - **Nachgeprüft ist das nicht.** `probe.py pitch` entscheidet es direkt, statt - am Umbruch abzuzählen: Es schreibt zehn Ziffern einmal mit dem Auto-Vorschub - der Anzeige und einmal auf feste Pixelpositionen `x = i·cell`. Ein Unterschied - von 1 px summiert sich über zehn Zeichen auf 9 px und ist unübersehbar. + - 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 -> 10 Zeichen - python3 probe.py pitch 7 # ... -> 9 Zeichen + python3 probe.py pitch 6 # stehen beide Zeilen untereinander -> 6 px/Zelle + python3 probe.py pitch 7 # ... -> 7 px/Zelle ``` - Sollten es doch 6 px sein, passt die Deklination mit abschließendem `"` in - voller Form (`+38°47'01"`, 10 Zeichen) — `coords.format_dec` wählt sie dann - von selbst, es genügt `CHARS_PER_LINE = 10`. -- Die Deklination wird deshalb als **9-Zeichen-Form ohne abschließendes `"`** - ausgegeben (`+38°47'01`) — Gradzeichen und Bogenminuten bleiben, die Bogensekunden - stehen als Ziffern da. `coords.format_dec` wählt automatisch die breiteste Form, - die in `CHARS_PER_LINE` passt. +- **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. + - **Zyklische Updates flimmerfrei:** `show_lines` löscht nicht bei jeder Aktualisierung, sondern überschreibt die Zeilen an Ort und Stelle (auf volle Breite aufgefüllt). `clear=True` nur beim ersten Bild. Wichtig: Zeilen dürfen **nicht breiter** als @@ -380,12 +394,18 @@ gegeneinander (verschliffener ~3-V-Pegel am Oszi). `loopback.py` grenzt so etwas ## 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`), 9 Zeichen/Zeile. - (Hier stand ursprünglich „10 Zeichen/Zeile" — das war die Annahme vor der - Messung vom 15.07., siehe *Geometrie und Darstellung*.) - Die schmale `1` wirkt dadurch etwas luftig — Font-Eigenschaft, nur per eigenem +- 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. diff --git a/config.py b/config.py index 1aa493a..1e05d37 100644 --- a/config.py +++ b/config.py @@ -55,14 +55,33 @@ CHARSET = 0 CHARSET_SPACED = True LINE_Y = (0, 9) -# 9 Zeichen: am Geraet ausgemessen (2026-07-15). Die Anzeige ist 64 px breit; -# eine reine 5x7-Matrix mit 1 px Luecke waere 6 px pro Zelle und ergaebe 10 -# Zeichen. Dass es nur 9 sind, passt zu 7 px pro Zelle -- die Sperrschrift -# sperrt eben, sie hat 2 px Luecke (9*7 = 63 von 64 px). Das ist die Erklaerung, -# nicht die Messung: "probe.py pitch 6" bzw. "pitch 7" entscheidet es direkt, -# indem es den Auto-Vorschub gegen feste Pixelabstaende stellt. -# Faende sich doch 6 px, passt die DEC-Form mit abschliessendem " (10 Zeichen). -CHARS_PER_LINE = 9 +# 10 Zeichen zu 6 px -- mit "probe.py pitch 6" am Geraet entschieden (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). Daher stand hier +# frueher 9 -- das war der Vorschub, nicht die Zellenbreite. +# - Die Zeichenmatrix selbst ist aber 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. So passen 10 Zeichen, und die DEC-Form bekommt +# ihr abschliessendes " (siehe coords.format_dec). +CHARS_PER_LINE = 10 +CELL_WIDTH = 6 # Pixel pro Zeichenzelle bei fester Positionierung +DISPLAY_WIDTH = 64 # Pixelbreite der Anzeige + +# Zeilen mittig setzen statt linksbuendig. Wirkt auf zwei Ebenen: +# - LINE_X rueckt den ganzen Zeichenblock in die Mitte: 10*6 = 60 px belegen +# nicht die vollen 64, das laesst je 2 px Rand. +# - Kuerzere Texte werden in _emit_line beidseitig mit Leerzeichen aufgefuellt, +# z.B. die 7 Zeichen von "+38°47'" bei Niedrigpraezision. +# Der Block bleibt dabei immer gleich breit und an derselben Stelle -- er +# ueberschreibt so bei jedem Durchlauf denselben Bereich, ohne Reste der vorigen +# Anzeige stehen zu lassen. Ein pixelgenau mitwandernder Offset koennte das nicht. +CENTER_LINES = True +LINE_X = (DISPLAY_WIDTH - CHARS_PER_LINE * CELL_WIDTH) // 2 COLOR = migra.RED # einfarbig rote Anzeige BACKGROUND = migra.BLACK diff --git a/deploy.sh b/deploy.sh index 1443d7e..2d17f4c 100755 --- a/deploy.sh +++ b/deploy.sh @@ -15,8 +15,14 @@ set -e PORT="${1:-auto}" # Alles, was auf dem Geraet gebraucht wird. run_display.py, show_coords.py, -# probe.py, mount_mock.py und die Tests bleiben auf dem Mac -- sie brauchen -# argparse bzw. threading. +# mount_mock.py und die Tests bleiben auf dem Mac -- sie brauchen argparse +# bzw. threading. +# +# probe.py kommt mit aufs Geraet: Seit die Anzeige am ESP32 haengt und nicht mehr +# am Mac, laesst sich die Geometrie nur noch von dort ausmessen. Es benutzt zwar +# sys.argv, aber nur in main() -- die Messfunktionen ruft man am REPL direkt auf: +# import probe, display +# d = display.open_display(); probe.pitch(d, 6); d.close() FILES=" ticks.py migra.py @@ -30,6 +36,7 @@ brightness.py displayloop.py wifi.py run_esp32.py +probe.py " if [ ! -f wifi_config.py ]; then diff --git a/display.py b/display.py index 2d4dc74..4279f3b 100644 --- a/display.py +++ b/display.py @@ -52,10 +52,18 @@ class Display: def show_lines(self, *lines, clear=False): """Zeilen ab Zeile 1 auf die Anzeige schreiben. - Baut ein einziges Telegramm: pro Zeile Cursor setzen und den Text - schreiben. Das Gradzeichen (config.DEGREE_MARKER) wird ueber einen kurzen + Pro Zeile ein eigenes Telegramm: Cursor setzen und Text schreiben. Das + Gradzeichen (config.DEGREE_MARKER) wird ueber einen kurzen Zeichensatzwechsel eingefuegt. + Frueher war das ein einziges Telegramm fuer beide Zeilen. Seit jedes + Zeichen einzeln positioniert wird (10 statt 9 Zeichen, siehe _emit_line), + kostet eine Zeile rund 100 Bytes -- beide zusammen kaemen mit Gradzeichen + und Vollbild-Loeschen auf 224 der erlaubten 230 Bytes (migra.MAX_DATA_UNIT). + Das ist zu wenig Reserve fuer ein Geraet, das monatelang durchlaeuft; + getrennte Telegramme halten jedes bei rund der Haelfte. Die Flusskontrolle + ueber das Antworttelegramm macht die zweite Sendung unproblematisch. + Jede Zeile wird mit Leerzeichen auf volle Breite (CHARS_PER_LINE) aufgefuellt und damit *an Ort und Stelle* ueberschrieben -- ohne die Anzeige vorher komplett zu loeschen. Das vermeidet das sichtbare Blitzen @@ -72,13 +80,15 @@ class Display: if len(lines) > len(config.LINE_Y): raise ValueError("Anzeige hat nur %d Zeilen" % len(config.LINE_Y)) - du = migra.DataUnit() - if clear: - du.fill(config.BACKGROUND) - du.attributes(fg=config.COLOR, bg=config.BACKGROUND, blink=migra.STATIC) + first = True for i in reversed(range(len(lines))): + du = migra.DataUnit() + if clear and first: + du.fill(config.BACKGROUND) + du.attributes(fg=config.COLOR, bg=config.BACKGROUND, blink=migra.STATIC) self._emit_line(du, config.LINE_Y[i], lines[i]) - self.send(du) + self.send(du) + first = False def show_line(self, index, text, clear=False): """Nur eine einzelne Zeile aktualisieren (eigenes, kurzes Telegramm). @@ -98,17 +108,26 @@ class Display: self.send(du) def _emit_line(self, du, y, text): - """Eine Zeile bei Pixelzeile y ausgeben, auf volle Breite aufgefuellt.""" - # Ohne str.ljust: das kennt MicroPython nicht (am Geraet aufgefallen). + """Eine Zeile bei Pixelzeile y ausgeben, auf volle Breite aufgefuellt. + + Jedes Zeichen wird einzeln auf x = i*CELL_WIDTH gesetzt, statt den + Auto-Vorschub der Anzeige laufen zu lassen. Der Vorschub der Sperrschrift + ist 7 px und liesse nur 9 Zeichen zu; die Zeichenmatrix ist aber nur 6 px + breit, sodass 10 Zeichen sauber nebeneinander passen (am Geraet gemessen + 2026-07-27, siehe config.CHARS_PER_LINE). + """ + # Ohne str.ljust/center: die kennt MicroPython nicht (am Geraet aufgefallen). text = text[:config.CHARS_PER_LINE] - text += " " * (config.CHARS_PER_LINE - len(text)) + rest = config.CHARS_PER_LINE - len(text) + links = rest // 2 if config.CENTER_LINES else 0 + text = " " * links + text + " " * (rest - links) du.charset(config.CHARSET, spaced=config.CHARSET_SPACED) - du.cursor(0, y) - for i, part in enumerate(text.split(config.DEGREE_MARKER)): - if i > 0: + for i in range(len(text)): + du.cursor(config.LINE_X + i * config.CELL_WIDTH, y) + if text[i] == config.DEGREE_MARKER: self._emit_degree(du) - if part: - du.text(part) + else: + du.text(text[i]) def _emit_degree(self, du): """Das Gradzeichen einfuegen -- ueber Zeichensatzwechsel oder als Fallback.""" diff --git a/test_display.py b/test_display.py index 419300e..3ea0802 100644 --- a/test_display.py +++ b/test_display.py @@ -16,6 +16,50 @@ def _lesbar(frame): return "".join(chr(b) if 32 <= b < 127 else "." for b in frame) +def zellen(frames): + """Aus gesendeten Telegrammen zurueckrechnen, was wo auf der Anzeige steht. + + Seit jedes Zeichen einzeln positioniert wird (config.CELL_WIDTH), steht der + Text nicht mehr am Stueck im Telegramm -- zwischen je zwei Zeichen liegt eine + Cursor-Sequenz. Ein Test auf "AB " im Rohtelegramm ginge deshalb ins Leere. + Diese Funktion liest die Cursor-Sequenzen mit und liefert {(x, y): zeichen}, + also das, was die Anzeige am Ende wirklich zeigt. + + Aufbau je Zeichen: ESC 'C' xxx yyy SEP + Beim Gradzeichen schiebt sich ein Zeichensatzwechsel dazwischen + (ESC 'z' nn), das Zeichen ist dann das erste nach dem naechsten SEP. + """ + out = {} + for frame in frames: + i = 0 + while i < len(frame): + if frame[i] == migra.ESC and i + 7 < len(frame) and frame[i + 1:i + 2] == b"C": + x = int(frame[i + 2:i + 5]) + y = int(frame[i + 5:i + 8]) + j = frame.find(bytes([migra.SEP]), i + 8) + if j >= 0 and j + 1 < len(frame): + out[(x, y)] = chr(frame[j + 1]) + i += 8 + else: + i += 1 + return out + + +def zeile(frames, index): + """Den Text einer Anzeigezeile aus den Telegrammen rekonstruieren.""" + y = config.LINE_Y[index] + treffer = [(x, ch) for (x, yy), ch in zellen(frames).items() if yy == y] + return "".join(ch for _, ch in sorted(treffer)) + + +def zentriert(text): + """Erwartungswert: Text so, wie _emit_line ihn auf die Zeile legt.""" + text = text[:config.CHARS_PER_LINE] + rest = config.CHARS_PER_LINE - len(text) + links = rest // 2 if config.CENTER_LINES else 0 + return " " * links + text + " " * (rest - links) + + class TestFesteTexte(unittest.TestCase): """Die Hinweiszeilen muessen in die Anzeige passen. @@ -43,8 +87,8 @@ class TestZeilenaufbau(unittest.TestCase): self.t = FakeTransport(echo=False) self.d = Display(self.t, response=True) - def _letztes(self): - return self.t.frames[-1] + def _alle(self): + return self.t.frames def test_zeile_wird_auf_volle_breite_aufgefuellt(self): """Ohne Auffuellen blieben Reste der vorigen Anzeige stehen. @@ -53,34 +97,85 @@ class TestZeilenaufbau(unittest.TestCase): nicht kennt (am Geraet aufgefallen). """ self.d.show_lines("AB", "CD") - text = _lesbar(self._letztes()) - self.assertIn("AB" + " " * (config.CHARS_PER_LINE - 2), text) - self.assertIn("CD" + " " * (config.CHARS_PER_LINE - 2), text) + self.assertEqual(zeile(self._alle(), 0), zentriert("AB")) + self.assertEqual(zeile(self._alle(), 1), zentriert("CD")) + # Es wird immer die volle Breite geschrieben, damit nichts stehen bleibt. + self.assertEqual(len(zeile(self._alle(), 0)), config.CHARS_PER_LINE) + + def test_kuerzere_zeile_wird_mittig_gesetzt(self): + """Zentriert heisst: links und rechts gleich viel Luft (Nutzerwunsch).""" + self.d.show_lines("+38°47'") # 7 Zeichen in 10 Zellen + text = zeile(self._alle(), 0) + links = len(text) - len(text.lstrip()) + rechts = len(text) - len(text.rstrip()) + self.assertLessEqual(abs(links - rechts), 1) + + def test_jedes_zeichen_sitzt_auf_seiner_zelle(self): + """Der Auto-Vorschub der Sperrschrift ist 7 px und liesse nur 9 Zeichen + zu; die Matrix ist aber 6 px breit. Deshalb wird jedes Zeichen selbst + gesetzt -- x muss genau LINE_X + i*CELL_WIDTH sein (am Geraet gemessen).""" + self.d.show_lines("0123456789") + gesetzt = zellen(self._alle()) + y = config.LINE_Y[0] + for i in range(config.CHARS_PER_LINE): + self.assertIn((config.LINE_X + i * config.CELL_WIDTH, y), gesetzt) + + def test_zeile_passt_in_die_anzeigenbreite(self): + """Auch die letzte Zelle darf nicht ueber den rechten Rand hinausragen -- + sonst quittiert die Anzeige mit Fehlercode 4 (ausserhalb).""" + rechts = config.LINE_X + config.CHARS_PER_LINE * config.CELL_WIDTH + self.assertLessEqual(rechts, config.DISPLAY_WIDTH) + self.assertGreaterEqual(config.LINE_X, 0) def test_zu_lange_zeile_wird_abgeschnitten(self): """Zu breit aufgefuellt bricht das Fuell-Leerzeichen um und beschaedigt die andere Zeile -- deshalb hart abschneiden.""" lang = "X" * (config.CHARS_PER_LINE + 5) self.d.show_lines(lang) - text = _lesbar(self._letztes()) - self.assertIn("X" * config.CHARS_PER_LINE, text) - self.assertNotIn("X" * (config.CHARS_PER_LINE + 1), text) + self.assertEqual(zeile(self._alle(), 0), "X" * config.CHARS_PER_LINE) + + 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): - """Schutz der Home-Zelle: ein Zeichensatzwechsel mitten im Telegramm - zerstoert (0,0), die oberste Zeile muss danach kommen.""" + """Schutz der Home-Zelle: ein Zeichensatzwechsel zerstoert (0,0), + die oberste Zeile muss danach kommen und sie ueberschreiben.""" self.d.show_lines("OBEN", "UNTEN") - text = _lesbar(self._letztes()) - self.assertLess(text.index("UNTEN"), text.index("OBEN")) + frames = self._alle() + self.assertEqual(len(frames), 2) + # Erst das Telegramm der unteren Zeile, dann das der oberen. + self.assertEqual(zeile(frames[:1], 1).strip(), "UNTEN") + self.assertEqual(zeile(frames[1:], 0).strip(), "OBEN") - def test_clear_erzeugt_ein_vollbild_loeschen(self): - self.d.show_lines("A", "B", clear=True) - mit = self._letztes() - self.d.show_lines("A", "B", clear=False) - ohne = self._letztes() + def test_telegramme_bleiben_unter_der_groessengrenze(self): + """Mit Einzelpositionierung kosten beide Zeilen zusammen 224 der + erlaubten 230 Bytes -- deshalb ein Telegramm je Zeile. Der Test haelt + fest, dass die Reserve nicht wieder verloren geht.""" + self.d.show_lines("18h36m56s", "+38" + config.DEGREE_MARKER + "47'01\"", + clear=True) + for frame in self._alle(): + self.assertLessEqual(len(frame), migra.MAX_DATA_UNIT) + + def test_clear_erzeugt_genau_ein_vollbild_loeschen(self): + """clear=True darf nur einmal loeschen -- sonst loescht das zweite + Telegramm die gerade geschriebene Zeile wieder weg.""" fuellen = bytes([migra.ESC]) + b"F" + config.BACKGROUND.encode("ascii") - self.assertIn(fuellen, mit) - self.assertNotIn(fuellen, ohne) + + self.d.show_lines("A", "B", clear=True) + mit = self.t.frames[:] + self.assertEqual(sum(f.count(fuellen) for f in mit), 1) + + self.t.frames.clear() + self.d.show_lines("A", "B", clear=False) + self.assertEqual(sum(f.count(fuellen) for f in self.t.frames), 0) def test_mehr_zeilen_als_die_anzeige_hat(self): with self.assertRaises(ValueError): diff --git a/test_esp32.py b/test_esp32.py index 65409df..409ec30 100644 --- a/test_esp32.py +++ b/test_esp32.py @@ -436,7 +436,7 @@ class TestWifi(unittest.TestCase): DEPLOYED = ( "ticks.py", "migra.py", "config.py", "transport.py", "display.py", "coords.py", "lx200.py", "mount_client.py", "brightness.py", - "displayloop.py", "wifi.py", "run_esp32.py", + "displayloop.py", "wifi.py", "run_esp32.py", "probe.py", ) @@ -574,12 +574,13 @@ class TestEsp32GegenMock(unittest.TestCase): self.assertTrue(any("Montierung" in line for line in self.log)) # Die Hinweiszeilen stehen wirklich im Telegramm -- ohne den Text hier # zu wiederholen, sonst bricht der Test bei jeder Umformulierung. - gesendet = "".join(chr(int(b, 16)) if 32 <= int(b, 16) < 127 else "." - for zeile in out.getvalue().splitlines() - if zeile.startswith("TX:") - for b in zeile.split(":", 1)[1].split()) - for zeile in config.NOLINK_LINES: - self.assertIn(zeile, gesendet) + # Seit jedes Zeichen einzeln positioniert wird, steht der Text nicht mehr + # am Stueck im Telegramm; deshalb ueber die Cursor-Sequenzen zurueckrechnen. + from test_display import zeile as _zeile + frames = [bytes(int(b, 16) for b in z.split(":", 1)[1].split()) + for z in out.getvalue().splitlines() if z.startswith("TX:")] + for i, erwartet in enumerate(config.NOLINK_LINES): + self.assertEqual(_zeile(frames, i).strip(), erwartet) def test_ohne_wlan_zeigt_die_anzeige_kein_wlan(self): import config diff --git a/test_lx200.py b/test_lx200.py index c3d7652..33d82dc 100644 --- a/test_lx200.py +++ b/test_lx200.py @@ -81,7 +81,8 @@ class TestClientGegenMock(unittest.TestCase): def test_get_coordinates_und_formatierung(self): ra_raw, dec_raw = self.client.get_coordinates() self.assertEqual(coords.format_ra(ra_raw, high_precision=True), "18h36m56s") - self.assertEqual(coords.format_dec(dec_raw, high_precision=True), "+38°47'01") + # Seit CHARS_PER_LINE = 10 passt die volle Form mit abschliessendem ". + self.assertEqual(coords.format_dec(dec_raw, high_precision=True), "+38°47'01\"") def test_produktname(self): self.assertEqual(self.client.query(lx200.GET_PRODUCT), mount_mock.PRODUCT) diff --git a/test_migra.py b/test_migra.py index a1ed52a..dcfd1d4 100644 --- a/test_migra.py +++ b/test_migra.py @@ -6,6 +6,7 @@ import unittest +import config import coords import migra from display import Display @@ -146,30 +147,44 @@ class TestKoordinaten(unittest.TestCase): class TestDisplay(unittest.TestCase): - def test_zeilen_ergeben_ein_telegramm(self): + def test_je_zeile_ein_telegramm(self): + """Eine Zeile je Telegramm, jeweils sauber gerahmt. + + Frueher war beides ein Telegramm. Seit jedes Zeichen einzeln + positioniert wird (10 statt 9 Zeichen), waeren beide Zeilen zusammen zu + dicht an migra.MAX_DATA_UNIT -- siehe display.show_lines. + """ t = FakeTransport(echo=False) Display(t, response=True).show_lines("12:34:56", "+45:12:30", clear=True) - self.assertEqual(len(t.frames), 1) - frame = t.frames[0] - self.assertEqual(frame[:4], bytes([0x02, 0x81, 0x80, 0x81])) - self.assertEqual(frame[-1], 0x03) + self.assertEqual(len(t.frames), 2) + for frame in t.frames: + self.assertEqual(frame[:4], bytes([0x02, 0x81, 0x80, 0x81])) + self.assertEqual(frame[-1], 0x03) + self.assertLessEqual(len(frame), migra.MAX_DATA_UNIT) - data = frame[4:-1] - self.assertIn(b"\x1bF0", data) # mit clear=True: vorher loeschen - self.assertIn(b"\x1bC000000\x1f12:34:56", data) # Zeile 1 bei y=0 - self.assertIn(b"\x1bC000009\x1f+45:12:30", data) # Zeile 2 bei y=9 + unten, oben = t.frames # untere Zeile zuerst + self.assertIn(b"\x1bF0", unten) # mit clear=True: einmal loeschen + self.assertNotIn(b"\x1bF0", oben) + + # Jedes Zeichen sitzt auf seiner eigenen Zelle: x = LINE_X + i*CELL_WIDTH. + def zelle(i, 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), + # "+45:12:30" hat 9 und beginnt daher schon bei Zelle 0. + self.assertIn(zelle(1, 0) + b"\x1f1", oben) + self.assertIn(zelle(2, 0) + b"\x1f2", oben) + self.assertIn(zelle(0, 9) + b"\x1f+", unten) def test_ohne_clear_kein_fill_aber_aufgefuellt(self): """Standard (clear=False): kein Vollbild-Loeschen, Zeilen auf volle Breite.""" + from test_display import zeile, zentriert t = FakeTransport(echo=False) Display(t, response=True).show_lines("12:34:56", clear=False) - data = t.frames[0][4:-1] - self.assertNotIn(b"\x1bF0", data) - # auf CHARS_PER_LINE mit Leerzeichen aufgefuellt - import config - padded = "12:34:56".ljust(config.CHARS_PER_LINE).encode() - self.assertIn(padded, data) + self.assertNotIn(b"\x1bF0", t.frames[0]) + # auf CHARS_PER_LINE mit Leerzeichen aufgefuellt (mittig, siehe CENTER_LINES) + self.assertEqual(zeile(t.frames, 0), zentriert("12:34:56")) def test_gradzeichen_via_zeichensatz(self): """Gradzeichen wird durch kurzen Wechsel auf Zeichensatz 1 erzeugt.""" @@ -180,11 +195,20 @@ class TestDisplay(unittest.TestCase): self.assertIn(b"\x1bz01\x1f/\x1bz00", data) def test_oberste_zeile_wird_zuletzt_gezeichnet(self): - """Wegen der Home-Zellen-Stoerung muss y=0 nach y=9 im Telegramm stehen.""" + """Wegen der Home-Zellen-Stoerung muss y=0 nach y=9 gesendet werden. + + Seit es ein Telegramm je Zeile gibt, entscheidet die Reihenfolge der + Telegramme statt der Reihenfolge innerhalb eines Telegramms. + """ t = FakeTransport(echo=False) Display(t, response=True).show_lines("12h34m56s", "+45°12'30\"") - data = t.frames[0][4:-1] - self.assertLess(data.index(b"C000009"), data.index(b"C000000")) + from test_display import zellen + self.assertEqual(len(t.frames), 2) + # Auf die y-Koordinaten schauen, nicht auf feste Cursor-Bytes. + y_zuerst = set(y for _, y in zellen(t.frames[:1])) + y_zuletzt = set(y for _, y in zellen(t.frames[1:])) + self.assertEqual(y_zuerst, {config.LINE_Y[1]}) # untere Zeile zuerst + self.assertEqual(y_zuletzt, {config.LINE_Y[0]}) # oberste Zeile zuletzt def test_gradzeichen_fallback_ohne_zeichensatz(self): """Ist DEGREE_CHARSET None, wird der Fallbacktext statt der Umschaltung genutzt.""" @@ -208,12 +232,12 @@ class TestDisplay(unittest.TestCase): def test_show_line_nur_eine_zeile(self): """show_line schreibt nur die angegebene Zeile, ohne Vollbild-Loeschen.""" + from test_display import zellen t = FakeTransport(echo=False) Display(t, response=True).show_line(0, "18h36m56s") - data = t.frames[0][4:-1] - self.assertNotIn(b"\x1bF0", data) # kein Loeschen - self.assertIn(b"\x1bC000000", data) # Cursor auf Zeile 1 (y=0) - self.assertNotIn(b"C000009", data) # Zeile 2 unberuehrt + self.assertNotIn(b"\x1bF0", t.frames[0]) # kein Loeschen + y_werte = set(y for _, y in zellen(t.frames)) + self.assertEqual(y_werte, {config.LINE_Y[0]}) # nur Zeile 1, Zeile 2 unberuehrt def test_show_line_ungueltiger_index(self): t = FakeTransport(echo=False)