ESP32 Scherm: mijn eerste hardwareproject met een OLED-schermpje
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.
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
- Ik kies een foto, meestal JPG of PNG. HEIC en HEIF van een iPhone werken ook.
- Als het nodig is zet de browser de foto client-side om naar JPEG met
heic2any. - Cropper.js laat de afbeelding zien met een vast 2:1 cropvak.
- Bij uploaden gaat de gecropte canvas als base64 dataURL naar
upload.php. - PHP decodeert de afbeelding, schaalt hem naar 128x64 pixels, past Floyd-Steinberg dithering toe en zet hem om naar zwart-wit.
- Het resultaat wordt als PNG opgeslagen in
uploadsen 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.phpgeeft de actieve bitmap terug als JSON, of de nieuwste als auto-latest aan staat.GET /api.php?action=listgeeft alle opgeslagen afbeeldingen terug, inclusief actief-status en datum.POST /api.phpmet JSON body ondersteuntset_active,deleteenset_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 sleutelname- bestandsnaam van de PNGwidth- altijd 128height- altijd 64data- hex-string van de 1-bit bitmapip_address- IP van de uploaderuser_agent- browser van de uploadercreated_at- tijdstip van uploadactive- 0 of 1, er is maar één actieve foto tegelijk
settings
key- instellingsnaamvalue- 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
heic2anyhet al client-side. - SQLite migratie: de
active-kolom en desettings-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.