Module ESP32-écran avec données météo publiques
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},
{"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 = "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"
else if (strcmp(boxes[index].label, "Reset Meteo") == 0) {
Serial.println("[Touch] Réinitialisation OpenWeather !");
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("Reinit Config Meteo\nConnect Wi-Fi ESP32_Config_API\nPuis 192.168.4.1",
screenW/2, screenH/2, 2);
}
}
}
}
}
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);
}
}
}