https://github.com/sascha-schieferdecker/zephyNtfyClient/

Ein ESP32 mit 1,3-Zoll-OLED hängt dauerhaft an meiner selbst gehosteten ntfy-Instanz und zeigt Benachrichtigungen an. Eigentlich ging es mir aber um etwas anderes: Ich wollte endlich Zephyr ausprobieren.

Der eigentliche Auslöser war nicht der Wunsch nach einem weiteren Display auf dem Schreibtisch, sondern Neugier auf Zephyr RTOS. Für ESP32-Boards greift man normalerweise zu ESP-IDF oder Arduino; Zephyr ist der dritte Weg — ein Echtzeit-Betriebssystem, das sich in vielem wie ein kleiner Linux-Kernel anfühlt: Devicetree für die Hardware, Kconfig für die Konfiguration, Threads und Message-Queues statt Superloop. Genau das wollte ich einmal richtig lernen, und dafür braucht es ein Projekt, das über Blinky hinausgeht.

Herausgekommen ist ein kleiner Notifier: Das Gerät verbindet sich per WLAN, stellt eine TLS-Verbindung zu meinem ntfy-Server her und hält einen HTTP-Stream offen. Kommt eine Nachricht rein, steht sie ein paar Augenblicke später auf dem OLED — Titel in der oberen Zeile, Text darunter. Nachrichten mit hoher Priorität invertieren das Panel, damit sie auch aus dem Augenwinkel auffallen. Ist gerade nichts zu zeigen, zeigt das Display den Verbindungszustand.

Board

ESP32 DevKit V1 (ESP-WROOM-32)

Display

1,3” OLED, SH1106, I²C

OS

Zephyr RTOS 4.4.2

Dienst

selbst gehostetes ntfy über HTTPS

Toolchain

komplett im Container, Serial-Port durchgereicht (Orbstack auf dem Mac kann das)

Zwei Threads, eine Queue

Der Aufbau ist bewusst so gehalten, wie Zephyr es vorsieht, und nicht so, wie es am schnellsten ginge. Ein Netz-Thread kümmert sich um TLS, HTTP und das Zerlegen des Chunked-Streams. Ein UI-Thread rendert auf das Display. Dazwischen liegt genau eine Message-Queue mit Nachrichten fester Größe. Weil nur der UI-Thread jemals den Framebuffer anfasst, braucht es keinen Mutex für das Display, und dynamischen Speicher benutzt der Anwendungscode überhaupt nicht.

Ein Detail, das ich unterschätzt hatte: Auf 8 × 2 Zeichen passt keine echte Benachrichtigung. Der Text wird deshalb umgebrochen und seitenweise durchgeblättert. Der Timer dafür ist keiner — das Warten an der Queue hat einfach ein Timeout, sodass eine neu eintreffende Nachricht die aktuelle Seite sofort verdrängt. Solche kleinen Vereinfachungen sind das, was das Projekt lehrreich gemacht hat.

Ein Kern von zweien

Zur Einschränkung, die man kennen sollte, bevor man ESP32 und Zephyr kombiniert: Der Chip hat zwei Xtensa-LX6-Kerne, aber gebaut wird hier für das Target doit_esp32_devkit_v1/esp32/procpu — also ausschließlich für den Protocol-CPU-Kern. Der zweite Kern (APPCPU) bleibt in diesem Projekt schlicht ungenutzt; SMP ist auf diesem SoC unter Zephyr für meine Zwecke keine Option gewesen. Wer von ESP-IDF kommt, wo der WLAN-Stack üblicherweise auf dem einen und die Anwendung auf dem anderen Kern läuft, sollte das einkalkulieren.

Alles teilt sich einen Kern: WLAN-Treiber, mbedTLS-Handshake, HTTP-Parser und das Zeichnen auf dem Panel.

In der Praxis heißt das vor allem: sauber blockieren statt pollen. Jede Warteschleife, die den Kern belegt, fehlt woanders — und ein Bildaufbau schiebt immerhin einen ganzen Kilobyte-Framebuffer über einen 100-kHz-Bus, was rund 90 Millisekunden dauert. Das war einer der Gründe, seitenweise zu blättern statt weich zu scrollen: Flüssiges Scrollen hätte den I²C-Bus dauerhaft beschäftigt, für einen Lesbarkeitsgewinn, den dieses Panel gar nicht hergibt. Der zweite Kern hätte daran übrigens nichts geändert — der Bus wäre derselbe geblieben.

Was mich am meisten Zeit gekostet hat

  • Voreinstellungen von mbedTLS. Ohne aktiviertes SNI schickt der Client keinen Hostnamen, und der Reverse Proxy liefert klaglos sein Default-Zertifikat aus. Solange die Zertifikatsprüfung abgeschaltet ist, sieht das monatelang gesund aus.
  • Speicher. Mit TLS liegt der DRAM-Verbrauch bei rund 86 Prozent. Das ist die härteste Grenze im ganzen Projekt.
  • Das Display selbst. Mein Modul leuchtet nur jede zweite Zeile — nach längerer Fehlersuche als Eigenart des Moduls abgehakt. Die Konsequenz ist kontraintuitiv: Die größte verfügbare Schrift ist die lesbarste.
  • Ein hängender I²C-Bus. Ein Reset mitten in einer Übertragung ließ das Panel die Datenleitung festhalten — und weil die Versorgung den Reset überlebt, blieb der Zustand über jeden weiteren Neustart bestehen. Ein früh laufender Init-Hook, der den Bus zurücksetzt, hat das behoben.

Fazit

Zephyr fordert am Anfang mehr, als ein Wochenendprojekt eigentlich rechtfertigt: Devicetree-Overlays, Kconfig-Abhängigkeiten, ein Board-Target, das man erst einmal richtig benennen muss. Danach bekommt man aber genau die Struktur, die man von einem ordentlichen Betriebssystem erwartet — Treiber, die man nicht selbst schreibt, ein Netzwerk-Stack mit TLS in der Socket-API, und eine Nebenläufigkeit, die man deklariert statt zusammenzuimprovisieren. Für „nur mal ausprobieren“ war das erstaunlich tragfähig. Dank Claude Code konnte man das zügig testen. Die Plattform gefällt mir gut!