fix(station): Panel schlaeft nicht mehr nach jedem Bild, Breitenkuerzung nach Pixeln
Fixrunde 2 zu Task 6a: der sleep()-Einbau aus Fixrunde 1 hatte drei neue Critical erzeugt. epd.sleep() schliesst ueber epdconfig.module_exit() die GPIO-Pins; ein Teilaufbau direkt danach fasste bei V3 eine bereits geschlossene Pin-Referenz an (AttributeError, ungefangen, Neustartschleife - binnen Sekunden nach dem Start, V3 ist die Vorgaberevision) und war bei V2 nach Deep Sleep ohnehin keine belastbare Sequenz (init(PART_UPDATE) allein setzt nicht die Register, die nur der FULL_UPDATE-Zweig setzt). Ausserdem setzte der V3-Vollaufbau nie den Referenzpuffer, den ein Teilaufbau zum Diffen braucht (displayPartBaseImage() statt display()). Panel bekommt ein internes _awake-Flag: draw_partial() wertet sich selbst zu einem Vollaufbau auf, wenn das Panel nicht wach ist (frisch erzeugt oder seit dem letzten Aufbau geschlafen), unabhaengig von der Aufrufreihenfolge. sleep() wird nur noch nach laengerer Ruhe (10 Minuten ohne Aenderung) gerufen, nicht mehr nach jedem Bild, und ist selbst idempotent. Dazu: epdconfig faengt jetzt gpiozero.GPIOZeroError statt nur GPIODeviceError (die PinError-Familie haengt nicht darunter), EnvironmentFile= in der Unit bekommt ein "-"-Praefix, und die Zeilenkuerzung wandert von einer Zeichenzahl-Grenze (ungueltig bei einer Proportionalschrift) zu einer echten Pixelbreitenmessung in compose() - lines() bleibt dafuer ungekuerzt und weiterhin ohne Pillow testbar.
This commit is contained in:
@@ -59,6 +59,19 @@ POLL_INTERVAL_S = 5
|
||||
# Geisterbilder stehen).
|
||||
FULL_REFRESH_INTERVAL_S = 30 * 60
|
||||
|
||||
# Panel erst schlafen legen, wenn seit dem letzten Bildaufbau schon eine Weile nichts
|
||||
# mehr passiert ist - NICHT nach jedem einzelnen Bild (Fixrunde 2, N1/N2/N3: das liess
|
||||
# V3 beim naechsten Teilaufbau abstuerzen und machte V2s Deep-Sleep-Aufwachsequenz
|
||||
# unbrauchbar, siehe Panel-Klasse). Migrationsereignisse kommen oft in kurzen Buendeln
|
||||
# (mehrere Lampen kurz nacheinander) - das Panel dafuer jedesmal schlafen zu legen und
|
||||
# wieder aufzuwecken waere nur zusaetzlicher Verschleiss ohne Nutzen fuer die
|
||||
# Bias-Alterung, die I4 eigentlich vermeiden soll. 10 Minuten ohne Aenderung sind lang
|
||||
# genug, dass ein Buendel zusammenhaengender Ereignisse das Panel nicht staendig
|
||||
# schlafen und wieder aufwachen laesst, aber kurz genug, dass es zwischen zwei
|
||||
# tatsaechlich seltenen Ereignissen (z.B. stundenlanger Rettungsmodus ohne Anfrage)
|
||||
# nicht dauerhaft unter Bias steht.
|
||||
SLEEP_AFTER_IDLE_S = 10 * 60
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict:
|
||||
"""Geparster Inhalt von `path`, oder {} wenn er fehlt/unlesbar/kaputt/kein Objekt
|
||||
@@ -192,47 +205,74 @@ def _load_epd_module(revision: str):
|
||||
class Panel:
|
||||
"""Kapselt die V2/V3-Unterschiede der Waveshare-Treiber hinter einer einheitlichen
|
||||
Schnittstelle (Fixrunde 1, C1): epd2in13_V2.init() braucht ein Pflichtargument
|
||||
(FULL_UPDATE/PART_UPDATE) und dessen displayPartial() setzt selbst keine LUT - vorher
|
||||
muessen init(PART_UPDATE) und (fuer den allerersten Teilaufbau nach einem Vollaufbau)
|
||||
displayPartBaseImage() gelaufen sein, sonst zeichnet das Panel mit der falschen
|
||||
Wellenform oder gegen einen veralteten Referenzpuffer. epd2in13_V3.init() braucht
|
||||
dagegen kein Argument und dessen displayPartial() setzt seine Teil-LUT selbst bei
|
||||
jedem Aufruf - dort reicht ein einfaches init() vor jedem Vollaufbau."""
|
||||
(FULL_UPDATE/PART_UPDATE), epd2in13_V3.init() braucht keins.
|
||||
|
||||
Verfolgt zusaetzlich selbst, ob das Panel gerade "wach" ist (Register initialisiert,
|
||||
Referenzpuffer fuer Teilaufbauten bekannt) oder zuletzt sleep() gerufen wurde
|
||||
(Fixrunde 2, N1/N3): epd.sleep() schickt das Panel in einen Zustand, aus dem heraus
|
||||
KEINE Revision direkt einen Teilaufbau vertraegt - V2s sleep() ist Deep Sleep Mode 2
|
||||
(RAM/Register verworfen; ein nachfolgendes init(PART_UPDATE) setzt nur einen Teil der
|
||||
Register, die ein Teilaufbau braucht - 0x01/0x11/0x44/0x45/0x4E/0x4F setzt
|
||||
ausschliesslich der FULL_UPDATE-Zweig), V3s sleep() ruft epdconfig.module_exit() und
|
||||
schliesst damit die GPIO-Pins, die epd2in13_V3.displayPartial() als Erstes anfasst
|
||||
(roher AttributeError auf ein geschlossenes Pin-Objekt). Ein Teilaufbau nach dem
|
||||
Schlafen wird deshalb hier, nicht beim Aufrufer, transparent zu einem Vollaufbau
|
||||
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."""
|
||||
|
||||
def __init__(self, revision: str) -> None:
|
||||
self.revision = revision
|
||||
self._epd_module = _load_epd_module(revision)
|
||||
self.epd = self._epd_module.EPD()
|
||||
# Vor dem ersten clear()/draw_full() ist nichts initialisiert - siehe Klassen-
|
||||
# Docstring, warum draw_partial() das genauso wie ein sleep() davor behandeln
|
||||
# muss (beides: kein gueltiger Referenzpuffer, keine vollstaendige Registerlage).
|
||||
self._awake = False
|
||||
|
||||
def clear(self) -> None:
|
||||
def _full_init(self) -> None:
|
||||
if self.revision == "V2":
|
||||
self.epd.init(self.epd.FULL_UPDATE)
|
||||
else:
|
||||
self.epd.init()
|
||||
|
||||
def clear(self) -> None:
|
||||
self._full_init()
|
||||
self.epd.Clear(0xFF)
|
||||
self._awake = True
|
||||
|
||||
def draw_full(self, image) -> None:
|
||||
buf = self.epd.getbuffer(image)
|
||||
if self.revision == "V2":
|
||||
self.epd.init(self.epd.FULL_UPDATE)
|
||||
# displayPartBaseImage() zeichnet UND schreibt gleichzeitig beide RAM-Puffer
|
||||
# des Controllers - genau das, was ein nachfolgender Teilaufbau als
|
||||
# Referenzpuffer braucht. display() allein wuerde nur einen Puffer setzen und
|
||||
# den ersten Teilaufbau danach gegen einen veralteten Referenzpuffer diffen
|
||||
# lassen (Fixrunde 1, C1).
|
||||
self.epd.displayPartBaseImage(buf)
|
||||
else:
|
||||
self.epd.init()
|
||||
self.epd.display(buf)
|
||||
self._full_init()
|
||||
self.epd.displayPartBaseImage(buf)
|
||||
self._awake = True
|
||||
|
||||
def draw_partial(self, image) -> None:
|
||||
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
|
||||
buf = self.epd.getbuffer(image)
|
||||
if self.revision == "V2":
|
||||
self.epd.init(self.epd.PART_UPDATE)
|
||||
self.epd.displayPartial(buf)
|
||||
|
||||
def sleep(self) -> None:
|
||||
# Idempotent: ein zweiter sleep()-Aufruf, waehrend das Panel schon schlaeft,
|
||||
# wuerde sonst versuchen, ueber bereits von module_exit() geschlossene GPIO-Pins
|
||||
# SPI-Befehle zu schicken (derselbe AttributeError-Mechanismus wie N1, nur
|
||||
# selbst herbeigefuehrt statt durch einen Teilaufbau).
|
||||
if not self._awake:
|
||||
return
|
||||
self.epd.sleep()
|
||||
self._awake = False
|
||||
|
||||
|
||||
def _test_image(revision: str):
|
||||
@@ -277,30 +317,36 @@ def run_daemon(revision: str) -> None:
|
||||
last_lines: list[str] | None = None
|
||||
last_mode: object = object() # garantiert ungleich jedem echten Modus beim ersten Lauf
|
||||
last_full = time.monotonic()
|
||||
last_draw = time.monotonic()
|
||||
|
||||
while True:
|
||||
state = build_state()
|
||||
current_lines = render.lines(state)
|
||||
mode = state.get("mode")
|
||||
now = time.monotonic()
|
||||
need_full = (mode != last_mode) or (now - last_full >= FULL_REFRESH_INTERVAL_S)
|
||||
|
||||
if current_lines != last_lines or need_full:
|
||||
mode_changed = mode != last_mode
|
||||
stale = (now - last_full) >= FULL_REFRESH_INTERVAL_S
|
||||
|
||||
if current_lines != last_lines or mode_changed or stale:
|
||||
image = render.compose(state)
|
||||
if need_full:
|
||||
if mode_changed or stale:
|
||||
panel.draw_full(image)
|
||||
last_full = now
|
||||
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)
|
||||
# Panel nach jedem Bildaufbau schlafen legen: die Pixel bleiben (e-Paper ist
|
||||
# bistabil), aber ohne dauerhafte Spannung altert das Panel deutlich weniger -
|
||||
# Waveshare warnt fuer diese Serie ausdruecklich davor, es dauerhaft unter
|
||||
# Bias zu lassen (Fixrunde 1, I4). epd.sleep() gibt auch GPIO/SPI frei
|
||||
# (epdconfig.module_exit()); der naechste Aufbau ruft ohnehin wieder init()
|
||||
# und damit module_init() auf, das das sauber neu aufsetzt.
|
||||
panel.sleep()
|
||||
last_draw = now
|
||||
last_lines = current_lines
|
||||
last_mode = mode
|
||||
elif (now - last_draw) >= SLEEP_AFTER_IDLE_S:
|
||||
# Nach laengerer Ruhe schlafen legen (I4: die Pixel bleiben - e-Paper ist
|
||||
# bistabil - aber ohne dauerhafte Spannung altert das Panel deutlich
|
||||
# weniger). Panel.sleep() ist idempotent, ein wiederholter Aufruf hier
|
||||
# (solange weiterhin nichts passiert) ist also unschaedlich.
|
||||
panel.sleep()
|
||||
|
||||
time.sleep(POLL_INTERVAL_S)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user