ESP8266 - Estação Meteorológica Inteligente

Este tutorial ensina você a usar o ESP8266 para criar uma estação meteorológica inteligente que lê o clima de um serviço online e o mostra em um display TFT LCD redondo de 1,28 polegada. Em detalhes, vamos aprender:

Sugerimos:

Estação meteorológica inteligente com ESP8266 NodeMCU e display TFT LCD redondo

Hardware Necessário

1×ESP8266 NodeMCU ESP-12F
1×Alternatively, ESP8266 D1 Mini NodeMCU ESP-12F
1×Alternatively, ESP8266 NodeMCU ESP-12E (Uno-form)
1×Alternatively, ESP8266 NodeMCU ESP-12F (SMD-form)
1×Cabo Micro USB
1×1.28 Inch Round Circular TFT LCD Display Module
1×Push Button
1×Button Module Alternatively,
1×Protoboard
1×Fios Jumper

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

1×(Recomendado) Placa de Expansão com Bornes de Parafuso para ESP8266

Or you can buy the following kits:

1×DIYables Sensor Kit (18 sensors/displays)
Divulgação: Alguns dos links desta seção são links de afiliado da Amazon. Podemos receber uma comissão pelas compras feitas por meio desses links, sem custo adicional para você. Agradecemos o seu apoio.

Sobre a Estação Meteorológica Inteligente

Esta estação meteorológica não tem sensor de temperatura. O ESP8266 entra na sua rede WiFi, pede a um serviço de clima os valores de uma cidade escolhida e desenha a resposta na tela redonda. Como os dados chegam de um serviço web, a estação também pode apresentar valores que um pequeno sensor não consegue medir, como a chance de chuva para as próximas horas e a previsão para os próximos dias.

A interface é dividida em oito páginas, e um botão de pressão percorre todas elas.

Página Conteúdo
HOME Ícone do clima, temperatura, descrição do céu, relógio NTP
TEMP Temperatura junto com a sensação térmica
HUMIDITY Umidade apresentada como um anel e como porcentagem
WIND Velocidade e direção do vento em uma bússola
RAIN Chance de chuva com um efeito animado de chuva
NEXT HOURS Temperatura e chance de chuva para as próximas horas
FORECAST Temperatura máxima e mínima ao longo de 3 dias
SYSTEM Estado do WiFi, intensidade do sinal, estado da API, localização

O Limite de Memória do ESP8266

O ESP8266 é a placa mais limitada que consegue executar este projeto. Seu heap tem cerca de 46 KB depois que o sketch inicia, e uma conexão HTTPS sozinha reserva cerca de 20 KB dele para os buffers do BearSSL. Duas decisões no código mantêm tudo dentro desse orçamento:

Decisão Efeito
Solicitar apenas 12 horas de previsão a resposta diminui de cerca de 5 KB para cerca de 1,8 KB
Aplicar um filtro do ArduinoJson os blocos "..._units" são descartados antes da análise
Liberar o payload após a análise o buffer de texto volta ao heap antes de desenhar
Criar o cliente TLS no heap o buffer grande desaparece quando a requisição termina

※ Nota:

O código imprime o heap livre antes e depois de cada requisição. Observe esses números no Serial Monitor. Se o valor cair abaixo de cerca de 12 KB, diminua as horas de previsão ou reduza o número de entradas diárias.

Diagrama de Ligação

O display GC9A01 é controlado pelo barramento SPI de hardware do ESP8266, onde o D5 leva o clock e o D7 leva os dados. Esses dois pinos são fixos e não devem ser movidos.

Pino do TFT LCD ESP8266 Descrição
VCC 3.3V Alimentação
GND GND Terra
SCL D5 Clock SPI, pino fixo
SDA D7 MOSI SPI, pino fixo
DC D2 Dados/Comando
CS D8 Seleção de Chip
RST D1 Reset

O botão é lido no D6. Tanto um botão de pressão simples quanto um módulo de botão podem ser instalados ali, e a ligação de cada um é mostrada abaixo.

ESP8266 - Ligação do Botão de Pressão

Um botão de pressão simples não tem resistor. Ele deve ser conectado entre o D6 e o terra, e o resistor de pull-up interno do ESP8266 cuida do resto, então nenhum resistor externo é necessário. O pino fica em HIGH em repouso e vai para LOW enquanto o botão está pressionado.

Diagrama de ligação da estação meteorológica inteligente com ESP8266 NodeMCU e botão de pressão

This image is created using Fritzing. Click to enlarge image

Para mais informações, veja Pinagem do ESP8266 e como alimentar o ESP8266 e outros componentes.

Pino do Botão ESP8266 Descrição
Pin 1 D6 Entrada do botão com pull-up interno
Pin 2 GND Terra

O sketch fornecido foi escrito para este arranjo.

ezButton button(PIN_BUTTON);

ESP8266 - Ligação do Módulo de Botão

Um módulo de botão traz um resistor de pull-down na placa e, por isso, precisa ser alimentado. Seu pino OUT fica em LOW enquanto o botão está solto e vai para HIGH enquanto ele está pressionado, o inverso do botão simples.

Diagrama de ligação da estação meteorológica inteligente com ESP8266 NodeMCU e módulo de botão

This image is created using Fritzing. Click to enlarge image

Pino do Módulo de Botão ESP8266 Descrição
VCC 3.3V Alimentação
GND GND Terra
OUT D6 Sinal, LOW em repouso, HIGH quando pressionado

O resistor do módulo substitui o interno, então o modo deve ser indicado no construtor.

ezButton button(PIN_BUTTON, EXTERNAL_PULLDOWN);

※ Nota:

Os rótulos de pinos impressos nas placas ESP8266 variam entre fabricantes. Os números D usados acima são os rótulos do NodeMCU. Sempre compare-os com os rótulos impressos na sua própria placa antes de ligar. Olhe com atenção!

Como Programar o ESP8266 para a Estação Meteorológica

O primeiro passo é incluir as bibliotecas. O core do ESP8266 fornece os cabeçalhos de rede e de hora, enquanto ArduinoJson, DIYables_TFT_Round e ezButton são instaladas pelo Library Manager.

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

Especifique os pinos do ESP8266 conectados ao display e depois declare o objeto do display.

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

Especifique as credenciais da rede e o local a ser observado.

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

O ESP8266 deve estar conectado à rede antes de qualquer requisição ser tentada.

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

O relógio é sincronizado por NTP. Um relógio correto também importa para o HTTPS, porque todo certificado tem uma data de início e uma data de término.

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

O core do ESP8266 não fornece a função getLocalTime(), ao contrário do core do ESP32, então o relógio do sistema precisa ser lido diretamente.

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

O cliente seguro é criado no heap. Quando a requisição termina, o objeto é destruído e seu grande buffer TLS volta ao heap.

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

Um filtro é declarado antes da análise. Apenas os três blocos que realmente são desenhados são mantidos, o que mantém o documento pequeno.

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

Um único valor é então extraído e desenhado no display circular.

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

Código ESP8266 para a Estação Meteorológica Inteligente

/* * Este código de ESP8266 NodeMCU foi desenvolvido por pt.newbiely.com * Este código de ESP8266 NodeMCU é disponibilizado ao público sem nenhuma restrição. * Para tutoriais completos e diagramas de ligação, visite: * https://pt.newbiely.com/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(); }

Passos Rápidos

Siga estas instruções passo a passo:

  • Se esta for sua primeira vez usando o ESP8266, consulte o tutorial sobre como configurar o ambiente para ESP8266 na Arduino IDE.
  • Conecte os componentes como mostrado no diagrama.
  • Conecte a placa ESP8266 ao seu computador usando um cabo USB.
  • Abra a Arduino IDE no seu computador.
  • Selecione a placa ESP8266 correta (por exemplo, NodeMCU 1.0 (ESP-12E Module)) e a porta COM.
  • Navegue até o ícone Libraries na barra lateral esquerda da Arduino IDE.
  • Pesquise por “DIYables TFT Round”, depois encontre a biblioteca DIYables_TFT_Round da DIYables.
  • Clique no botão Install para instalar a biblioteca.
  • Você precisará instalar dependências adicionais de biblioteca.
  • Clique no botão Install All para instalar todas as bibliotecas necessárias.
  • Pesquise por “ArduinoJson”, depois encontre a biblioteca ArduinoJson de Benoit Blanchon.
  • Clique no botão Install para adicionar a biblioteca.
  • Pesquise por “ezButton”, depois encontre a biblioteca ezButton da ArduinoGetStarted.
  • Clique no botão Install para adicionar a biblioteca.
  • Copie o código e abra-o na Arduino IDE.
  • Substitua WIFI_SSID e WIFI_PASSWORD pelo nome e pela senha da sua própria rede.
  • Substitua LATITUDE, LONGITUDE, LOCATION_NAME, TIMEZONE e GMT_OFFSET_SEC pelos valores da sua própria cidade.
  • Clique no botão Upload na Arduino IDE para transferir o código para o ESP8266.
  • Abra o Serial Monitor na Arduino IDE e defina a taxa de transmissão (baud rate) como 115200.
  • Aguarde enquanto a tela de inicialização, a tela de WiFi e depois o clima aparecem.
  • Pressione o botão para percorrer as páginas.
  • Confira os resultados no 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

Explicação do Código

Confira as explicações dadas nos comentários do código-fonte para cada linha!

Como o Botão é Lido

Um único botão executa três ações separadas. A duração do toque decide qual delas é executada.

Tempo pressionado Ação
Menos de 0,6 segundo Avança para a próxima página, voltando para HOME depois da última
0,6 a 2 segundos Solicita imediatamente dados novos da API de clima
Mais de 2 segundos Volta diretamente para a página HOME

O botão é gerenciado pela biblioteca ezButton. O efeito de bounce mecânico do contato é removido pela biblioteca, portanto um único toque nunca é registrado duas vezes. O pino é colocado no modo INPUT_PULLUP pelo construtor, então nenhuma chamada a pinMode() é necessária.

ezButton button(PIN_BUTTON);

A janela de debounce deve ser declarada uma vez dentro de setup().

button.setDebounceTime(40);

Dentro do loop, button.loop() precisa ser chamada antes de os eventos serem examinados. O intervalo entre os dois eventos fornece o tempo de pressão.

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 }

O tutorial ESP8266 - Botão explica a técnica de debounce isoladamente.

Como Mudar o Local

O sketch é entregue com Seul, Coreia do Sul. Cinco definições no topo do código controlam o local:

#define LATITUDE 37.5665 #define LONGITUDE 126.9780 #define LOCATION_NAME "SEOUL" #define TIMEZONE "Asia%2FSeoul" #define GMT_OFFSET_SEC (9 * 3600)
  • LATITUDE e LONGITUDE identificam a cidade. Qualquer site de mapas fornece esses valores.
  • LOCATION_NAME é o rótulo desenhado na tela. Um nome curto combina com o display circular.
  • TIMEZONE é colocado dentro de um endereço web, portanto o caractere / deve ser escrito como %2F. Europe/Berlin é escrito como Europe%2FBerlin.
  • GMT_OFFSET_SEC é usado pelo relógio NTP. Berlim no inverno é UTC+1, o que resulta em (1 * 3600).
  • DST_OFFSET_SEC cobre o horário de verão. Deve ser definido como 3600 onde o horário de verão se aplica; caso contrário, 0.

Conhecimento Adicional

Vale a pena comparar o ESP8266 com o ESP32 antes de escolher uma placa para este projeto.

ESP8266 ESP32
Heap livre para o sketch cerca de 46 KB cerca de 280 KB
Horas de previsão solicitadas 12, para economizar memória 72, os três dias completos
Filtro JSON obrigatório opcional
Função de hora local escrita à mão getLocalTime() é fornecida
Núcleos de CPU 1 2
Preço típico menor maior

É evidente que o ESP32 é muito mais confortável para este projeto, porque a pressão sobre a memória desaparece por completo. Ainda assim, o ESP8266 continua sendo uma opção capaz e mais barata, desde que as duas economias de memória descritas acima sejam mantidas.

Solução de Problemas

  • O display permanece preto. Os fios SCL e SDA devem chegar ao D5 e ao D7. Esses dois pinos pertencem ao barramento SPI de hardware e não podem ser trocados de lugar.
  • A placa reinicia durante a requisição. O heap acabou. Diminua as horas de previsão em getWeatherURL(), ou solicite duas entradas diárias em vez de três.
  • HTTP CODE é negativo. O handshake seguro falhou. Confirme que a hora foi sincronizada, pois um relógio incorreto faz todo certificado parecer inválido.
  • O WiFi nunca conecta. O ESP8266 suporta apenas redes de 2,4 GHz. Uma rede que funciona exclusivamente em 5 GHz nunca será acessada.
  • O relógio mostra --:--. A resposta do NTP não chegou. A porta UDP 123 é bloqueada em algumas redes.

Vídeo

※ NOSSAS MENSAGENS

  • Fique à vontade para compartilhar o link deste tutorial. Porém, por favor, não use nosso conteúdo em outros sites. Investimos muito esforço e tempo para criar este conteúdo, por favor respeite nosso trabalho!