Chaya2MQTT turns two small ESP32-S3 e-paper displays into a private connection: press the button on yours and a red heart appears on the other. It could be across the room or across the world—as long as both hearts can reach the same MQTT broker.
Each display remembers how many hearts were sent and received. If one heart goes offline for a while, it catches up automatically when it reconnects. The technical details live below; the idea is simply a quiet little thinking of you. ❤️
| Area | Description |
|---|---|
| E-Ink display | Red heart with RX/TX counters (delta display), Waveshare 1.54G, 200×200, black/white/red/yellow |
| MQTT sync | Publish on button press or via the web UI; subscribe sets the counter to the received value |
| TLS | Broker connection via mqtts:// with the Mozilla CA bundle (port 8883) |
| Web admin | Captive portal in AP mode, then http://chaya2mqtt-<deviceId>.local |
| Pairing | Random device ID in NVS → automatic topics chaya2mqtt/<id> |
| Button | BOOT: press → send via MQTT |
| OTA | Automatic daily GitHub release check + manual trigger |
Only Waveshare ESP32-S3-ePaper-1.54G SKU 34586. Pins, battery, buttons: HARDWARE.md.
Tests and quality gates: TESTING.md
- PlatformIO (CLI or IDE)
- MQTT broker with TLS and a valid server certificate from a public CA (Let's Encrypt, DigiCert, etc.)
- Waveshare ESP32-S3-ePaper-1.54G SKU 34586 (USB-C and/or included LiPo)
Browser: Open the hosted web flasher in Chrome, Edge, or another Chromium browser and connect the device over USB-C. For local flasher development, see flasher/README.md.
# In the project directory
pio run # Build (default env: esp32s3-release)
pio run -e esp32s3 # Development build (CORE_DEBUG_LEVEL=4)
pio run -t upload # Flash (default: esp32s3-release)
pio device monitor # Serial monitor (115200 baud)Alternatively, use the Makefile (upload targets default to debug):
make upload # Build and flash debug; keep saved settings
make upload-erase # Erase settings, build, and flash debug
make monitor # Serial monitorIf pio: command not found appears, PlatformIO is located at ~/.platformio/penv/bin/pio. Either set export PATH="$HOME/.platformio/penv/bin:$PATH" or use make.
- Power the device or restart it after flashing.
- If no WiFi configuration is stored, the ESP32-S3 opens the WPA2/WPA3
Chaya2MQTTaccess point with an individual alphanumeric SoftAP password. The E-Ink display shows the WIFI QR (join by scanning only). On battery, press PWR to start (firmware must hold GPIO17 HIGH). - Scan the WIFI QR on the display with your phone camera and open the captive portal / browser (
http://chaya2mqtt.localorhttp://4.3.2.1). - Enter the WiFi SSID and password. After switching to STA mode, configure MQTT under MQTT:
- MQTT server (hostname or IP) — required
- MQTT port (default: 8883) — required
- MQTT username / password (optional; leave empty for anonymous brokers)
- Partner ID of the other device (required for the heart display; 6 hex characters). Use Unpair to clear.
- In AP mode, the WiFi connection is tested before the credentials are saved.
- After a successful connection, the admin UI is available at
http://chaya2mqtt-<deviceId>.local, for examplehttp://chaya2mqtt-a1b2c3.local. The address and ID are shown on the dashboard.
Two devices can be set up in parallel. Both APs use the same SSID, but they are isolated networks; scanning the QR on the intended display joins the matching device. Once both devices share the normal LAN, their ID-suffixed hostnames keep web and mDNS access unambiguous. If static addressing is used, assign a different IP to every device.
Flash both devices with the same firmware version. Broker credentials must be identical (WiFi can differ).
- Open the MQTT page (
/mqtt) on both devices. - Enter the same broker on both devices.
- Note each device's own device ID (6 hexadecimal characters from the MAC), save it as the partner ID on the other device, and vice versa.
- Topics are set automatically:
- Publish topic:
chaya2mqtt/<own_id>(e.g.chaya2mqtt/a1b2c3) - Subscribe topic:
chaya2mqtt/<partner_id>(e.g.chaya2mqtt/f5e6d7)
- Publish topic:
This allows multiple pairs to use the same broker without topic collisions. Without a partner, the device can still connect to the broker but does not subscribe, keeps the waiting title (no heart with counters), and rejects heart send until a partner is set.
Details: MQTT.md
Use Settings → Device → Factory reset in the web admin. This deletes all NVS
namespaces (wifi, mqtt, cfg, chaya) and restarts into the Chaya2MQTT
setup SoftAP. There is intentionally no physical factory-reset gesture; if the web
admin is unreachable, erase and reflash the device over USB.
chaya2mqtt/
├── README.md # Brief introduction
├── platformio.ini # Build configuration
├── partitions_chaya_8mb.csv # Dual-OTA table (8 MB flash; ~3.75 MB per slot)
├── Makefile # PlatformIO wrapper
├── docs/ # This documentation
├── flasher/ # Svelte/Vite/Tailwind web flasher (esptool-js, GitHub Pages)
├── frontend/ # Svelte 5 SPA (Vite, Tailwind, Lucide) + mock device
└── src/
├── main.cpp # Bootstrap, task startup
├── constants.h # Device-wide identity, NTP, syntax validation
├── async/ # Queues, mutexes, app task
├── config/ # app_config, nvs_utils, version.h
├── diag/ # Stack monitor, task WDT
├── display/ # EPD (GxEPD2) + drawing + display task
├── heart/ # Counters, baselines, NVS
├── hw/ # Button, battery ADC, SD hold-off, 1.54G pins
├── mqtt/ # Config + client/events/publish/reconnect
├── network/ # network_task (WiFi + MQTT loop)
├── ota/ # GitHub check, flash installation, health gate
├── tls/ # CA bundle (MQTT + OTA)
├── util/ # Time helpers, logging, IP formatting
├── web/ # Admin API + SPA serving (routes/, assets/)
└── wifi/ # WiFi, captive portal, recovery, connection test
| Environment | Purpose | Debug level | Optimization |
|---|---|---|---|
esp32s3 |
Development (ESP32-S3 1.54G) | CORE_DEBUG_LEVEL=4 |
Default |
esp32s3-release |
Production (default) | CORE_DEBUG_LEVEL=0 |
-Os, -DNDEBUG |
The 1.54G has 8 MB flash. partitions_chaya_8mb.csv is a dual-OTA map (~3.75 MB per slot).
| Library | Purpose |
|---|---|
| GxEPD2 | E-paper driver (1.54G 4-color / GxEPD2_4C) |
| Adafruit GFX / BusIO | Graphics primitives for e-paper |
ESP-IDF MQTT (esp_mqtt_client) |
mqtt or mqtts (TLS selectable; default TLS) |
| ESPAsyncWebServer | HTTP admin + captive portal |
| File | Content |
|---|---|
| ARCHITECTURE.md | FreeRTOS tasks, queues, data flows |
| MODULES.md | Code reference for all modules |
| MQTT.md | Protocol, topics, TLS, pairing |
| WEB_ADMIN.md | SPA, Host allowlist; REST/SSE live in OpenAPI/AsyncAPI |
| openapi.yaml | REST API (OpenAPI 3.2) |
| asyncapi.yaml | SSE events (AsyncAPI 3) |
| HARDWARE.md | The only board: 1.54G SKU 34586, pins, battery |
| OTA.md | Firmware updates via GitHub |
| CONFIGURATION.md | NVS namespaces, defaults |
| DISPLAY.md | Display task, Lucide icons, delta logic |
GNU General Public License v3.0 only—use, modification, and distribution are permitted, including commercially. Distributed modified versions and binaries must make the corresponding source code available under the same license.