Files
grossanzeige/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

177 lines
7.4 KiB
Python

"""Treiber fuer die Grossanzeige: Telegramme bauen, senden, Antwort pruefen."""
import time
import migra
import config
class Display:
"""Sendet Telegramme an die Anzeige und wertet das Antworttelegramm aus.
Das Antworttelegramm ist zugleich die Flusskontrolle: Nach seinem Erhalt
darf sofort das naechste Telegramm folgen (Handbuch 3.5). Ohne Antwort
muesste zwischen den Telegrammen "Empfangstimeout + 150 ms" gewartet werden.
"""
def __init__(self, transport,
dest=config.DISPLAY_ADDR,
src=config.HOST_ADDR,
checksum=config.USE_CHECKSUM,
response=config.WANT_RESPONSE):
self._t = transport
self._dest = dest
self._src = src
self._checksum = checksum
self._response = response
def send(self, data_unit):
"""Data-Unit rahmen, senden und ggf. auf das Antworttelegramm warten."""
frame = migra.build_telegram(
data_unit,
dest=self._dest,
src=self._src,
checksum=self._checksum,
response=self._response,
)
self._t.write(frame)
if self._response:
migra.parse_response(self._t.read_response(), src=self._src)
else:
# Ohne Antwort ist die Anzeige erst nach Abarbeitung des Telegramms
# wieder empfangsbereit: Empfangstimeout + 150 ms (Handbuch 3.5).
time.sleep(config.QUIET_TIME)
# -- Hoehere Funktionen ------------------------------------------------
def clear(self):
self.send(migra.DataUnit().fill(config.BACKGROUND))
def set_brightness(self, percent):
self.send(migra.DataUnit().brightness(percent, color=config.COLOR))
def show_lines(self, *lines, clear=False):
"""Zeilen ab Zeile 1 auf die Anzeige schreiben.
Pro Zeile ein eigenes Telegramm: Cursor setzen und Text schreiben. Das
Gradzeichen (config.DEGREE_MARKER) wird ueber einen kurzen
Zeichensatzwechsel eingefuegt.
Frueher war das ein einziges Telegramm fuer beide Zeilen. Seit jedes
Zeichen einzeln positioniert wird (10 statt 9 Zeichen, siehe _emit_line),
kostet eine Zeile rund 100 Bytes -- beide zusammen kaemen mit Gradzeichen
und Vollbild-Loeschen auf 224 der erlaubten 230 Bytes (migra.MAX_DATA_UNIT).
Das ist zu wenig Reserve fuer ein Geraet, das monatelang durchlaeuft;
getrennte Telegramme halten jedes bei rund der Haelfte. Die Flusskontrolle
ueber das Antworttelegramm macht die zweite Sendung unproblematisch.
Jede Zeile wird mit Leerzeichen auf volle Breite (CHARS_PER_LINE)
aufgefuellt und damit *an Ort und Stelle* ueberschrieben -- ohne die
Anzeige vorher komplett zu loeschen. Das vermeidet das sichtbare Blitzen
bei zyklischen Updates und haelt das Telegramm kurz (sonst kommt das
naechste zu frueh und das letzte Zeichen kann verloren gehen).
clear=True erzwingt zusaetzlich ein Vollbild-Loeschen (einmal beim Start).
Reihenfolge: Die Zeilen werden von unten nach oben gezeichnet, damit die
oberste Zeile zuletzt kommt. Grund: Ein Zeichensatzwechsel mitten im
Telegramm beschaedigt die Home-Zelle (0,0) -- die oberste Zeile muss also
nach allen Wechseln geschrieben werden, damit sie diese Zelle ueberschreibt.
(Am Geraet verifiziert 2026-07-15.)
"""
if len(lines) > len(config.LINE_Y):
raise ValueError("Anzeige hat nur %d Zeilen" % len(config.LINE_Y))
first = True
for i in reversed(range(len(lines))):
du = migra.DataUnit()
if clear and first:
du.fill(config.BACKGROUND)
du.attributes(fg=config.COLOR, bg=config.BACKGROUND, blink=migra.STATIC)
self._emit_line(du, config.LINE_Y[i], lines[i])
self.send(du)
first = False
def show_line(self, index, text, clear=False):
"""Nur eine einzelne Zeile aktualisieren (eigenes, kurzes Telegramm).
Fuer zyklische Updates: die Zeile ohne Gradzeichen (also ohne
Zeichensatzwechsel) laesst sich beliebig oft neu schreiben, ohne dass die
Anzeige Bytes verschluckt. Zeilen *mit* Gradzeichen sollten dagegen selten
und moeglichst per show_lines (Vollbild) gezeichnet werden.
"""
if not 0 <= index < len(config.LINE_Y):
raise ValueError("ungueltiger Zeilenindex %d" % index)
du = migra.DataUnit()
if clear:
du.fill(config.BACKGROUND)
du.attributes(fg=config.COLOR, bg=config.BACKGROUND, blink=migra.STATIC)
self._emit_line(du, config.LINE_Y[index], text)
self.send(du)
def _emit_line(self, du, y, text):
"""Eine Zeile bei Pixelzeile y ausgeben, auf volle Breite aufgefuellt.
Jedes Zeichen wird einzeln auf x = i*CELL_WIDTH gesetzt, statt den
Auto-Vorschub der Anzeige laufen zu lassen. Der Vorschub der Sperrschrift
ist 7 px und liesse nur 9 Zeichen zu; die Zeichenmatrix ist aber nur 6 px
breit, sodass 10 Zeichen sauber nebeneinander passen (am Geraet gemessen
2026-07-27, siehe config.CHARS_PER_LINE).
"""
# Ohne str.ljust/center: die kennt MicroPython nicht (am Geraet aufgefallen).
text = text[:config.CHARS_PER_LINE]
rest = config.CHARS_PER_LINE - len(text)
links = rest // 2 if config.CENTER_LINES else 0
text = " " * links + text + " " * (rest - links)
du.charset(config.CHARSET, spaced=config.CHARSET_SPACED)
for i in range(len(text)):
du.cursor(config.LINE_X + i * config.CELL_WIDTH, y)
if text[i] == config.DEGREE_MARKER:
self._emit_degree(du)
else:
du.text(text[i])
def _emit_degree(self, du):
"""Das Gradzeichen einfuegen -- ueber Zeichensatzwechsel oder als Fallback."""
if config.DEGREE_CHARSET is None:
du.text(config.DEGREE_FALLBACK)
return
du.charset(config.DEGREE_CHARSET, spaced=config.CHARSET_SPACED)
du.text(config.DEGREE_CHAR)
du.charset(config.CHARSET, spaced=config.CHARSET_SPACED)
def close(self):
self._t.close()
def open_display(port=None, fake=False, response=config.WANT_RESPONSE):
"""Transport passend zur Plattform oeffnen und Display-Objekt liefern.
Auf dem ESP32-C3 wird machine.UART genommen (Pins aus config), auf dem Mac
pyserial mit config.PORT. fake=True haengt stattdessen den Mitschreiber ein.
"""
if fake:
from transport import FakeTransport
t = FakeTransport(src=config.HOST_ADDR, display_addr=config.DISPLAY_ADDR)
elif config.MICROPYTHON:
from transport import UartTransport
t = UartTransport(
uart_id=config.UART_ID,
baudrate=config.BAUDRATE,
parity=config.PARITY,
bytesize=config.BYTESIZE,
stopbits=config.STOPBITS,
timeout=config.RESPONSE_TIMEOUT,
tx=config.UART_TX,
rx=config.UART_RX,
)
else:
from transport import SerialTransport
t = SerialTransport(
port=config.PORT if port is None else port,
baudrate=config.BAUDRATE,
parity=config.PARITY,
bytesize=config.BYTESIZE,
stopbits=config.STOPBITS,
timeout=config.RESPONSE_TIMEOUT,
)
return Display(t, response=response)