4f7375add3
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>
187 lines
7.6 KiB
Python
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()
|