ESP32 - Intelligente Wetterstation

Dieses Tutorial zeigt Ihnen, wie Sie einen ESP32 und ein rundes 1,28-Zoll-TFT-LCD-Display in eine intelligente Wetterstation verwandeln, die Live-Wetterdaten aus dem Internet statt von einem Sensor liest.

Das werden Sie lernen:

ESP32 intelligente Wetterstation mit rundem TFT-LCD-Display

Da der ESP32 WLAN integriert hat, wird kein zusätzliches Netzwerkmodul benötigt. Die Wetterdaten stammen von einem Webdienst, daher kann die Station auch Werte anzeigen, die ein günstiger Sensor nicht messen kann, etwa die Regenwahrscheinlichkeit für die nächsten Stunden und die Vorhersage für die nächsten drei Tage.

Erforderliche Hardware

1×ESP32 Development Board
1×Micro-USB-Kabel
1×1.28 Inch Round Circular TFT LCD Display Module
1×Push Button
1×Button Module Alternativ:
1×Breadboard
1×Verbindungskabel

Only one of the two button parts is required. Both options are wired below.

1×(Empfohlen) Schraubklemmen-Erweiterungsboard für ESP32
1×(Empfohlen) Breakout Expansion Board for ESP32
1×(Empfohlen) Stromverteiler für ESP32

Oder Sie können die folgenden Kits kaufen:

1×DIYables ESP32 Starter-Kit (ESP32 enthalten)
1×DIYables Sensor-Kit (18 Sensoren/Displays)
Offenlegung: Einige der in diesem Abschnitt bereitgestellten Links sind Amazon-Affiliate-Links. Wir können eine Provision für Käufe erhalten, die über diese Links getätigt werden, ohne zusätzliche Kosten für Sie. Wir schätzen Ihre Unterstützung.

Über die Seiten der Wetterstation

Die Station enthält acht Seiten. Ein einziger Taster schaltet durch sie hindurch:

Seite Was sie anzeigt
HOME Wettersymbol, Temperatur, Himmelsbeschreibung und eine NTP-Uhr
TEMP Temperatur und die gefühlte Temperatur („feels like“)
HUMIDITY Luftfeuchtigkeit als Ring dargestellt plus der genaue Prozentwert
WIND Windgeschwindigkeit und Windrichtung auf einer Kompassrose
RAIN Regenwahrscheinlichkeit mit animiertem Regeneffekt
NEXT HOURS Temperatur und Regenwahrscheinlichkeit Stunde für Stunde
FORECAST Höchst- und Tiefsttemperatur für die nächsten 3 Tage
SYSTEM WLAN-Status, Signalstärke, API-Status und Standort

Der ESP32 aktualisiert die Wetterdaten alle 10 Minuten. Zwischen zwei Aktualisierungen läuft die Uhr auf der Startseite weiter, da der ESP32 nach der NTP-Synchronisierung seine eigene Zeit hält.

Über die Open-Meteo-Wetter-API

Open-Meteo liefert die Wetterdaten. Sie eignet sich gut für ein ESP32-Projekt:

Open-Meteo
Der API-Schlüssel nicht erforderlich, keine Registrierung
Die Kosten kostenlos für die private Nutzung
Das Antwortformat einfaches JSON
Der Ort über Breiten- und Längengrad gewählt
Das Protokoll HTTPS auf Port 443

Eine einzige Anfrage liefert das aktuelle Wetter, die stündlichen Werte für die nächsten Tage und eine dreitägige Tageszusammenfassung. Der Parameter timezone sorgt dafür, dass der Dienst lokale Zeiten statt UTC zurückgibt.

Verdrahtungsdiagramm

Das Display kommuniziert über den standardmäßigen Hardware-SPI-Bus des ESP32, daher sind GPIO18 und GPIO23 fest vorgegeben.

TFT-LCD-Pin ESP32 Beschreibung
VCC 3.3V Stromversorgung
GND GND Masse
SCL GPIO18 SPI-Takt (fest)
SDA GPIO23 SPI MOSI (fest)
DC GPIO25 Daten/Befehl
CS GPIO26 Chip Select
RST GPIO27 Reset

Der Tastereingang an GPIO16 akzeptiert entweder einen einfachen Taster oder ein fertiges Tastermodul. Verdrahten Sie das Bauteil, das Sie besitzen, und passen Sie dann den Code daran an.

※ Notiz:

GPIO16 ist auf dem hier verwendeten ESP32-WROOM-32-Board ein einfacher Eingangspin. Er ist kein Strapping-Pin und besitzt einen funktionierenden internen Pull-up.

Eine Ausnahme sollten Sie kennen: Bei ESP32-WROVER-Modulen, die PSRAM besitzen, sind GPIO16 und GPIO17 mit dem PSRAM-Chip verbunden und können nicht verwendet werden. Wenn Ihr Board PSRAM hat, verlegen Sie den Taster auf einen anderen freien Pin, zum Beispiel GPIO22, und ändern Sie PIN_BUTTON im Code entsprechend.

Option 1: Einfacher Taster

Ein Taster hat keinen eigenen Widerstand. Ein Bein kommt an GPIO16, das andere an Masse, und der ESP32 stellt den Pull-up intern bereit. Der Pin liegt daher auf HIGH und fällt auf LOW, solange der Taster gedrückt wird.

ESP32 intelligente Wetterstation Schaltplan mit Taster

Dieses Bild wurde mit Fritzing erstellt. Klicken Sie, um das Bild zu vergrößern.

Wenn Sie nicht wissen, wie Sie ESP32 und andere Komponenten mit Strom versorgen, finden Sie Anleitungen im folgenden Tutorial: Wie man ESP32 mit Strom versorgt.

Taster-Pin ESP32 Beschreibung
Pin 1 GPIO16 Tastereingang, interner Pull-up
Pin 2 GND Masse

Der Sketch ist ab Werk für diese Option konfiguriert:

ezButton button(PIN_BUTTON);

Option 2: Tastermodul

Ein Tastermodul hat einen Pull-down-Widerstand auf der eigenen Platine und benötigt eine Versorgungsspannung. Sein OUT-Pin liegt in Ruhe auf LOW und steigt beim Drücken auf HIGH, also genau umgekehrt wie beim einfachen Taster.

ESP32 intelligente Wetterstation Schaltplan mit Tastermodul

Dieses Bild wurde mit Fritzing erstellt. Klicken Sie, um das Bild zu vergrößern.

Tastermodul-Pin ESP32 Beschreibung
VCC 3.3V Stromversorgung
GND GND Masse
OUT GPIO16 Signal, LOW in Ruhe, HIGH beim Drücken

Da das Modul seinen eigenen Widerstand mitbringt, muss der interne ausgeschaltet bleiben. Eine Zeile ändert sich:

ezButton button(PIN_BUTTON, EXTERNAL_PULLDOWN);

※ Notiz:

Versorgen Sie das Display mit 3,3V. Das GC9A01-Modul verträgt auf vielen Platinen 5V, aber 3,3V entspricht dem Logikpegel des ESP32 und ist die sicherere Wahl.

Wie man den ESP32 für die Wetterstation programmiert

Binden Sie die Bibliotheken ein. WiFi.h, WiFiClientSecure.h, HTTPClient.h und time.h sind alle im ESP32-Core enthalten, daher kommen nur ArduinoJson, DIYables_TFT_Round und ezButton aus dem Bibliotheksverwalter.

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

Erstellen Sie das Display-Objekt mit den Pins für Reset, Daten/Befehl und Chip Select.

DIYables_TFT_GC9A01_Round tft(27, 25, 26);

Tragen Sie die WLAN-Zugangsdaten und den Ort ein, den Sie beobachten möchten.

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

Verbinden Sie sich mit dem Netzwerk.

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

Synchronisieren Sie die Uhr per NTP. GMT_OFFSET_SEC enthält die Abweichung von UTC in Sekunden.

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

Öffnen Sie eine sichere Verbindung und senden Sie die Anfrage. setInsecure() überspringt die Zertifikatsprüfung, was den Sketch für einen Prototyp kurz hält.

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

Übergeben Sie die Antwort an ArduinoJson und lesen Sie einen Wert aus.

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

Zeichnen Sie ihn auf den runden Bildschirm.

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

ESP32-Code - Intelligente Wetterstation

/* * Dieser ESP32 Code wurde von newbiely.de entwickelt * Dieser ESP32 Code wird der Öffentlichkeit ohne jegliche Einschränkung zur Verfügung gestellt. * Für vollständige Anleitungen und Schaltpläne besuchen Sie bitte: * https://newbiely.de/tutorials/esp32/esp32-smart-weather-station */ /* ============================================================ SMART WEATHER STATION ESP32 + 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 VCC -> 3.3V GND -> GND SCL -> GPIO18 (hardware SPI clock, fixed) SDA -> GPIO23 (hardware SPI MOSI, fixed) RST -> GPIO27 DC -> GPIO25 CS -> GPIO26 Button GPIO16 -> 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 (GPIO18) and MOSI (GPIO23) are the hardware SPI pins of // the ESP32. The library uses them on its own, so they are not // listed here. #define PIN_BUTTON 16 #define PIN_RST 27 #define PIN_DC 25 #define PIN_CS 26 // ===== 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(); }

Schnelle Schritte

  • Wenn Sie den ESP32 zum ersten Mal verwenden, sehen Sie sich an, wie man die Umgebung für den ESP32 in der Arduino IDE einrichtet.
  • Schließen Sie das runde TFT-LCD-Display und den Taster gemäß dem bereitgestellten Schaltplan an den ESP32 an.
  • Verbinden Sie das ESP32-Board über ein USB-Kabel mit Ihrem PC
  • Öffnen Sie die Arduino IDE auf Ihrem PC.
  • Wählen Sie das richtige ESP32-Board (z. B. ESP32 Dev Module) und den COM-Port aus.
  • Klicken Sie auf das Symbol Libraries in der linken Leiste der Arduino IDE.
  • Suchen Sie nach “DIYables TFT Round” und finden Sie dann die Bibliothek DIYables_TFT_Round von DIYables.
  • Klicken Sie auf die Schaltfläche Install, um die Bibliothek zu installieren.
ESP32 TFT-LCD Bibliotheksinstallation
  • Ein Fenster erscheint und fordert Sie auf, Abhängigkeiten zu installieren.
  • Klicken Sie auf die Schaltfläche Install All, um alle Bibliotheksabhängigkeiten zu installieren.
ESP32 TFT-LCD Installation der Abhängigkeiten
  • Geben Sie “ArduinoJson” in das Suchfeld ein, suchen Sie die ArduinoJson-Bibliothek von Benoit Blanchon und klicken Sie dann auf Install.
  • Geben Sie “ezButton” in das Suchfeld ein, suchen Sie die ezButton-Bibliothek von ArduinoGetStarted und klicken Sie dann auf Install.
  • Kopieren Sie den obigen Code und fügen Sie ihn in den Editor der Arduino IDE ein.
  • Ersetzen Sie WIFI_SSID und WIFI_PASSWORD durch Ihren eigenen WLAN-Namen und Ihr Passwort.
  • Ersetzen Sie LATITUDE, LONGITUDE, LOCATION_NAME, TIMEZONE und GMT_OFFSET_SEC durch die Werte Ihrer eigenen Stadt.
  • Klicken Sie in der Arduino IDE auf die Schaltfläche Upload, um den Code auf den ESP32 hochzuladen.
  • Öffnen Sie den Seriellen Monitor und stellen Sie die Baudrate auf 115200 ein.
  • Beobachten Sie den Startbildschirm, dann den WLAN-Bildschirm und dann die erste Wetterseite.
  • Drücken Sie den Taster, um zur nächsten Seite zu wechseln.
  • Prüfen Sie das Ergebnis im Seriellen Monitor.
∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
ESP32 Dev Module
Newbiely.ino
···
8 Serial.println("Hello World!");
Output
Serial Monitor
Message (Enter to send message to 'ESP32 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
ESP32 Dev Module on COM15
2

Ein Taster, drei Aktionen

Der Sketch misst, wie lange der Taster gedrückt bleibt, und wählt daraus eine Aktion:

Haltedauer Aktion
Unter 0,6 Sekunden Zur nächsten Seite wechseln, nach der letzten zurück zu HOME
0,6 bis 2 Sekunden Sofort neue Daten von der Wetter-API anfordern
Über 2 Sekunden Direkt zur Seite HOME zurückspringen

Der Taster wird von der ezButton-Bibliothek verarbeitet, die sich um das Kontaktprellen kümmert, sodass ein einzelner Druck nie doppelt registriert wird. Das Objekt legt den Pin fest, und die Bibliothek setzt ihn selbstständig auf INPUT_PULLUP.

ezButton button(PIN_BUTTON);

Das Entprellzeitfenster wird einmalig in setup() festgelegt.

button.setDebounceTime(40);

Innerhalb der Schleife läuft zuerst button.loop(), dann werden die Drück- und Loslass-Ereignisse ausgelesen. Die Zeitspanne dazwischen ist die Haltedauer, die die Aktion bestimmt.

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 }

Das Tutorial ESP32 - Button erklärt das Prinzip des Entprellens ausführlicher.

Ihre eigene Stadt einstellen

Seoul ist der im Sketch voreingestellte Ort. Fünf Zeilen weit oben steuern ihn:

#define LATITUDE 37.5665 #define LONGITUDE 126.9780 #define LOCATION_NAME "SEOUL" #define TIMEZONE "Asia%2FSeoul" #define GMT_OFFSET_SEC (9 * 3600)
  • LATITUDE und LONGITUDE zeigen auf Ihre Stadt. Jede Kartenwebsite liefert Ihnen diese Werte.
  • LOCATION_NAME ist die auf dem Bildschirm angezeigte Beschriftung. Ein kurzer Name passt besser auf das runde Display.
  • TIMEZONE wird in eine Webadresse eingefügt, daher muss das Zeichen / als %2F geschrieben werden. Aus Europe/Berlin wird Europe%2FBerlin.
  • GMT_OFFSET_SEC speist die NTP-Uhr. Berlin ist im Winter UTC+1, also (1 * 3600).
  • DST_OFFSET_SEC fügt dort, wo sie gilt, die Sommerzeit hinzu. Verwenden Sie in diesem Fall 3600, andernfalls lassen Sie den Wert bei 0.

※ Notiz:

Die Messwerte kommen in Celsius und km/h. Ändern Sie die Parameter temperature_unit und wind_speed_unit innerhalb von getWeatherURL() auf fahrenheit und mph, wenn Sie imperiale Einheiten bevorzugen.

Code-Erklärung

Sehen Sie sich die Erklärungen in den Kommentaren des Quellcodes zu jeder Zeile an!

Fehlerbehebung

  • Auf dem Display erscheint nichts. Überprüfen Sie erneut SCL an GPIO18 und SDA an GPIO23. Diese beiden gehören zum Hardware-SPI-Bus und können nicht verlegt werden.
  • Der Sketch bleibt auf dem WLAN-Bildschirm hängen. Der ESP32 verbindet sich nur mit 2,4-GHz-Netzwerken. Ein reines 5-GHz-Netzwerk wird sich nie verbinden.
  • HTTP CODE ist negativ. Die sichere Verbindung ist fehlgeschlagen. Vergewissern Sie sich, dass der ESP32 Internetzugang hat, und beachten Sie, dass ein WLAN mit Captive Portal den API-Aufruf blockiert.
  • Die Uhr zeigt --:-- an. Die NTP-Synchronisierung wurde nicht abgeschlossen. Manche Netzwerke blockieren den UDP-Port 123, den NTP benötigt.
  • Der Sketch passt nicht in den Flash-Speicher. Öffnen Sie in der Arduino IDE Tools und wählen Sie ein Partitionsschema mit einem größeren Anwendungsbereich, zum Beispiel Huge APP.

Video Tutorial

Wir erwägen die Erstellung von Video-Tutorials. Wenn Sie Video-Tutorials für wichtig halten, abonnieren Sie bitte unseren YouTube-Kanal , um uns zu motivieren, die Videos zu erstellen.

※ UNSERE NACHRICHTEN

  • Sie können gerne den Link zu diesem Tutorial teilen. Bitte verwenden Sie jedoch unsere Inhalte nicht auf anderen Websites. Wir haben viel Mühe und Zeit in die Erstellung der Inhalte investiert, bitte respektieren Sie unsere Arbeit!