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.