ikzeizet.
← Alle verhalen

ESP32 Scherm: mijn eerste hardwareproject met een OLED-schermpje

Hoe ik met een ESP32, een klein OLED-scherm en een zelfgehoste webapp mijn eerste hardwareproject buiten de browser heb gebouwd.

Dit project begon uit nieuwsgierigheid, zonder plan. Ik wilde weten hoe het is om naast software ook echt iets met hardware te doen: draden aansluiten, een microcontroller inrichten, en zelf een apparaat laten reageren op een webapp.

Daarom heb ik een klein OLED-schermpje aan een ESP32 gehangen en er een zelfgehoste webapp omheen gebouwd. Het resultaat is een systeem waarmee ik foto's kan uploaden, bijsnijden en beheren, waarna de ESP32 de gekozen bitmap toont op een 128x64 schermpje.

Het scherm zelf was bijzaak. Ik wilde voelen hoe het is om van browserlogica naar hardware te gaan: iets aansluiten, de software schrijven en het werkend krijgen op een goedkope microcontroller.

Overzicht van de ESP32 webapp met uploadzone en galerij met OLED-afbeeldingen
Het overzicht: uploaden bovenaan, opgeslagen bitmaps eronder en de ESP32 die de actieve afbeelding toont.
Uploadzone van de ESP32 webapp met icoon en tekst om een foto te kiezen of te slepen
De uploadzone in detail: klikbaar, simpel en direct duidelijk wat je kunt doen.

Wat is het?

Het is een zelfgehoste webapplicatie waarmee ik afbeeldingen kan uploaden, bijsnijden en beheren voor een ESP32-microcontroller met een 128x64 OLED-scherm. De ESP32 vraagt via een HTTP API steeds de actieve bitmap op en laat die zien op het schermpje.

De webinterface draait in Docker op mijn thuisserver en is bereikbaar via espfoto.lan. Daardoor voelt het als een klein systeem dat gewoon thuis blijft draaien.


De stack

Ik heb de stack eenvoudig gehouden. Dit was voor mij ook een experiment om te zien hoe ver ik kom met iets lichts, snel op te zetten en goed te begrijpen.

  • Frontend: Vanilla HTML, CSS en JavaScript, zonder framework
  • Backend: PHP 8.2 met PDO SQLite
  • Database: SQLite, één bestand: bitmaps.sqlite
  • Beeldverwerking: PHP GD met resize, Floyd-Steinberg dithering en 1-bit conversie
  • Hosting: Docker via docker-compose, met een PHP 8.2 Apache image
  • Hardware: ESP32 met Adafruit SSD1306 OLED 128x64, firmware in C++ met PlatformIO
  • Extra libs: Cropper.js voor bijsnijden en heic2any voor iPhone HEIC-conversie

Hoe werkt het?

Uploadflow

  1. Ik kies een foto, meestal JPG of PNG. HEIC en HEIF van een iPhone werken ook.
  2. Als het nodig is zet de browser de foto client-side om naar JPEG met heic2any.
  3. Cropper.js laat de afbeelding zien met een vast 2:1 cropvak.
  4. Bij uploaden gaat de gecropte canvas als base64 dataURL naar upload.php.
  5. PHP decodeert de afbeelding, schaalt hem naar 128x64 pixels, past Floyd-Steinberg dithering toe en zet hem om naar zwart-wit.
  6. Het resultaat wordt als PNG opgeslagen in uploads en als hex-string in SQLite bewaard.

De hex-string stelt de bitmap voor als 128x64 bits, MSB-first per rij. Dat maakt hem makkelijk op te halen door de ESP32. Voor mij was dit ook een van de interessantste delen: de foto was in de browser nog gewoon een normale afbeelding, en op het eind zat er ineens een klein zwart-wit patroon in een database.

API

api.php heeft drie modi:

  • GET /api.php geeft de actieve bitmap terug als JSON, of de nieuwste als auto-latest aan staat.
  • GET /api.php?action=list geeft alle opgeslagen afbeeldingen terug, inclusief actief-status en datum.
  • POST /api.php met JSON body ondersteunt set_active, delete en set_auto_latest.

ESP32 firmware

De ESP32 maakt verbinding met wifi, doet elke seconde een HTTP GET naar de API, leest de JSON in met ArduinoJson en zet de hex-string om naar een byte-array. Daarna tekent de firmware de bitmap op het OLED-scherm via de Adafruit SSD1306 library.

De wifi-gegevens staan in secrets.h, dat niet in git zit. Dat was even wennen: ineens ging het ook over hardware die op een echt netwerk reageert.


De database

SQLite past hier goed: klein, simpel en genoeg voor dit soort beheer.

bitmaps

  • id - auto-increment primaire sleutel
  • name - bestandsnaam van de PNG
  • width - altijd 128
  • height - altijd 64
  • data - hex-string van de 1-bit bitmap
  • ip_address - IP van de uploader
  • user_agent - browser van de uploader
  • created_at - tijdstip van upload
  • active - 0 of 1, er is maar één actieve foto tegelijk

settings

  • key - instellingsnaam
  • value - bijbehorende waarde

De enige instelling is auto_latest, die 0 of 1 kan zijn. Daardoor kon ik later kiezen of de nieuwste upload automatisch actief wordt of dat ik alles handmatig beheer.


UI en gedrag

Header

Bovenaan staat een sticky blauwe balk met de app-titel en een toggle voor Automatisch nieuwste actief. Uit is grijs, aan is groen. Die status moest in één oogopslag duidelijk zijn, omdat ik dit zelf ook snel wilde kunnen begrijpen zonder eerst in instellingen te duiken.

Uploadzone

De uploadzone is een klikbaar vlak met een dashed border, upload-icoon en uitleg. Zodra ik een foto kies, verschijnt de Cropper.js-interface in datzelfde vak. De knop Uploaden blijft uitgeschakeld totdat er een gecropte afbeelding klaarstaat. Dat klinkt klein, maar dit soort details maken het verschil tussen een testproject en iets dat prettig aanvoelt.

Galerij

De galerij gebruikt een responsive grid met twee kolommen op kleine schermen. Elke kaart toont de afbeelding op een zwarte achtergrond, een compact uploadtijdstip en knoppen voor actief zetten en verwijderen. Ik wilde dat het er een beetje netjes uitzag, omdat ik er regelmatig doorheen blader.

  • Linksboven: een groene Actief-badge als de foto al actief is
  • Linksboven: anders een semi-transparant blauw vinkje om die foto actief te maken
  • Rechtsboven: een rood kruis om de foto te verwijderen, met bevestiging
  • Bij de actieve foto: een blauwe rand met glow

Auto-latest gedrag

  • Als de toggle aan staat, wordt elke nieuwe upload automatisch actief.
  • De API geeft dan altijd de nieuwste bitmap terug.
  • De activeerknoppen verdwijnen uit de galerij.
  • Als ik handmatig een foto activeer terwijl de toggle aan staat, gaat de toggle automatisch uit.

Dat laatste was een kleine maar nuttige keuze: als ik zelf iets actief zet, wil ik niet dat een andere instelling me daarna weer verrast.


Infrastructuur

De hele stack draait in Docker Compose met één service. De PHP-image wordt lokaal gebouwd via een Dockerfile die GD, PDO en pdo_sqlite installeert. De mappen data en uploads zijn volumes, zodat een rebuild geen data wist.

De deployflow is simpel: git pull op de server, daarna docker compose up -d --build. Dat paste goed bij hoe ik dit project wilde aanpakken: klein, herhaalbaar en zonder afhankelijk te zijn van een externe dienst.


Aandachtspunten

  • Floyd-Steinberg dithering: de server herberekent altijd de dithering. Dat kost CPU, maar geeft wel consistente resultaten.
  • HEIC op de server: er is ook een fallback via Imagick of ImageMagick CLI, maar meestal doet heic2any het al client-side.
  • SQLite migratie: de active-kolom en de settings-tabel worden automatisch aangemaakt als ze ontbreken.
  • Geen authenticatie: dit is bedoeld voor een privé thuisnetwerk, dus er zit geen login op.

De grootste les was hoe anders het voelt als je software en hardware samen laat werken. De beperkingen zitten ineens ook in stroom, geheugen, schermformaat en netwerkgedrag. Daar moest ik echt even aan wennen.


Resultaat

Het eindresultaat is een klein maar volledig systeem dat doet wat ik wilde. Foto's uploaden gaat via de browser, de server maakt er een bruikbare bitmap van en de ESP32 laat die direct zien op het OLED-schermpje.

Voor mij is vooral belangrijk dat ik nu begrijp hoe zo'n keten in elkaar zit. Ik weet hoe de browser, de server, de database en de ESP32 samenkomen. Dat maakt het een goede opstap naar de volgende hardware-ideeën die ik al in mijn hoofd heb, zoals een e-paper scherm met gegevens uit Home Assistant.

Vragen over de firmware of de uploadflow? Stuur me een bericht via het contactformulier.