ESP8266 - Intelligente Wetterstation

Dieses Tutorial zeigt Ihnen, wie Sie mit dem ESP8266 eine intelligente Wetterstation bauen, die das Wetter von einem Online-Dienst abruft und auf einem 1,28 Zoll runden TFT-LCD-Display anzeigt. Im Einzelnen lernen wir:

Wir empfehlen:

ESP8266 NodeMCU intelligente Wetterstation mit rundem TFT-LCD-Display

Erforderliche Hardware

1×ESP8266 NodeMCU ESP-12F
1×Alternativ: ESP8266 D1 Mini NodeMCU ESP-12F
1×Alternativ: ESP8266 NodeMCU ESP-12E (Uno-form)
1×Alternativ: ESP8266 NodeMCU ESP-12F (SMD-form)
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

One button part is sufficient. Both the push button and the button module are wired in the section below.

1×(Empfohlen) Schraubklemmen-Erweiterungsboard für ESP8266

Oder Sie können die folgenden Kits kaufen:

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 intelligente Wetterstation

Diese Wetterstation hat keinen Temperatursensor. Der ESP8266 verbindet sich mit Ihrem WLAN, fragt bei einem Wetterdienst die Werte einer gewählten Stadt ab und zeichnet die Antwort auf den runden Bildschirm. Da die Daten von einem Webdienst kommen, kann die Station auch Werte anzeigen, die ein kleiner Sensor nicht messen kann, etwa die Regenwahrscheinlichkeit für die kommenden Stunden und eine Vorhersage für die nächsten Tage.

Die Oberfläche ist in acht Seiten aufgeteilt, und ein einziger Taster schaltet durch alle hindurch.

Seite Inhalt
HOME Wettersymbol, Temperatur, Himmelsbeschreibung, NTP-Uhr
TEMP Temperatur zusammen mit der gefühlten Temperatur
HUMIDITY Luftfeuchtigkeit als Ring und als Prozentwert
WIND Windgeschwindigkeit und Windrichtung auf einem Kompass
RAIN Regenwahrscheinlichkeit mit animiertem Regeneffekt
NEXT HOURS Temperatur und Regenwahrscheinlichkeit für die kommenden Stunden
FORECAST Höchst- und Tiefsttemperatur über 3 Tage
SYSTEM WLAN-Status, Signalstärke, API-Status, Standort

Die Speichergrenze des ESP8266

Der ESP8266 ist das am stärksten eingeschränkte Board, auf dem dieses Projekt laufen kann. Sein Heap beträgt nach dem Start des Sketches etwa 46 KB, und eine HTTPS-Verbindung allein beansprucht davon rund 20 KB für die BearSSL-Puffer. Zwei Entscheidungen im Code halten alles innerhalb dieses Budgets:

Entscheidung Wirkung
Nur 12 Vorhersagestunden anfordern Die Antwort schrumpft von etwa 5 KB auf etwa 1,8 KB
Einen ArduinoJson-Filter anwenden Die "..._units"-Blöcke werden vor dem Parsen verworfen
Den Payload nach dem Parsen freigeben Der Textpuffer kehrt vor dem Zeichnen in den Heap zurück
Den TLS-Client auf dem Heap erzeugen Der große Puffer verschwindet, sobald die Anfrage endet

※ Notiz:

Der Code gibt den freien Heap vor und nach jeder Anfrage aus. Beobachten Sie diese Zahlen im Serial Monitor. Fällt der Wert unter etwa 12 KB, verringern Sie die Vorhersagestunden oder die Anzahl der Tageseinträge.

Verdrahtungsdiagramm

Das GC9A01-Display wird über den Hardware-SPI-Bus des ESP8266 angesteuert, wobei D5 den Takt und D7 die Daten überträgt. Diese beiden Pins sind fest vorgegeben und dürfen nicht verlegt werden.

TFT-LCD-Pin ESP8266 Beschreibung
VCC 3.3V Stromversorgung
GND GND Masse
SCL D5 SPI-Takt, fester Pin
SDA D7 SPI MOSI, fester Pin
DC D2 Daten/Befehl
CS D8 Chip Select
RST D1 Reset

Der Taster wird an D6 gelesen. Dort kann entweder ein einfacher Taster oder ein Tastermodul angeschlossen werden; die Verdrahtung für beide ist unten angegeben.

ESP8266 - Verdrahtung des Tasters

Ein einfacher Taster hat keinen Widerstand. Er muss zwischen D6 und Masse angeschlossen werden, und der interne Pull-up-Widerstand des ESP8266 erledigt den Rest, sodass kein externer Widerstand erforderlich ist. Der Pin liegt im Ruhezustand auf HIGH und wird auf LOW gezogen, solange der Taster gedrückt wird.

Schaltplan ESP8266 NodeMCU intelligente Wetterstation mit Taster

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

Weitere Informationen finden Sie unter ESP8266-Pinbelegung und wie man ESP8266 und andere Komponenten mit Strom versorgt.

Taster-Pin ESP8266 Beschreibung
Pin 1 D6 Tastereingang mit internem Pull-up
Pin 2 GND Masse

Der mitgelieferte Sketch ist für diese Anordnung geschrieben.

ezButton button(PIN_BUTTON);

ESP8266 - Verdrahtung des Tastermoduls

Ein Tastermodul hat einen Pull-down-Widerstand auf der Platine und muss daher mit Strom versorgt werden. Sein OUT-Pin liegt auf LOW, solange der Taster nicht gedrückt ist, und wird auf HIGH gezogen, während er gedrückt wird – also umgekehrt wie beim einfachen Taster.

Schaltplan ESP8266 NodeMCU intelligente Wetterstation mit Tastermodul

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

Tastermodul-Pin ESP8266 Beschreibung
VCC 3.3V Stromversorgung
GND GND Masse
OUT D6 Signal, LOW in Ruhe, HIGH bei Druck

Der Widerstand auf dem Modul ersetzt den internen, daher muss der Modus im Konstruktor angegeben werden.

ezButton button(PIN_BUTTON, EXTERNAL_PULLDOWN);

※ Notiz:

Die auf ESP8266-Boards aufgedruckten Pin-Bezeichnungen unterscheiden sich je nach Hersteller. Die oben verwendeten D-Nummern sind die NodeMCU-Bezeichnungen. Vergleichen Sie sie vor dem Verdrahten immer mit den Bezeichnungen auf Ihrem eigenen Board. Schauen Sie genau hin!

Wie man den ESP8266 für die Wetterstation programmiert

Im ersten Schritt werden die Bibliotheken eingebunden. Der ESP8266-Core liefert die Netzwerk- und Zeit-Header, während ArduinoJson, DIYables_TFT_Round und ezButton über den Bibliotheksverwalter installiert werden.

#include <ESP8266WiFi.h> #include <ESP8266HTTPClient.h> #include <WiFiClientSecureBearSSL.h> #include <ArduinoJson.h> #include <time.h> #include <DIYables_TFT_Round.h> #include <ezButton.h>

Legen Sie die ESP8266-Pins fest, die mit dem Display verbunden sind, und deklarieren Sie dann das Display-Objekt.

#define PIN_RST D1 #define PIN_DC D2 #define PIN_CS D8 DIYables_TFT_GC9A01_Round tft(PIN_RST, PIN_DC, PIN_CS);

Legen Sie die Netzwerk-Zugangsdaten und den zu beobachtenden Ort fest.

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

Der ESP8266 muss mit dem Netzwerk verbunden sein, bevor eine Anfrage versucht wird.

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

Die Uhr wird über NTP synchronisiert. Eine korrekte Uhrzeit ist auch für HTTPS wichtig, weil jedes Zertifikat ein Anfangs- und ein Enddatum hat.

configTime(GMT_OFFSET_SEC, DST_OFFSET_SEC, "pool.ntp.org", "time.nist.gov");

Anders als der ESP32-Core bietet der ESP8266-Core keine Funktion getLocalTime(), daher muss die Systemuhr direkt gelesen werden.

time_t now = time(nullptr); struct tm timeinfo; localtime_r(&now, &timeinfo);

Der sichere Client wird auf dem Heap erzeugt. Sobald die Anfrage abgeschlossen ist, wird das Objekt zerstört und sein großer TLS-Puffer an den Heap zurückgegeben.

std::unique_ptr<BearSSL::WiFiClientSecure> client(new BearSSL::WiFiClientSecure); client->setInsecure(); HTTPClient http; http.begin(*client, url); int httpCode = http.GET(); String payload = http.getString();

Vor dem Parsen wird ein Filter deklariert. Nur die drei Blöcke, die tatsächlich gezeichnet werden, bleiben erhalten, wodurch das Dokument klein bleibt.

JsonDocument filter; filter["current"] = true; filter["hourly"] = true; filter["daily"] = true; JsonDocument doc; deserializeJson(doc, payload, DeserializationOption::Filter(filter));

Anschließend wird ein einzelner Wert ausgelesen und auf dem runden Display dargestellt.

float temperature = doc["current"]["temperature_2m"] | 0.0; tft.setTextSize(3); tft.setTextColor(DIYables_TFT::colorRGB(255, 255, 255)); tft.setCursor(80, 100); tft.print(temperature, 1);

ESP8266-Code für die intelligente Wetterstation

/* * Dieser ESP8266 NodeMCU Code wurde von newbiely.de entwickelt * Dieser ESP8266 NodeMCU 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/esp8266/esp8266-smart-weather-station */ /* ============================================================ SMART WEATHER STATION ESP8266 NodeMCU + 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 -> ESP8266 VCC -> 3.3V GND -> GND SCL -> D5 (hardware SPI clock, fixed) SDA -> D7 (hardware SPI MOSI, fixed) RST -> D1 DC -> D2 CS -> D8 Button D6 -> BUTTON -> GND ------------------------------------------------------------ BUTTON Short press -> Next page Long press -> Refresh weather Very long press -> Back to home page ------------------------------------------------------------ The ESP8266 has a small heap, and the HTTPS connection needs a large part of it. Two steps keep the sketch inside the free memory: 1. Only 12 hours of forecast are requested. 2. A JSON filter drops the parts that are not drawn. ============================================================ */ #include <ESP8266WiFi.h> #include <ESP8266HTTPClient.h> #include <WiFiClientSecureBearSSL.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 ===== // D5 (SCLK) and D7 (MOSI) are the hardware SPI pins of the // ESP8266. The library drives them on its own, so they are // not listed here. #define PIN_BUTTON D6 #define PIN_RST D1 #define PIN_DC D2 #define PIN_CS D8 // ===== 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"; // Only 12 hours are asked for. The answer then starts at the // current hour and stays near 1.8 KB, which matters on the // small heap of the ESP8266. url += "&forecast_hours=12"; url += "&temperature_unit=celsius"; url += "&wind_speed_unit=kmh"; return url; } // ===== LOCAL TIME HELPER ===== // The ESP8266 core has no getLocalTime() like the ESP32, so // the system clock is read directly here. bool readLocalTime(struct tm* info) { time_t now = time(nullptr); // Before the NTP answer arrives, the clock sits near 1970. if (now < 100000) { return false; } localtime_r(&now, info); return true; } // ===== PARSE WEATHER ===== bool parseWeather(const String& payload) { // A filter keeps only the three blocks that are drawn. // The "..._units" blocks are dropped, which saves a good // part of the ESP8266 heap. JsonDocument filter; filter["current"] = true; filter["hourly"] = true; filter["daily"] = true; JsonDocument doc; DeserializationError error = deserializeJson(doc, payload, DeserializationOption::Filter(filter)); 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 // "forecast_hours=12" makes the list begin at the current // hour, so index 0 is now and index 11 is 11 hours later. JsonArray hourlyTime = doc["hourly"]["time"]; JsonArray hourlyTemp = doc["hourly"]["temperature_2m"]; JsonArray hourlyRain = doc["hourly"]["precipitation_probability"]; JsonArray hourlyCode = doc["hourly"]["weather_code"]; for (int i = 0; i < 12; i++) { // Safety check if (i >= (int)hourlyTemp.size()) { weather.hourlyTemp[i] = weather.temperature; weather.hourlyRainChance[i] = 0; weather.hourlyWeatherCode[i] = weather.weatherCode; weather.hourlyTime[i] = "--:--"; continue; } weather.hourlyTemp[i] = hourlyTemp[i] | weather.temperature; weather.hourlyRainChance[i] = hourlyRain[i] | 0; weather.hourlyWeatherCode[i] = hourlyCode[i] | weather.weatherCode; // Extract HH:MM // Example: // 2026-09-22T14:00 // // Result: // 14:00 String fullTime = hourlyTime[i] | ""; 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("================================"); Serial.print("FREE HEAP BEFORE: "); Serial.println(ESP.getFreeHeap()); isFetching = true; // Loading screen is shown only once. drawLoadingScreen("FETCHING WEATHER..."); // BearSSL does the HTTPS part on the ESP8266. The object is // built on the heap and freed right after the request, so the // large TLS buffer does not stay in memory. std::unique_ptr<BearSSL::WiFiClientSecure> client(new BearSSL::WiFiClientSecure); // Prototype / testing mode: the server certificate is not // checked. It keeps the sketch short and saves memory. 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(); http.end(); Serial.print("PAYLOAD SIZE: "); Serial.println(payload.length()); bool result = parseWeather(payload); // The text is not needed any more, so the heap is given back // before the screen is drawn. payload = ""; 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); Serial.print("FREE HEAP AFTER: "); Serial.println(ESP.getFreeHeap()); 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 < WIFI_CONNECT_TIMEOUT) { 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 ===== // A correct clock also helps the HTTPS handshake, because a // certificate carries a start date and an end date. void setupTime() { configTime( GMT_OFFSET_SEC, DST_OFFSET_SEC, "pool.ntp.org", "time.nist.gov", "time.google.com" ); Serial.println("SYNCING TIME..."); struct tm timeinfo; unsigned long start = millis(); while (!readLocalTime(&timeinfo) && millis() - start < 10000) { delay(250); Serial.print("."); } Serial.println(); if (readLocalTime(&timeinfo)) { Serial.println("TIME SYNCED"); char buffer[32]; strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo); Serial.println(buffer); } 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 (!readLocalTime(&timeinfo)) { 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("================================"); Serial.print("FREE HEAP AT BOOT: "); Serial.println(ESP.getFreeHeap()); // 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

Folgen Sie diesen Anweisungen Schritt für Schritt:

  • Wenn Sie den ESP8266 zum ersten Mal verwenden, lesen Sie das Tutorial zur Einrichtung der Umgebung für den ESP8266 in der Arduino IDE.
  • Verdrahten Sie die Bauteile wie im Schaltplan gezeigt.
  • Verbinden Sie das ESP8266-Board über ein USB-Kabel mit Ihrem Computer.
  • Öffnen Sie die Arduino IDE auf Ihrem Computer.
  • Wählen Sie das richtige ESP8266-Board (z. B. NodeMCU 1.0 (ESP-12E Module)) und den COM-Port.
  • Klicken Sie auf das Symbol Libraries in der linken Leiste der Arduino IDE.
  • Suchen Sie nach “DIYables TFT Round” und finden Sie die Bibliothek DIYables_TFT_Round von DIYables.
  • Klicken Sie auf die Schaltfläche Install, um die Bibliothek zu installieren.
  • Sie müssen zusätzliche Bibliotheksabhängigkeiten installieren.
  • Klicken Sie auf die Schaltfläche Install All, um alle benötigten Bibliotheken zu installieren.
  • Suchen Sie nach “ArduinoJson” und finden Sie die Bibliothek ArduinoJson von Benoit Blanchon.
  • Klicken Sie auf die Schaltfläche Install, um die Bibliothek hinzuzufügen.
  • Suchen Sie nach “ezButton” und finden Sie die Bibliothek ezButton von ArduinoGetStarted.
  • Klicken Sie auf die Schaltfläche Install, um die Bibliothek hinzuzufügen.
  • Kopieren Sie den Code und öffnen Sie ihn in der Arduino IDE.
  • Ersetzen Sie WIFI_SSID und WIFI_PASSWORD durch Ihren eigenen Netzwerknamen 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 ESP8266 zu übertragen.
  • Öffnen Sie den Serial Monitor in der Arduino IDE und stellen Sie die Baudrate auf 115200 ein.
  • Warten Sie, bis der Startbildschirm, der WLAN-Bildschirm und dann das Wetter erscheinen.
  • Drücken Sie den Taster, um durch die Seiten zu blättern.
  • Prüfen Sie die Ergebnisse im Serial Monitor.
∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
Nodemcu 1.0 (ESP-12E Module)
Newbiely.ino
···
8 Serial.println("Hello World!");
Output
Serial Monitor
Message (Enter to send message to 'Nodemcu 1.0 (ESP-12E Module)' on 'COM15')
New Line
9600 baud
================================ SMART WEATHER STATION SEOUL ================================ FREE HEAP AT BOOT: 46512 CONNECTING TO WIFI... ....... WIFI CONNECTED IP: 192.168.1.58 RSSI: -66 dBm SYNCING TIME... .... TIME SYNCED 2026-09-22 18:05:31 ================================ FETCHING WEATHER ================================ FREE HEAP BEFORE: 44120 https://api.open-meteo.com/v1/forecast?latitude=37.5665&longitude=126.9780¤t=... HTTP CODE: 200 PAYLOAD SIZE: 1754 WEATHER UPDATED Temperature: 19.40 Humidity: 72.00 Weather: SHOWERS Wind: 16.50 Rain Chance: 54 FREE HEAP AFTER: 41864 SHORT PRESS -> PAGE 2 LONG PRESS -> REFRESH VERY LONG PRESS -> HOME
Ln 11, Col 1
Nodemcu 1.0 (ESP-12E Module) on COM15
2

Code-Erklärung

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

Wie der Taster gelesen wird

Ein einziger Taster führt drei verschiedene Aktionen aus. Die Dauer des Drückens entscheidet, welche davon ausgeführt wird.

Haltezeit 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 HOME-Seite zurückkehren

Der Taster wird von der ezButton-Bibliothek verwaltet. Das mechanische Prellen des Kontakts wird von der Bibliothek beseitigt, sodass ein einzelner Druck nie doppelt erkannt wird. Der Pin wird vom Konstruktor in den Modus INPUT_PULLUP versetzt, daher ist kein Aufruf von pinMode() erforderlich.

ezButton button(PIN_BUTTON);

Das Entprellfenster muss einmal in setup() festgelegt werden.

button.setDebounceTime(40);

In der Schleife muss button.loop() aufgerufen werden, bevor die Ereignisse ausgewertet werden. Der Abstand zwischen den beiden Ereignissen ergibt die Haltezeit.

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 ESP8266 - Taster erklärt die Entprelltechnik im Einzelnen.

Wie man den Standort ändert

Der Sketch ist auf Seoul, Südkorea, voreingestellt. Fünf Definitionen am Anfang des Codes legen den Ort fest:

#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 bestimmen die Stadt. Jede Karten-Website liefert diese Werte.
  • LOCATION_NAME ist die Bezeichnung, die auf dem Bildschirm angezeigt wird. Ein kurzer Name passt am besten zum runden Display.
  • TIMEZONE wird in eine Webadresse eingesetzt, daher muss das Zeichen / als %2F geschrieben werden. Europe/Berlin wird als Europe%2FBerlin geschrieben.
  • GMT_OFFSET_SEC wird von der NTP-Uhr verwendet. Berlin hat im Winter UTC+1, das ergibt (1 * 3600).
  • DST_OFFSET_SEC berücksichtigt die Sommerzeit. Es muss auf 3600 gesetzt werden, wo Sommerzeit gilt, andernfalls auf 0.

Zusätzliches Wissen

Es lohnt sich, den ESP8266 mit dem ESP32 zu vergleichen, bevor Sie ein Board für dieses Projekt wählen.

ESP8266 ESP32
Freier Heap für den Sketch etwa 46 KB etwa 280 KB
Angeforderte Vorhersagestunden 12, um Speicher zu sparen 72, die vollen drei Tage
JSON-Filter erforderlich optional
Funktion für die lokale Zeit von Hand geschrieben getLocalTime() wird bereitgestellt
CPU-Kerne 1 2
Typischer Preis niedriger höher

Es ist offensichtlich, dass der ESP32 für dieses Projekt deutlich komfortabler ist, weil der Speicherdruck vollständig wegfällt. Der ESP8266 bleibt dennoch eine leistungsfähige und günstigere Option, sofern die beiden oben beschriebenen Speichereinsparungen beibehalten werden.

Fehlerbehebung

  • Das Display bleibt schwarz. Die SCL- und SDA-Drähte müssen an D5 und D7 angeschlossen sein. Diese beiden Pins gehören zum Hardware-SPI-Bus und können nicht verlegt werden.
  • Das Board startet während der Anfrage neu. Der Heap ist erschöpft. Verringern Sie die Vorhersagestunden in getWeatherURL() oder fordern Sie zwei statt drei Tageseinträge an.
  • HTTP CODE ist negativ. Der sichere Handshake ist fehlgeschlagen. Prüfen Sie, ob die Zeit synchronisiert wurde, denn bei falscher Uhrzeit erscheint jedes Zertifikat ungültig.
  • Das WLAN verbindet sich nie. Der ESP8266 unterstützt nur 2,4-GHz-Netzwerke. Mit einem Netzwerk, das ausschließlich auf 5 GHz läuft, kann er sich nie verbinden.
  • Die Uhr zeigt --:-- an. Die NTP-Antwort ist nicht angekommen. In manchen Netzwerken ist der UDP-Port 123 gesperrt.

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!