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