Files
grossanzeige/test_display.py
T
admin 4f7375add3 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>
2026-07-27 15:56:15 +02:00

187 lines
7.6 KiB
Python

"""Tests fuer den Anzeigetreiber -- Zeilenaufbau und die festen Hinweistexte.
python3 -m unittest test_display
"""
import unittest
import config
import migra
from display import Display
from transport import FakeTransport
def _lesbar(frame):
"""Telegramm als lesbarer Text; Steuerzeichen werden zu '.'."""
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.
Am Geraet aufgefallen: "MONTIERUNG" hat 10 Zeichen, die Anzeige fasst 9 --
angezeigt wurde "MONTIERUN". _emit_line schneidet stillschweigend ab, also
faellt so etwas sonst erst auf der Anzeige auf.
"""
def test_hinweiszeilen_passen_in_die_zeilenbreite(self):
for name in ("NOLINK_LINES", "NOWIFI_LINES"):
for zeile in getattr(config, name):
self.assertLessEqual(
len(zeile), config.CHARS_PER_LINE,
"config.%s: %r hat %d Zeichen, die Anzeige fasst %d"
% (name, zeile, len(zeile), config.CHARS_PER_LINE))
def test_es_gibt_nicht_mehr_hinweiszeilen_als_zeilen(self):
for name in ("NOLINK_LINES", "NOWIFI_LINES"):
self.assertLessEqual(len(getattr(config, name)), len(config.LINE_Y))
class TestZeilenaufbau(unittest.TestCase):
def setUp(self):
self.t = FakeTransport(echo=False)
self.d = Display(self.t, response=True)
def _alle(self):
return self.t.frames
def test_zeile_wird_auf_volle_breite_aufgefuellt(self):
"""Ohne Auffuellen blieben Reste der vorigen Anzeige stehen.
Gleichzeitig die Regressionsprobe fuer str.ljust, das MicroPython
nicht kennt (am Geraet aufgefallen).
"""
self.d.show_lines("AB", "CD")
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)
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 zerstoert (0,0),
die oberste Zeile muss danach kommen und sie ueberschreiben."""
self.d.show_lines("OBEN", "UNTEN")
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_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.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):
self.d.show_lines(*(["X"] * (len(config.LINE_Y) + 1)))
if __name__ == "__main__":
unittest.main()