Terug naar blog

ESP32 Scherm: mijn eerste hardwareproject met een OLED-schermpje

Project · ESP32 · PHP · Docker · IoT

Intro

Dit project begon eigenlijk niet met een helder productplan, maar met nieuwsgierigheid. Ik wilde weten hoe het is om niet alleen software te bouwen, maar 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.

Wat mij hierin trok was niet alleen het scherm zelf, maar vooral het leerproces. Ik wilde voelen hoe het is om van browserlogica naar hardware te gaan. Hoe sluit je iets aan? Hoe maak je de software? Hoe zet je het ook echt werkend op een goedkope microcontroller? Dat was het echte doel.

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 niet als een los scriptje, maar als een klein systeem dat gewoon thuis blijft draaien.


De stack

Ik heb de stack bewust 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. Dit was voor mij ook een kleine drempel: ineens moest ik niet alleen denken aan code, maar ook aan hardware die op een echt netwerk reageert.


De database

SQLite is hier precies goed: klein, simpel en voldoende 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 hier was niet een specifieke bug, maar hoe anders het voelt als je software en hardware samen laat werken. De beperkingen zitten niet alleen in code, maar ook in stroom, geheugen, schermformaat en netwerkgedrag. Daar moest ik echt even aan wennen.


Resultaat

Het eindresultaat is een klein maar volledig systeem dat precies 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 niet alleen een werkend project, maar ook een goede stap richting de volgende hardware-ideeën die ik al in mijn hoofd heb, zoals een e-paper scherm met gegevens uit Home Assistant.

Het voelt daardoor niet als een eindpunt, maar als een eerste serieuze hardware-oefening die gewoon goed uitpakte.


Vragen / feedback

Als je vragen hebt over de aanpak, de stack of een specifiek onderdeel, stel ze dan via mijn contactformulier en ik neem contact met je op.