LED Ticker Build Log · 05
[IoT DIY Part 5] Upgrading a Personal Toy Project to a Deployable IoT Device: Captive Portal, Web Settings UI, and OTA Updates
![[IoT DIY Part 5] Upgrading a Personal Toy Project to a Deployable IoT Device: Captive Portal, Web Settings UI, and OTA Updates](/iot-lab/assets/led-ticker/hero-en-5.jpg)
An embedded device made only for my own desk can have messy code and a hard-coded Wi-Fi password without much trouble. But the moment it is given to someone else or produced in small batches, the device moves from a personal toy project into the realm of a product that must operate independently.
This article documents how a single-purpose ESP32 LED ticker was upgraded into an architecture that multiple users could configure and maintain remotely, using Captive Portal, NVS, and OTA.
1. Limits of hard-coded WiFi information and Captive Portal adoption
During development, SSID and password strings are often placed directly inside the source code. But once a device moves to another location, the router information changes, and compiling/uploading through USB every time is not practical. Receiving a user’s private Wi-Fi password is also a security burden.
I introduced a Captive Portal architecture to solve this.
- On boot, the device tries to connect using WiFi data stored in non-volatile memory(EEPROM/NVS).
- On failure, the ESP32 switches to AP mode and broadcasts an SSID such as
ESP_LED_SETUP. - When the user connects, the DNS server captures port-80 requests and redirects them to the device’s internal web settings page.
- After saving, the user’s router information is stored in NVS and the device reboots into STA mode.
2. User-specific option fragmentation and built-in web-server UI
As more people used the device, requirements split quickly. One person wanted crypto prices, another wanted U.S. stock indexes and exchange rates, and another wanted only news headlines and weather. Burning custom firmware for each user was not realistic.
I integrated a Dynamic Configuration UI into the Captive Portal web server and stored settings permanently in flash memory as JSON-style configuration values.
| Setting | Storage key | User control range |
|---|---|---|
| News headlines | cfg_news_en | RSS news feed rolling on/off |
| Weather and region | cfg_weather_loc | OpenWeatherMap city code such as Seoul or Busan |
| Stock and FX | cfg_stock_fx | KOSPI, Nasdaq, and USD/KRW display order |
| Night mode | cfg_night_mode | Automatic PWM brightness reduction at night |
By standardizing the hardware and moving functional branching to runtime variables, maintenance effort was reduced by more than 80%.
3. OTA wireless update architecture for remote maintenance
The most serious issue after deployment appears when a critical bug fix or API-spec change is required. Opening a physically separated device, connecting USB, and uploading firmware again is not realistic.
To solve this, I added OTA(Over-The-Air) firmware updates over Wi-Fi.
ESP32 memory partition design notes
OTA requires splitting the ESP32 flash memory, typically 4MB, into two firmware areas such as Factory/App1 and App2. If the current firmware runs from App1, the new firmware is written to App2. After integrity verification(MD5 Checksum), the boot pointer moves to App2 and the device reboots safely.
[Practical code block] ESP32 WiFiManager and OTA integration boilerplate
The following boilerplate runs Captive Portal(WiFiManager-style setup) and ArduinoOTA in one stable loop.
#include <WiFi.h> #include <ESPAsyncWebServer.h> #include <AsyncTCP.h> #include <DNSServer.h> #include <ArduinoOTA.h> const byte DNS_PORT = 53; DNSServer dnsServer; AsyncWebServer server(80); // ============================================================================== // 1. Captive Portal setup (AP mode and DNS redirect) // ============================================================================== void setupCaptivePortal() { WiFi.mode(WIFI_AP); WiFi.softAP("LED_DISPLAY_SETUP"); dnsServer.start(DNS_PORT, "*", WiFi.softAPIP()); server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(200, "text/html", "<h1>WiFi Setup</h1><form action='/save'>...</form>"); }); server.onNotFound([](AsyncWebServerRequest *request){ request->redirect("http://192.168.4.1/"); }); server.begin(); } // ============================================================================== // 2. OTA initialization and callbacks // ============================================================================== void setupOTA() { ArduinoOTA.setHostname("IoT-LED-Display"); ArduinoOTA.onStart([]() { String type = (ArduinoOTA.getCommand() == U_FLASH) ? "sketch" : "filesystem"; Serial.println("OTA Update Start: " + type); }); ArduinoOTA.onEnd([]() { Serial.println(" OTA Update Complete! Rebooting..."); }); ArduinoOTA.onError([](ota_error_t error) { Serial.printf("OTA Error[%u]: ", error); }); ArduinoOTA.begin(); } void setup() { Serial.begin(115200); if (!connectToWiFi()) setupCaptivePortal(); else setupOTA(); } void loop() { if (WiFi.getMode() == WIFI_AP) { dnsServer.processNextRequest(); } else { ArduinoOTA.handle(); } }
4. Hardware feel and practical data-scraping challenges
Beyond the technical architecture, deployment introduced both hardware-feel considerations and external data parsing problems.
The E-E-A-T value of a handmade wooden case
The early small version used wooden boards from a stationery store, cut, sanded, and glued by hand. It had a unique value different from factory-made products. The texture, finish variation, and handmade feel helped the device spread through acquaintances, and nearly ten units were actually deployed.
Data crawling troubleshooting in a microcontroller environment
Fetching exchange rates, news, and stock data looks simple, but the microcontroller’s heap-memory limit caused many failures.
- Problem: News portals and stock APIs often require HTTPS, and JSON payloads can reach hundreds of KB. If an ESP32 directly performs SSL handshakes and parses large JSON in memory, Out of Memory(OOM) crashes and Watchdog Reset can occur.
- Solution: The embedded device should not fight the heavy web ecosystem directly. I used the Python helper server as a proxy. The Python server handled crawling and JSON processing, then sent only lightweight plain text or compressed streaming data through MQTT/HTTP to the ESP32.
5. Conclusion: from Toy Project to Product
Turning a personal project into a device other people can use is a strong way to raise technical completeness.
Captive Portal without hard-coded WiFi passwords, a web settings UI that stores preferences in non-volatile memory, and OTA updates that patch firmware without physical access are the three core engineering elements of an IoT device that actually runs in the field. The crypto boom of 2022 has passed, but the wooden ticker built on this architecture still sits on a desk and displays stock indexes and exchange rates.