« Module ESP32-écran avec données météo publiques » : différence entre les versions

De Wiki de Mémoire Vive
Aller à la navigation Aller à la recherche
Aucun résumé des modifications
Fred (discussion | contributions)
Aucun résumé des modifications
 
(3 versions intermédiaires par le même utilisateur non affichées)
Ligne 1 : Ligne 1 :
Une version plus générique de l'utilisation du module écran ESP32, avec des données météo publiques (Openweathermap).
Une version plus générique de l'utilisation du module écran ESP32, avec des données météo publiques (Openweathermap).


On peut faire fonctionner ce code n'importe où en reparamétrant le wifi.
On peut faire fonctionner ce code n'importe où en reparamétrant le wifi.
Ligne 447 : Ligne 448 :
     tft.drawString("Reinit Config Meteo\nConnect Wi-Fi ESP32_Config_API\nPuis 192.168.4.1",  
     tft.drawString("Reinit Config Meteo\nConnect Wi-Fi ESP32_Config_API\nPuis 192.168.4.1",  
                   screenW/2, screenH/2, 2);
                   screenW/2, screenH/2, 2);
  }
}
</pre>
Code avec 3 écrans, météo, heure et réveil.
<pre>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <TFT_eSPI.h>
#include <TimeLib.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "time.h"
#define PAGE_MAIN    0
#define PAGE_Heure    1
#define PAGE_METEO    2
#define PAGE_Reveil  3
// ====== Variables globales ======
WebServer server(80);
Preferences preferences;
TFT_eSPI tft = TFT_eSPI();
String ssid, password;
String apiKey, lat, lon;
bool wifiConfigured = false;
bool weatherConfigured = false;
// Variables pour le réveil
int heureReveil = 7;  // Heure par défaut
int minuteReveil = 0; // Minute par défaut
bool reveilActif = false;
char timeStrReveil[6]; // Format "HH:MM" pour l'affichage du réveil
// ====== Dashboard ======
const int screenW = 480;
const int screenH = 320;
const int cols = 4;
const int rows = 4;
const int boxW = screenW / cols - 10;
const int boxH = screenH / rows - 10;
struct Box {
  const char* label;
  char value[32];
  bool clickable;
};
Box boxes[rows * cols] = {
  {"Temp Locale", "--", false},
  {"Vent", "--", false},
  {"Heure", "00:00", true},
  {"Wi-Fi", "OFF", false},
  {"Lever Soleil", "--", false},
  {"Coucher Soleil", "--", false},
  {"Humid.", "", false},
  {"Pression", "", false},
  {"", "", false},
  {"Ciel", "", false},
  {"Case11", "", false},
  {"Case12", "", false},
  {"Reveil", "", true},    // La valeur sera mise à jour avec timeStrReveil
  {"Case14", "", false},
  {"Reset Wifi", "Tap", true},
  {"Reset Meteo", "Tap", true}  // ← Case tactile
};
// ====== HTML Wi-Fi ======
const char* htmlWiFiForm = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<h2>Configurer le Wi-Fi</h2>
<form action="/save" method="POST">
  SSID:<br>
  <input type="text" name="ssid"><br>
  Mot de passe:<br>
  <input type="password" name="password"><br><br>
  <input type="submit" value="Sauvegarder">
</form>
</body>
</html>
)rawliteral";
// ====== HTML OpenWeather ======
const char* htmlApiForm = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<h2>Configurer OpenWeather</h2>
<form action="/saveApi" method="POST">
  API Key:<br>
  <input type="text" name="apikey"><br>
  Latitude:<br>
  <input type="text" name="lat"><br>
  Longitude:<br>
  <input type="text" name="lon"><br><br>
  <input type="submit" value="Sauvegarder">
</form>
</body>
</html>
)rawliteral";
// ====== Fonctions Wi-Fi ======
void saveWiFi(String s, String p){
  preferences.begin("wifi", false);
  preferences.putString("ssid", s);
  preferences.putString("password", p);
  preferences.end();
  Serial.println("[Wi-Fi] Paramètres sauvegardés en flash");
}
bool loadWiFi(){
  preferences.begin("wifi", true);
  ssid = preferences.getString("ssid", "");
  password = preferences.getString("password", "");
  preferences.end();
  Serial.print("[Wi-Fi] Chargement depuis flash -> SSID: "); Serial.println(ssid);
  return ssid != "" && password != "";
}
void setupWiFiAP(){
  WiFi.softAP("ESP32_Config");
  Serial.println("[Wi-Fi] Point d’accès activé : ESP32_Config");
  Serial.println("[Wi-Fi] Connectez-vous à 192.168.4.1 pour configurer le Wi-Fi");
  server.on("/", [](){ server.send(200, "text/html", htmlWiFiForm); });
  server.on("/save", HTTP_POST, [](){
    if(server.hasArg("ssid") && server.hasArg("password")){
      ssid = server.arg("ssid");
      password = server.arg("password");
      saveWiFi(ssid, password);
      server.send(200, "text/html", "<h2>Wi-Fi sauvegarde ok, redemarrage...</h2>");
      Serial.println("[Wi-Fi] Formulaire reçu -> redemarrage");
      delay(2000);
      ESP.restart();
    }
  });
  server.begin();
  Serial.println("[Wi-Fi] Serveur Web démarré pour configuration");
}
void connectWiFi(){
  if(loadWiFi()){
    Serial.print("[Wi-Fi] Tentative de connexion à : "); Serial.println(ssid);
    WiFi.begin(ssid.c_str(), password.c_str());
    int tries = 0;
    while(WiFi.status() != WL_CONNECTED && tries < 20){
      delay(500);
      Serial.print(".");
      tries++;
    }
    if(WiFi.status() == WL_CONNECTED){
      Serial.println("\n[Wi-Fi] Connecté avec succès!");
      wifiConfigured = true;
      strcpy(boxes[3].value, "ON");
      return;
    }
    Serial.println("\n[Wi-Fi] Échec de connexion Wi-Fi");
  }
  setupWiFiAP();
}
// ====== Fonctions OpenWeather ======
void saveWeatherConfig(String key, String latitude, String longitude){
  preferences.begin("openweather", false);
  preferences.putString("apikey", key);
  preferences.putString("lat", latitude);
  preferences.putString("lon", longitude);
  preferences.end();
  Serial.println("[OpenWeather] Config sauvegardée en flash");
}
bool loadWeatherConfig(){
  preferences.begin("openweather", true);
  apiKey = preferences.getString("apikey", "");
  lat = preferences.getString("lat", "");
  lon = preferences.getString("lon", "");
  preferences.end();
  Serial.print("[OpenWeather] Chargement depuis flash -> API: "); Serial.println(apiKey);
  return apiKey != "" && lat != "" && lon != "";
}
void configWeather(){
  WiFi.softAP("ESP32_Config_API");
  Serial.println("[OpenWeather] Point d’accès activé : ESP32_Config_API");
  Serial.println("[OpenWeather] Connectez-vous à 192.168.4.1 pour configurer OpenWeather");
  server.on("/", [](){ server.send(200, "text/html", htmlApiForm); });
  server.on("/saveApi", HTTP_POST, [](){
    if(server.hasArg("apikey") && server.hasArg("lat") && server.hasArg("lon")){
      apiKey = server.arg("apikey");
      lat = server.arg("lat");
      lon = server.arg("lon");
      saveWeatherConfig(apiKey, lat, lon);
      server.send(200, "text/html", "<h2>Config meteo sauvegarde ok, redemarrage...</h2>");
      Serial.println("[OpenWeather] Formulaire reçu -> redemarrage");
      delay(2000);
      ESP.restart();
    }
  });
  server.begin();
  Serial.println("[OpenWeather] Serveur Web démarré pour configuration");
}
// ====== OpenWeather ======
void updateWeather() {
  if (WiFi.status() != WL_CONNECTED || apiKey == "") return;
  HTTPClient http;
  String url = "https://api.openweathermap.org/data/2.5/weather?lat=" + lat + "&lon=" + lon +
              "&units=metric&appid=" + apiKey + "&lang=fr";
  Serial.print("[OpenWeather] URL: "); Serial.println(url);
  http.begin(url);
  int httpCode = http.GET();
  Serial.print("[OpenWeather] HTTP code: "); Serial.println(httpCode);
  if (httpCode == 200) {
    String payload = http.getString();
    Serial.println("[OpenWeather] JSON complet:");
    Serial.println(payload);
    StaticJsonDocument<2048> doc;
    DeserializationError error = deserializeJson(doc, payload);
    if (error) {
      Serial.print("[OpenWeather] Erreur JSON: "); Serial.println(error.c_str());
      http.end();
      return;
    }
    // Données météo
    float temp      = doc["main"]["temp"];
    float feels_like = doc["main"]["feels_like"];
    float wind      = doc["wind"]["speed"];
    int windDeg      = doc["wind"]["deg"];
    int humidity    = doc["main"]["humidity"];
    int pressure    = doc["main"]["pressure"];
    long sunrise    = doc["sys"]["sunrise"];
    long sunset      = doc["sys"]["sunset"];
    const char* city = doc["name"];
    const char* weatherDesc = doc["weather"][0]["description"];
// Conversion lever/coucher soleil en heure locale (NTP + fuseau pris en compte)
struct tm ts;
char buf[10];
time_t sunrise_ts = (time_t)sunrise;
time_t sunset_ts  = (time_t)sunset;
// Lever du soleil
localtime_r(&sunrise_ts, &ts);
strftime(buf, sizeof(buf), "%H:%M", &ts);
snprintf(boxes[4].value, sizeof(boxes[4].value), "%s", buf);
// Coucher du soleil
localtime_r(&sunset_ts, &ts);
strftime(buf, sizeof(buf), "%H:%M", &ts);
snprintf(boxes[5].value, sizeof(boxes[5].value), "%s", buf);
    // Mettre à jour le tableau de bord
    snprintf(boxes[0].value, sizeof(boxes[0].value), "%.1f°C", temp);
    snprintf(boxes[1].value, sizeof(boxes[1].value), "%.1f m/s %d°", wind, windDeg);
    snprintf(boxes[6].value, sizeof(boxes[6].value), "%d%%", humidity);
    snprintf(boxes[7].value, sizeof(boxes[7].value), "%dhPa", pressure);
    snprintf(boxes[8].value, sizeof(boxes[8].value), "%s", city);
    snprintf(boxes[9].value, sizeof(boxes[9].value), "%s", weatherDesc);
    // Traces console
    Serial.print("[OpenWeather] Température: "); Serial.println(temp);
    Serial.print("[OpenWeather] Vent: "); Serial.print(wind); Serial.print(" m/s "); Serial.println(windDeg);
    Serial.print("[OpenWeather] Humidité: "); Serial.println(humidity);
    Serial.print("[OpenWeather] Pression: "); Serial.println(pressure);
    Serial.print("[OpenWeather] Lever soleil: "); Serial.println(boxes[4].value);
    Serial.print("[OpenWeather] Coucher soleil: "); Serial.println(boxes[5].value);
    Serial.print("[OpenWeather] Ville: "); Serial.println(city);
    Serial.print("[OpenWeather] Description: "); Serial.println(weatherDesc);
  } else {
    Serial.println("[OpenWeather] Erreur HTTP ou clé API invalide");
  }
  http.end();
}
// ====== TFT ======
void drawBox(int x, int y, int w, int h, Box b){
  tft.drawRect(x, y, w, h, b.clickable ? TFT_GREEN : TFT_WHITE); 
  // ===== Label =====
  tft.setTextFont(2);
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.setCursor(x + 5, y + 5);
  tft.print(b.label);
  // ===== Valeur =====
  int fontSize = 4; // taille initiale
  tft.setTextFont(fontSize);
  int tw = tft.textWidth(b.value);
  // Réduire la taille si le texte dépasse la largeur de la case
  while(tw > w - 10 && fontSize > 1){
    fontSize--;
    tft.setTextFont(fontSize);
    tw = tft.textWidth(b.value);
  }
  int th = tft.fontHeight();
  int cx = x + (w - tw) / 2;
  int cy = y + (h - th) / 2 + th / 4;
  tft.setCursor(cx, cy);
  // === Couleur spéciale pour Wi-Fi ===
  if (strcmp(b.label, "Wi-Fi") == 0) {
    if (strcmp(b.value, "ON") == 0)
      tft.setTextColor(TFT_GREEN, TFT_BLACK);
    else
      tft.setTextColor(TFT_RED, TFT_BLACK);
  } else {
    tft.setTextColor(TFT_CYAN, TFT_BLACK);
  }
  tft.print(b.value);
}
void drawAllBoxes(){
  tft.fillScreen(TFT_BLACK);
  for (int r = 0; r < rows; r++){
    for (int c = 0; c < cols; c++){
      int index = r * cols + c;
      int x = c * (boxW + 10) + 5;
      int y = r * (boxH + 10) + 5;
      drawBox(x, y, boxW, boxH, boxes[index]);
    }
  }
}
void drawPageHeure() {
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.drawString("Page Heure", screenW/2, 20, 2);
  // Affiche l'heure (case "Heure") au centre de la page
  tft.setTextColor(TFT_CYAN, TFT_BLACK);
  tft.drawString(boxes[2].value, screenW/2, 80, 8);
  // Dessine un bouton "Retour" (rectangle + texte)
  tft.drawRect(10, screenH - 30, 80, 25, TFT_WHITE);
  tft.setTextDatum(TL_DATUM);
  tft.drawString("<- Retour", 15, screenH - 28, 2);
}
// ====== Setup ======
void setup(){
  Serial.begin(115200);
  delay(2000);
  Serial.println("=== ESP32 Tableau de bord météo ===");
  Serial.print("Nom du fichier source : "); Serial.println(__FILE__);
  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
 
  // Charger les paramètres du réveil
  chargerReveil();
// Use this calibration code in setup():
  uint16_t calData[5] = { 308, 3404, 466, 3148, 3 };
  tft.setTouch(calData);
  connectWiFi();
  if(!loadWeatherConfig()){
    configWeather();
  } else {
    weatherConfigured = true;
    Serial.println("[Setup] OpenWeather déjà configuré");
    // Appel immédiat pour récupérer la météo dès le démarrage
    Serial.println("[Setup] Premier appel OpenWeather immédiat");
    updateWeather();   
  }
// Config NTP et attente de synchronisation
configTime(3600, 3600, "pool.ntp.org", "time.nist.gov");
Serial.println("[Setup] NTP configuré, attente de synchronisation...");
struct tm timeinfo;
int retry = 0;
const int retry_max = 10;
while(!getLocalTime(&timeinfo) && retry < retry_max){
    Serial.println("[Setup] En attente de la synchronisation NTP...");
    delay(1000);
    retry++;
}
if(retry == retry_max){
    Serial.println("[Setup] Attention: NTP non synchronisé !");
} else {
    Serial.println("[Setup] Heure NTP synchronisée !");
    if(weatherConfigured){
        Serial.println("[Setup] Premier appel OpenWeather immédiat");
        updateWeather();
    }
}
  drawAllBoxes();
}
// ====== Fonctions Réveil ======
void sauvegarderReveil() {
  preferences.begin("reveil", false);
  preferences.putInt("heure", heureReveil);
  preferences.putInt("minute", minuteReveil);
  preferences.putBool("actif", reveilActif);
  preferences.end();
}
void chargerReveil() {
  preferences.begin("reveil", true);
  heureReveil = preferences.getInt("heure", 7);
  minuteReveil = preferences.getInt("minute", 0);
  reveilActif = preferences.getBool("actif", false);
  preferences.end();
 
  // Formater l'heure du réveil
  snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
}
void drawPageReveil() {
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.drawString("Reglage Reveil", screenW/2, 20, 2);
 
  // Affichage de l'heure de réveil
  char timeStr[6];
  snprintf(timeStr, sizeof(timeStr), "%02d:%02d", heureReveil, minuteReveil);
  tft.setTextColor(TFT_CYAN, TFT_BLACK);
  tft.drawString(timeStr, screenW/2, 140, 8);
 
  // Boutons + et - pour les heures
  tft.drawRect(screenW/2 - 170, 150, 40, 40, TFT_WHITE);
  tft.drawString("-", screenW/2 - 150, 170, 4);
  tft.drawRect(screenW/2 - 170, 100, 40, 40, TFT_WHITE);
  tft.drawString("+", screenW/2 - 150, 120, 4);
 
  // Boutons + et - pour les minutes
  tft.drawRect(screenW/2 + 130, 150, 40, 40, TFT_WHITE);
  tft.drawString("-", screenW/2 + 150, 170, 4);
  tft.drawRect(screenW/2 + 130, 100, 40, 40, TFT_WHITE);
  tft.drawString("+", screenW/2 + 150, 120, 4);
 
  // Bouton ON/OFF
  tft.drawRect(screenW/2 - 50, 240, 100, 40, TFT_WHITE);
  tft.setTextColor(reveilActif ? TFT_GREEN : TFT_RED, TFT_BLACK);
  tft.drawString(reveilActif ? "ON" : "OFF", screenW/2, 260, 4);
 
  // Bouton Retour
  tft.drawRect(10, screenH - 30, 80, 25, TFT_WHITE);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(TL_DATUM);
  tft.drawString("<- Retour", 15, screenH - 28, 2);
}
// ====== Loop ======
unsigned long lastUpdate = 0;
unsigned long lastWeather = 0;
unsigned long lastMessageDisplay = 0;
void loop() {
  server.handleClient();
  static int currentPage = PAGE_MAIN;
  static bool pageChanged = true; // Flag pour éviter les redessins inutiles
  // Vérification du réveil toutes les minutes
  if (reveilActif) {
    struct tm timeinfo;
    if (getLocalTime(&timeinfo)) {
      if (timeinfo.tm_hour == heureReveil && timeinfo.tm_min == minuteReveil) {
        // Ajouter ici votre code pour l'alarme (buzzer, LED, etc.)
      }
    }
  }
  // === Gestion de l'affichage ===
  if (pageChanged) {
    switch (currentPage) {
      case PAGE_MAIN:
        drawAllBoxes();
        break;
      case PAGE_Heure:
        drawPageHeure();
        break;
      case PAGE_Reveil:
        drawPageReveil();
        break;
    }
    pageChanged = false;
  }
  if (wifiConfigured && weatherConfigured) {
    // Heure toutes les 10s
    if (millis() - lastUpdate > 10000) {
      lastUpdate = millis();
      struct tm timeinfo;
      if (getLocalTime(&timeinfo)) {
        snprintf(boxes[2].value, sizeof(boxes[2].value), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
        pageChanged = true; // <--- AJOUT : force le rafraîchissement
      }
    }
    // Météo toutes les 10 min
    if (millis() - lastWeather > 600000) {
      lastWeather = millis();
      Serial.println("[Loop] Mise à jour météo...");
      updateWeather();
      pageChanged = true; // <--- AJOUT : force le rafraîchissement
    }
    // === Détection tactile ===
    uint16_t tx, ty;
    if (tft.getTouch(&tx, &ty)) {
      // Vérifie d'abord le bouton "Retour" sur la page Heure
      if (currentPage == PAGE_Heure && tx >= 10 && tx <= 90 && ty >= (screenH - 30) && ty <= screenH) {
        currentPage = PAGE_MAIN;
        pageChanged = true;
        delay(200);
      }
      // Sinon, vérifie les autres boxes
      else {
        for (int r = 0; r < rows; r++) {
          for (int c = 0; c < cols; c++) {
            int index = r * cols + c;
            int boxX = c * (boxW + 10) + 5;
            int boxY = r * (boxH + 10) + 5;
            if (tx >= boxX && tx <= boxX + boxW &&
                ty >= boxY && ty <= boxY + boxH &&
                boxes[index].clickable) {
              // Action pour Heure
              if (strcmp(boxes[index].label, "Heure") == 0) {
                currentPage = PAGE_Heure;
                pageChanged = true;
                delay(200);
              }
              // Action pour Reveil
              else if (strcmp(boxes[index].label, "Reveil") == 0) {
                currentPage = PAGE_Reveil;
                pageChanged = true;
                delay(200);
              }
              // Action pour "Reset Meteo" -> efface la config meteo puis redémarre l'ESP
              else if (strcmp(boxes[index].label, "Reset Meteo") == 0) {
                Serial.println("[Touch] Réinitialisation OpenWeather ! -- redemarrage");
                preferences.begin("openweather", false);
                preferences.clear();
                preferences.end();
                weatherConfigured = false;
                tft.fillScreen(TFT_BLACK);
                tft.setTextColor(TFT_RED, TFT_BLACK);
                tft.setTextDatum(MC_DATUM);
                tft.drawString("Redémmarrage de l'ESP pour reconfigurer la météo",
                              screenW/2, screenH/2, 2);
                delay(2000);
                ESP.restart();
              }
              // Action pour "Reset Wifi" -> efface la config wifi puis redémarre l'ESP
              else if (strcmp(boxes[index].label, "Reset Wifi") == 0) {
                Serial.println("[Touch] Réinitialisation Wifi ! -- redemarrage");
                preferences.begin("wifi", false);
                preferences.clear();
                preferences.end();
                weatherConfigured = false;
                tft.fillScreen(TFT_BLACK);
                tft.setTextColor(TFT_RED, TFT_BLACK);
                tft.setTextDatum(MC_DATUM);
                tft.drawString("Redémmarrage de l'ESP pour reconfigurer le wifi",
                              screenW/2, screenH/2, 2);
                delay(2000);
                ESP.restart();
            }
          }
        }
      }
      if (currentPage == PAGE_Reveil) {
        if (tx >= 10 && tx <= 90 && ty >= (screenH - 30) && ty <= screenH) {
          // Bouton Retour
          currentPage = PAGE_MAIN;
          pageChanged = true;
        sauvegarderReveil();
        } else if (tx >= screenW/2 - 170 && tx <= screenW/2 - 130) {
          if (ty >= 100 && ty <= 140) {
            // Augmenter les heures
            heureReveil = (heureReveil + 1) % 24;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          } else if (ty >= 150 && ty <= 190) {
            // Diminuer les heures
            heureReveil = (heureReveil - 1 + 24) % 24;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          }
        } else if (tx >= screenW/2 + 130 && tx <= screenW/2 + 170) {
          if (ty >= 100 && ty <= 140) {
            // Augmenter les minutes
            minuteReveil = (minuteReveil + 1) % 60;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          } else if (ty >= 150 && ty <= 190) {
            // Diminuer les minutes
            minuteReveil = (minuteReveil - 1 + 60) % 60;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          }
        } else if (tx >= screenW/2 - 50 && tx <= screenW/2 + 50 &&
                  ty >= 240 && ty <= 280) {
          // Toggle ON/OFF
          reveilActif = !reveilActif;
          pageChanged = true;
        }
      }
      delay(200); // anti-rebond
    }
  }
  else if (!weatherConfigured) {
    // Affiche le message de réinitialisation seulement toutes les 5 secondes
    if (millis() - lastMessageDisplay > 5000) {
      lastMessageDisplay = millis();
      tft.fillScreen(TFT_BLACK);
      tft.setTextColor(TFT_RED, TFT_BLACK);
      tft.setTextDatum(MC_DATUM);
      tft.drawString("Reinit Config Meteo\nConnect Wi-Fi ESP32_Config_API\nPuis 192.168.4.1",
                    screenW/2, screenH/2, 2);
      }
    }
   }
   }
}
}


</pre>
</pre>

Dernière version du 28 octobre 2025 à 21:56

Une version plus générique de l'utilisation du module écran ESP32, avec des données météo publiques (Openweathermap).


On peut faire fonctionner ce code n'importe où en reparamétrant le wifi.

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <TFT_eSPI.h>
#include <TimeLib.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "time.h"

// ====== Variables globales ======
WebServer server(80);
Preferences preferences;
TFT_eSPI tft = TFT_eSPI();

String ssid, password;
String apiKey, lat, lon;
bool wifiConfigured = false;
bool weatherConfigured = false;

// ====== Dashboard ======
const int screenW = 480;
const int screenH = 320;
const int cols = 4;
const int rows = 4;
const int boxW = screenW / cols - 10;
const int boxH = screenH / rows - 10;

struct Box {
  const char* label;
  char value[32];
  bool clickable;
};

Box boxes[rows * cols] = {
  {"Temp Locale", "--", false},
  {"Vent", "--", false},
  {"Heure", "00:00", false},
  {"Wi-Fi", "OFF", false},
  {"Lever Soleil", "--", false},
  {"Coucher Soleil", "--", false},
  {"Humid.", "", false},
  {"Pression", "", false},
  {"", "", false},
  {"Ciel", "", false},
  {"Case11", "", false},
  {"Case12", "", false},
  {"Case13", "", false},
  {"Case14", "", false},
  {"Case15", "", false},
  {"Reset Meteo", "Tap", true}   // ← Case tactile

};

// ====== HTML Wi-Fi ======
const char* htmlWiFiForm = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<h2>Configurer le Wi-Fi</h2>
<form action="/save" method="POST">
  SSID:<br>
  <input type="text" name="ssid"><br>
  Mot de passe:<br>
  <input type="password" name="password"><br><br>
  <input type="submit" value="Sauvegarder">
</form>
</body>
</html>
)rawliteral";

// ====== HTML OpenWeather ======
const char* htmlApiForm = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<h2>Configurer OpenWeather</h2>
<form action="/saveApi" method="POST">
  API Key:<br>
  <input type="text" name="apikey"><br>
  Latitude:<br>
  <input type="text" name="lat"><br>
  Longitude:<br>
  <input type="text" name="lon"><br><br>
  <input type="submit" value="Sauvegarder">
</form>
</body>
</html>
)rawliteral";

// ====== Fonctions Wi-Fi ======
void saveWiFi(String s, String p){
  preferences.begin("wifi", false);
  preferences.putString("ssid", s);
  preferences.putString("password", p);
  preferences.end();
  Serial.println("[Wi-Fi] Paramètres sauvegardés en flash");
}

bool loadWiFi(){
  preferences.begin("wifi", true);
  ssid = preferences.getString("ssid", "");
  password = preferences.getString("password", "");
  preferences.end();
  Serial.print("[Wi-Fi] Chargement depuis flash -> SSID: "); Serial.println(ssid);
  return ssid != "" && password != "";
}

void setupWiFiAP(){
  WiFi.softAP("ESP32_Config");
  Serial.println("[Wi-Fi] Point d’accès activé : ESP32_Config");
  Serial.println("[Wi-Fi] Connectez-vous à 192.168.4.1 pour configurer le Wi-Fi");

  server.on("/", [](){ server.send(200, "text/html", htmlWiFiForm); });

  server.on("/save", HTTP_POST, [](){
    if(server.hasArg("ssid") && server.hasArg("password")){
      ssid = server.arg("ssid");
      password = server.arg("password");
      saveWiFi(ssid, password);
      server.send(200, "text/html", "<h2>Wi-Fi sauvegarde ok, redemarrage...</h2>");
      Serial.println("[Wi-Fi] Formulaire reçu -> redemarrage");
      delay(2000);
      ESP.restart();
    }
  });

  server.begin();
  Serial.println("[Wi-Fi] Serveur Web démarré pour configuration");
}

void connectWiFi(){
  if(loadWiFi()){
    Serial.print("[Wi-Fi] Tentative de connexion à : "); Serial.println(ssid);
    WiFi.begin(ssid.c_str(), password.c_str());
    int tries = 0;
    while(WiFi.status() != WL_CONNECTED && tries < 20){
      delay(500);
      Serial.print(".");
      tries++;
    }
    if(WiFi.status() == WL_CONNECTED){
      Serial.println("\n[Wi-Fi] Connecté avec succès!");
      wifiConfigured = true;
      strcpy(boxes[3].value, "ON");
      return;
    }
    Serial.println("\n[Wi-Fi] Échec de connexion Wi-Fi");
  }
  setupWiFiAP();
}

// ====== Fonctions OpenWeather ======
void saveWeatherConfig(String key, String latitude, String longitude){
  preferences.begin("openweather", false);
  preferences.putString("apikey", key);
  preferences.putString("lat", latitude);
  preferences.putString("lon", longitude);
  preferences.end();
  Serial.println("[OpenWeather] Config sauvegardée en flash");
}

bool loadWeatherConfig(){
  preferences.begin("openweather", true);
  apiKey = preferences.getString("apikey", "");
  lat = preferences.getString("lat", "");
  lon = preferences.getString("lon", "");
  preferences.end();
  Serial.print("[OpenWeather] Chargement depuis flash -> API: "); Serial.println(apiKey);
  return apiKey != "" && lat != "" && lon != "";
}

void configWeather(){
  WiFi.softAP("ESP32_Config_API");
  Serial.println("[OpenWeather] Point d’accès activé : ESP32_Config_API");
  Serial.println("[OpenWeather] Connectez-vous à 192.168.4.1 pour configurer OpenWeather");

  server.on("/", [](){ server.send(200, "text/html", htmlApiForm); });

  server.on("/saveApi", HTTP_POST, [](){
    if(server.hasArg("apikey") && server.hasArg("lat") && server.hasArg("lon")){
      apiKey = server.arg("apikey");
      lat = server.arg("lat");
      lon = server.arg("lon");
      saveWeatherConfig(apiKey, lat, lon);
      server.send(200, "text/html", "<h2>Config meteo sauvegarde ok, redemarrage...</h2>");
      Serial.println("[OpenWeather] Formulaire reçu -> redemarrage");
      delay(2000);
      ESP.restart();
    }
  });

  server.begin();
  Serial.println("[OpenWeather] Serveur Web démarré pour configuration");
}

// ====== OpenWeather ======
void updateWeather() {
  if (WiFi.status() != WL_CONNECTED || apiKey == "") return;

  HTTPClient http;
  String url = "http://api.openweathermap.org/data/2.5/weather?lat=" + lat + "&lon=" + lon +
               "&units=metric&appid=" + apiKey + "&lang=fr";
  Serial.print("[OpenWeather] URL: "); Serial.println(url);

  http.begin(url);
  int httpCode = http.GET();
  Serial.print("[OpenWeather] HTTP code: "); Serial.println(httpCode);

  if (httpCode == 200) {
    String payload = http.getString();
    Serial.println("[OpenWeather] JSON complet:");
    Serial.println(payload);

    StaticJsonDocument<2048> doc;
    DeserializationError error = deserializeJson(doc, payload);
    if (error) {
      Serial.print("[OpenWeather] Erreur JSON: "); Serial.println(error.c_str());
      http.end();
      return;
    }

    // Données météo
    float temp       = doc["main"]["temp"];
    float feels_like = doc["main"]["feels_like"];
    float wind       = doc["wind"]["speed"];
    int windDeg      = doc["wind"]["deg"];
    int humidity     = doc["main"]["humidity"];
    int pressure     = doc["main"]["pressure"];
    long sunrise     = doc["sys"]["sunrise"];
    long sunset      = doc["sys"]["sunset"];
    const char* city = doc["name"];
    const char* weatherDesc = doc["weather"][0]["description"];

// Conversion lever/coucher soleil en heure locale (NTP + fuseau pris en compte)
struct tm ts;
char buf[10];

time_t sunrise_ts = (time_t)sunrise;
time_t sunset_ts  = (time_t)sunset;

// Lever du soleil
localtime_r(&sunrise_ts, &ts);
strftime(buf, sizeof(buf), "%H:%M", &ts);
snprintf(boxes[4].value, sizeof(boxes[4].value), "%s", buf);

// Coucher du soleil
localtime_r(&sunset_ts, &ts);
strftime(buf, sizeof(buf), "%H:%M", &ts);
snprintf(boxes[5].value, sizeof(boxes[5].value), "%s", buf);


    // Mettre à jour le tableau de bord
    snprintf(boxes[0].value, sizeof(boxes[0].value), "%.1f°C", temp);
    snprintf(boxes[1].value, sizeof(boxes[1].value), "%.1f m/s %d°", wind, windDeg);
    snprintf(boxes[6].value, sizeof(boxes[6].value), "%d%%", humidity);
    snprintf(boxes[7].value, sizeof(boxes[7].value), "%dhPa", pressure);
    snprintf(boxes[8].value, sizeof(boxes[8].value), "%s", city);
    snprintf(boxes[9].value, sizeof(boxes[9].value), "%s", weatherDesc);

    // Traces console
    Serial.print("[OpenWeather] Température: "); Serial.println(temp);
    Serial.print("[OpenWeather] Vent: "); Serial.print(wind); Serial.print(" m/s "); Serial.println(windDeg);
    Serial.print("[OpenWeather] Humidité: "); Serial.println(humidity);
    Serial.print("[OpenWeather] Pression: "); Serial.println(pressure);
    Serial.print("[OpenWeather] Lever soleil: "); Serial.println(boxes[4].value);
    Serial.print("[OpenWeather] Coucher soleil: "); Serial.println(boxes[5].value);
    Serial.print("[OpenWeather] Ville: "); Serial.println(city);
    Serial.print("[OpenWeather] Description: "); Serial.println(weatherDesc);

  } else {
    Serial.println("[OpenWeather] Erreur HTTP ou clé API invalide");
  }

  http.end();
}

// ====== TFT ======
void drawBox(int x, int y, int w, int h, Box b){
  tft.drawRect(x, y, w, h, b.clickable ? TFT_GREEN : TFT_WHITE);  

  // ===== Label =====
  tft.setTextFont(2);
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.setCursor(x + 5, y + 5);
  tft.print(b.label);

  // ===== Valeur =====
  int fontSize = 4; // taille initiale
  tft.setTextFont(fontSize);
  int tw = tft.textWidth(b.value);

  // Réduire la taille si le texte dépasse la largeur de la case
  while(tw > w - 10 && fontSize > 1){
    fontSize--;
    tft.setTextFont(fontSize);
    tw = tft.textWidth(b.value);
  }

  int th = tft.fontHeight();
  int cx = x + (w - tw) / 2;
  int cy = y + (h - th) / 2 + th / 4;
  tft.setCursor(cx, cy);

  // === Couleur spéciale pour Wi-Fi ===
  if (strcmp(b.label, "Wi-Fi") == 0) {
    if (strcmp(b.value, "ON") == 0)
      tft.setTextColor(TFT_GREEN, TFT_BLACK);
    else
      tft.setTextColor(TFT_RED, TFT_BLACK);
  } else {
    tft.setTextColor(TFT_CYAN, TFT_BLACK);
  }

  tft.print(b.value);
}

void drawAllBoxes(){
  for (int r = 0; r < rows; r++){
    for (int c = 0; c < cols; c++){
      int index = r * cols + c;
      int x = c * (boxW + 10) + 5;
      int y = r * (boxH + 10) + 5;
      drawBox(x, y, boxW, boxH, boxes[index]);
    }
  }
}

// ====== Setup ======
void setup(){
  Serial.begin(115200);
  delay(2000);
  Serial.println("=== ESP32 Tableau de bord météo ===");
  Serial.print("Nom du fichier source : "); Serial.println(__FILE__);

  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);

  connectWiFi();

  if(!loadWeatherConfig()){
    configWeather();
  } else {
    weatherConfigured = true;
    Serial.println("[Setup] OpenWeather déjà configuré");
    // Appel immédiat pour récupérer la météo dès le démarrage
    Serial.println("[Setup] Premier appel OpenWeather immédiat");
    updateWeather();    
  }

// Config NTP et attente de synchronisation
configTime(3600, 3600, "pool.ntp.org", "time.nist.gov");
Serial.println("[Setup] NTP configuré, attente de synchronisation...");

struct tm timeinfo;
int retry = 0;
const int retry_max = 10;
while(!getLocalTime(&timeinfo) && retry < retry_max){
    Serial.println("[Setup] En attente de la synchronisation NTP...");
    delay(1000);
    retry++;
}

if(retry == retry_max){
    Serial.println("[Setup] Attention: NTP non synchronisé !");
} else {
    Serial.println("[Setup] Heure NTP synchronisée !");
    if(weatherConfigured){
        Serial.println("[Setup] Premier appel OpenWeather immédiat");
        updateWeather();
    }
}

  drawAllBoxes();
}

// ====== Loop ======
unsigned long lastUpdate = 0;
unsigned long lastWeather = 0;

void loop() {
  server.handleClient(); // Toujours gérer le serveur

  if (wifiConfigured && weatherConfigured) {
    // Heure toutes les 10s
    if (millis() - lastUpdate > 10000) {
      lastUpdate = millis();
      struct tm timeinfo;
      if (getLocalTime(&timeinfo)) {
        snprintf(boxes[2].value, sizeof(boxes[2].value), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
      }
    }

    // Météo toutes les 10 min
    if (millis() - lastWeather > 600000) {
      lastWeather = millis();
      Serial.println("[Loop] Mise à jour météo...");
      updateWeather();
    }

    drawAllBoxes();

    // === Détection tactile ===
    uint16_t tx, ty;
    if (tft.getTouch(&tx, &ty)) {
      for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
          int index = r * cols + c;
          int boxX = c * (boxW + 10) + 5;
          int boxY = r * (boxH + 10) + 5;
          if (tx >= boxX && tx <= boxX + boxW &&
              ty >= boxY && ty <= boxY + boxH &&
              boxes[index].clickable) {

            if (strcmp(boxes[index].label, "Reset Meteo") == 0) {
              Serial.println("[Touch] Réinitialisation OpenWeather !");
              preferences.begin("openweather", false);
              preferences.clear();
              preferences.end();

              weatherConfigured = false; // la météo n'est plus configurée

              // Affiche message TFT
              tft.fillScreen(TFT_BLACK);
              tft.setTextColor(TFT_RED, TFT_BLACK);
              tft.setTextDatum(MC_DATUM); // centre le texte
              tft.drawString("Reinit Config Meteo\nConnect Wi-Fi ESP32_Config_API\nPuis 192.168.4.1", 
                             screenW/2, screenH/2, 2);
            }
          }
        }
      }
    }

    delay(200); // anti-rebond
  } 
  else if (!weatherConfigured) {
    // Affiche le message de réinitialisation en continu si besoin
    tft.fillScreen(TFT_BLACK);
    tft.setTextColor(TFT_RED, TFT_BLACK);
    tft.setTextDatum(MC_DATUM);
    tft.drawString("Reinit Config Meteo\nConnect Wi-Fi ESP32_Config_API\nPuis 192.168.4.1", 
                   screenW/2, screenH/2, 2);
  }
}

Code avec 3 écrans, météo, heure et réveil.

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <TFT_eSPI.h>
#include <TimeLib.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "time.h"
#define PAGE_MAIN     0
#define PAGE_Heure    1
#define PAGE_METEO    2
#define PAGE_Reveil   3

// ====== Variables globales ======
WebServer server(80);
Preferences preferences;
TFT_eSPI tft = TFT_eSPI();

String ssid, password;
String apiKey, lat, lon;
bool wifiConfigured = false;
bool weatherConfigured = false;

// Variables pour le réveil
int heureReveil = 7;  // Heure par défaut
int minuteReveil = 0; // Minute par défaut
bool reveilActif = false;
char timeStrReveil[6]; // Format "HH:MM" pour l'affichage du réveil

// ====== Dashboard ======
const int screenW = 480;
const int screenH = 320;
const int cols = 4;
const int rows = 4;
const int boxW = screenW / cols - 10;
const int boxH = screenH / rows - 10;

struct Box {
  const char* label;
  char value[32];
  bool clickable;
};

Box boxes[rows * cols] = {
  {"Temp Locale", "--", false},
  {"Vent", "--", false},
  {"Heure", "00:00", true},
  {"Wi-Fi", "OFF", false},
  {"Lever Soleil", "--", false},
  {"Coucher Soleil", "--", false},
  {"Humid.", "", false},
  {"Pression", "", false},
  {"", "", false},
  {"Ciel", "", false},
  {"Case11", "", false},
  {"Case12", "", false},
  {"Reveil", "", true},     // La valeur sera mise à jour avec timeStrReveil
  {"Case14", "", false},
  {"Reset Wifi", "Tap", true},
  {"Reset Meteo", "Tap", true}   // ← Case tactile

};

// ====== HTML Wi-Fi ======
const char* htmlWiFiForm = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<h2>Configurer le Wi-Fi</h2>
<form action="/save" method="POST">
  SSID:<br>
  <input type="text" name="ssid"><br>
  Mot de passe:<br>
  <input type="password" name="password"><br><br>
  <input type="submit" value="Sauvegarder">
</form>
</body>
</html>
)rawliteral";

// ====== HTML OpenWeather ======
const char* htmlApiForm = R"rawliteral(
<!DOCTYPE html>
<html>
<body>
<h2>Configurer OpenWeather</h2>
<form action="/saveApi" method="POST">
  API Key:<br>
  <input type="text" name="apikey"><br>
  Latitude:<br>
  <input type="text" name="lat"><br>
  Longitude:<br>
  <input type="text" name="lon"><br><br>
  <input type="submit" value="Sauvegarder">
</form>
</body>
</html>
)rawliteral";

// ====== Fonctions Wi-Fi ======
void saveWiFi(String s, String p){
  preferences.begin("wifi", false);
  preferences.putString("ssid", s);
  preferences.putString("password", p);
  preferences.end();
  Serial.println("[Wi-Fi] Paramètres sauvegardés en flash");
}

bool loadWiFi(){
  preferences.begin("wifi", true);
  ssid = preferences.getString("ssid", "");
  password = preferences.getString("password", "");
  preferences.end();
  Serial.print("[Wi-Fi] Chargement depuis flash -> SSID: "); Serial.println(ssid);
  return ssid != "" && password != "";
}

void setupWiFiAP(){
  WiFi.softAP("ESP32_Config");
  Serial.println("[Wi-Fi] Point d’accès activé : ESP32_Config");
  Serial.println("[Wi-Fi] Connectez-vous à 192.168.4.1 pour configurer le Wi-Fi");

  server.on("/", [](){ server.send(200, "text/html", htmlWiFiForm); });

  server.on("/save", HTTP_POST, [](){
    if(server.hasArg("ssid") && server.hasArg("password")){
      ssid = server.arg("ssid");
      password = server.arg("password");
      saveWiFi(ssid, password);
      server.send(200, "text/html", "<h2>Wi-Fi sauvegarde ok, redemarrage...</h2>");
      Serial.println("[Wi-Fi] Formulaire reçu -> redemarrage");
      delay(2000);
      ESP.restart();
    }
  });

  server.begin();
  Serial.println("[Wi-Fi] Serveur Web démarré pour configuration");
}

void connectWiFi(){
  if(loadWiFi()){
    Serial.print("[Wi-Fi] Tentative de connexion à : "); Serial.println(ssid);
    WiFi.begin(ssid.c_str(), password.c_str());
    int tries = 0;
    while(WiFi.status() != WL_CONNECTED && tries < 20){
      delay(500);
      Serial.print(".");
      tries++;
    }
    if(WiFi.status() == WL_CONNECTED){
      Serial.println("\n[Wi-Fi] Connecté avec succès!");
      wifiConfigured = true;
      strcpy(boxes[3].value, "ON");
      return;
    }
    Serial.println("\n[Wi-Fi] Échec de connexion Wi-Fi");
  }
  setupWiFiAP();
}

// ====== Fonctions OpenWeather ======
void saveWeatherConfig(String key, String latitude, String longitude){
  preferences.begin("openweather", false);
  preferences.putString("apikey", key);
  preferences.putString("lat", latitude);
  preferences.putString("lon", longitude);
  preferences.end();
  Serial.println("[OpenWeather] Config sauvegardée en flash");
}

bool loadWeatherConfig(){
  preferences.begin("openweather", true);
  apiKey = preferences.getString("apikey", "");
  lat = preferences.getString("lat", "");
  lon = preferences.getString("lon", "");
  preferences.end();
  Serial.print("[OpenWeather] Chargement depuis flash -> API: "); Serial.println(apiKey);
  return apiKey != "" && lat != "" && lon != "";
}

void configWeather(){
  WiFi.softAP("ESP32_Config_API");
  Serial.println("[OpenWeather] Point d’accès activé : ESP32_Config_API");
  Serial.println("[OpenWeather] Connectez-vous à 192.168.4.1 pour configurer OpenWeather");

  server.on("/", [](){ server.send(200, "text/html", htmlApiForm); });

  server.on("/saveApi", HTTP_POST, [](){
    if(server.hasArg("apikey") && server.hasArg("lat") && server.hasArg("lon")){
      apiKey = server.arg("apikey");
      lat = server.arg("lat");
      lon = server.arg("lon");
      saveWeatherConfig(apiKey, lat, lon);
      server.send(200, "text/html", "<h2>Config meteo sauvegarde ok, redemarrage...</h2>");
      Serial.println("[OpenWeather] Formulaire reçu -> redemarrage");
      delay(2000);
      ESP.restart();
    }
  });

  server.begin();
  Serial.println("[OpenWeather] Serveur Web démarré pour configuration");
}

// ====== OpenWeather ======
void updateWeather() {
  if (WiFi.status() != WL_CONNECTED || apiKey == "") return;

  HTTPClient http;
  String url = "https://api.openweathermap.org/data/2.5/weather?lat=" + lat + "&lon=" + lon +
               "&units=metric&appid=" + apiKey + "&lang=fr";
  Serial.print("[OpenWeather] URL: "); Serial.println(url);

  http.begin(url);
  int httpCode = http.GET();
  Serial.print("[OpenWeather] HTTP code: "); Serial.println(httpCode);

  if (httpCode == 200) {
    String payload = http.getString();
    Serial.println("[OpenWeather] JSON complet:");
    Serial.println(payload);

    StaticJsonDocument<2048> doc;
    DeserializationError error = deserializeJson(doc, payload);
    if (error) {
      Serial.print("[OpenWeather] Erreur JSON: "); Serial.println(error.c_str());
      http.end();
      return;
    }

    // Données météo
    float temp       = doc["main"]["temp"];
    float feels_like = doc["main"]["feels_like"];
    float wind       = doc["wind"]["speed"];
    int windDeg      = doc["wind"]["deg"];
    int humidity     = doc["main"]["humidity"];
    int pressure     = doc["main"]["pressure"];
    long sunrise     = doc["sys"]["sunrise"];
    long sunset      = doc["sys"]["sunset"];
    const char* city = doc["name"];
    const char* weatherDesc = doc["weather"][0]["description"];

// Conversion lever/coucher soleil en heure locale (NTP + fuseau pris en compte)
struct tm ts;
char buf[10];

time_t sunrise_ts = (time_t)sunrise;
time_t sunset_ts  = (time_t)sunset;

// Lever du soleil
localtime_r(&sunrise_ts, &ts);
strftime(buf, sizeof(buf), "%H:%M", &ts);
snprintf(boxes[4].value, sizeof(boxes[4].value), "%s", buf);

// Coucher du soleil
localtime_r(&sunset_ts, &ts);
strftime(buf, sizeof(buf), "%H:%M", &ts);
snprintf(boxes[5].value, sizeof(boxes[5].value), "%s", buf);


    // Mettre à jour le tableau de bord
    snprintf(boxes[0].value, sizeof(boxes[0].value), "%.1f°C", temp);
    snprintf(boxes[1].value, sizeof(boxes[1].value), "%.1f m/s %d°", wind, windDeg);
    snprintf(boxes[6].value, sizeof(boxes[6].value), "%d%%", humidity);
    snprintf(boxes[7].value, sizeof(boxes[7].value), "%dhPa", pressure);
    snprintf(boxes[8].value, sizeof(boxes[8].value), "%s", city);
    snprintf(boxes[9].value, sizeof(boxes[9].value), "%s", weatherDesc);

    // Traces console
    Serial.print("[OpenWeather] Température: "); Serial.println(temp);
    Serial.print("[OpenWeather] Vent: "); Serial.print(wind); Serial.print(" m/s "); Serial.println(windDeg);
    Serial.print("[OpenWeather] Humidité: "); Serial.println(humidity);
    Serial.print("[OpenWeather] Pression: "); Serial.println(pressure);
    Serial.print("[OpenWeather] Lever soleil: "); Serial.println(boxes[4].value);
    Serial.print("[OpenWeather] Coucher soleil: "); Serial.println(boxes[5].value);
    Serial.print("[OpenWeather] Ville: "); Serial.println(city);
    Serial.print("[OpenWeather] Description: "); Serial.println(weatherDesc);

  } else {
    Serial.println("[OpenWeather] Erreur HTTP ou clé API invalide");
  }

  http.end();
}

// ====== TFT ======
void drawBox(int x, int y, int w, int h, Box b){
  tft.drawRect(x, y, w, h, b.clickable ? TFT_GREEN : TFT_WHITE);  

  // ===== Label =====
  tft.setTextFont(2);
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.setCursor(x + 5, y + 5);
  tft.print(b.label);

  // ===== Valeur =====
  int fontSize = 4; // taille initiale
  tft.setTextFont(fontSize);
  int tw = tft.textWidth(b.value);

  // Réduire la taille si le texte dépasse la largeur de la case
  while(tw > w - 10 && fontSize > 1){
    fontSize--;
    tft.setTextFont(fontSize);
    tw = tft.textWidth(b.value);
  }

  int th = tft.fontHeight();
  int cx = x + (w - tw) / 2;
  int cy = y + (h - th) / 2 + th / 4;
  tft.setCursor(cx, cy);

  // === Couleur spéciale pour Wi-Fi ===
  if (strcmp(b.label, "Wi-Fi") == 0) {
    if (strcmp(b.value, "ON") == 0)
      tft.setTextColor(TFT_GREEN, TFT_BLACK);
    else
      tft.setTextColor(TFT_RED, TFT_BLACK);
  } else {
    tft.setTextColor(TFT_CYAN, TFT_BLACK);
  }

  tft.print(b.value);
}

void drawAllBoxes(){
  tft.fillScreen(TFT_BLACK);
  for (int r = 0; r < rows; r++){
    for (int c = 0; c < cols; c++){
      int index = r * cols + c;
      int x = c * (boxW + 10) + 5;
      int y = r * (boxH + 10) + 5;
      drawBox(x, y, boxW, boxH, boxes[index]);
    }
  }
}

void drawPageHeure() {
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.drawString("Page Heure", screenW/2, 20, 2);

  // Affiche l'heure (case "Heure") au centre de la page
  tft.setTextColor(TFT_CYAN, TFT_BLACK);
  tft.drawString(boxes[2].value, screenW/2, 80, 8);

  // Dessine un bouton "Retour" (rectangle + texte)
  tft.drawRect(10, screenH - 30, 80, 25, TFT_WHITE);
  tft.setTextDatum(TL_DATUM);
  tft.drawString("<- Retour", 15, screenH - 28, 2);
}

// ====== Setup ======
void setup(){
  Serial.begin(115200);
  delay(2000);
  Serial.println("=== ESP32 Tableau de bord météo ===");
  Serial.print("Nom du fichier source : "); Serial.println(__FILE__);

  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  
  // Charger les paramètres du réveil
  chargerReveil();

// Use this calibration code in setup():
  uint16_t calData[5] = { 308, 3404, 466, 3148, 3 };
  tft.setTouch(calData);

  connectWiFi();

  if(!loadWeatherConfig()){
    configWeather();
  } else {
    weatherConfigured = true;
    Serial.println("[Setup] OpenWeather déjà configuré");
    // Appel immédiat pour récupérer la météo dès le démarrage
    Serial.println("[Setup] Premier appel OpenWeather immédiat");
    updateWeather();    
  }

// Config NTP et attente de synchronisation
configTime(3600, 3600, "pool.ntp.org", "time.nist.gov");
Serial.println("[Setup] NTP configuré, attente de synchronisation...");

struct tm timeinfo;
int retry = 0;
const int retry_max = 10;
while(!getLocalTime(&timeinfo) && retry < retry_max){
    Serial.println("[Setup] En attente de la synchronisation NTP...");
    delay(1000);
    retry++;
}

if(retry == retry_max){
    Serial.println("[Setup] Attention: NTP non synchronisé !");
} else {
    Serial.println("[Setup] Heure NTP synchronisée !");
    if(weatherConfigured){
        Serial.println("[Setup] Premier appel OpenWeather immédiat");
        updateWeather();
    }
}

  drawAllBoxes();
}

// ====== Fonctions Réveil ======
void sauvegarderReveil() {
  preferences.begin("reveil", false);
  preferences.putInt("heure", heureReveil);
  preferences.putInt("minute", minuteReveil);
  preferences.putBool("actif", reveilActif);
  preferences.end();
}

void chargerReveil() {
  preferences.begin("reveil", true);
  heureReveil = preferences.getInt("heure", 7);
  minuteReveil = preferences.getInt("minute", 0);
  reveilActif = preferences.getBool("actif", false);
  preferences.end();
  
  // Formater l'heure du réveil
  snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
}

void drawPageReveil() {
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.drawString("Reglage Reveil", screenW/2, 20, 2);
  
  // Affichage de l'heure de réveil
  char timeStr[6];
  snprintf(timeStr, sizeof(timeStr), "%02d:%02d", heureReveil, minuteReveil);
  tft.setTextColor(TFT_CYAN, TFT_BLACK);
  tft.drawString(timeStr, screenW/2, 140, 8);
  
  // Boutons + et - pour les heures
  tft.drawRect(screenW/2 - 170, 150, 40, 40, TFT_WHITE);
  tft.drawString("-", screenW/2 - 150, 170, 4);
  tft.drawRect(screenW/2 - 170, 100, 40, 40, TFT_WHITE);
  tft.drawString("+", screenW/2 - 150, 120, 4);
  
  // Boutons + et - pour les minutes
  tft.drawRect(screenW/2 + 130, 150, 40, 40, TFT_WHITE);
  tft.drawString("-", screenW/2 + 150, 170, 4);
  tft.drawRect(screenW/2 + 130, 100, 40, 40, TFT_WHITE);
  tft.drawString("+", screenW/2 + 150, 120, 4);
  
  // Bouton ON/OFF
  tft.drawRect(screenW/2 - 50, 240, 100, 40, TFT_WHITE);
  tft.setTextColor(reveilActif ? TFT_GREEN : TFT_RED, TFT_BLACK);
  tft.drawString(reveilActif ? "ON" : "OFF", screenW/2, 260, 4);
  
  // Bouton Retour
  tft.drawRect(10, screenH - 30, 80, 25, TFT_WHITE);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(TL_DATUM);
  tft.drawString("<- Retour", 15, screenH - 28, 2);
}

// ====== Loop ======
unsigned long lastUpdate = 0;
unsigned long lastWeather = 0;
unsigned long lastMessageDisplay = 0;

void loop() {
  server.handleClient();
  static int currentPage = PAGE_MAIN;
  static bool pageChanged = true; // Flag pour éviter les redessins inutiles

  // Vérification du réveil toutes les minutes
  if (reveilActif) {
    struct tm timeinfo;
    if (getLocalTime(&timeinfo)) {
      if (timeinfo.tm_hour == heureReveil && timeinfo.tm_min == minuteReveil) {
        // Ajouter ici votre code pour l'alarme (buzzer, LED, etc.)
      }
    }
  }

  // === Gestion de l'affichage ===
  if (pageChanged) {
    switch (currentPage) {
      case PAGE_MAIN:
        drawAllBoxes();
        break;
      case PAGE_Heure:
        drawPageHeure();
        break;
      case PAGE_Reveil:
        drawPageReveil();
        break;
    }
    pageChanged = false;
  }

  if (wifiConfigured && weatherConfigured) {
    // Heure toutes les 10s
    if (millis() - lastUpdate > 10000) {
      lastUpdate = millis();
      struct tm timeinfo;
      if (getLocalTime(&timeinfo)) {
        snprintf(boxes[2].value, sizeof(boxes[2].value), "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
        pageChanged = true; // <--- AJOUT : force le rafraîchissement
      }
    }
    // Météo toutes les 10 min
    if (millis() - lastWeather > 600000) {
      lastWeather = millis();
      Serial.println("[Loop] Mise à jour météo...");
      updateWeather();
      pageChanged = true; // <--- AJOUT : force le rafraîchissement
    }

    // === Détection tactile ===
    uint16_t tx, ty;
    if (tft.getTouch(&tx, &ty)) {
      // Vérifie d'abord le bouton "Retour" sur la page Heure
      if (currentPage == PAGE_Heure && tx >= 10 && tx <= 90 && ty >= (screenH - 30) && ty <= screenH) {
        currentPage = PAGE_MAIN;
        pageChanged = true;
        delay(200);
      }
      // Sinon, vérifie les autres boxes
      else {
        for (int r = 0; r < rows; r++) {
          for (int c = 0; c < cols; c++) {
            int index = r * cols + c;
            int boxX = c * (boxW + 10) + 5;
            int boxY = r * (boxH + 10) + 5;
            if (tx >= boxX && tx <= boxX + boxW &&
                ty >= boxY && ty <= boxY + boxH &&
                boxes[index].clickable) {
              // Action pour Heure
              if (strcmp(boxes[index].label, "Heure") == 0) {
                currentPage = PAGE_Heure;
                pageChanged = true;
                delay(200);
              }
              // Action pour Reveil
              else if (strcmp(boxes[index].label, "Reveil") == 0) {
                currentPage = PAGE_Reveil;
                pageChanged = true;
                delay(200);
              }
              // Action pour "Reset Meteo" -> efface la config meteo puis redémarre l'ESP
              else if (strcmp(boxes[index].label, "Reset Meteo") == 0) {
                Serial.println("[Touch] Réinitialisation OpenWeather ! -- redemarrage");
                preferences.begin("openweather", false);
                preferences.clear();
                preferences.end();
                weatherConfigured = false;
                tft.fillScreen(TFT_BLACK);
                tft.setTextColor(TFT_RED, TFT_BLACK);
                tft.setTextDatum(MC_DATUM);
                tft.drawString("Redémmarrage de l'ESP pour reconfigurer la météo",
                               screenW/2, screenH/2, 2);
                delay(2000);
                ESP.restart();
              }
              // Action pour "Reset Wifi" -> efface la config wifi puis redémarre l'ESP
              else if (strcmp(boxes[index].label, "Reset Wifi") == 0) {
                Serial.println("[Touch] Réinitialisation Wifi ! -- redemarrage");
                preferences.begin("wifi", false);
                preferences.clear();
                preferences.end();
                weatherConfigured = false;
                tft.fillScreen(TFT_BLACK);
                tft.setTextColor(TFT_RED, TFT_BLACK);
                tft.setTextDatum(MC_DATUM);
                tft.drawString("Redémmarrage de l'ESP pour reconfigurer le wifi",
                               screenW/2, screenH/2, 2);
                delay(2000);
                ESP.restart();
            }
          }
        }
      }
      if (currentPage == PAGE_Reveil) {
        if (tx >= 10 && tx <= 90 && ty >= (screenH - 30) && ty <= screenH) {
          // Bouton Retour
          currentPage = PAGE_MAIN;
          pageChanged = true;
        sauvegarderReveil();
        } else if (tx >= screenW/2 - 170 && tx <= screenW/2 - 130) {
          if (ty >= 100 && ty <= 140) {
            // Augmenter les heures
            heureReveil = (heureReveil + 1) % 24;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          } else if (ty >= 150 && ty <= 190) {
            // Diminuer les heures
            heureReveil = (heureReveil - 1 + 24) % 24;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          }
        } else if (tx >= screenW/2 + 130 && tx <= screenW/2 + 170) {
          if (ty >= 100 && ty <= 140) {
            // Augmenter les minutes
            minuteReveil = (minuteReveil + 1) % 60;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          } else if (ty >= 150 && ty <= 190) {
            // Diminuer les minutes
            minuteReveil = (minuteReveil - 1 + 60) % 60;
            snprintf(timeStrReveil, sizeof(timeStrReveil), "%02d:%02d", heureReveil, minuteReveil);
            strcpy(boxes[12].value, timeStrReveil);  // Mise à jour de la case réveil
            pageChanged = true;
          }
        } else if (tx >= screenW/2 - 50 && tx <= screenW/2 + 50 && 
                   ty >= 240 && ty <= 280) {
          // Toggle ON/OFF
          reveilActif = !reveilActif;
          pageChanged = true;
        }
      }
      delay(200); // anti-rebond
    }
  }
  else if (!weatherConfigured) {
    // Affiche le message de réinitialisation seulement toutes les 5 secondes
    if (millis() - lastMessageDisplay > 5000) {
      lastMessageDisplay = millis();
      tft.fillScreen(TFT_BLACK);
      tft.setTextColor(TFT_RED, TFT_BLACK);
      tft.setTextDatum(MC_DATUM);
      tft.drawString("Reinit Config Meteo\nConnect Wi-Fi ESP32_Config_API\nPuis 192.168.4.1",
                     screenW/2, screenH/2, 2);
      }
    }
  }
}