ESP32 S3 UNO - Smart Weather Station

This ESP32 S3 Uno smart weather station tutorial shows you how to build a live WiFi weather display with a 1.28 inch round GC9A01 TFT and one push button. The ESP32 S3 Uno form board gets the weather from the free Open-Meteo weather API over HTTPS, so you need no temperature or humidity sensor. You will see the temperature, wind, rain chance and a 3-day forecast on eight round pages.

In this tutorial, you will:

  1. Wire a round GC9A01 TFT display and a push button to the ESP32 S3 Uno
  2. Call the Open-Meteo weather API over HTTPS with WiFi
  3. Read the JSON answer with the ArduinoJson library
  4. Keep a clock running with NTP time sync
  5. Move between eight weather pages with one button
ESP32 S3 Uno smart weather station

The ESP32 S3 Uno has WiFi on the chip, so you do not need any extra network module. Because the data comes from a web service, the station can show things a cheap sensor cannot measure, like the rain chance for the next hours and the forecast for the next three days.

Hardware Preparation

1×ESP32 S3 Uno-form Board
1×USB Cable Type-A to Type-C (for USB-A PC)
1×USB Cable Type-C to Type-C (for USB-C PC)
1×1.28 Inch Round Circular TFT LCD Display Module
1×Push Button
1×Button Module Alternatively,
1×Breadboard
1×Jumper Wires

You only need one of the two button parts. The wiring for both is shown below.

Or you can buy the following kits:

1×DIYables Sensor Kit (18 sensors/displays)
Disclosure: Some of the links provided in this section are Amazon affiliate links. We may receive a commission for any purchases made through these links at no additional cost to you.
Additionally, some of these links are for products from our own brand, DIYables .

Overview of the Weather Station Pages

It helps to know what each screen shows before you build it. Then you can check every page after upload. The station has eight pages, and one push button steps through them.

Page What it shows
HOME Weather icon, temperature, sky description and an NTP clock
TEMP Temperature and the "feels like" temperature
HUMIDITY Humidity drawn as a ring, plus the exact percentage
WIND Wind speed and wind direction on a compass
RAIN Rain chance with a small rain animation
NEXT HOURS Temperature and rain chance for the next hours
FORECAST Highest and lowest temperature for the next 3 days
SYSTEM WiFi state, signal strength, API state and location

The ESP32 S3 Uno asks for new weather every 10 minutes. Between two updates, the clock on the home page keeps going, because the board keeps its own time after the NTP sync.

Overview of the Open-Meteo Weather API

Every weather station needs a data source. Here, the source is Open-Meteo, a free web service that works well with a small board like the ESP32 S3 Uno.

Open-Meteo
API key not needed, no sign-up
Cost free for personal use
Answer format plain JSON
Place set by latitude and longitude
Protocol HTTPS on port 443

One request gives you the weather now, hourly values for the next days, and a three-day daily summary. The timezone parameter makes the service send local times instead of UTC.

ESP32 S3 Uno Pinout

The image below shows the pinout of the ESP32 S3 Uno form board. Use it to find the Uno header pins (D2, D8, D9, D10, D11, D13) and their GPIO numbers while you wire the station.

ESP32 S3 Uno pinout diagram

Wiring Diagram

The round display talks over SPI. On the ESP32 S3 Uno, the D11 and D13 headers are the default hardware SPI pins (GPIO11 for MOSI and GPIO12 for the clock), so these two wires cannot move to other pins.

TFT LCD Pin ESP32 S3 Uno Pin Description
VCC 5V Power supply
GND GND Ground
SCL D13 (GPIO12) SPI clock (fixed)
SDA D11 (GPIO11) SPI MOSI (fixed)
DC D9 (GPIO46) Data/Command
CS D10 (GPIO10) Chip Select
RST D8 (GPIO21) Reset

WARNING

D9 is GPIO46, a boot strapping pin on the ESP32 S3 Uno. The chip reads it at power-up to pick the boot mode. The display DC input does not normally pull this pin, so the board boots fine. If the board will not start or upload while the display is connected, unplug the DC wire, upload, and connect it again.

※ NOTE THAT:

The display only receives signals from the board. No signal goes back into an ESP32 S3 Uno pin, so powering the module from 5V is safe. The module also runs from 3.3V if you prefer.

The button goes to D2 (GPIO18). You can use a plain push button or a ready-made button module. Wire the part you have, then make the code match it.

Option 1: Bare Push Button

A plain push button has no resistor. One leg goes to D2 and the other goes to GND. The ESP32 S3 Uno turns on its internal pull-up resistor, so the pin reads HIGH at rest and LOW while you press.

The wiring diagram between ESP32 S3 Uno smart weather station  with push button

This image is created using Fritzing. Click to enlarge image

Button Pin ESP32 S3 Uno Pin Description
Pin 1 D2 (GPIO18) Button input, internal pull-up
Pin 2 GND Ground

The sketch is already set up for this option:

ezButton button(PIN_BUTTON);

Option 2: Button Module

A button module has its own pull-down resistor and needs power. Its OUT pin is LOW at rest and goes HIGH when you press, which is the opposite of the bare button.

The wiring diagram between ESP32 S3 Uno smart weather station  with button module

This image is created using Fritzing. Click to enlarge image

Button Module Pin ESP32 S3 Uno Pin Description
VCC 3.3V Power supply
GND GND Ground
OUT D2 (GPIO18) Signal, LOW at rest, HIGH when pressed

WARNING

Power the button module from 3.3V, not 5V. Its OUT pin gives the same voltage as its VCC, and ESP32 S3 Uno pins are NOT 5V tolerant. A 5V signal on D2 can damage GPIO18.

The module has its own resistor, so the internal one must stay off. Change one line:

ezButton button(PIN_BUTTON, EXTERNAL_PULLDOWN);

How To Program ESP32 S3 Uno for the Weather Station

Before you open the full sketch, it helps to see the main building blocks one by one. Each short piece below is a part of the complete code.

Include the libraries

WiFi.h, WiFiClientSecure.h, HTTPClient.h and time.h come with the esp32 board package by Espressif Systems. You only need to install ArduinoJson, DIYables_TFT_Round and ezButton from the Library Manager.

#include <WiFi.h> #include <WiFiClientSecure.h> #include <HTTPClient.h> #include <ArduinoJson.h> #include <time.h> #include <DIYables_TFT_Round.h> #include <ezButton.h>

Create the display object

The display object takes the reset, data/command and chip select pins. On the ESP32 S3 Uno these are D8 (GPIO21), D9 (GPIO46) and D10 (GPIO10).

DIYables_TFT_GC9A01_Round tft(21, 46, 10);

Set your WiFi and your place

#define WIFI_SSID "YOUR_WIFI_SSID" #define WIFI_PASSWORD "YOUR_WIFI_PASSWORD" #define LATITUDE 37.5665 #define LONGITUDE 126.9780

Join the network

WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

Sync the clock with NTP

GMT_OFFSET_SEC is your offset from UTC, in seconds.

configTime(GMT_OFFSET_SEC, DST_OFFSET_SEC, "pool.ntp.org", "time.nist.gov"); struct tm timeinfo; getLocalTime(&timeinfo, 10000);

Send the HTTPS request

The code opens a secure connection and sends the request. setInsecure() skips the certificate check. This keeps the sketch short for a hobby project.

WiFiClientSecure client; client.setInsecure(); HTTPClient http; http.begin(client, url); int httpCode = http.GET(); String payload = http.getString();

Read a value from the JSON answer

JsonDocument doc; deserializeJson(doc, payload); float temperature = doc["current"]["temperature_2m"] | 0.0;

Draw it on the round screen

tft.setTextSize(3); tft.setTextColor(DIYables_TFT::colorRGB(255, 255, 255)); tft.setCursor(80, 100); tft.print(temperature, 1);

ESP32 S3 Uno Code - Smart Weather Station

The full sketch below joins your WiFi, syncs the clock, and asks Open-Meteo for new weather every 10 minutes. It draws the eight pages on the round display and reads the button to switch pages, refresh the data, or go home.

/* * This ESP32 S3 Uno code was developed by newbiely.com * * This ESP32 S3 Uno code is made available for public use without any restriction * * For comprehensive instructions and wiring diagrams, please visit: * https://newbiely.com/tutorials/esp32-s3-uno/esp32-s3-uno-smart-weather-station */ /* ============================================================ SMART WEATHER STATION ESP32 S3 Uno + GC9A01 Round TFT + Push Button Location: Seoul, South Korea (change it in USER CONFIGURATION) Weather API: Open-Meteo (free, no API key needed) ------------------------------------------------------------ GC9A01 Round TFT -> ESP32 S3 Uno VCC -> 5V GND -> GND SCL -> D13 (GPIO12, hardware SPI clock, fixed) SDA -> D11 (GPIO11, hardware SPI MOSI, fixed) RST -> D8 (GPIO21) DC -> D9 (GPIO46) CS -> D10 (GPIO10) Button D2 (GPIO18) -> BUTTON -> GND ------------------------------------------------------------ BUTTON Short press -> Next page Long press -> Refresh weather Very long press -> Back to home page ============================================================ */ #include <WiFi.h> #include <WiFiClientSecure.h> #include <HTTPClient.h> #include <ArduinoJson.h> #include <time.h> #include <DIYables_TFT_Round.h> #include <ezButton.h> // ===== USER CONFIGURATION ===== #define WIFI_SSID "YOUR_WIFI_SSID" #define WIFI_PASSWORD "YOUR_WIFI_PASSWORD" #define LATITUDE 37.5665 #define LONGITUDE 126.9780 #define LOCATION_NAME "SEOUL" // Time zone name for the API, URL-encoded. // "/" must be written as "%2F". #define TIMEZONE "Asia%2FSeoul" // Offset from UTC in seconds, used by the NTP clock. // Seoul is UTC+9, so 9 * 3600. #define GMT_OFFSET_SEC (9 * 3600) // Extra offset for summer time. Use 3600 where it applies. #define DST_OFFSET_SEC 0 // ===== PIN CONFIGURATION ===== // SCK (GPIO12) and MOSI (GPIO11) are the hardware SPI pins of // the ESP32 S3 Uno. The library uses them on its own, so they are not // listed here. #define PIN_BUTTON 18 // D2 #define PIN_RST 21 // D8 #define PIN_DC 46 // D9 #define PIN_CS 10 // D10 // ===== TIMING ===== const unsigned long API_UPDATE_INTERVAL = 10UL * 60UL * 1000UL; const unsigned long WIFI_RETRY_INTERVAL = 10UL * 1000UL; const unsigned long WIFI_CONNECT_TIMEOUT = 15UL * 1000UL; const unsigned long BUTTON_DEBOUNCE = 40; const unsigned long LONG_PRESS_TIME = 600; const unsigned long VERY_LONG_PRESS_TIME = 2000; const unsigned long RAIN_ANIMATION_INTERVAL = 90; const unsigned long CLOCK_UPDATE_INTERVAL = 1000; // ===== TFT ===== DIYables_TFT_GC9A01_Round tft(PIN_RST, PIN_DC, PIN_CS); // ===== SCREEN ===== const int SCREEN_W = 240; const int SCREEN_H = 240; const int CENTER_X = 120; const int CENTER_Y = 120; // ===== COLORS ===== #define BLACK DIYables_TFT::colorRGB(0, 0, 0) #define WHITE DIYables_TFT::colorRGB(255, 255, 255) #define CYAN DIYables_TFT::colorRGB(0, 220, 255) #define BLUE DIYables_TFT::colorRGB(40, 120, 255) #define LIGHT_BLUE DIYables_TFT::colorRGB(90, 190, 255) #define GREEN DIYables_TFT::colorRGB(60, 230, 130) #define YELLOW DIYables_TFT::colorRGB(255, 210, 60) #define ORANGE DIYables_TFT::colorRGB(255, 150, 40) #define RED DIYables_TFT::colorRGB(255, 70, 70) #define PURPLE DIYables_TFT::colorRGB(180, 100, 255) #define GRAY DIYables_TFT::colorRGB(125, 125, 135) #define DARK_GRAY DIYables_TFT::colorRGB(42, 42, 52) #define DARK_BLUE DIYables_TFT::colorRGB(12, 18, 48) // ===== PAGE ===== enum Page { PAGE_HOME = 0, PAGE_TEMP, PAGE_HUMIDITY, PAGE_WIND, PAGE_RAIN, PAGE_HOURLY, PAGE_FORECAST, PAGE_SYSTEM, PAGE_COUNT }; Page currentPage = PAGE_HOME; // ===== WEATHER DATA ===== struct WeatherData { bool valid = false; float temperature = 0; float humidity = 0; float feelsLike = 0; float windSpeed = 0; float windDirection = 0; float pressure = 0; int weatherCode = 0; bool isDay = true; int currentRainChance = 0; float uvIndex = 0; String sunrise; String sunset; // NEXT HOURS float hourlyTemp[12]; int hourlyRainChance[12]; int hourlyWeatherCode[12]; String hourlyTime[12]; // DAILY String dailyDate[3]; float dailyTempMax[3]; float dailyTempMin[3]; int dailyRainChance[3]; int dailyWeatherCode[3]; float dailyUV[3]; }; WeatherData weather; // ===== SYSTEM STATE ===== bool apiOnline = false; bool wifiOnline = false; bool screenDirty = true; bool isFetching = false; bool pendingRefresh = false; unsigned long lastApiUpdate = 0; unsigned long lastWifiRetry = 0; unsigned long wifiConnectStart = 0; unsigned long lastRainAnimation = 0; unsigned long lastClockUpdate = 0; bool wifiConnecting = false; // ===== BUTTON ===== // ezButton handles the debouncing, so the sketch only has to // measure how long the button was held down. // // The line below is for a BARE push button wired to GND. ezButton then // uses the internal pull-up resistor of the board, and the pin reads LOW // while the button is held down. ezButton button(PIN_BUTTON); // If you use a BUTTON MODULE instead, the module carries its own resistor // on the board, so the internal one must not be used. Comment the line // above and use this line instead: // ezButton button(PIN_BUTTON, EXTERNAL_PULLDOWN); unsigned long buttonPressStart = 0; // ===== RAIN ANIMATION ===== const int RAIN_DROP_COUNT = 16; int rainX[RAIN_DROP_COUNT]; int rainY[RAIN_DROP_COUNT]; bool rainInitialized = false; const int RAIN_AREA_X = 30; const int RAIN_AREA_Y = 125; const int RAIN_AREA_W = 180; const int RAIN_AREA_H = 78; // ===== WEATHER DESCRIPTION ===== String weatherDescription(int code) { switch (code) { case 0: return "CLEAR"; case 1: return "MAINLY CLEAR"; case 2: return "PARTLY CLOUDY"; case 3: return "OVERCAST"; case 45: case 48: return "FOG"; case 51: case 53: case 55: return "DRIZZLE"; case 56: case 57: return "FREEZING DRIZZLE"; case 61: case 63: case 65: return "RAIN"; case 66: case 67: return "FREEZING RAIN"; case 71: case 73: case 75: return "SNOW"; case 77: return "SNOW GRAINS"; case 80: case 81: case 82: return "SHOWERS"; case 85: case 86: return "SNOW SHOWERS"; case 95: return "THUNDERSTORM"; case 96: case 99: return "STORM"; default: return "UNKNOWN"; } } // ===== WEATHER COLOR ===== uint16_t weatherColor(int code) { if (code == 0) return YELLOW; if (code == 1 || code == 2) return CYAN; if (code == 3) return GRAY; if (code >= 51 && code <= 67) return BLUE; if (code >= 80 && code <= 82) return LIGHT_BLUE; if (code >= 95) return RED; return WHITE; } // ===== WIND DIRECTION ===== String windDirectionText(float degrees) { const char* directions[] = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" }; int index = (int)((degrees + 22.5) / 45.0); index %= 8; return directions[index]; } // ===== CENTER TEXT ===== void drawCenteredText(String text, int y, int size, uint16_t color) { tft.setTextSize(size); tft.setTextColor(color); int width = text.length() * 6 * size; int x = CENTER_X - width / 2; if (x < 0) x = 0; tft.setCursor(x, y); tft.print(text); } // ===== HEADER ===== void drawHeader(String title, uint16_t color) { // Safe radius from screen center (slightly less than the // physical 120px radius, to keep a small margin from the // round bezel). const float SAFE_RADIUS = 116.0; int size = 2; int width = title.length() * 6 * size; int half = width / 2; int y; if (half >= SAFE_RADIUS) { // Too wide even at the lowest safe position -> use a // smaller font instead of clipping. size = 1; width = title.length() * 6 * size; half = width / 2; y = 14; } else { // Compute the minimum y (top of text) so the top corners // of the text stay inside the round visible area. float insideSpan = sqrt((SAFE_RADIUS * SAFE_RADIUS) - ((float)half * (float)half)); int minY = (int)(120.0 - insideSpan); y = (minY > 14) ? minY : 14; } drawCenteredText(title, y, size, color); int lineY = y + (size == 2 ? 16 : 8) + 6; tft.drawLine(45, lineY, 195, lineY, DARK_GRAY); } // ===== FOOTER ===== void drawFooter() { tft.drawLine(55, 214, 185, 214, DARK_GRAY); drawCenteredText(String(currentPage + 1) + "/" + String(PAGE_COUNT), 220, 1, GRAY); } // ===== SUN ICON ===== void drawSun(int x, int y, int radius) { tft.fillCircle(x, y, radius, YELLOW); for (int i = 0; i < 8; i++) { float angle = i * PI / 4.0; int x1 = x + cos(angle) * (radius + 7); int y1 = y + sin(angle) * (radius + 7); int x2 = x + cos(angle) * (radius + 14); int y2 = y + sin(angle) * (radius + 14); tft.drawLine(x1, y1, x2, y2, YELLOW); } } // ===== CLOUD ICON ===== void drawCloud(int x, int y) { tft.fillCircle(x - 20, y, 15, LIGHT_BLUE); tft.fillCircle(x, y - 8, 20, LIGHT_BLUE); tft.fillCircle(x + 20, y, 15, LIGHT_BLUE); tft.fillRoundRect(x - 35, y, 70, 20, 10, LIGHT_BLUE); } // ===== STATIC RAIN ICON ===== void drawRainIcon(int x, int y) { drawCloud(x, y); for (int i = 0; i < 5; i++) { int dx = -24 + i * 12; int dy = 28; tft.drawLine(x + dx, y + dy, x + dx - 4, y + dy + 10, BLUE); } } // ===== WEATHER ICON ===== void drawWeatherIcon(int code, int x, int y) { if (code == 0) { drawSun(x, y, 18); } else if (code <= 2) { drawSun(x - 12, y - 7, 13); drawCloud(x + 10, y + 10); } else if (code == 3) { drawCloud(x, y); } else if ((code >= 51 && code <= 67) || (code >= 80 && code <= 82)) { drawRainIcon(x, y); } else if (code >= 95) { drawCloud(x, y); tft.drawLine(x - 5, y + 28, x - 12, y + 43, YELLOW); tft.drawLine(x + 10, y + 28, x + 3, y + 43, YELLOW); } else { drawCloud(x, y); } } void drawWifiIcon(int cx, int cy, int level) { const int radii[3] = { 14, 27, 40 }; for (int arc = 0; arc < 3; arc++) { uint16_t color = (arc < level) ? CYAN : DARK_GRAY; for (int deg = -150; deg <= -30; deg += 5) { float angle = deg * PI / 180.0; int x = cx + (int)(cos(angle) * radii[arc]); int y = cy + (int)(sin(angle) * radii[arc]); tft.fillCircle(x, y, 2, color); } } // Base dot (the "device") tft.fillCircle(cx, cy, 5, CYAN); } // ===== PAGE 1 - HOME ===== void drawHome() { uint16_t bg = weather.isDay ? BLACK : DARK_BLUE; tft.fillScreen(bg); if (!weather.valid) { drawCenteredText("NO DATA", 90, 3, RED); drawCenteredText("CHECK CONNECTION", 135, 1, WHITE); drawFooter(); return; } // Weather icon drawWeatherIcon(weather.weatherCode, CENTER_X, 65); // Temperature drawCenteredText(String(weather.temperature, 1) + " C", 112, 3, WHITE); // Location drawCenteredText(LOCATION_NAME, 153, 2, CYAN); // Weather condition drawCenteredText(weatherDescription(weather.weatherCode), 178, 1, weatherColor(weather.weatherCode)); // Clock area drawHomeClock(); drawFooter(); } // ===== HOME CLOCK ===== void drawHomeClock() { uint16_t bg = weather.isDay ? BLACK : DARK_BLUE; // Only clear a small region. // This prevents full-screen flicker. tft.fillRect(82, 196, 76, 12, bg); drawCenteredText(getTimeString(), 197, 1, GRAY); } // ===== PAGE 2 - TEMPERATURE ===== void drawTemperature() { tft.fillScreen(BLACK); drawHeader("TEMPERATURE", ORANGE); int cx = 120; int cy = 125; int radius = 68; int value = constrain((int)weather.temperature, 0, 40); int filled = map(value, 0, 40, 0, 240); // Temperature gauge for (int i = 0; i < 240; i += 4) { float angle = (-135.0 + i) * PI / 180.0; int x = cx + cos(angle) * radius; int y = cy + sin(angle) * radius; uint16_t color = (i < filled) ? ORANGE : DARK_GRAY; tft.fillCircle(x, y, 2, color); } // Main value drawCenteredText(String(weather.temperature, 1), 101, 3, WHITE); drawCenteredText("C", 138, 2, ORANGE); drawCenteredText("FEELS " + String(weather.feelsLike, 1) + " C", 168, 1, CYAN); drawFooter(); } // ===== PAGE 3 - HUMIDITY ===== void drawHumidity() { tft.fillScreen(BLACK); drawHeader("HUMIDITY", BLUE); int cx = 120; int cy = 125; int radius = 68; int progress = constrain((int)weather.humidity, 0, 100); int filled = map(progress, 0, 100, 0, 270); for (int i = 0; i < 270; i += 3) { float angle = (-135.0 + i) * PI / 180.0; int x = cx + cos(angle) * radius; int y = cy + sin(angle) * radius; uint16_t color = (i < filled) ? BLUE : DARK_GRAY; tft.fillCircle(x, y, 3, color); } drawCenteredText(String((int)weather.humidity) + "%", 105, 3, WHITE); String comfort; if (weather.humidity < 40) comfort = "DRY"; else if (weather.humidity < 70) comfort = "COMFORTABLE"; else if (weather.humidity < 85) comfort = "HUMID"; else comfort = "VERY HUMID"; drawCenteredText(comfort, 153, 1, CYAN); drawFooter(); } // ===== PAGE 4 - WIND ===== void drawWind() { tft.fillScreen(BLACK); drawHeader("WIND", GREEN); int cx = 120; int cy = 104; int radius = 49; // Compass tft.drawCircle(cx, cy, radius, DARK_GRAY); drawCenteredText("N", 43, 1, WHITE); drawCenteredText("S", 157, 1, WHITE); tft.setCursor(62, 101); tft.setTextSize(1); tft.setTextColor(WHITE); tft.print("W"); tft.setCursor(172, 101); tft.print("E"); // Direction arrow float angle = weather.windDirection * PI / 180.0; int x2 = cx + sin(angle) * 38; int y2 = cy - cos(angle) * 38; tft.drawLine(cx, cy, x2, y2, GREEN); tft.fillCircle(cx, cy, 5, GREEN); // Wind speed drawCenteredText(String(weather.windSpeed, 1) + " km/h", 174, 2, WHITE); drawCenteredText(windDirectionText(weather.windDirection), 198, 1, CYAN); drawFooter(); } // ===== PAGE 5 - RAIN ===== void drawRain() { tft.fillScreen(BLACK); drawHeader("RAIN", LIGHT_BLUE); int rain = weather.currentRainChance; // Static weather icon if (rain >= 60) { drawRainIcon(CENTER_X, 68); } else { drawCloud(CENTER_X, 73); } // Rain percentage drawCenteredText(String(rain) + "%", 105, 3, WHITE); drawCenteredText("RAIN CHANCE", 140, 1, LIGHT_BLUE); // Status if (rain >= 60) { drawCenteredText("HIGH", 158, 1, RED); } else if (rain >= 30) { drawCenteredText("MODERATE", 158, 1, YELLOW); } else { drawCenteredText("LOW", 158, 1, GREEN); } // Animation area if (rain >= 20) { initializeRainAnimation(); } drawFooter(); } // ===== INITIALIZE RAIN ANIMATION ===== void initializeRainAnimation() { for (int i = 0; i < RAIN_DROP_COUNT; i++) { rainX[i] = RAIN_AREA_X + random(RAIN_AREA_W); rainY[i] = RAIN_AREA_Y + random(RAIN_AREA_H); } rainInitialized = true; } // ===== UPDATE RAIN ANIMATION ===== void updateRainAnimation() { if (currentPage != PAGE_RAIN) return; if (weather.currentRainChance < 20) return; if (millis() - lastRainAnimation < RAIN_ANIMATION_INTERVAL) { return; } lastRainAnimation = millis(); uint16_t bg = BLACK; // Erase old drops for (int i = 0; i < RAIN_DROP_COUNT; i++) { tft.drawLine(rainX[i], rainY[i], rainX[i] - 3, rainY[i] + 7, bg); } // Move drops for (int i = 0; i < RAIN_DROP_COUNT; i++) { rainY[i] += 5; if (rainY[i] > RAIN_AREA_Y + RAIN_AREA_H) { rainX[i] = RAIN_AREA_X + random(RAIN_AREA_W); rainY[i] = RAIN_AREA_Y; } tft.drawLine(rainX[i], rainY[i], rainX[i] - 3, rainY[i] + 7, BLUE); } } // ===== PAGE 6 - NEXT HOURS ===== void drawHourly() { tft.fillScreen(BLACK); drawHeader("NEXT HOURS", PURPLE); for (int i = 0; i < 5; i++) { int y = 52 + i * 31; // Row separator if (i > 0) { tft.drawLine(25, y - 8, 215, y - 8, DARK_GRAY); } // Time tft.setCursor(25, y); tft.setTextSize(1); tft.setTextColor(WHITE); tft.print(weather.hourlyTime[i]); // Weather int code = weather.hourlyWeatherCode[i]; String condition; if (code == 0) condition = "SUN"; else if (code <= 3) condition = "CLOUD"; else if (code >= 95) condition = "STORM"; else condition = "RAIN"; tft.setCursor(75, y); tft.setTextColor(weatherColor(code)); tft.print(condition); // Temperature tft.setCursor(142, y); tft.setTextColor(WHITE); tft.print(weather.hourlyTemp[i], 0); tft.print("C"); // Rain tft.setCursor(180, y); tft.setTextColor(LIGHT_BLUE); tft.print(weather.hourlyRainChance[i]); tft.print("%"); } drawFooter(); } // ===== PAGE 7 - FORECAST ===== // UI FIX: // The card border (drawRoundRect) previously spanned x=15..225 // (width 210). For the TOP row, the corners of that rectangle // fell outside the visible round area of the screen and were // clipped. The card width below was reduced to 190px // (x=25..215) which keeps every corner, on every row, safely // inside the round bezel. Text start x was nudged in slightly // to match the new card padding. void drawForecast() { tft.fillScreen(BLACK); drawHeader("3-DAY FORECAST", CYAN); const char* labels[] = { "TODAY", "TOMORROW", "DAY 3" }; // Card geometry (UI fix: narrower so it never touches // the round bezel, even on the top row). const int CARD_X = 25; const int CARD_W = 190; for (int i = 0; i < 3; i++) { int y = 62 + i * 48; // Card tft.drawRoundRect(CARD_X, y - 5, CARD_W, 40, 8, DARK_GRAY); // Day tft.setCursor(CARD_X + 8, y + 4); tft.setTextSize(1); tft.setTextColor(WHITE); tft.print(labels[i]); // Temperature tft.setCursor(CARD_X + 65, y + 4); tft.print(weather.dailyTempMin[i], 0); tft.print("/"); tft.print(weather.dailyTempMax[i], 0); tft.print("C"); // Rain tft.setCursor(CARD_X + 120, y + 4); tft.setTextColor(LIGHT_BLUE); tft.print(weather.dailyRainChance[i]); tft.print("%"); // Weather tft.setCursor(CARD_X + 155, y + 4); tft.setTextColor(weatherColor(weather.dailyWeatherCode[i])); int code = weather.dailyWeatherCode[i]; if (code == 0) tft.print("SUN"); else if (code <= 3) tft.print("CLD"); else if (code >= 95) tft.print("STM"); else tft.print("RAIN"); } drawFooter(); } // ===== PAGE 8 - SYSTEM ===== void drawSystem() { tft.fillScreen(BLACK); drawHeader("SYSTEM", CYAN); // WIFI tft.setCursor(25, 57); tft.setTextSize(1); tft.setTextColor(WHITE); tft.print("WIFI"); tft.setCursor(125, 57); if (WiFi.status() == WL_CONNECTED) { tft.setTextColor(GREEN); tft.print("ONLINE"); } else { tft.setTextColor(RED); tft.print("OFFLINE"); } // RSSI tft.setCursor(25, 82); tft.setTextColor(WHITE); tft.print("RSSI"); tft.setCursor(125, 82); if (WiFi.status() == WL_CONNECTED) { tft.setTextColor(CYAN); tft.print(WiFi.RSSI()); tft.print(" dBm"); } else { tft.print("--"); } // API tft.setCursor(25, 107); tft.setTextColor(WHITE); tft.print("API"); tft.setCursor(125, 107); if (apiOnline) { tft.setTextColor(GREEN); tft.print("ONLINE"); } else { tft.setTextColor(RED); tft.print("OFFLINE"); } // LOCATION tft.setCursor(25, 132); tft.setTextColor(WHITE); tft.print("LOCATION"); tft.setCursor(125, 132); tft.setTextColor(CYAN); tft.print(LOCATION_NAME); // TEMP tft.setCursor(25, 157); tft.setTextColor(WHITE); tft.print("TEMP"); tft.setCursor(125, 157); tft.print(weather.temperature, 1); tft.print(" C"); // STATUS tft.setCursor(25, 182); tft.setTextColor(WHITE); tft.print("STATUS"); tft.setCursor(125, 182); if (weather.valid) { tft.setTextColor(GREEN); tft.print("READY"); } else { tft.setTextColor(RED); tft.print("NO DATA"); } drawFooter(); } // ===== RENDER PAGE ===== void renderPage() { // Rain animation must be initialized // only when entering the page. rainInitialized = false; switch (currentPage) { case PAGE_HOME: drawHome(); break; case PAGE_TEMP: drawTemperature(); break; case PAGE_HUMIDITY: drawHumidity(); break; case PAGE_WIND: drawWind(); break; case PAGE_RAIN: drawRain(); break; case PAGE_HOURLY: drawHourly(); break; case PAGE_FORECAST: drawForecast(); break; case PAGE_SYSTEM: drawSystem(); break; } } // ===== WEATHER API URL ===== String getWeatherURL() { String url = "https://api.open-meteo.com/v1/forecast"; url += "?latitude=" + String(LATITUDE, 4); url += "&longitude=" + String(LONGITUDE, 4); url += "&current=" "temperature_2m," "relative_humidity_2m," "apparent_temperature," "weather_code," "wind_speed_10m," "wind_direction_10m," "pressure_msl," "is_day"; url += "&hourly=" "temperature_2m," "precipitation_probability," "weather_code"; url += "&daily=" "temperature_2m_max," "temperature_2m_min," "precipitation_probability_max," "weather_code," "sunrise," "sunset," "uv_index_max"; url += "&timezone=" TIMEZONE; url += "&forecast_days=3"; url += "&temperature_unit=celsius"; url += "&wind_speed_unit=kmh"; return url; } // ===== PARSE WEATHER ===== bool parseWeather(String payload) { JsonDocument doc; DeserializationError error = deserializeJson(doc, payload); if (error) { Serial.print("JSON ERROR: "); Serial.println(error.c_str()); return false; } // CURRENT JsonObject current = doc["current"]; if (current.isNull()) { Serial.println("CURRENT DATA MISSING"); return false; } weather.temperature = current["temperature_2m"] | 0.0; weather.humidity = current["relative_humidity_2m"] | 0.0; weather.feelsLike = current["apparent_temperature"] | 0.0; weather.weatherCode = current["weather_code"] | 0; weather.windSpeed = current["wind_speed_10m"] | 0.0; weather.windDirection = current["wind_direction_10m"] | 0.0; weather.pressure = current["pressure_msl"] | 0.0; int isDay = current["is_day"] | 1; weather.isDay = (isDay == 1); // HOURLY JsonArray hourlyTime = doc["hourly"]["time"]; JsonArray hourlyTemp = doc["hourly"]["temperature_2m"]; JsonArray hourlyRain = doc["hourly"]["precipitation_probability"]; JsonArray hourlyCode = doc["hourly"]["weather_code"]; struct tm timeinfo; int currentHour = 0; if (getLocalTime(&timeinfo, 1000)) { currentHour = timeinfo.tm_hour; } for (int i = 0; i < 12; i++) { int index = currentHour + i; // Safety check if (index >= hourlyTemp.size()) { weather.hourlyTemp[i] = weather.temperature; weather.hourlyRainChance[i] = 0; weather.hourlyWeatherCode[i] = weather.weatherCode; weather.hourlyTime[i] = "--:--"; continue; } weather.hourlyTemp[i] = hourlyTemp[index] | weather.temperature; weather.hourlyRainChance[i] = hourlyRain[index] | 0; weather.hourlyWeatherCode[i] = hourlyCode[index] | weather.weatherCode; // Extract HH:MM // Example: // 2026-09-11T14:00 // // Result: // 14:00 String fullTime = hourlyTime[index] | ""; if (fullTime.length() >= 16) { weather.hourlyTime[i] = fullTime.substring(11, 16); } else { weather.hourlyTime[i] = "--:--"; } } weather.currentRainChance = weather.hourlyRainChance[0]; // DAILY JsonArray dailyMax = doc["daily"]["temperature_2m_max"]; JsonArray dailyMin = doc["daily"]["temperature_2m_min"]; JsonArray dailyRain = doc["daily"]["precipitation_probability_max"]; JsonArray dailyCode = doc["daily"]["weather_code"]; JsonArray dailyDate = doc["daily"]["time"]; JsonArray dailySunrise = doc["daily"]["sunrise"]; JsonArray dailySunset = doc["daily"]["sunset"]; JsonArray dailyUV = doc["daily"]["uv_index_max"]; for (int i = 0; i < 3; i++) { weather.dailyTempMax[i] = dailyMax[i] | 0.0; weather.dailyTempMin[i] = dailyMin[i] | 0.0; weather.dailyRainChance[i] = dailyRain[i] | 0; weather.dailyWeatherCode[i] = dailyCode[i] | 0; weather.dailyDate[i] = dailyDate[i] | ""; weather.dailyUV[i] = dailyUV[i] | 0.0; } if (dailySunrise.size() > 0) { weather.sunrise = dailySunrise[0] | ""; } if (dailySunset.size() > 0) { weather.sunset = dailySunset[0] | ""; } if (dailyUV.size() > 0) { weather.uvIndex = dailyUV[0] | 0.0; } weather.valid = true; return true; } // ===== FETCH WEATHER ===== bool fetchWeather() { if (WiFi.status() != WL_CONNECTED) { Serial.println("WIFI NOT CONNECTED"); apiOnline = false; screenDirty = true; return false; } Serial.println(); Serial.println("================================"); Serial.println("FETCHING WEATHER"); Serial.println("================================"); isFetching = true; // Loading screen is shown only once. drawLoadingScreen("FETCHING WEATHER..."); WiFiClientSecure client; // Prototype / testing mode. client.setInsecure(); HTTPClient http; String url = getWeatherURL(); Serial.println(url); if (!http.begin(client, url)) { Serial.println("HTTP BEGIN FAILED"); isFetching = false; apiOnline = false; screenDirty = true; return false; } http.setTimeout(15000); int httpCode = http.GET(); Serial.print("HTTP CODE: "); Serial.println(httpCode); if (httpCode <= 0) { Serial.print("HTTP ERROR: "); Serial.println(http.errorToString(httpCode)); http.end(); isFetching = false; apiOnline = false; screenDirty = true; return false; } if (httpCode != HTTP_CODE_OK) { Serial.println("API ERROR"); http.end(); isFetching = false; apiOnline = false; screenDirty = true; return false; } String payload = http.getString(); Serial.print("PAYLOAD SIZE: "); Serial.println(payload.length()); bool result = parseWeather(payload); http.end(); isFetching = false; if (result) { apiOnline = true; lastApiUpdate = millis(); Serial.println("WEATHER UPDATED"); Serial.print("Temperature: "); Serial.println(weather.temperature); Serial.print("Humidity: "); Serial.println(weather.humidity); Serial.print("Weather: "); Serial.println(weatherDescription(weather.weatherCode)); Serial.print("Wind: "); Serial.println(weather.windSpeed); Serial.print("Rain Chance: "); Serial.println(weather.currentRainChance); screenDirty = true; return true; } apiOnline = false; screenDirty = true; return false; } // ===== LOADING SCREEN ===== void drawLoadingScreen(String message) { tft.fillScreen(BLACK); drawCenteredText("SMART WEATHER", 48, 2, CYAN); drawCenteredText("STATION", 73, 2, WHITE); // Loading ring for (int i = 0; i < 12; i++) { float angle = i * 2.0 * PI / 12.0; int x = CENTER_X + cos(angle) * 42; int y = CENTER_Y + sin(angle) * 42; uint16_t color = (i == 0) ? CYAN : DARK_GRAY; tft.fillCircle(x, y, 4, color); } drawCenteredText(message, 182, 1, WHITE); } // ===== WIFI CONNECT ===== bool connectWiFi() { Serial.println(); Serial.println("CONNECTING TO WIFI..."); tft.fillScreen(BLACK); drawCenteredText("CONNECTING", 26, 2, CYAN); drawWifiIcon(CENTER_X, 95, 0); drawCenteredText("WI-FI", 145, 2, WHITE); drawCenteredText(WIFI_SSID, 178, 1, GRAY); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); unsigned long start = millis(); int iconFrame = 0; while (WiFi.status() != WL_CONNECTED && millis() - start < 15000) { drawWifiIcon(CENTER_X, 95, iconFrame % 4); iconFrame++; delay(250); Serial.print("."); } Serial.println(); if (WiFi.status() == WL_CONNECTED) { wifiOnline = true; wifiConnecting = false; drawWifiIcon(CENTER_X, 95, 3); Serial.println("WIFI CONNECTED"); Serial.print("IP: "); Serial.println(WiFi.localIP()); Serial.print("RSSI: "); Serial.print(WiFi.RSSI()); Serial.println(" dBm"); return true; } wifiOnline = false; Serial.println("WIFI CONNECTION FAILED"); return false; } // ===== START WIFI RECONNECT ===== void startWiFiReconnect() { Serial.println("START WIFI RECONNECT"); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); wifiConnecting = true; wifiConnectStart = millis(); } // ===== HANDLE WIFI ===== void handleWiFi() { wl_status_t status = WiFi.status(); // Connected if (status == WL_CONNECTED) { if (!wifiOnline) { Serial.println("WIFI CONNECTED"); screenDirty = true; } wifiOnline = true; wifiConnecting = false; return; } wifiOnline = false; // Currently connecting if (wifiConnecting) { if (millis() - wifiConnectStart > WIFI_CONNECT_TIMEOUT) { Serial.println("WIFI CONNECT TIMEOUT"); WiFi.disconnect(true); wifiConnecting = false; lastWifiRetry = millis(); } return; } // Start another attempt if (millis() - lastWifiRetry >= WIFI_RETRY_INTERVAL) { lastWifiRetry = millis(); startWiFiReconnect(); } } // ===== NTP TIME ===== void setupTime() { // The offset comes from USER CONFIGURATION. configTime( GMT_OFFSET_SEC, DST_OFFSET_SEC, "pool.ntp.org", "time.nist.gov", "time.google.com" ); Serial.println("SYNCING TIME..."); struct tm timeinfo; if (getLocalTime(&timeinfo, 10000)) { Serial.println("TIME SYNCED"); Serial.println(&timeinfo, "%Y-%m-%d %H:%M:%S"); } else { Serial.println("TIME SYNC FAILED"); } } // ===== BUTTON HANDLER ===== void handleButton() { button.loop(); // MUST call the loop() function first // The moment the button goes down, remember when it happened. if (button.isPressed()) buttonPressStart = millis(); // The action is chosen when the button comes back up. if (button.isReleased()) { unsigned long duration = millis() - buttonPressStart; if (duration >= VERY_LONG_PRESS_TIME) { // VERY LONG PRESS -> back to the home page currentPage = PAGE_HOME; screenDirty = true; Serial.println("VERY LONG PRESS -> HOME"); } else if (duration >= LONG_PRESS_TIME) { // LONG PRESS -> ask the weather API for fresh data pendingRefresh = true; Serial.println("LONG PRESS -> REFRESH"); } else { // SHORT PRESS -> next page currentPage = (Page)((currentPage + 1) % PAGE_COUNT); screenDirty = true; Serial.print("SHORT PRESS -> PAGE "); Serial.println(currentPage + 1); } } } // ===== PROCESS MANUAL REFRESH ===== void handleManualRefresh() { if (!pendingRefresh) return; pendingRefresh = false; fetchWeather(); } // ===== AUTO API UPDATE ===== void handleApiUpdate() { if (!weather.valid) { return; } if (millis() - lastApiUpdate >= API_UPDATE_INTERVAL) { fetchWeather(); } } // ===== CLOCK ===== String getTimeString() { struct tm timeinfo; if (!getLocalTime(&timeinfo, 10)) { return "--:--"; } char buffer[10]; strftime(buffer, sizeof(buffer), "%H:%M", &timeinfo); return String(buffer); } // ===== HANDLE CLOCK ===== void handleClock() { if (currentPage != PAGE_HOME) { return; } if (millis() - lastClockUpdate < CLOCK_UPDATE_INTERVAL) { return; } lastClockUpdate = millis(); if (weather.valid && !isFetching) { drawHomeClock(); } } // ===== SETUP ===== void setup() { Serial.begin(115200); delay(500); Serial.println(); Serial.println("================================"); Serial.println("SMART WEATHER STATION"); Serial.println(LOCATION_NAME); Serial.println("================================"); // BUTTON // ezButton already set the pin to INPUT_PULLUP in its constructor. button.setDebounceTime(BUTTON_DEBOUNCE); // TFT tft.begin(); tft.setRotation(2); tft.fillScreen(BLACK); // BOOT SCREEN drawCenteredText("SMART WEATHER", 65, 2, CYAN); drawCenteredText("STATION", 92, 2, WHITE); drawCenteredText(LOCATION_NAME, 135, 2, YELLOW); delay(1500); // WIFI if (connectWiFi()) { setupTime(); delay(500); fetchWeather(); } else { tft.fillScreen(BLACK); drawCenteredText("WIFI ERROR", 82, 2, RED); drawCenteredText("CHECK SETTINGS", 120, 1, WHITE); drawCenteredText("RETRYING...", 150, 1, GRAY); delay(1500); } screenDirty = true; } // ===== LOOP ===== void loop() { // Input handleButton(); // WiFi handleWiFi(); // Manual refresh handleManualRefresh(); // Automatic API update handleApiUpdate(); // Clock handleClock(); // Static page rendering if (screenDirty && !isFetching) { renderPage(); screenDirty = false; } // Rain animation updateRainAnimation(); }

Detailed Instructions

Follow these steps in order. Most problems come from a skipped library or a wrong WiFi name.

  1. New to the ESP32 S3 Uno? Follow ESP32 S3 Uno - Getting Started first.
  2. Wire it up as shown in the diagram.
  3. Connect the board to your computer with a USB Type-C cable.
  4. Open Arduino IDE, choose the ESP32S3 Dev Module board and the correct COM port.
  5. Open the Library Manager with the Libraries icon on the left bar of the Arduino IDE.
  6. Install the display library. Search “DIYables TFT Round”, find the DIYables_TFT_Round library by DIYables, and click Install.
  • Search for DIYables TFT Round created by DIYables.io and click the Install button.
∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Library Manager
Type:
All
Topic:
All
DIYables TFT Round by DIYables.io
This library is designed for the DIYables 1.28-inch Round Circular TFT LCD Display Module and is compatible with a wide range of hardware platforms, including Uno R3, Uno R4 WiFi/Minima, Mega, Giga, Due, ESP32, ESP8266, and more. More info
1.1.0
INSTALL
Newbiely.ino
···
1 void setup() {
Output
Serial Monitor
Ln 1, Col 1
ESP32S3 Dev Module on COM15
1
  1. Install the dependencies. When a window asks about other libraries, click Install All.
  2. Install ArduinoJson. Search “ArduinoJson”, find the library by Benoit Blanchon, and click Install.
  3. Install ezButton. Search “ezButton”, find the library by ArduinoGetStarted, and click Install.
  4. Copy the code above and paste it into the Arduino IDE.
  5. Enter your WiFi details. Change WIFI_SSID and WIFI_PASSWORD to your own WiFi name and password.
  6. Set your city. Change LATITUDE, LONGITUDE, LOCATION_NAME, TIMEZONE and GMT_OFFSET_SEC to match your city. See the section below.
  7. Upload the code by clicking the Upload button.
  8. Open the Serial Monitor and set the baud rate to 115200.
  9. Watch the screen. You see the boot screen, then the WiFi screen, then the first weather page.
  10. Press the button to go to the next page, and check the Serial Monitor.
  11. Tip: if the upload fails with a "sketch too big" error, open Tools > Partition Scheme and pick Huge APP.
∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Newbiely.ino
···
8 Serial.println("Hello World!");
Output
Serial Monitor
Message (Enter to send message to 'ESP32S3 Dev Module' on 'COM15')
New Line
9600 baud
================================ SMART WEATHER STATION SEOUL ================================ CONNECTING TO WIFI... ...... WIFI CONNECTED IP: 192.168.1.113 RSSI: -52 dBm SYNCING TIME... TIME SYNCED 2026-09-22 09:15:42 ================================ FETCHING WEATHER ================================ https://api.open-meteo.com/v1/forecast?latitude=37.5665&longitude=126.9780¤t=... HTTP CODE: 200 PAYLOAD SIZE: 5188 WEATHER UPDATED Temperature: 21.80 Humidity: 68.00 Weather: OVERCAST Wind: 13.70 Rain Chance: 39 SHORT PRESS -> PAGE 2 SHORT PRESS -> PAGE 3 LONG PRESS -> REFRESH VERY LONG PRESS -> HOME
Ln 11, Col 1
ESP32S3 Dev Module on COM15
2

One Button, Three Actions

A single button keeps the build small, but it still needs to do several jobs. The sketch solves this by measuring how long you hold the button down.

Hold time Action
Under 0.6 second Go to the next page, back to HOME after the last one
0.6 to 2 seconds Ask the weather API for new data right away
Over 2 seconds Jump straight back to the HOME page

The ezButton library handles the contact bounce for you, so one press never counts twice. When you create the object, the library sets the pin to INPUT_PULLUP by itself.

ezButton button(PIN_BUTTON);

You set the debounce time once in setup().

button.setDebounceTime(40);

In the loop, button.loop() must run first. Then the code reads the press and release events. The time between them decides the action.

button.loop(); // MUST call the loop() function first if (button.isPressed()) buttonPressStart = millis(); if (button.isReleased()) { unsigned long duration = millis() - buttonPressStart; // duration decides the action }

To learn more about bounce and how to fix it, see the ESP32 S3 Uno - Button - Debounce tutorial.

Setting Your Own City

The sketch comes set to Seoul, South Korea. To show the weather for your home, you only need to edit five lines near the top of the code.

#define LATITUDE 37.5665 #define LONGITUDE 126.9780 #define LOCATION_NAME "SEOUL" #define TIMEZONE "Asia%2FSeoul" #define GMT_OFFSET_SEC (9 * 3600)

LATITUDE and LONGITUDE

These set the position of your city. Any map website shows them.

LOCATION_NAME

This is the label on the screen. A short name fits the round display better.

TIMEZONE

This value goes into a web address, so you must write the / character as %2F. For example, Europe/Berlin becomes Europe%2FBerlin.

GMT_OFFSET_SEC and DST_OFFSET_SEC

GMT_OFFSET_SEC sets the NTP clock. Berlin in winter is UTC+1, so you write (1 * 3600). DST_OFFSET_SEC adds summer time. Set it to 3600 where summer time applies, or leave it at 0.

※ NOTE THAT:

The values come in Celsius and km/h. To use Fahrenheit and mph, change the temperature_unit and wind_speed_unit parameters inside getWeatherURL() to fahrenheit and mph.

Code Explanation

The full sketch has comments on almost every line. Read them in the code above to see how each part works.

Video Tutorial

Watch the video below to see this ESP32 S3 Uno project step by step.

FAQ

Do I need a temperature or humidity sensor for this ESP32 S3 Uno weather station?

No. The ESP32 S3 Uno gets all the data from the Open-Meteo weather API over WiFi. The values are for your city, not for your room. If you want indoor readings too, you can add a sensor such as a DHT22 later.

Does the Open-Meteo API need an API key?

No. Open-Meteo is free for personal use and needs no sign-up or key. You only set the latitude, longitude and time zone in the code.

Can I use a different pin for the button or the display?

The button and the DC, CS and RST pins can move to other free pins. Change PIN_BUTTON, PIN_DC, PIN_CS and PIN_RST in the code to match. The SCL and SDA display wires must stay on D13 (GPIO12) and D11 (GPIO11), because they are the hardware SPI pins of the ESP32 S3 Uno.

Can I power the button module from 5V like on the Arduino UNO R4?

No. The Arduino UNO R4 uses 5V logic, but ESP32 S3 Uno pins work at 3.3V and are NOT 5V tolerant. A button module powered from 5V sends 5V into D2 when you press it. Power it from the 3.3V pin instead. The display can stay on 5V, because no signal from it goes back to the board.

What if my project needs more pins than the Uno headers give?

The ESP32 S3 Uno has two extra rows of holes with more GPIOs. GPIO15 and GPIO16 support I/O, PWM and analog. GPIO45 and GPIO35 to GPIO42 support I/O and PWM. GPIO47 and GPIO48 are output only, with PWM. Solder pin headers to use them and write the GPIO number in code. For this station, a second button or a buzzer fits well on GPIO15, GPIO16 or GPIO35 to GPIO42. Do not use GPIO47 or GPIO48 for button inputs. Be careful with two things. First, GPIO47 and GPIO48 may run at 1.8V instead of 3.3V on some modules (especially ones with "V" in the name, like R8V or R16V), so 3.3V devices can be damaged or act strangely. Second, GPIO0, GPIO3 (D6), GPIO45 and GPIO46 (D9) are boot strapping pins, and a wrong connection can stop the board from booting or uploading code. Use the other extra pins first.

Why does the screen show the right weather but the wrong time?

The clock comes from NTP, not from the weather answer. Check GMT_OFFSET_SEC and DST_OFFSET_SEC for your city. Also make sure TIMEZONE matches, so the forecast hours line up with your local time.

Troubleshooting

Problem Possible Cause Solution
No COM port appears or the upload fails Missing USB driver or the board is not in download mode Install the CP210x or CH340 driver, or hold BOOT while you press RESET, then upload again
The display stays black SCL or SDA is on the wrong pin, or VCC/GND is loose Put SCL on D13 (GPIO12) and SDA on D11 (GPIO11), then check the power wires
Board will not boot or upload with the display connected D9 (GPIO46) is a boot strapping pin Unplug the DC wire during upload, then connect it again
Stuck on the WiFi screen Wrong WiFi name or password, or a 5 GHz network Check WIFI_SSID and WIFI_PASSWORD and use a 2.4 GHz network
HTTP CODE is negative The HTTPS connection failed Make sure the WiFi has Internet access and is not a captive portal network
The clock shows --.-- The NTP sync did not finish Check that your network does not block UDP port 123, then restart the board
Button does nothing or pages jump by themselves Wrong button option in code, or button module on 5V Use EXTERNAL_PULLDOWN only with the module, and power the module from 3.3V
Sketch too big error The default partition is too small for this sketch In Tools > Partition Scheme, choose Huge APP

※ OUR MESSAGES

  • As freelancers, We are AVAILABLE for HIRE. See how to outsource your project to us
  • Please feel free to share the link of this tutorial. However, Please do not use our content on any other websites. We invested a lot of effort and time to create the content, please respect our work!