RA in Zehntelminuten mit Beschriftung: "RA 18h36.9m" / "DE +38°47'"

Die GM4000 liefert im Auslieferzustand keine Sekunden, sondern Zehntelminuten.
Bisher wurde daraus eine Sekundenanzeige gerechnet -- die sprang in
6-Sekunden-Schritten, weil die Rohdaten sie gar nicht hergeben. Die Zehntelform
ist ehrlicher und kuerzer, und der gewonnene Platz traegt die Beschriftung:

    RA 18h36.9m
    DE +38°47'

Bei Hochpraezision entfaellt die Beschriftung, dort braucht die Deklination mit
+38°47'01" alle Zellen.

Der Mock konnte das gar nicht nachstellen -- er sendete immer das
Hochpraezisionsformat. Neu: "mount_mock.py --low" bildet den Auslieferzustand ab
(RA als HH:MM.T, DEC ohne Bogensekunden). Vorgabe bleibt hoch, damit die
bestehenden Tests mit ihren festen Erwartungen unveraendert durchlaufen.

Zur Darstellung, alles am Geraet ausprobiert:

- **Schmale Zeichen** (config.CELL_NARROW): Dezimalpunkt und der Abstand
  zwischen Beschriftung und Wert bekommen 3 statt 6 px -- in der Sperrschrift
  standen sie sonst sehr luftig. Fuer die Leerzeichen am Zeilenende gilt das
  nicht, die sollen den Rest der Zeile ueberschreiben.
- Damit entscheidet die **Pixelbreite** ueber das Abschneiden, nicht mehr die
  Zeichenzahl: "RA 18h36.9m" sind 11 Zeichen und trotzdem nur 60 px.
- **Linksbuendig ab LINE_X = 0**, der Schalter zum Zentrieren ist entfallen.
  Zentriert standen die Zeilen um wenige Pixel gegeneinander versetzt, weil die
  RA-Zeile mit ihrem schmalen Punkt kuerzer ist als die DEC-Zeile.

Ein Zwischenschritt, der wieder verworfen wurde und als Warnung im Code steht:
Ein Doppelpunkt als Trenner ("RA:18h36.9m") sah in Zeichensatz 0 zu wuchtig aus;
der aus Zeichensatz 1 ist schlanker, steht aber mitten in der obersten Zeile --
und jeder Zeichensatzwechsel zerstoert die Home-Zelle. Das erste Zeichen danach
noch einmal zu schreiben machte es schlimmer: Das nachgezogene Zeichen bemalt
GLYPH_WIDTH Pixel und frisst die linke Spalte seines rechten Nachbarn. Deshalb
steht in der obersten Zeile jetzt bewusst kein Sonderzeichen.

161 Tests gruen, am Geraet bestaetigt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-27 18:23:07 +02:00
parent dae7694a07
commit 433c53aabd
10 changed files with 468 additions and 140 deletions
+110 -33
View File
@@ -38,7 +38,14 @@ def zellen(frames):
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])
ch = chr(frame[j + 1])
# Das Gradzeichen entsteht ueber einen Zeichensatzwechsel und
# steht als DEGREE_CHAR ("/") im Telegramm. Hier wieder in das
# zurueckuebersetzen, was auf der Anzeige zu sehen ist.
if (frame[i + 8:j].startswith(b"\x1bz")
and ch == config.DEGREE_CHAR):
ch = config.DEGREE_MARKER
out[(x, y)] = ch
i += 8
else:
i += 1
@@ -52,12 +59,22 @@ def zeile(frames, index):
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)
# So viele *volle* Zellen passen in eine Zeile. Das ist weniger als
# config.CHARS_PER_LINE: Dessen 11 Zeichen gehen nur auf, wenn mindestens ein
# schmales dabei ist (config.CELL_NARROW). Fuer Tests mit normalen Zeichen ist
# das hier die Zeilenbreite.
VOLLE_ZELLEN = (config.DISPLAY_WIDTH - config.LINE_X) // config.CELL_WIDTH
def aufbereitet(text):
"""Erwartungswert: Text so, wie er auf der Zeile landet.
Nutzt bewusst dieselbe Funktion wie der Treiber. Wie breit eine Zeile ist,
haengt seit config.CELL_NARROW davon ab, welche Zeichen darin stehen -- eine
zweite Nachbildung hier waere nur eine Fehlerquelle. Was der Wert *bedeutet*,
pruefen die Tests darunter mit eigenen Zusicherungen.
"""
return Display._aufbereiten(text)
class TestFesteTexte(unittest.TestCase):
@@ -174,8 +191,8 @@ class TestNurGeaenderteZellen(unittest.TestCase):
frames = self._neu()
# Loeschtelegramm + beide Zeilen vollstaendig
self.assertEqual(len(frames), 3)
self.assertEqual(len(zeile(frames, 0)), config.CHARS_PER_LINE)
self.assertEqual(len(zeile(frames, 1)), config.CHARS_PER_LINE)
self.assertEqual(len(zeile(frames, 0)), VOLLE_ZELLEN)
self.assertEqual(len(zeile(frames, 1)), VOLLE_ZELLEN)
def test_nach_clear_kein_rest_der_alten_anzeige(self):
"""clear muss den gemerkten Inhalt verwerfen.
@@ -211,7 +228,7 @@ class TestNurGeaenderteZellen(unittest.TestCase):
# Dieselbe Zeile noch einmal: muss vollstaendig geschrieben werden.
self.d.show_lines("18h36m57s", "+38°47'01\"")
self.assertEqual(len(zeile(self._neu(), 0)), config.CHARS_PER_LINE)
self.assertEqual(len(zeile(self._neu(), 0)), VOLLE_ZELLEN)
def test_gradzeichen_wird_nicht_staendig_neu_gesetzt(self):
"""Spart nicht nur Bytes: Jeder Zeichensatzwechsel beschaedigt (0,0)."""
@@ -221,6 +238,8 @@ class TestNurGeaenderteZellen(unittest.TestCase):
gesendet = b"".join(self._neu())
self.assertNotIn(b"\x1bz01", gesendet)
def test_neues_gradzeichen_zieht_die_oberste_zeile_nach(self):
"""Wird der Zeichensatz doch gewechselt, ist die Home-Zelle hin.
@@ -233,7 +252,66 @@ class TestNurGeaenderteZellen(unittest.TestCase):
frames = self._neu()
self.assertIn(b"\x1bz01", b"".join(frames)) # Wechsel fand statt
# Obere Zeile trotz unveraendertem Text komplett neu geschrieben.
self.assertEqual(len(zeile(frames, 0)), config.CHARS_PER_LINE)
self.assertEqual(len(zeile(frames, 0)), VOLLE_ZELLEN)
class TestSchmaleZeichen(unittest.TestCase):
"""Der Dezimalpunkt der Zehntelminuten braucht weniger als eine volle Zelle.
In der Sperrschrift stand "RA 18:37.1" sonst sehr luftig -- der Punkt belegt
6 px, obwohl er nur ein, zwei Pixel breit ist. 3 px am Geraet ausprobiert
(2026-07-27). Moeglich ist das nur, weil jedes Zeichen ohnehin einzeln
positioniert wird.
"""
def setUp(self):
self.t = FakeTransport(echo=False)
self.d = Display(self.t, response=True)
def _neu(self):
frames = self.t.frames[:]
self.t.frames.clear()
return frames
def test_punkt_bekommt_weniger_platz(self):
pos = Display._positionen("1.2")
self.assertEqual(pos[1] - pos[0], config.CELL_WIDTH) # vor dem Punkt
self.assertEqual(pos[2] - pos[1], config.CELL_NARROW["."]) # nach dem Punkt
def test_zeile_mit_punkt_wird_schmaler_und_bleibt_mittig(self):
def breite(positionen):
return positionen[-1] + config.CELL_WIDTH - positionen[0]
ohne = Display._positionen("RA 18:3701") # gleiche Laenge, kein Punkt
mit = Display._positionen("RA 18:37.1")
self.assertEqual(breite(ohne) - breite(mit),
config.CELL_WIDTH - config.CELL_NARROW["."])
# Weiterhin vollstaendig innerhalb der Anzeige.
self.assertGreaterEqual(mit[0], 0)
self.assertLessEqual(mit[-1] + config.CELL_WIDTH, config.DISPLAY_WIDTH)
def test_zehntel_wechsel_schreibt_weiter_nur_wenige_zellen(self):
"""Der Alltagsfall bei Niedrigpraezision -- das Nachfuehren muss greifen."""
self.d.show_lines("RA 18:37.1", "DE +38°47'")
self._neu()
self.d.show_lines("RA 18:37.2", "DE +38°47'")
frames = self._neu()
self.assertEqual(len(frames), 1) # DEC unveraendert
self.assertLessEqual(len(zellen(frames)), 2) # nur die Zehntelziffer
def test_strukturwechsel_schreibt_die_zeile_vollstaendig_neu(self):
"""Kommt ein Punkt dazu, sitzen alle folgenden Zellen woanders.
Ein Vergleich Zeichen fuer Zeichen waere dann sinnlos -- es muss alles
neu geschrieben werden.
"""
self.d.show_lines("RA 18:3701") # ohne Punkt: 10 volle Zellen
self._neu()
self.d.show_lines("RA 18:37.1") # mit Punkt: andere Positionen
gesetzt = zellen(self._neu())
# Alles neu -- und dank des schmalen Punktes passt sogar ein Zeichen mehr.
self.assertEqual(len(gesetzt), len(Display._aufbereiten("RA 18:37.1")))
self.assertGreater(len(gesetzt), VOLLE_ZELLEN)
class TestZeilenaufbau(unittest.TestCase):
@@ -252,18 +330,28 @@ class TestZeilenaufbau(unittest.TestCase):
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"))
self.assertEqual(zeile(self._alle(), 0), aufbereitet("AB"))
self.assertEqual(zeile(self._alle(), 1), aufbereitet("CD"))
# Es wird immer die volle Breite geschrieben, damit nichts stehen bleibt.
self.assertEqual(len(zeile(self._alle(), 0)), config.CHARS_PER_LINE)
self.assertEqual(len(zeile(self._alle(), 0)), VOLLE_ZELLEN)
def test_kuerzere_zeile_wird_mittig_gesetzt(self):
"""Zentriert heisst: links und rechts gleich viel Luft (Nutzerwunsch)."""
def test_kuerzere_zeile_steht_linksbuendig(self):
"""Nutzerwunsch 2026-07-27: beide Zeilen buendig untereinander.
Zentriert standen sie um wenige Pixel gegeneinander versetzt, weil die
RA-Zeile mit ihrem schmalen Dezimalpunkt kuerzer ist als die DEC-Zeile.
"""
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)
self.assertEqual(text, "+38°47'" + " " * (VOLLE_ZELLEN - 7))
def test_beide_zeilen_beginnen_an_derselben_stelle(self):
"""Der eigentliche Punkt: RA und DE muessen buendig starten."""
self.d.show_lines("RA:18h37.1", "DE:+38°47'")
gesetzt = zellen(self._alle())
erste_oben = min(x for (x, y) in gesetzt if y == config.LINE_Y[0])
erste_unten = min(x for (x, y) in gesetzt if y == config.LINE_Y[1])
self.assertEqual(erste_oben, erste_unten)
def test_jedes_zeichen_sitzt_auf_seiner_zelle(self):
"""Der Auto-Vorschub der Sperrschrift ist 7 px und liesse nur 9 Zeichen
@@ -272,13 +360,13 @@ class TestZeilenaufbau(unittest.TestCase):
self.d.show_lines("0123456789")
gesetzt = zellen(self._alle())
y = config.LINE_Y[0]
for i in range(config.CHARS_PER_LINE):
for i in range(VOLLE_ZELLEN):
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
rechts = config.LINE_X + VOLLE_ZELLEN * config.CELL_WIDTH
self.assertLessEqual(rechts, config.DISPLAY_WIDTH)
self.assertGreaterEqual(config.LINE_X, 0)
@@ -287,18 +375,7 @@ class TestZeilenaufbau(unittest.TestCase):
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
self.assertEqual(zeile(self._alle(), 0), "X" * VOLLE_ZELLEN)
def test_oberste_zeile_wird_zuletzt_gezeichnet(self):
"""Schutz der Home-Zelle: ein Zeichensatzwechsel zerstoert (0,0),