10 Zeichen pro Zeile: Zellenbreite am Geraet gemessen, Zeilen zentriert

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) <noreply@anthropic.com>
This commit is contained in:
2026-07-27 15:56:15 +02:00
parent 42fc7f52d4
commit 4f7375add3
8 changed files with 292 additions and 106 deletions
+52 -32
View File
@@ -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.
+27 -8
View File
@@ -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
+9 -2
View File
@@ -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
+34 -15
View File
@@ -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."""
+114 -19
View File
@@ -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 <zeichen>
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):
+8 -7
View File
@@ -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
+2 -1
View File
@@ -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)
+46 -22
View File
@@ -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)