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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user