This project was developed with AI-assisted code generation and human oversight.
Real-time ambient weather and room sensor display system. ESP32 Hub fetches live weather via Wi-Fi, resolves city name from GPS coordinates via OpenStreetMap, and broadcasts to an ESP32-2432S028 Display over ESP-NOW. The dashboard auto-centers all UI elements for perfect alignment regardless of value length.
- Live Weather — Temperature, humidity, pressure, wind speed/direction, sunrise/sunset via Open-Meteo API (free, no key required)
- Room Metrics — DHT22 temperature and humidity
- Auto-Centering UI — Every row recalculates position based on actual text width
- Dual Themes — Dark/Light with persistent preferences in NVS
- Configurable — Wi-Fi credentials, NTP server, date format, show/hide seconds
- Offline Resilient — Shows placeholders when Hub is unreachable
- ESP-NOW Communication — Low-latency, no router required, auto-channel sync
- Reverse Geocoding — City name resolved from GPS coordinates via Nominatim (no API key)
Before building and flashing, ensure you have:
- PlatformIO IDE (VS Code extension or CLI)
- ESP32 USB drivers (CP210x or CH340 depending on your board)
- USB cable (data-capable, not just charging)
The project uses PlatformIO's dependency management — all required libraries download automatically during the first build.
Toggle between Dark and Light themes in the settings screen. Preferences persist across reboots via NVS.
| Dark Theme | Light Theme |
|---|---|
![]() |
![]() |
- Show Password — Toggle password visibility with eye icon
- Brightness Slider — Adjust backlight brightness with live preview
- Persistent Credentials — SSID, Password, NTP server saved to NVS
- Auto-Scroll — Text fields scroll into view when focused
- Tab Change — Keyboard automatically hides when switching tabs
Every UI row recalculates position on each update using actual text widths and empirically-determined gaps. Ensures perfect centering even when values change length.
| Config Screen | Settings Tab |
|---|---|
![]() |
![]() |
flowchart TB
subgraph Hub["Hub (ESP32 WROOM-32)"]
direction LR
RTC[DS3231 RTC] --> Network
DHT[DHT22 Sensor] --> Network
Weather[Weather API<br>Open-Meteo] --> Network
Location[LocationResolver<br>Nominatim] --> Network
Network[Network Module<br>ESP-NOW Broadcast]
end
subgraph Display["Display (ESP32-2432S028 CYD)"]
direction LR
ESPNOW[ESP-NOW RX] --> UI[UI Module<br>LVGL Dashboard]
UI --> LCD[(320x240 IPS TFT)]
Touch[Touch Input<br>XPT2046] --> UI
Preferences[NVS Preferences] --> UI
end
Hub -->|DataPacket<br>every 250ms| Display
Display -->|ConfigPacket<br>on demand| Hub
Edit hub/include/config/LocationConfig.h:
constexpr float LOCATION_LAT = 40.7128; // Your latitude
constexpr float LOCATION_LON = -74.0060; // Your longitude
cd hub/
pio run -t upload
pio device monitor -b 115200
Expected output 1 (No config):
[MAIN] AmbiSense Hub booting...
[RTC] DS3231 INIT
[GEO] LocationResolver INIT
[NET] ESP-NOW INIT
[ 113][E][Preferences.cpp:50] begin(): nvs_open failed: NOT_FOUND
[NET] No saved config. Waiting for Display to push credentials...
[MAIN] Boot complete.
Expected output 2 (Config saved):
[MAIN] AmbiSense Hub booting...
[RTC] DS3231 INIT
[GEO] LocationResolver INIT
[NET] ESP-NOW INIT
[NET] Config loaded: SSID: YOUR_SSID_HERE NTP: pool.ntp.org
[NET] Connecting to YOUR_SSID_HERE...
[MAIN] Boot complete.
[NET] Connected to IP: 192.168.xxx.xxx
[RTC] NTP sync starting...
[RTC] NTP sync done (UTC): xxxx-xx-xx xx:xx:xx
[RTC] Local time: xx:xx:xx
cd display/
pio run -t upload
pio device monitor -b 115200
Expected output:
[MAIN] AmbiSense Display booting...
[DISP] Backlight initialized: 80%
[DISP] DisplayManager initialized.
[NET] Scanning on ALL Channels...
[NET] ESP-NOW INIT
[NET] LOCKED to Hub Channel: 1
[NET] Connection Restored in 723ms on Channel 1
[NET] Hub MAC learned
[UI] Dashboard built.
[UI] Initialized.
[MAIN] Boot complete.
- ESP32 (WROOM-32)
- DHT22 (temperature/humidity sensor)
- DS3231 (real-time clock)
- Wi-Fi antenna
- ESP32-2432S028 CYD (320x240 IPS TFT, XPT2046 resistive touch)
Tap the Settings icon (bottom-right) to configure:
| Setting | Description |
|---|---|
| Wi-Fi SSID | Hub network name |
| Password | Hub network password |
| NTP Server | Time server (default: pool.ntp.org) |
| Brightness | Backlight brightness (persists) |
| Theme | Dark / Light (persists) |
| Show Seconds | Toggle clock seconds (persists) |
| Date Format | Text (07 Mar 2024) / Numeric (07/03/2024) (persists) |
All settings persist across reboots via NVS.
| File | Setting | Default |
|---|---|---|
| hub/config/HubConfig.h | GMT_OFFSET_SEC | 7 * 3600 (UTC+7) |
| hub/config/HubConfig.h | WEATHER_INTERVAL_MS | 1800000 |
| hub/config/LocationConfig.h | LOCATION_LAT / LOCATION_LON | Required |
| display/config/UIConfig.h | Screen layout constants | 320x240 |
| display/config/DisplayConfig.h | HUB_OFFLINE_TIMEOUT_MS | 15000 |
DataPacket (Hub -> Display, ~100 bytes):
| Field | Size | Description |
|---|---|---|
| type | 1 | PACKET_TYPE_DATA (0x01) |
| seq | 1 | Rolling sequence number |
| channel | 1 | Wi-Fi channel for auto-sync |
| timestamp | 4 | Unix timestamp from RTC |
| locationValid | 1 | 1 = city name valid |
| city | 33 | Location name |
| weatherValid | 1 | 1 = weather data valid |
| weatherCode | 1 | WMO weather code |
| outsideTemp | 4 | Degrees Celsius |
| apparentTemp | 4 | Feels like temperature |
| outsideHumi | 1 | Percent |
| outsidePress | 2 | hPa |
| windSpeed | 4 | km/h |
| windDirection | 2 | Degrees 0-360 |
| sunrise | 8 | HH:MM |
| sunset | 8 | HH:MM |
| roomValid | 1 | 1 = room data valid |
| roomTemp | 4 | Degrees Celsius |
| roomHumi | 4 | Percent |
| Dot Color | Meaning |
|---|---|
| Green | Hub online, weather valid |
| Gold | Hub online, weather stale (no internet) |
| Red | Hub offline |
AmbiSense/
├── display/ # ESP32-2432S028 Display firmware
│ ├── include/
│ │ ├── config/ # Config, HardwareConfig, UIConfig
│ │ ├── log/ # log.h
│ │ ├── display/ # display_manager.h, ui.h
│ │ ├── fonts/ # LVGL font headers
│ │ ├── network/ # network.h
│ │ └── weather/ # weather_types.h
│ ├── src/
│ │ ├── log/ # log.cpp
│ │ ├── display/ # display_manager.cpp, ui.cpp
│ │ ├── fonts/ # Font bitmaps (.c)
│ │ ├── network/ # network.cpp
│ │ ├── weather/ # weather_types.cpp
│ │ └── main.cpp
│ ├── lv_conf.h
│ ├── platformio.ini
│ └── User_Setup.h
├── hub/ # ESP32 Hub firmware
│ ├── include/
│ │ ├── config/ # HubConfig, HardwareConfig, LocationConfig
│ │ ├── log/ # log.h
│ │ ├── network/ # network.h
│ │ ├── sensors/ # sensors.h
│ │ ├── services/
│ │ │ ├── location/ # location_resolver.h
│ │ │ └── weather/ # weather.h
│ │ └── time/ # rtc_manager.h
│ ├── src/
│ │ ├── log/ # log.cpp
│ │ ├── network/ # network.cpp
│ │ ├── sensors/ # sensors.cpp
│ │ ├── services/
│ │ │ ├── location/ # location_resolver.cpp
│ │ │ └── weather/ # weather.cpp
│ │ ├── time/ # rtc_manager.cpp
│ │ └── main.cpp
│ └── platformio.ini
├── docs/
│ ├── Code_Layout_Standard.md
│ ├── Log_Standard.md
│ └── MDI_Symbols.md
├── AmbiSense_ARCHITECTURE.md
└── README.md
| Symptom | Likely Fix |
|---|---|
| Red indicator dot | Check Hub power; both on same 2.4GHz band |
| Unknown city | Hub needs internet at boot; check LOCATION_LAT/LON |
| Time incorrect | NTP sync required; check GMT_OFFSET_SEC |
| Config not saving | pio run -t erase |
| Keyboard covers text fields | Dynamic padding should handle this; check _onTaEvent callback |
| Theme switch crashes | Async deletion fix applied; update if still occurs |
| No log output | LOG_ENABLED=0, or the channel mask is empty. Call Log::setChannel(...) and Log::dump() |
| Metric | Value |
|---|---|
| ESP-NOW latency | <50ms |
| Broadcast rate | 250ms (4Hz) |
| Sensor polling | 2 seconds |
| Weather refresh | 30 minutes |
| Display FPS | ~60 FPS |
| Power (total) | ~2W |
| Aspect | File | Setting |
|---|---|---|
| Timezone | hub/config/HubConfig.h | GMT_OFFSET_SEC |
| Weather interval | hub/config/HubConfig.h | WEATHER_INTERVAL_MS |
| Brightness limits | display/config/DisplayConfig.h | BRIGHTNESS_MIN_PERCENT / BRIGHTNESS_MAX_PERCENT |
| Theme colors | display/ui.cpp | DARK / LIGHT palettes |
| Location | hub/config/LocationConfig.h | LOCATION_LAT / LOCATION_LON |
| UI Layout | display/config/UIConfig.h | Screen size, margins, gaps |
This project enforces a strict code layout standard documented in docs/Code_Layout_Standard.md. Key rules:
- One logical module per file
- Headers declare public API only — no implementation
- Source files contain all implementation and internal state
- Comment hierarchy: T1 (file header) to T7 (inline notes)
When contributing, follow the visual hierarchy scale defined in the standard document.
Each firmware ships a leveled log system. Enable channels and levels at boot:
Log::init(); Log::setChannel(Log::Ch::CH_NET, true); Log::setLevel(Log::Lvl::D, true);
Call sites use LOG_D(Log::Ch::CH_NET, "message %d", value); or LOG_I, LOG_W, LOG_E. See docs/Log_Standard.md.
The master compile-time gate is LOG_ENABLED in config/LogConfig.h. With the gate off, the macros expand to nothing.
- LVGL Documentation: https://docs.lvgl.io/
- ESP-NOW Guide: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_now.html
- Open-Meteo API: https://open-meteo.com/
- Nominatim Geocoding: https://nominatim.openstreetmap.org/
MIT




