dae7694a07
Seit der Einzelpositionierung blitzten bei jeder Aktualisierung willkuerliche Pixel auf, in beiden Zeilen. Ursache ist die Telegrammlaenge: Eine volle Zeile kostet mit einzeln gesetzten Zeichen rund 100 Bytes statt 20, bei 19200 Baud und 8E1 (11 Bit je Byte) dauerte ein kompletter Bildwechsel damit rund 300 ms. Die Anzeige stellt schon waehrend des Empfangs dar, also war der Aufbau zu sehen -- im Sekundentakt ein Drittel der Zeit. Von einer Sekunde zur naechsten aendert sich aber fast nichts: "18h36m56s" -> "18h36m57s" ist eine einzige Ziffer, die Deklination steht meist still. show_lines vergleicht deshalb gegen den zuletzt dargestellten Inhalt und schreibt nur die betroffenen Zellen; eine unveraenderte Zeile erzeugt gar kein Telegramm mehr. Am Geraet gemessen: 306 ms -> 66 ms je Bildwechsel. Dabei am Geraet aufgefallen und mitbehoben: Dem "s" hinter den Sekunden fehlten die linken Pixel, dem "m" hinter den Minuten ebenso -- beides Zeichen direkt rechts von einer Stelle, die sich gerade geaendert hatte. Die Anzeige malt je Zeichen 7 px breit (daher auch der 7-px-Auto-Vorschub), gesetzt wird aber auf 6-px-Raster: Ein neu geschriebenes Zeichen loescht die erste Pixelspalte seines rechten Nachbarn. Beim vollstaendigen Neuaufbau fiel das nie auf, weil der Nachbar gleich danach ohnehin neu gemalt wurde. _zelle_faellig zieht ihn jetzt mit. Weitere Details: - clear() verwirft den gemerkten Inhalt, sonst haelt der Vergleich Zellen faelschlich fuer vorhanden und die Anzeige bliebe teilweise leer. - Schlaegt ein Telegramm fehl, wird der gemerkte Inhalt der Zeile verworfen -- was angekommen ist, ist dann ungewiss. - Der Zeichensatz wird nur noch bei der ersten Ausgabe einer Zeile gesetzt. Das spart nicht nur Bytes: Jeder Wechsel beschaedigt die Home-Zelle. Findet doch einer statt (neues Gradzeichen), wird die oberste Zeile vollstaendig nachgezogen. Zu beiden Befunden gibt es Tests mit Gegenprobe (mit der alten Fassung schlagen sie fehl). 155 Tests gruen, am Geraet bestaetigt: ruhig, keine fehlenden Pixel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
266 lines
11 KiB
Python
266 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests fuer den Protokoll-Layer, gepruefte Referenz ist das Handbuch.
|
|
|
|
python3 -m unittest -v test_migra
|
|
"""
|
|
|
|
import unittest
|
|
|
|
import config
|
|
import coords
|
|
import migra
|
|
from display import Display
|
|
from transport import FakeTransport
|
|
|
|
|
|
class TestTelegramm(unittest.TestCase):
|
|
|
|
def test_handbuch_beispiel_mit_pruefsumme(self):
|
|
"""Handbuch 3.10, Beispiel 2: Zeichen 'A' an Anzeige mit Adresse 1.
|
|
|
|
Master: 02 81 80 83 F0 F1 41 FA F6 03
|
|
"""
|
|
frame = migra.build_telegram(b"A", dest=1, src=0, checksum=True, response=True)
|
|
self.assertEqual(frame, bytes.fromhex("02818083F0F141FAF603"))
|
|
|
|
def test_handbuch_beispiel_ohne_pruefsumme(self):
|
|
"""Handbuch 3.10, Beispiel 1: ohne Pruefsumme, mit Antwort.
|
|
|
|
Ohne Pruefsumme duerfen LEN-H/L gerade *nicht* mitgesendet werden.
|
|
"""
|
|
frame = migra.build_telegram(b"Hallo Welt", dest=1, src=0,
|
|
checksum=False, response=True)
|
|
self.assertEqual(frame[:4], bytes([0x02, 0x81, 0x80, 0x81]))
|
|
self.assertEqual(frame[4:-1], b"Hallo Welt")
|
|
self.assertEqual(frame[-1], 0x03)
|
|
|
|
def test_frame_control_bits(self):
|
|
def fc(checksum, response):
|
|
return migra.build_telegram(b"x", checksum=checksum, response=response)[3]
|
|
|
|
self.assertEqual(fc(False, False), 0x80)
|
|
self.assertEqual(fc(False, True), 0x81) # so faehrt das alte C-Programm
|
|
self.assertEqual(fc(True, False), 0x82)
|
|
self.assertEqual(fc(True, True), 0x83)
|
|
|
|
def test_data_unit_zu_lang(self):
|
|
with self.assertRaises(migra.MigraError):
|
|
migra.build_telegram(b"x" * 231)
|
|
|
|
|
|
class TestDataUnit(unittest.TestCase):
|
|
|
|
def test_trennzeichen_nach_esc_sequenz(self):
|
|
"""Handbuch 3.9: Data-Unit = 1B 'Z01' 1B 'C002003' 1B 'A301' 1F Text."""
|
|
du = (migra.DataUnit()
|
|
.charset(1, spaced=False)
|
|
.cursor(2, 3)
|
|
.attributes(fg=migra.YELLOW, bg=migra.BLACK, blink=migra.BLINKING)
|
|
.text("Online-Text"))
|
|
self.assertEqual(
|
|
du.bytes(),
|
|
b"\x1bZ01\x1bC002003\x1bA301\x1fOnline-Text",
|
|
)
|
|
|
|
def test_kein_trennzeichen_zwischen_esc_sequenzen(self):
|
|
du = migra.DataUnit().fill(migra.BLACK).charset(0)
|
|
self.assertEqual(du.bytes(), b"\x1bF0\x1bz00")
|
|
|
|
def test_trennzeichen_nur_einmal_pro_textblock(self):
|
|
du = migra.DataUnit().cursor(0, 0).text("AB").text("CD")
|
|
self.assertEqual(du.bytes(), b"\x1bC000000\x1fABCD")
|
|
|
|
def test_cursor_ist_ascii_codiert(self):
|
|
"""Handbuch 3.7: Position 123 wird als '1' '2' '3' uebertragen."""
|
|
self.assertEqual(migra.DataUnit().cursor(123, 45).bytes(), b"\x1bC123045")
|
|
|
|
def test_loeschen_und_helligkeit(self):
|
|
self.assertEqual(migra.DataUnit().fill(migra.BLACK).bytes(), b"\x1bF0")
|
|
self.assertEqual(migra.DataUnit().brightness(100, migra.RED).bytes(), b"\x1bH2100")
|
|
self.assertEqual(migra.DataUnit().brightness(80, migra.RED).bytes(), b"\x1bH2080")
|
|
|
|
def test_helligkeit_ausserhalb_bereich(self):
|
|
with self.assertRaises(migra.MigraError):
|
|
migra.DataUnit().brightness(101)
|
|
|
|
def test_text_encoding_latin1(self):
|
|
self.assertEqual(migra.encode_text("+45°12'"), b"+45\xb012'")
|
|
self.assertEqual(migra.encode_text("✓"), b"?") # nicht darstellbar
|
|
|
|
|
|
class TestAntwort(unittest.TestCase):
|
|
|
|
def test_ok(self):
|
|
frame = bytes([0x02, 0x80, 0x81, 0x80, ord("0"), 0x03])
|
|
self.assertEqual(migra.parse_response(frame), "0")
|
|
|
|
def test_fehlercode_wirft(self):
|
|
frame = bytes([0x02, 0x80, 0x81, 0x80, ord("3"), 0x03])
|
|
with self.assertRaises(migra.MigraError) as ctx:
|
|
migra.parse_response(frame)
|
|
self.assertIn("ESC-Sequenz", str(ctx.exception))
|
|
|
|
def test_timeout_wirft(self):
|
|
with self.assertRaises(migra.MigraError):
|
|
migra.parse_response(b"")
|
|
|
|
|
|
class TestKoordinaten(unittest.TestCase):
|
|
|
|
def test_ra_formate_werden_erkannt(self):
|
|
for text in ("12h34m56s", "12:34:56", "12 34 56"):
|
|
self.assertEqual(coords.parse_ra(text), (12, 34, 56))
|
|
|
|
def test_ra_dezimal(self):
|
|
self.assertEqual(coords.parse_ra("12.5"), (12, 30, 0))
|
|
|
|
def test_dec_mit_vorzeichen(self):
|
|
self.assertEqual(coords.parse_dec("+45d12m30s"), ("+", 45, 12, 30))
|
|
self.assertEqual(coords.parse_dec("-07:15:00"), ("-", 7, 15, 0))
|
|
|
|
def test_dec_ohne_vorzeichen_ist_positiv(self):
|
|
self.assertEqual(coords.parse_dec("45:12:30"), ("+", 45, 12, 30))
|
|
|
|
def test_bereichsgrenzen(self):
|
|
with self.assertRaises(ValueError):
|
|
coords.parse_ra("24:00:01")
|
|
with self.assertRaises(ValueError):
|
|
coords.parse_dec("+91:00:00")
|
|
|
|
def test_formatierung_hochpraezision(self):
|
|
hp = dict(high_precision=True)
|
|
self.assertEqual(coords.format_ra("12h34m56s", width=9, **hp), "12h34m56s")
|
|
self.assertEqual(coords.format_ra("12h34m56s", width=8, **hp), "12:34:56")
|
|
self.assertEqual(coords.format_dec("+45d12m30s", width=10, **hp), "+45°12'30\"")
|
|
self.assertEqual(coords.format_dec("+45d12m30s", width=9, **hp), "+45°12'30")
|
|
self.assertEqual(coords.format_dec("+45d12m30s", width=7, **hp), "+45°12'")
|
|
|
|
def test_formatierung_niedrigpraezision(self):
|
|
lp = dict(high_precision=False)
|
|
# RA: Zehntel-Minute (0.9 min) kommt als Sekunden (54 s) heraus
|
|
self.assertEqual(coords.format_ra("11:36.9", width=9, **lp), "11h36m54s")
|
|
self.assertEqual(coords.format_ra("11:36.9", width=6, **lp), "11h36m")
|
|
# DEC: ohne Sekunden
|
|
self.assertEqual(coords.format_dec("-00°55", width=9, **lp), "-00°55'")
|
|
self.assertEqual(coords.format_dec("-00°55", width=6, **lp), "-00:55")
|
|
|
|
|
|
class TestDisplay(unittest.TestCase):
|
|
|
|
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)
|
|
|
|
# clear=True ist ein eigenes, sehr kurzes Telegramm, danach je Zeile eins.
|
|
self.assertEqual(len(t.frames), 3)
|
|
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)
|
|
|
|
loeschen, unten, oben = t.frames # untere Zeile zuerst
|
|
self.assertIn(b"\x1bF0", loeschen)
|
|
self.assertNotIn(b"\x1bF0", unten)
|
|
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)
|
|
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."""
|
|
t = FakeTransport(echo=False)
|
|
Display(t, response=True).show_lines("12h34m56s", "+45°12'30\"")
|
|
data = t.frames[0][4:-1]
|
|
# Umschalten auf Zeichensatz 1, "/", zurueck auf Zeichensatz 0
|
|
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 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\"")
|
|
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."""
|
|
import config
|
|
t = FakeTransport(echo=False)
|
|
alt = config.DEGREE_CHARSET
|
|
try:
|
|
config.DEGREE_CHARSET = None
|
|
Display(t, response=True).show_lines("+45°12'30\"")
|
|
finally:
|
|
config.DEGREE_CHARSET = alt
|
|
data = t.frames[0][4:-1]
|
|
self.assertNotIn(b"\x1bz01", data)
|
|
self.assertIn(config.DEGREE_FALLBACK.encode(), data)
|
|
|
|
def test_fehlercode_der_anzeige_wirft(self):
|
|
t = FakeTransport(echo=False)
|
|
t._response = bytes([0x02, 0x80, 0x81, 0x80, ord("4"), 0x03])
|
|
with self.assertRaises(migra.MigraError):
|
|
Display(t, response=True).clear()
|
|
|
|
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")
|
|
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)
|
|
with self.assertRaises(ValueError):
|
|
Display(t, response=True).show_line(5, "x")
|
|
|
|
def test_ohne_antwort_fc_bit0_ist_null(self):
|
|
"""Standardbetrieb ohne Antwort: FC = 0x80, kein Fehlercode wird gelesen."""
|
|
import config
|
|
alt = config.QUIET_TIME
|
|
try:
|
|
config.QUIET_TIME = 0.0
|
|
t = FakeTransport(echo=False)
|
|
# selbst wenn die (ignorierte) Antwort einen Fehler meldet, darf nichts fliegen
|
|
t._response = bytes([0x02, 0x80, 0x81, 0x80, ord("4"), 0x03])
|
|
Display(t, response=False).clear()
|
|
finally:
|
|
config.QUIET_TIME = alt
|
|
self.assertEqual(t.frames[0][3], 0x80) # FC ohne Antwort-Bit
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|