fix(station): EnvironmentFile-Syntax, Bias-Zyklus, echte Panel-Tests
Fixrunde 3 zu Task 6a. EnvironmentFile=-/etc/ninlight-station.conf statt -EnvironmentFile= (Fixrunde 2 hatte das Praefix vor den Direktivennamen statt vor den Dateipfad gesetzt - systemd verwarf die Zeile komplett, die Konfigurationsdatei wurde ueberhaupt nicht mehr gelesen). clear() stellte die in Panel dokumentierte Awake-Invariante nicht her (kein 0x26-Referenzpuffer) und verwendet jetzt wie draw_full() displayPartBaseImage(). Die 30-Minuten- Vollaufbau-Uhr feuerte bisher auch ohne vorherige Teilaufbauten und erzeugte im eingeschwungenen Ruhezustand einen endlosen Schlaf-/Wach-Zyklus mit 33 Prozent Bias-Anteil - sie greift jetzt nur noch, wenn seit dem letzten Vollaufbau tatsaechlich Teilaufbauten stattfanden, und der Zaehler wird beim Einschlafen zurueckgesetzt. draw_partial() meldet zurueck, ob es intern zu einem Vollaufbau aufgewertet hat, damit die 30-Minuten-Uhr korrekt bleibt. compose() ersetzt eingebettete Zeilenumbrueche vor der Breitenmessung. Dazu ein Kommando-Rekorder fuer die Tests, der die tatsaechlich gesendete SPI-Byte-Folge protokolliert: die vorherigen Tests fuer den Referenzpuffer (N2) und die Registerfolge nach dem Schlafen (N3) pruefften nur "keine Ausnahme" und blieben laut Reviewer-Mutation gruen, wenn die eigentliche Fehlerursache wieder eingefuehrt wurde. Die neuen Tests pruefen die gesendeten Kommandos direkt und wurden gegen genau diese Mutationen verifiziert.
This commit is contained in:
@@ -220,11 +220,16 @@ class Panel:
|
||||
aufgewertet - unabhaengig davon, in welcher Reihenfolge draw_full()/draw_partial()/
|
||||
sleep() aufgerufen werden.
|
||||
|
||||
draw_full() verwendet fuer BEIDE Revisionen displayPartBaseImage() statt display()
|
||||
(Fixrunde 2, N2): das schreibt beide RAM-Puffer des Controllers (0x24 UND 0x26) und
|
||||
legt damit erst die Referenz an, gegen die ein nachfolgender Teilaufbau diffen kann -
|
||||
display() allein haette nur 0x24 beschrieben und den ersten Teilaufbau danach gegen
|
||||
einen veralteten/undefinierten Puffer laufen lassen."""
|
||||
draw_full() (und clear(), Fixrunde 3, F2) verwendet fuer BEIDE Revisionen
|
||||
displayPartBaseImage() statt display()/Clear() (Fixrunde 2, N2): das schreibt beide
|
||||
RAM-Puffer des Controllers (0x24 UND 0x26) und legt damit erst die Referenz an, gegen
|
||||
die ein nachfolgender Teilaufbau diffen kann - display() allein haette nur 0x24
|
||||
beschrieben (und Clear() ebenso - bei V2 ist das 0x26-Schreiben in Clear() sogar
|
||||
auskommentiert) und den ersten Teilaufbau danach gegen einen veralteten/undefinierten
|
||||
Puffer laufen lassen. self._awake wird deshalb nur dort auf True gesetzt, wo
|
||||
tatsaechlich ein gueltiger Referenzpuffer entstanden ist - vorher (Fixrunde 2) setzte
|
||||
clear() es, obwohl es nur epd.Clear() (kein 0x26) rief, und die im Klassen-Docstring
|
||||
behauptete Invariante stimmte nach clear() nicht."""
|
||||
|
||||
def __init__(self, revision: str) -> None:
|
||||
self.revision = revision
|
||||
@@ -241,9 +246,23 @@ class Panel:
|
||||
else:
|
||||
self.epd.init()
|
||||
|
||||
def _blank_buffer(self) -> list[int]:
|
||||
"""Ein reiner Weiss-Puffer in der Groesse, die epd.getbuffer() fuer ein
|
||||
vollformatiges Bild liefern wuerde - dieselbe Formel wie in epd.Clear() selbst
|
||||
(siehe epd2in13_V2.py/epd2in13_V3.py), nur ohne ueber Pillow ein echtes weisses
|
||||
Bild zu erzeugen."""
|
||||
width, height = self.epd.width, self.epd.height
|
||||
linewidth = width // 8 if width % 8 == 0 else width // 8 + 1
|
||||
return [0xFF] * (linewidth * height)
|
||||
|
||||
def clear(self) -> None:
|
||||
self._full_init()
|
||||
self.epd.Clear(0xFF)
|
||||
# displayPartBaseImage() statt epd.Clear() (Fixrunde 3, F2): dieselbe sichtbare
|
||||
# Wirkung (Panel wird weiss), aber schreibt zusaetzlich den 0x26-Referenzpuffer,
|
||||
# den ein nachfolgender draw_partial() braucht - epd.Clear() allein wuerde
|
||||
# self._awake=True vortaeuschen, ohne die im Klassen-Docstring beschriebene
|
||||
# Invariante wirklich herzustellen.
|
||||
self.epd.displayPartBaseImage(self._blank_buffer())
|
||||
self._awake = True
|
||||
|
||||
def draw_full(self, image) -> None:
|
||||
@@ -252,17 +271,24 @@ class Panel:
|
||||
self.epd.displayPartBaseImage(buf)
|
||||
self._awake = True
|
||||
|
||||
def draw_partial(self, image) -> None:
|
||||
def draw_partial(self, image) -> bool:
|
||||
"""Zeichnet teilweise - oder, falls das Panel nicht wach ist, vollstaendig.
|
||||
Gibt zurueck, ob intern zu einem Vollaufbau aufgewertet wurde (Fixrunde 3, F4):
|
||||
der Aufrufer muss das wie einen regulaeren Vollaufbau behandeln (z.B. fuer die
|
||||
30-Minuten-Uhr in run_daemon()), sonst kommt der naechste dortige Vollaufbau zu
|
||||
frueh und altert das Panel zusaetzlich, ohne dass ein Teilaufbau ueberhaupt
|
||||
stattgefunden haette."""
|
||||
if not self._awake:
|
||||
# Siehe Klassen-Docstring (Fixrunde 2, N1/N3): ein Teilaufbau direkt nach dem
|
||||
# Schlafen (oder vor dem allerersten Vollaufbau) ist auf keiner Revision eine
|
||||
# gueltige Sequenz - stattdessen einen vollwertigen Vollaufbau ausfuehren.
|
||||
self.draw_full(image)
|
||||
return
|
||||
return True
|
||||
buf = self.epd.getbuffer(image)
|
||||
if self.revision == "V2":
|
||||
self.epd.init(self.epd.PART_UPDATE)
|
||||
self.epd.displayPartial(buf)
|
||||
return False
|
||||
|
||||
def sleep(self) -> None:
|
||||
# Idempotent: ein zweiter sleep()-Aufruf, waehrend das Panel schon schlaeft,
|
||||
@@ -318,6 +344,15 @@ def run_daemon(revision: str) -> None:
|
||||
last_mode: object = object() # garantiert ungleich jedem echten Modus beim ersten Lauf
|
||||
last_full = time.monotonic()
|
||||
last_draw = time.monotonic()
|
||||
# Zaehlt Teilaufbauten seit dem letzten Vollaufbau. FULL_REFRESH_INTERVAL_S existiert
|
||||
# nur, um Geisterbilder aus AUFEINANDERFOLGENDEN Teilaufbauten auszugleichen - ohne
|
||||
# mindestens einen Teilaufbau seit dem letzten Vollaufbau gibt es nichts auszugleichen,
|
||||
# und ein Vollaufbau des UNVERAENDERTEN Bildes waere reine, nutzlose Alterung
|
||||
# (Fixrunde 3, F3): ohne diese Absicherung entstand im eingeschwungenen Ruhezustand ein
|
||||
# endloser 10-Minuten-schlafen/10-Minuten-wach-Zyklus (33% Bias-Anteil) rein aus der
|
||||
# 30-Minuten-Uhr, obwohl sich der Inhalt nie aenderte - genau das, was I4 verhindern
|
||||
# sollte.
|
||||
partials_since_full = 0
|
||||
|
||||
while True:
|
||||
state = build_state()
|
||||
@@ -326,18 +361,25 @@ def run_daemon(revision: str) -> None:
|
||||
now = time.monotonic()
|
||||
|
||||
mode_changed = mode != last_mode
|
||||
stale = (now - last_full) >= FULL_REFRESH_INTERVAL_S
|
||||
stale = partials_since_full > 0 and (now - last_full) >= FULL_REFRESH_INTERVAL_S
|
||||
|
||||
if current_lines != last_lines or mode_changed or stale:
|
||||
image = render.compose(state)
|
||||
if mode_changed or stale:
|
||||
panel.draw_full(image)
|
||||
last_full = now
|
||||
partials_since_full = 0
|
||||
else:
|
||||
# Panel.draw_partial() wertet selbst zu einem Vollaufbau auf, falls das
|
||||
# Panel seit dem letzten Aufbau geschlafen hat (siehe Panel-Klasse) -
|
||||
# run_daemon() muss den Schlafzustand dafuer nicht selbst nachhalten.
|
||||
panel.draw_partial(image)
|
||||
upgraded = panel.draw_partial(image)
|
||||
if upgraded:
|
||||
# Panel.draw_partial() hat selbst zu einem Vollaufbau aufgewertet
|
||||
# (Panel war nicht wach, siehe Panel-Klasse) - das zaehlt fuer die
|
||||
# 30-Minuten-Uhr wie ein regulaerer Vollaufbau (Fixrunde 3, F4), sonst
|
||||
# kaeme der naechste "stale"-Vollaufbau zu frueh.
|
||||
last_full = now
|
||||
partials_since_full = 0
|
||||
else:
|
||||
partials_since_full += 1
|
||||
last_draw = now
|
||||
last_lines = current_lines
|
||||
last_mode = mode
|
||||
@@ -347,6 +389,12 @@ def run_daemon(revision: str) -> None:
|
||||
# weniger). Panel.sleep() ist idempotent, ein wiederholter Aufruf hier
|
||||
# (solange weiterhin nichts passiert) ist also unschaedlich.
|
||||
panel.sleep()
|
||||
# Nach dem Schlafen wertet Panel.draw_partial() den naechsten Aufbau ohnehin
|
||||
# zu einem Vollaufbau auf - der 30-Minuten-Zaehler soll dieses "natuerliche"
|
||||
# Zuruecksetzen nicht durch einen zusaetzlichen, inhaltslosen "stale"-
|
||||
# Vollaufbau vorwegnehmen, falls vor dem Einschlafen schon Teilaufbauten
|
||||
# stattgefunden hatten (Fixrunde 3, F3, zweiter Teil).
|
||||
partials_since_full = 0
|
||||
|
||||
time.sleep(POLL_INTERVAL_S)
|
||||
|
||||
|
||||
@@ -151,9 +151,17 @@ def compose(state: dict):
|
||||
return draw.textlength(text, font=font)
|
||||
|
||||
y = TEXT_MARGIN_PX
|
||||
for line in lines(state):
|
||||
for raw_line in lines(state):
|
||||
if y > PANEL_HEIGHT_PX - LINE_HEIGHT_PX:
|
||||
break
|
||||
# draw.textlength() wirft ValueError("can't measure length of multiline text"),
|
||||
# sobald eine Zeile ein "\n" enthaelt (mit Pillow 12 nachgeprueft) - waere von
|
||||
# main()s except (DisplayUnavailable, ImportError) nicht gefangen (Fixrunde 3,
|
||||
# F5). lines() erzeugt selbst kein "\n", aber Werte aus status.json/last_seen
|
||||
# (z.B. product_name) sind letztlich Daten von der Lampe/dem Netzwerk, kein
|
||||
# Wert, den lines() selbst kontrolliert - ein Zeilenumbruch dort waere ohnehin
|
||||
# nur ein zerstoertes Layout, kein sinnvoller Inhalt.
|
||||
line = raw_line.replace("\n", " ")
|
||||
fitted = _truncate_to_width(line, measure=measure, max_width_px=max_width_px)
|
||||
draw.text((TEXT_MARGIN_PX, y), fitted, font=font, fill=0)
|
||||
y += LINE_HEIGHT_PX
|
||||
|
||||
@@ -24,10 +24,15 @@ Type=simple
|
||||
Environment=EPD_REVISION=V3
|
||||
Environment=NINLIGHT_RUN_DIR=/run/ninlight-station
|
||||
Environment=NINLIGHT_STATION_CONF=/etc/ninlight-station.conf
|
||||
# "-"-Praefix: ohne die Konfigurationsdatei soll die Anzeige trotzdem starten (mit den
|
||||
# Environment=-Vorgaben oben) statt mit einem harten Fehler - "die Anzeige darf nirgends
|
||||
# Voraussetzung sein" gilt auch fuer ihre eigene Konfiguration (Fixrunde 1/2, Hinweis).
|
||||
-EnvironmentFile=/etc/ninlight-station.conf
|
||||
# "-"-Praefix VOR dem Dateipfad, nicht vor dem Direktivennamen (Fixrunde 3, F1 - das war
|
||||
# in Fixrunde 2 falsch herum: "-EnvironmentFile=..." ist kein gueltiger Schluesselname,
|
||||
# systemd verwirft die Zeile komplett mit "Unknown key name", und die Konfigurationsdatei
|
||||
# wurde dadurch ueberhaupt nicht mehr gelesen - Environment=EPD_REVISION=V3 oben haette
|
||||
# IMMER gewonnen, unabhaengig davon, was in /etc/ninlight-station.conf steht). Mit dem
|
||||
# Praefix am richtigen Platz gilt: ohne die Konfigurationsdatei startet die Anzeige
|
||||
# trotzdem (mit den Environment=-Vorgaben oben) statt mit einem harten Fehler - "die
|
||||
# Anzeige darf nirgends Voraussetzung sein" gilt auch fuer ihre eigene Konfiguration.
|
||||
EnvironmentFile=-/etc/ninlight-station.conf
|
||||
ExecStart=/usr/bin/python3 /opt/ninlight-station/display/display.py
|
||||
|
||||
# display.py faengt ein fehlendes/abgestuerztes Panel als DisplayUnavailable ab,
|
||||
|
||||
@@ -62,38 +62,31 @@ def test_truncate_to_width_leaves_short_text_untouched():
|
||||
assert render._truncate_to_width("kurz", measure=len, max_width_px=10) == "kurz"
|
||||
|
||||
|
||||
def test_compose_keeps_every_line_within_panel_width():
|
||||
"""Regressionstest zu Fixrunde 2: eine Kuerzung nach Zeichenzahl (die fruehere
|
||||
MAX_COLS-Fassung) ist bei der von compose() verwendeten Proportionalschrift
|
||||
(PIL.ImageFont.load_default(), seit Pillow 10.1 eine TrueType-Schrift) keine
|
||||
gueltige Breitengrenze - eine grossbuchstabenlastige Zeile wie ein Produktname aus
|
||||
der Lampenerkennung ("KLYQA GU10 RGB CW/WW") ist pro Zeichen breiter als eine
|
||||
gemischte Beispielzeile und lief trotz weniger Zeichen als die alte Grenze ueber die
|
||||
Panelbreite hinaus. Pillow ist in dieser Testumgebung vorhanden (siehe Bericht); auf
|
||||
dem Ziel-Pi kommt es erst per "apt install python3-pil" hinzu, genau wie fuer
|
||||
compose() selbst."""
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
def test_compose_does_not_draw_into_the_last_column_for_long_lines():
|
||||
"""Regressionstest zu Fixrunde 3: der Reviewer hat per Mutation belegt, dass eine
|
||||
deaktivierte Kuerzung in compose() (fitted = line) die vorherige Testsuite NICHT
|
||||
aufgehalten haette, weil der alte Test die Kuerzung selbst nachbaute (mit derselben
|
||||
_truncate_to_width()-Funktion, die auch geprueft werden sollte) statt das von
|
||||
compose() tatsaechlich GEZEICHNETE BILD zu inspizieren.
|
||||
|
||||
Dieser Test prueft stattdessen die Pixel direkt: bei korrekt wirkender Kuerzung
|
||||
bleibt die letzte Panel-Spalte (x = PANEL_WIDTH_PX - 1) fuer eine absichtlich sehr
|
||||
lange Zeile weiss, weil die Kuerzung auf PANEL_WIDTH_PX - 2*TEXT_MARGIN_PX zielt
|
||||
(empirisch mit echtem Pillow bestaetigt: die rechte Kante liegt bei ca. Spalte 237,
|
||||
deutlich vor Spalte 249). Mit deaktivierter Kuerzung fuellt der zeichenweise laengere
|
||||
Text die Zeile bis zum Rand - ebenfalls empirisch bestaetigt (die letzte Spalte
|
||||
enthaelt dann einen schwarzen Pixel)."""
|
||||
state = {"mode": "migration", "phase": "listen", "served": 0, "recent": [],
|
||||
"ssid": "qconnex-firmware",
|
||||
"last_seen": {"product_name": "KLYQA GU10 RGB CW/WW " + "W" * 40,
|
||||
"target": "e27_rgb", "variant": "dev", "lamp_version": "V1"}}
|
||||
"last_seen": {"product_name": "W" * 200, "target": "e27_rgb",
|
||||
"variant": "dev", "lamp_version": "V1"}}
|
||||
|
||||
# compose() muss uebrigens auch ohne Ausnahme durchlaufen - reine Rauchprobe.
|
||||
image = render.compose(state)
|
||||
assert image.size == (render.PANEL_WIDTH_PX, render.PANEL_HEIGHT_PX)
|
||||
pixels = image.load()
|
||||
last_col = render.PANEL_WIDTH_PX - 1
|
||||
|
||||
# Dieselbe Kuerzung, die compose() intern verwendet (echte Schrift/Messfunktion),
|
||||
# muss jede Zeile unter die verfuegbare Panelbreite bringen.
|
||||
dummy = Image.new("1", (1, 1))
|
||||
draw = ImageDraw.Draw(dummy)
|
||||
font = ImageFont.load_default()
|
||||
max_width_px = render.PANEL_WIDTH_PX - 2 * render.TEXT_MARGIN_PX
|
||||
for raw_line in render.lines(state):
|
||||
fitted = render._truncate_to_width(
|
||||
raw_line, measure=lambda s: draw.textlength(s, font=font),
|
||||
max_width_px=max_width_px)
|
||||
assert draw.textlength(fitted, font=font) <= max_width_px
|
||||
black_in_last_column = any(pixels[last_col, y] == 0 for y in range(render.PANEL_HEIGHT_PX))
|
||||
assert not black_in_last_column, "Text erreicht die letzte Spalte - Kuerzung wirkt nicht"
|
||||
|
||||
|
||||
def test_missing_fields_do_not_raise():
|
||||
@@ -275,12 +268,13 @@ def test_v3_partial_draw_after_sleep_does_not_crash(monkeypatch):
|
||||
|
||||
|
||||
def test_v2_partial_draw_after_sleep_does_not_crash(monkeypatch):
|
||||
"""Dieselbe Absicherung fuer V2 (Fixrunde 2, N3): epd2in13_V2.sleep() ist Deep Sleep
|
||||
Mode 2 (RAM/Register verworfen); ein init(PART_UPDATE) direkt danach setzt nur einen
|
||||
Teil der fuer einen Teilaufbau noetigen Register (0x01/0x11/0x44/0x45/0x4E/0x4F setzt
|
||||
ausschliesslich der FULL_UPDATE-Zweig) - keine Ausnahme, aber eine auf echter
|
||||
Hardware unbrauchbare Sequenz. draw_partial() muss auch hier nach sleep() zu einem
|
||||
vollwertigen FULL_UPDATE-Vollaufbau (inklusive displayPartBaseImage()) aufwerten."""
|
||||
"""Dieselbe Rauchprobe fuer V2 wie oben fuer V3 (Fixrunde 2, N3) - dass hier NICHTS
|
||||
crasht, ist aber nur die halbe Absicherung: ob dabei auch die RICHTIGEN Register
|
||||
gesetzt werden, prueft erst der staerkere Test unten
|
||||
(test_v2_wake_after_sleep_sets_full_update_registers) mit einem Kommando-Rekorder.
|
||||
Der Reviewer hat zu Recht angemerkt, dass eine Fassung dieses Tests ohne jede
|
||||
Zusicherung nichts ueber die Registerlage aussagt - deshalb hier wenigstens die
|
||||
beobachtbaren Panel-Zustandsuebergaenge pruefen."""
|
||||
_stub_hardware(monkeypatch)
|
||||
|
||||
panel = display.Panel("V2")
|
||||
@@ -289,16 +283,170 @@ def test_v2_partial_draw_after_sleep_does_not_crash(monkeypatch):
|
||||
panel.clear()
|
||||
panel.draw_full(image)
|
||||
panel.sleep()
|
||||
panel.draw_partial(image)
|
||||
assert panel._awake is False # Vorbedingung fuer den eigentlich interessanten Schritt
|
||||
|
||||
panel.draw_partial(image) # das ist der Schritt, der bei falscher Sequenz froeren wuerde
|
||||
|
||||
assert panel._awake is True
|
||||
assert epdconfig._spi is not None # Panel hat sich nach dem Schlafen neu initialisiert
|
||||
|
||||
|
||||
def test_panel_sleep_is_idempotent(monkeypatch):
|
||||
"""Ein zweiter sleep()-Aufruf, waehrend das Panel schon schlaeft, darf nicht
|
||||
versuchen, ueber die von module_exit() bereits geschlossenen Pins SPI-Befehle zu
|
||||
schicken (derselbe AttributeError-Mechanismus wie N1, nur selbst herbeigefuehrt)."""
|
||||
schicken (derselbe AttributeError-Mechanismus wie N1, nur selbst herbeigefuehrt).
|
||||
Die staerkere Fassung unten (test_panel_sleep_sends_no_commands_when_already_asleep)
|
||||
prueft das zusaetzlich direkt an der gesendeten Kommandofolge."""
|
||||
_stub_hardware(monkeypatch)
|
||||
|
||||
panel = display.Panel("V3")
|
||||
panel.clear()
|
||||
panel.sleep()
|
||||
assert panel._awake is False
|
||||
|
||||
panel.sleep() # darf nicht erneut versuchen, epd.sleep() aufzurufen
|
||||
|
||||
assert panel._awake is False
|
||||
|
||||
|
||||
# --- Kommando-Rekorder: Registerzustand nach echten Bytes pruefen, nicht nur "kein Absturz"
|
||||
#
|
||||
# Der Reviewer hat per Mutation gezeigt, dass die Stubs oben (_stub_hardware) N2/N3 nicht
|
||||
# wirklich absichern: displayPartBaseImage(buf) durch display(buf) ersetzen, oder die
|
||||
# V2-Registerfolge nach sleep() kaputt machen, liess alle bisherigen Tests gruen. Ein
|
||||
# stiller Registerzustandsfehler wirft keine Ausnahme - die einzige Art, ihn in Software
|
||||
# statt erst am Panel selbst zu fassen, ist die tatsaechlich gesendete Byte-Folge zu
|
||||
# protokollieren. Ersetzt dafuer epdconfig's Funktionen direkt (nicht gpiozero/spidev
|
||||
# darunter) und wertet aus, welche Bytes waehrend DC=0 (Kommando) vs. DC=1 (Daten)
|
||||
# gesendet wurden - siehe send_command()/send_data() in den vendored Treibern.
|
||||
|
||||
class _RecordingHardware:
|
||||
def __init__(self):
|
||||
self.commands: list[int] = []
|
||||
self._dc = 0
|
||||
self._busy = 0
|
||||
|
||||
def digital_write(self, pin, value):
|
||||
if pin == epdconfig.DC_PIN:
|
||||
self._dc = value
|
||||
# RST_PIN/CS_PIN sind fuer die Kommandofolge nicht relevant.
|
||||
|
||||
def digital_read(self, pin):
|
||||
return self._busy
|
||||
|
||||
def delay_ms(self, ms):
|
||||
pass
|
||||
|
||||
def spi_writebyte(self, data):
|
||||
if self._dc == 0:
|
||||
self.commands.extend(data)
|
||||
|
||||
def spi_writebyte2(self, data):
|
||||
pass # Nutzdaten (Bildpuffer/LUT) - fuer die Kommandofolge nicht relevant.
|
||||
|
||||
def module_init(self):
|
||||
return 0
|
||||
|
||||
def module_exit(self):
|
||||
pass
|
||||
|
||||
|
||||
def _install_recorder(monkeypatch) -> _RecordingHardware:
|
||||
recorder = _RecordingHardware()
|
||||
monkeypatch.setattr(epdconfig, "digital_write", recorder.digital_write)
|
||||
monkeypatch.setattr(epdconfig, "digital_read", recorder.digital_read)
|
||||
monkeypatch.setattr(epdconfig, "delay_ms", recorder.delay_ms)
|
||||
monkeypatch.setattr(epdconfig, "spi_writebyte", recorder.spi_writebyte)
|
||||
monkeypatch.setattr(epdconfig, "spi_writebyte2", recorder.spi_writebyte2)
|
||||
monkeypatch.setattr(epdconfig, "module_init", recorder.module_init)
|
||||
monkeypatch.setattr(epdconfig, "module_exit", recorder.module_exit)
|
||||
return recorder
|
||||
|
||||
|
||||
def test_v3_full_refresh_writes_reference_buffer(monkeypatch):
|
||||
"""Regressionstest zu Fixrunde 2, N2 - mit einer echten Zusicherung (Fixrunde 3): ein
|
||||
Vollaufbau muss bei V3 sowohl RAM 0x24 als auch 0x26 beschreiben
|
||||
(displayPartBaseImage() statt display()), sonst arbeitet der naechste Teilaufbau
|
||||
gegen einen undefinierten Referenzpuffer. Der Reviewer hat per Mutation
|
||||
(displayPartBaseImage(buf) -> display(buf)) gezeigt, dass die vorherige Testsuite das
|
||||
nicht bemerkt haette - dieser Test waere mit dieser Mutation rot."""
|
||||
recorder = _install_recorder(monkeypatch)
|
||||
panel = display.Panel("V3")
|
||||
image = render.compose({"mode": "off"})
|
||||
|
||||
panel.clear()
|
||||
recorder.commands.clear() # nur der eigentliche Vollaufbau interessiert hier
|
||||
|
||||
panel.draw_full(image)
|
||||
|
||||
assert 0x24 in recorder.commands, f"0x24 (aktuelles RAM) fehlt: {recorder.commands}"
|
||||
assert 0x26 in recorder.commands, f"0x26 (Referenzpuffer) fehlt: {recorder.commands}"
|
||||
assert recorder.commands.index(0x26) > recorder.commands.index(0x24)
|
||||
|
||||
|
||||
def test_v2_wake_after_sleep_sets_full_update_registers(monkeypatch):
|
||||
"""Regressionstest zu Fixrunde 2, N3 - mit einer echten Zusicherung (Fixrunde 3,
|
||||
statt der vorherigen Fassung ganz ohne): nach sleep() muss der naechste Aufbau
|
||||
(draw_partial(), das sich wegen Panel._awake selbst zu einem Vollaufbau aufwertet)
|
||||
die Register senden, die NUR der FULL_UPDATE-Zweig von epd2in13_V2.init() setzt
|
||||
(0x01 Driver output control, 0x11 Data entry mode, 0x44/0x45 RAM-Fenster, 0x4E/0x4F
|
||||
RAM-Zaehler), und danach sowohl 0x24 als auch 0x26 beschreiben. Dieser Fehler wirft
|
||||
keine Ausnahme - er ist nur an der gesendeten Byte-Folge erkennbar, sonst erst am
|
||||
Panel selbst, wo er von einem Verdrahtungsfehler nicht zu unterscheiden waere."""
|
||||
recorder = _install_recorder(monkeypatch)
|
||||
panel = display.Panel("V2")
|
||||
image = render.compose({"mode": "off"})
|
||||
|
||||
panel.clear()
|
||||
panel.draw_full(image)
|
||||
panel.sleep()
|
||||
recorder.commands.clear() # nur die Kommandofolge NACH dem Schlafen interessiert hier
|
||||
|
||||
panel.draw_partial(image)
|
||||
|
||||
required = [0x01, 0x11, 0x44, 0x45, 0x4E, 0x4F]
|
||||
positions = [recorder.commands.index(cmd) for cmd in required
|
||||
if cmd in recorder.commands]
|
||||
assert len(positions) == len(required), (
|
||||
f"FULL_UPDATE-Register fehlen nach sleep(): {recorder.commands}")
|
||||
assert positions == sorted(positions), (
|
||||
f"FULL_UPDATE-Register nicht in der erwarteten Reihenfolge: {recorder.commands}")
|
||||
|
||||
assert 0x24 in recorder.commands and 0x26 in recorder.commands, (
|
||||
f"0x24/0x26 (Referenzpuffer) fehlen: {recorder.commands}")
|
||||
idx_24 = recorder.commands.index(0x24)
|
||||
idx_26 = recorder.commands.index(0x26)
|
||||
assert idx_24 > positions[-1], "0x24 vor den FULL_UPDATE-Registern geschrieben"
|
||||
assert idx_26 > idx_24, "0x26 (Referenzpuffer) fehlt oder kommt vor 0x24"
|
||||
|
||||
|
||||
def test_panel_clear_establishes_reference_buffer(monkeypatch):
|
||||
"""Regressionstest zu Fixrunde 3, F2: clear() setzte vorher self._awake=True, ohne
|
||||
einen 0x26-Referenzpuffer zu schreiben (epd.Clear() schreibt nur 0x24, bei V2 ist das
|
||||
0x26-Schreiben in Clear() sogar auskommentiert) - ein draw_partial() direkt nach
|
||||
clear() liefe gegen denselben undefinierten Puffer, den N2 eigentlich beheben
|
||||
sollte. clear() verwendet jetzt wie draw_full() displayPartBaseImage()."""
|
||||
recorder = _install_recorder(monkeypatch)
|
||||
panel = display.Panel("V3")
|
||||
|
||||
panel.clear()
|
||||
|
||||
assert 0x24 in recorder.commands
|
||||
assert 0x26 in recorder.commands
|
||||
assert panel._awake is True
|
||||
|
||||
|
||||
def test_panel_sleep_sends_no_commands_when_already_asleep(monkeypatch):
|
||||
"""Staerkere Fassung von test_panel_sleep_is_idempotent (Fixrunde 3): ein zweiter
|
||||
sleep()-Aufruf darf NICHT nochmal versuchen, ueber die schon geschlossenen Pins
|
||||
SPI-Befehle zu schicken - hier direkt an der aufgezeichneten Kommandofolge geprueft
|
||||
statt nur daran, dass keine Ausnahme fliegt."""
|
||||
recorder = _install_recorder(monkeypatch)
|
||||
panel = display.Panel("V3")
|
||||
panel.clear()
|
||||
panel.sleep()
|
||||
recorder.commands.clear()
|
||||
|
||||
panel.sleep() # bereits asleep - darf keine weiteren Kommandos senden
|
||||
|
||||
assert recorder.commands == []
|
||||
|
||||
Reference in New Issue
Block a user