ESP8266 - Station Météo Intelligente

Ce tutoriel vous explique comment utiliser l'ESP8266 pour construire une station météo intelligente qui récupère la météo depuis un service en ligne et l'affiche sur un écran TFT LCD rond de 1,28 pouce. En détail, nous allons apprendre :

Nous vous suggérons :

Station météo intelligente ESP8266 NodeMCU avec écran TFT LCD rond

Matériel Requis

1×ESP8266 NodeMCU ESP-12F
1×Alternativement: ESP8266 D1 Mini NodeMCU ESP-12F
1×Alternativement: ESP8266 NodeMCU ESP-12E (Uno-form)
1×Alternativement: ESP8266 NodeMCU ESP-12F (SMD-form)
1×Câble Micro USB
1×1.28 Inch Round Circular TFT LCD Display Module
1×Push Button
1×Button Module Alternativement:
1×Plaque d'essai
1×Fils de connexion

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

1×Recommandé: Carte d'extension à bornier à vis pour ESP8266

Ou vous pouvez acheter les kits suivants:

1×Kit de Capteurs DIYables (18 capteurs/écrans)
Divulgation : Certains des liens fournis dans cette section sont des liens affiliés Amazon. Nous pouvons recevoir une commission pour tout achat effectué via ces liens, sans coût supplémentaire pour vous. Nous vous remercions de votre soutien.

À Propos de la Station Météo Intelligente

Cette station météo ne comporte aucun capteur de température. L'ESP8266 rejoint votre réseau WiFi, demande à un service météo les valeurs d'une ville choisie et affiche la réponse sur l'écran rond. Comme les données proviennent d'un service web, la station peut aussi présenter des valeurs qu'un petit capteur est incapable de mesurer, comme la probabilité de pluie pour les prochaines heures et une prévision pour les jours suivants.

L'interface est divisée en huit pages, et un seul bouton-poussoir permet de les parcourir toutes.

Page Contenu
ACCUEIL Icône météo, température, description du ciel, horloge NTP
TEMP Température accompagnée de la température ressentie
HUMIDITÉ Humidité présentée sous forme d'anneau et de pourcentage
VENT Vitesse du vent et direction du vent sur une boussole
PLUIE Probabilité de pluie avec un effet de pluie animé
PROCHAINES HEURES Température et probabilité de pluie pour les prochaines heures
PRÉVISIONS Températures maximale et minimale sur 3 jours
SYSTÈME État du WiFi, force du signal, état de l'API, localisation

La Limite de Mémoire de l'ESP8266

L'ESP8266 est la carte la plus limitée capable de faire tourner ce projet. Son tas (heap) est d'environ 46 Ko une fois le sketch démarré, et une connexion HTTPS à elle seule en réclame environ 20 Ko pour les tampons BearSSL. Deux décisions dans le code permettent de tout faire tenir dans ce budget :

Décision Effet
Ne demander que 12 heures de prévision la réponse passe d'environ 5 Ko à environ 1,8 Ko
Appliquer un filtre ArduinoJson les blocs "..._units" sont supprimés avant l'analyse
Libérer le contenu après l'analyse le tampon de texte retourne au tas avant le dessin
Créer le client TLS sur le tas le grand tampon disparaît dès que la requête se termine

※ Note:

Le code affiche la mémoire libre du tas avant et après chaque requête. Surveillez ces valeurs dans le Moniteur Série. Si la valeur descend sous environ 12 Ko, réduisez le nombre d'heures de prévision ou le nombre d'entrées quotidiennes.

Schéma de Câblage

L'écran GC9A01 est piloté via le bus SPI matériel de l'ESP8266, où D5 transporte l'horloge et D7 transporte les données. Ces deux broches sont fixes et ne doivent pas être déplacées.

Broche TFT LCD ESP8266 Description
VCC 3.3V Alimentation
GND GND Masse
SCL D5 Horloge SPI, broche fixe
SDA D7 SPI MOSI, broche fixe
DC D2 Données/Commande
CS D8 Sélection de puce
RST D1 Réinitialisation

Le bouton est lu sur D6. Un simple bouton-poussoir ou un module bouton peut y être branché, et le câblage de chacun est donné ci-dessous.

ESP8266 - Câblage du Bouton-Poussoir

Un simple bouton-poussoir ne comporte aucune résistance. Il doit être connecté entre D6 et la masse, et la résistance de pull-up interne de l'ESP8266 s'occupe du reste, donc aucune résistance externe n'est nécessaire. La broche est au repos à HIGH et passe à LOW tant que le bouton est maintenu.

Schéma de câblage de la station météo intelligente ESP8266 NodeMCU avec bouton-poussoir

Cette image a été créée avec Fritzing. Cliquez pour agrandir l'image.

Pour plus d'informations, consultez Brochage ESP8266. et Comment alimenter l'ESP8266..

Broche du bouton ESP8266 Description
Broche 1 D6 Entrée du bouton avec pull-up interne
Broche 2 GND Masse

Le sketch fourni est écrit pour cette configuration.

ezButton button(PIN_BUTTON);

ESP8266 - Câblage du Module Bouton

Un module bouton comporte une résistance de pull-down sur la carte et doit donc être alimenté. Sa broche OUT est maintenue à LOW lorsque le bouton est relâché et passe à HIGH lorsqu'il est appuyé, ce qui est l'inverse du simple bouton.

Schéma de câblage de la station météo intelligente ESP8266 NodeMCU avec module bouton

Cette image a été créée avec Fritzing. Cliquez pour agrandir l'image.

Broche du module bouton ESP8266 Description
VCC 3.3V Alimentation
GND GND Masse
OUT D6 Signal, LOW au repos, HIGH lorsqu'appuyé

La résistance du module remplace la résistance interne, le mode doit donc être indiqué dans le constructeur.

ezButton button(PIN_BUTTON, EXTERNAL_PULLDOWN);

※ Note:

Les marquages des broches imprimés sur les cartes ESP8266 varient selon les fabricants. Les numéros D utilisés ci-dessus sont les marquages NodeMCU. Comparez-les toujours avec les marquages imprimés sur votre propre carte avant le câblage. Regardez attentivement !

Comment Programmer l'ESP8266 pour la Station Météo

La première étape consiste à inclure les bibliothèques. Le cœur ESP8266 fournit les en-têtes réseau et temps, tandis qu'ArduinoJson, DIYables_TFT_Round et ezButton sont installées depuis le Gestionnaire de bibliothèques.

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

Indiquez les broches de l'ESP8266 connectées à l'écran, puis déclarez l'objet écran.

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

Indiquez les identifiants du réseau et le lieu à observer.

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

L'ESP8266 doit être connecté au réseau avant toute tentative de requête.

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

L'horloge est synchronisée via NTP. Une horloge correcte est également importante pour le HTTPS, car chaque certificat comporte une date de début et une date de fin.

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

Le cœur ESP8266 ne fournit pas de fonction getLocalTime(), contrairement au cœur ESP32, donc l'horloge système doit être lue directement.

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

Le client sécurisé est créé sur le tas. Une fois la requête terminée, l'objet est détruit et son grand tampon TLS est rendu au tas.

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();

Un filtre est déclaré avant l'analyse. Seuls les trois blocs réellement affichés sont conservés, ce qui garde le document petit.

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

Une valeur est ensuite extraite et affichée sur l'écran circulaire.

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);

Code ESP8266 pour la Station Météo Intelligente

/* * Ce code ESP8266 NodeMCU a été développé par newbiely.fr * Ce code ESP8266 NodeMCU est mis à disposition du public sans aucune restriction. * Pour des instructions complètes et des schémas de câblage, veuillez visiter: * https://newbiely.fr/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(); }

Étapes Rapides

Suivez ces instructions étape par étape :

  • Si c'est la première fois que vous utilisez l'ESP8266, consultez le tutoriel sur Installation du logiciel ESP8266..
  • Câblez les composants comme indiqué sur le schéma.
  • Connectez la carte ESP8266 à votre ordinateur à l'aide d'un câble USB.
  • Ouvrez Arduino IDE sur votre ordinateur.
  • Sélectionnez la bonne carte ESP8266 (par ex. NodeMCU 1.0 (ESP-12E Module)) et le port COM.
  • Accédez à l'icône Bibliothèques dans la barre de gauche d'Arduino IDE.
  • Recherchez “DIYables TFT Round”, puis trouvez la bibliothèque DIYables_TFT_Round de DIYables.
  • Cliquez sur le bouton Installer pour installer la bibliothèque.
  • Vous devrez installer des dépendances de bibliothèques supplémentaires.
  • Cliquez sur le bouton Tout installer pour installer toutes les bibliothèques requises.
  • Recherchez “ArduinoJson”, puis trouvez la bibliothèque ArduinoJson de Benoit Blanchon.
  • Cliquez sur le bouton Installer pour ajouter la bibliothèque.
  • Recherchez “ezButton”, puis trouvez la bibliothèque ezButton d'ArduinoGetStarted.
  • Cliquez sur le bouton Installer pour ajouter la bibliothèque.
  • Copiez le code et ouvrez-le dans Arduino IDE.
  • Remplacez WIFI_SSID et WIFI_PASSWORD par le nom et le mot de passe de votre propre réseau.
  • Remplacez LATITUDE, LONGITUDE, LOCATION_NAME, TIMEZONE et GMT_OFFSET_SEC par les valeurs de votre propre ville.
  • Cliquez sur le bouton Téléverser dans Arduino IDE pour transférer le code vers l'ESP8266.
  • Ouvrez le Moniteur Série dans Arduino IDE et réglez le débit en bauds sur 115200.
  • Attendez que l'écran de démarrage, l'écran WiFi, puis la météo apparaissent.
  • Appuyez sur le bouton pour parcourir les pages.
  • Consultez les résultats dans le Moniteur Série.
∞
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

Explication du Code

Consultez les explications données dans les commentaires du code source pour chaque ligne !

Comment le Bouton Est Lu

Un seul bouton réalise trois actions distinctes. La durée de l'appui détermine laquelle est exécutée.

Durée d'appui Action
Moins de 0,6 seconde Passer à la page suivante, en revenant à ACCUEIL après la dernière
0,6 à 2 secondes Demander immédiatement de nouvelles données à l'API météo
Plus de 2 secondes Revenir directement à la page ACCUEIL

Le bouton est géré par la bibliothèque ezButton. Le rebond mécanique du contact est éliminé par la bibliothèque, donc un seul appui n'est jamais enregistré deux fois. La broche est placée en mode INPUT_PULLUP par le constructeur, donc aucun appel à pinMode() n'est nécessaire.

ezButton button(PIN_BUTTON);

La fenêtre anti-rebond doit être déclarée une fois dans setup().

button.setDebounceTime(40);

Dans la boucle, button.loop() doit être appelé avant d'examiner les événements. L'intervalle entre les deux événements donne la durée d'appui.

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 }

Le tutoriel ESP8266 - Bouton. explique la technique anti-rebond en détail.

Comment Changer la Localisation

Le sketch est fourni avec Séoul, Corée du Sud. Cinq définitions en haut du code contrôlent le lieu :

#define LATITUDE 37.5665 #define LONGITUDE 126.9780 #define LOCATION_NAME "SEOUL" #define TIMEZONE "Asia%2FSeoul" #define GMT_OFFSET_SEC (9 * 3600)
  • LATITUDE et LONGITUDE identifient la ville. N'importe quel site de cartographie les fournit.
  • LOCATION_NAME est le libellé affiché à l'écran. Un nom court convient à l'écran circulaire.
  • TIMEZONE est placé dans une adresse web, donc le caractère / doit être écrit %2F. Europe/Berlin s'écrit Europe%2FBerlin.
  • GMT_OFFSET_SEC est utilisé par l'horloge NTP. Berlin en hiver est à UTC+1, ce qui donne (1 * 3600).
  • DST_OFFSET_SEC gère l'heure d'été. Il doit être réglé à 3600 là où l'heure d'été s'applique, sinon à 0.

Connaissances Supplémentaires

Il est utile de comparer l'ESP8266 et l'ESP32 avant de choisir une carte pour ce projet.

ESP8266 ESP32
Tas libre pour le sketch environ 46 Ko environ 280 Ko
Heures de prévision demandées 12, pour économiser la mémoire 72, les trois jours complets
Filtre JSON obligatoire optionnel
Fonction d'heure locale écrite à la main getLocalTime() est fournie
Cœurs CPU 1 2
Prix typique plus bas plus élevé

Il est évident que l'ESP32 est bien plus confortable pour ce projet, car la pression sur la mémoire disparaît complètement. L'ESP8266 reste néanmoins une option capable et moins chère, à condition de conserver les deux économies de mémoire décrites ci-dessus.

Dépannage

  • L'écran reste noir. Les fils SCL et SDA doivent arriver sur D5 et D7. Ces deux broches appartiennent au bus SPI matériel et ne peuvent pas être déplacées.
  • La carte redémarre pendant la requête. Le tas est épuisé. Réduisez le nombre d'heures de prévision dans getWeatherURL(), ou demandez deux entrées quotidiennes au lieu de trois.
  • HTTP CODE est négatif. La négociation sécurisée a échoué. Vérifiez que l'heure a bien été synchronisée, car une horloge incorrecte fait apparaître chaque certificat comme invalide.
  • Le WiFi ne se connecte jamais. L'ESP8266 ne prend en charge que les réseaux 2,4 GHz. Un réseau fonctionnant exclusivement en 5 GHz ne sera jamais rejoint.
  • L'horloge affiche --:--. La réponse NTP n'est pas arrivée. Le port UDP 123 est bloqué sur certains réseaux.

Vidéo

Commentaires

※ NOS MESSAGES

  • N'hésitez pas à partager le lien de ce tutoriel. Cependant, veuillez ne pas utiliser notre contenu sur d'autres sites web. Nous avons investi beaucoup d'efforts et de temps pour créer ce contenu, veuillez respecter notre travail !