A Wi-Fi connected work status display for the LilyGo T-Display-S3 Touch (ESP32-S3). Shows your current work state (BUSY, FREE, FOCUS, CALL, etc.) on a colour touchscreen with a matching WS2812B LED ring. Configured via a built-in mobile-friendly web UI — no app required.
- Instant status display — full-screen colour + large text for at-a-glance readability across a room
- LED ring — 6× WS2812B LEDs match the active mode colour and brightness
- Touch gestures — single tap cycles modes; double tap fires a configurable HTTP webhook
- Captive portal setup — first boot opens an AP with a browser-based Wi-Fi config page
- Persistent storage — modes and settings survive reboots (NVS + LittleFS)
- REST API — full CRUD for modes; integrates with Home Assistant, IFTTT, or any HTTP client
- Configurable webhooks — single and double tap can POST to any URL (supports HTTPS + custom headers/JWT tokens)
- mDNS — reachable as
http://workdisplay.localon the local network
| Component | Details |
|---|---|
| Board | LilyGo T-Display-S3 Touch |
| MCU | ESP32-S3 (dual-core 240 MHz, 8 MB OPI PSRAM) |
| Flash | 16 MB |
| Display | ST7789V 1.9" 170×320 IPS, I8080 8-bit parallel |
| Touch | CST816S capacitive, I2C |
| LEDs | 6× WS2812B NeoPixel ring on GPIO 43 |
Five modes are created on first boot:
| Mode | Colour | LED |
|---|---|---|
| BUSY | Red #ff3b30 |
Red, 50% |
| FREE | Green #34c759 |
Green, 50% |
| FOCUS | Orange #ff9500 |
Orange, 50% |
| CALL | Blue #007aff |
Blue, 50% |
| ASLEEP | Dark #1c1c1e |
Off |
All modes are fully customisable via the web UI or REST API.
- PlatformIO (Core or IDE extension)
- LilyGo T-Display-S3 Touch board with USB-C cable
# Clone the repo
git clone https://github.com/your-username/OpenMagTag.git
cd OpenMagTag
# Compile + flash firmware
pio run --target upload
# Build + flash LittleFS (web UI and default config files)
pio run --target uploadfs
# Open serial monitor (115200 baud)
pio device monitorWhen to reflash what:
| Changed files | Command needed |
|---|---|
data/index.html only |
pio run --target uploadfs |
Any .cpp / .h file |
pio run --target upload |
| Both firmware and web UI | pio run --target upload && pio run --target uploadfs |
- Flash firmware and filesystem as above.
- Power on the board. The display shows SETUP MODE and the device creates an open Wi-Fi AP called
WorkDisplay. - Connect your phone or laptop to
WorkDisplay. - A captive portal opens automatically (or navigate to
192.168.4.1). - Select your home/office Wi-Fi network and enter the password.
- The device connects, shows its IP address, and begins displaying the active mode.
- Access the web UI at the IP shown on screen, or
http://workdisplay.local.
The single-page app has three tabs:
- View all configured modes with their colours
- Tap a mode card to activate it immediately
- Use the + button to create a new mode (name, background colour, text colour, font size, LED colour, LED brightness)
- Long-press / delete button to remove a mode
- Rename the header label (e.g. change "Work State" to "Studio" or "Lab Status")
- Shows current connection status, IP address, and hostname
- Scan and connect to a different network
- Reset Wi-Fi credentials (device returns to AP/setup mode)
- Configure single tap and double tap actions
- Each action can fire an HTTP/HTTPS request with custom method, URL, headers, and body
- Set the double-tap detection window (250 ms / 400 ms / 600 ms / 800 ms)
- If single tap has no URL configured, it falls back to cycling through modes
| Gesture | Default behaviour | Configured behaviour |
|---|---|---|
| Single tap | Cycle to next mode | Fire HTTP webhook (if URL set) |
| Double tap | Nothing | Fire HTTP webhook (if URL set) |
All endpoints are on port 80. POST endpoints with a JSON body use Content-Type: application/json.
GET /api/status
Returns Wi-Fi state, IP, active mode name, and UI label.
{
"connected": true,
"mode": "station",
"ssid": "MyNetwork",
"ip": "192.168.1.42",
"hostname": "workdisplay",
"active": "BUSY",
"label": "Work State"
}GET /api/modes
Returns all modes and the active mode name.
POST /api/modes
{ "name": "DND", "bg": "#8e44ad", "fg": "#ffffff", "size": 2, "ledColor": "#8e44ad", "ledBrightness": 128 }
Creates or updates a mode. Name is stored uppercase.
DELETE /api/modes?name=DND
Deletes a mode by name.
GET /api/active
Returns the active mode details (name, bg, fg).
POST /api/active
{ "name": "FOCUS" }
Sets the active mode and triggers an immediate display + LED update.
POST /api/connect
{ "ssid": "MyNetwork", "password": "secret" }
POST /api/wifi/reset
Erases credentials and restarts in AP mode.
GET /api/scan
Returns latest Wi-Fi scan results.
POST /api/label
{ "label": "Studio Status" }
Renames the UI header label (max 24 characters).
GET /api/button
POST /api/button
{
"doubleTapMs": 400,
"single": {
"enabled": true,
"method": "POST",
"url": "https://home-assistant.local/api/webhook/my-hook",
"headers": "Authorization: Bearer eyJ...",
"body": "{\"state\": \"busy\"}"
},
"double": {
"enabled": false,
"method": "POST",
"url": "",
"headers": "",
"body": ""
}
}
Set single tap to call a Home Assistant webhook:
| Field | Value |
|---|---|
| Method | POST |
| URL | https://your-ha-instance/api/webhook/work-status |
| Headers | Authorization: Bearer <your-long-lived-token> |
| Body | {"state": "busy"} |
The device uses WiFiClientSecure with certificate verification disabled — suitable for local network HA instances with self-signed certs.
AsyncWebServer callbacks run on core 0 (Wi-Fi/TCP task). All LovyanGFX display calls must run on core 1 (Arduino loop()). Direct display calls from web handlers cause race conditions and screen corruption.
The dispatch pattern used throughout the codebase:
// Core 0 — web handler sets a flag
_modePending = true;
// Core 1 — loop() reads the flag and applies the update
if (_modePending.exchange(false)) {
ModeEntry m = webSrv.getActiveMode();
display.showMode(m);
led.setColor(m.ledColor, m.ledBrightness);
}| File | Responsibility |
|---|---|
src/config.h |
All GPIO pin definitions and app constants — single source of truth |
src/lgfx_config.h |
LovyanGFX LGFX class — I8080 parallel bus + CST816S touch configuration |
src/display_manager.h/.cpp |
LovyanGFX wrapper; ModeEntry struct; double-tap state machine |
src/led_manager.h/.cpp |
FastLED wrapper for 6× WS2812B on GPIO 43 |
src/wifi_manager.h/.cpp |
STA/AP state machine with DNS captive portal; persists credentials to NVS |
src/web_server.h/.cpp |
AsyncWebServer REST API + LittleFS static file serving |
src/main.cpp |
Wires all modules; owns _modePending and _btnCfgPending atomic flags |
data/index.html |
Single-file SPA (no build step); served from LittleFS |
NVS namespace "wsd" (via Preferences):
ssid/pass— Wi-Fi credentialshost— mDNS hostnameactive— name of the current active modelabel— UI header label
LittleFS:
/modes.json— JSON array of mode objects; created with defaults on first boot/button.json— Button click config; created on first save via web UI
| Library | Version | Purpose |
|---|---|---|
| LovyanGFX | ^1.2.0 |
Display + touch driver |
| ESPAsyncWebServer | ^3.3.23 |
Async HTTP server |
| AsyncTCP | ^3.2.14 |
Async TCP for ESPAsyncWebServer |
| ArduinoJson | ^7.2.0 |
JSON serialisation (v7 API) |
| FastLED | ~3.9.0 |
WS2812B LED control |
Note: FastLED is pinned to
~3.9.0. Version 3.10.x has a broken ESP-IDF 5.x audio initialiser that causes a compile error on this platform.
MIT