Files
ESP32-P10-RGB-32x16-Digital…/examples/fullscreen_countdown_web.cpp
T
Clément SAILLANT f575dfb64d gpt5
2025-08-15 20:45:56 +02:00

1295 lines
43 KiB
C++

/**
* ESP32 P10 RGB Fullscreen Countdown avec Interface Web
* Compte à rebours plein écran avec réglages via interface web
*
* Ce programme affiche un compte à rebours en plein écran et permet de configurer :
* - La date/heure cible du compte à rebours
* - La couleur du texte
* - La taille de police
*
* L'interface web est accessible à l'adresse http://ip_de_l'esp32/
*
* Auteur: GitHub Copilot
* Date: Août 2025
*/
#define PxMATRIX_SPI_FREQUENCY 10000000
#include <Arduino.h>
#include <PxMatrix.h>
#include <RTClib.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeMono9pt7b.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <DNSServer.h>
#include <time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
// Pins pour la matrice LED
#define P_LAT 5
#define P_A 19
#define P_B 23
#define P_C 18
#define P_OE 4
// Définition des priorités des tâches (ajustées pour éviter les conflits WiFi)
#define TASK_DISPLAY_PRIORITY 2
#define TASK_WEBSERVER_PRIORITY 1
#define TASK_COUNTDOWN_PRIORITY 1
#define TASK_NETWORK_PRIORITY 3 // Priorité plus élevée pour le réseau
// Taille des stacks pour les tâches
#define TASK_DISPLAY_STACK 4096
#define TASK_WEBSERVER_STACK 4096
#define TASK_COUNTDOWN_STACK 2048
#define TASK_NETWORK_STACK 4096
// Handles pour les tâches
TaskHandle_t displayTaskHandle = NULL;
TaskHandle_t webServerTaskHandle = NULL;
TaskHandle_t countdownTaskHandle = NULL;
TaskHandle_t networkTaskHandle = NULL;
// Mutex pour protéger les ressources partagées
SemaphoreHandle_t displayMutex;
SemaphoreHandle_t countdownMutex;
SemaphoreHandle_t preferencesMutex;
// Configuration des panneaux - définie par les build flags
#ifndef MATRIX_WIDTH
#define MATRIX_WIDTH 32
#endif
#ifndef MATRIX_HEIGHT
#define MATRIX_HEIGHT 16
#endif
#ifndef MATRIX_PANELS_X
#define MATRIX_PANELS_X 3
#endif
#ifndef MATRIX_PANELS_Y
#define MATRIX_PANELS_Y 1
#endif
// Calcul des dimensions totales
#define TOTAL_WIDTH (MATRIX_WIDTH * MATRIX_PANELS_X)
#define TOTAL_HEIGHT (MATRIX_HEIGHT * MATRIX_PANELS_Y)
// Configuration du timer
hw_timer_t * timer = nullptr;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
// Temps d'affichage ajusté selon le nombre de panneaux
uint8_t display_draw_time = (MATRIX_PANELS_X * MATRIX_PANELS_Y > 4) ? 20 : 30;
// Objet matrice
PxMATRIX display(TOTAL_WIDTH, TOTAL_HEIGHT, P_LAT, P_OE, P_A, P_B, P_C);
// RTC
RTC_DS3231 rtc;
// Préférences
Preferences preferences;
// Configuration WiFi - Modifiez selon vos besoins
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// Configuration Point d'accès (si pas de WiFi)
// DNS pour le portail captif
const byte DNS_PORT = 53;
DNSServer dnsServer;
const char* ap_ssid = "ESP32_Countdown";
const char* ap_password = "countdown123";
// Clé de sécurité pour l'interface web
#define KEY_TXT "countdown2025"
// Mode de fonctionnement WiFi
bool useStationMode = false; // true = se connecter au WiFi, false = créer un point d'accès
// Serveur web
WebServer server(80);
// Couleurs prédéfinies
uint16_t myRED = display.color565(255, 0, 0);
uint16_t myGREEN = display.color565(0, 255, 0);
uint16_t myBLUE = display.color565(0, 0, 255);
uint16_t myYELLOW = display.color565(255, 255, 0);
uint16_t myCYAN = display.color565(0, 255, 255);
uint16_t myMAGENTA = display.color565(255, 0, 255);
uint16_t myWHITE = display.color565(255, 255, 255);
uint16_t myBLACK = display.color565(0, 0, 0);
uint16_t myORANGE = display.color565(255, 165, 0);
// Prototypes des fonctions
void IRAM_ATTR display_updater();
// Utilitaire : copie tronquée en respectant les limites UTF-8 (nombre de caractères et non octets)
// maxChars : nombre maximum de caractères (glyphes) à conserver (excluant le nul final)
// destSize : taille du buffer destination (incluant place pour nul)
size_t utf8SafeCopyTruncate(const String &src, char *dest, size_t destSize, size_t maxChars) {
if (destSize == 0) return 0;
const uint8_t *bytes = (const uint8_t*)src.c_str();
size_t i = 0; // index source en octets
size_t o = 0; // index destination
size_t chars = 0; // compte de caractères
while (bytes[i] != '\0' && chars < maxChars && o + 1 < destSize) {
uint8_t c = bytes[i];
size_t seqLen = 1;
if ((c & 0x80) == 0x00) seqLen = 1; // 0xxxxxxx
else if ((c & 0xE0) == 0xC0) seqLen = 2; // 110xxxxx
else if ((c & 0xF0) == 0xE0) seqLen = 3; // 1110xxxx
else if ((c & 0xF8) == 0xF0) seqLen = 4; // 11110xxx
else { // octet invalide -> arrêter
break;
}
// Vérifier que la séquence tient dans la source
for (size_t k = 1; k < seqLen; ++k) {
uint8_t nc = bytes[i + k];
if ((nc & 0xC0) != 0x80) { // pas une continuation valide
seqLen = 1; // tronquer au premier octet
break;
}
}
// Vérifier que ça rentre dans dest
if (o + seqLen + 1 >= destSize) break;
// Copier
for (size_t k = 0; k < seqLen; ++k) {
dest[o++] = (char)bytes[i + k];
}
i += seqLen;
chars++;
}
dest[o] = '\0';
return o; // octets copiés
}
// Prototypes des tâches
void DisplayTask(void * parameter);
void WebServerTask(void * parameter);
void CountdownTask(void * parameter);
void NetworkTask(void * parameter);
// Variables pour le countdown
DateTime countdownTarget;
bool countdownExpired = false;
bool blinkLastSeconds = false; // Clignotement pour les 10 dernières secondes
bool blinkState = true;
unsigned long lastBlinkTime = 0;
const int BLINK_INTERVAL = 500; // Intervalle de clignotement 0.5 seconde
// Variable pour indiquer qu'il faut sauvegarder les paramètres
volatile bool saveRequested = false;
volatile unsigned long saveRequestTime = 0;
// Paramètres configurables via l'interface web
int countdownYear = 2025;
int countdownMonth = 12;
int countdownDay = 31;
int countdownHour = 23;
int countdownMinute = 59;
int countdownSecond = 0;
char countdownTitle[51] = "COUNTDOWN";
int countdownTextSize = 2; // 1-3
int countdownColorR = 0;
int countdownColorG = 255;
int countdownColorB = 0;
int fontIndex = 0; // 0=Default, 1=Sans, 2=Sans Bold, 3=Mono
uint16_t countdownColor;
// Format d'affichage (0=jours, 1=heures, 2=minutes, 3=secondes uniquement)
int displayFormat = 0;
// HTML Page
const char MAIN_page[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>ESP32 P10 Countdown Configuration</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
h1 {
color: #009688;
text-align: center;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, select {
width: 100%;
padding: 8px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 4px;
}
input[type="color"] {
height: 40px;
}
button {
background-color: #009688;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
display: block;
width: 100%;
margin-top: 10px;
}
button:hover {
background-color: #00796b;
}
.section {
border-top: 1px solid #eee;
padding-top: 15px;
margin-top: 15px;
}
.color-picker {
display: flex;
align-items: center;
}
.color-preview {
width: 30px;
height: 30px;
margin-left: 10px;
border: 1px solid #ccc;
}
#countdown-preview {
text-align: center;
font-size: 22px;
margin: 15px 0;
padding: 10px;
border-radius: 4px;
background-color: #000;
color: #0f0;
}
.footer {
text-align: center;
margin-top: 20px;
font-size: 14px;
color: #777;
}
</style>
</head>
<body>
<div class="container">
<h1>Configuration du Compte à Rebours</h1>
<div id="countdown-preview">12:34:56</div>
<form action="/settings" method="POST" id="settingsForm">
// Champ clé supprimé
<div class="section">
<h2>Date et Heure Cible</h2>
<div class="form-group">
<label for="title">Titre du Compte à Rebours:</label>
<input type="text" id="title" name="title" maxlength="50" required>
</div>
<div class="form-group">
<label for="date">Date:</label>
<input type="date" id="date" name="date" required>
</div>
<div class="form-group">
<label for="time">Heure:</label>
<input type="time" id="time" name="time" required step="1">
</div>
</div>
<div class="section">
<h2>Apparence</h2>
<div class="form-group">
<label for="fontIndex">Police:</label>
<select id="fontIndex" name="fontIndex">
<option value="0">Standard</option>
<option value="1">Sans Serif</option>
<option value="2">Sans Serif Bold</option>
<option value="3">Monospace</option>
</select>
</div>
<div class="form-group">
<label for="textSize">Taille du texte:</label>
<select id="textSize" name="textSize">
<option value="1">Petite</option>
<option value="2" selected>Moyenne</option>
<option value="3">Grande</option>
</select>
</div>
<div class="form-group">
<label for="colorPicker">Couleur:</label>
<div class="color-picker">
<input type="color" id="colorPicker" value="#00ff00">
<div class="color-preview" id="colorPreview"></div>
</div>
<input type="hidden" id="colorR" name="colorR" value="0">
<input type="hidden" id="colorG" name="colorG" value="255">
<input type="hidden" id="colorB" name="colorB" value="0">
</div>
</div>
<button type="submit">Enregistrer</button>
<button type="button" onclick="resetCountdown()">Réinitialiser</button>
</form>
<div class="footer">
ESP32 P10 RGB Fullscreen Countdown &copy; 2025
</div>
</div>
<script>
// Fonctions pour convertir couleur hex en RGB
function hexToRgb(hex) {
// Supprimer le # si présent
hex = hex.replace('#', '');
// Convertir en RGB
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return {r, g, b};
}
// Fonctions pour convertir RGB en hex
function rgbToHex(r, g, b) {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
// Mettre à jour les champs cachés RGB quand la couleur change
document.getElementById('colorPicker').addEventListener('input', function() {
const color = hexToRgb(this.value);
document.getElementById('colorR').value = color.r;
document.getElementById('colorG').value = color.g;
document.getElementById('colorB').value = color.b;
document.getElementById('colorPreview').style.backgroundColor = this.value;
document.getElementById('countdown-preview').style.color = this.value;
});
// Mettre à jour la prévisualisation du compte à rebours
document.getElementById('textSize').addEventListener('change', function() {
const size = this.value;
document.getElementById('countdown-preview').style.fontSize = (16 + (size * 6)) + 'px';
});
// Fonction pour réinitialiser le compte à rebours
function resetCountdown() {
if (confirm("Voulez-vous vraiment réinitialiser le compte à rebours ?")) {
document.getElementById('settingsForm').action = "/reset";
document.getElementById('settingsForm').submit();
}
}
// Charger les valeurs actuelles au chargement de la page
window.onload = function() {
// Définir la date courante + 1 jour comme valeur par défaut
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(tomorrow.getDate() + 1);
const dateStr = tomorrow.toISOString().split('T')[0];
document.getElementById('date').value = dateStr;
const timeStr = '23:59:59';
document.getElementById('time').value = timeStr;
// Charger les valeurs depuis l'ESP32
fetch('/getSettings')
.then(response => response.json())
.then(data => {
document.getElementById('title').value = data.title;
// Format date YYYY-MM-DD
const dateStr = `${data.year}-${String(data.month).padStart(2, '0')}-${String(data.day).padStart(2, '0')}`;
document.getElementById('date').value = dateStr;
// Format heure HH:MM:SS
const timeStr = `${String(data.hour).padStart(2, '0')}:${String(data.minute).padStart(2, '0')}:${String(data.second).padStart(2, '0')}`;
document.getElementById('time').value = timeStr;
document.getElementById('fontIndex').value = data.fontIndex;
document.getElementById('textSize').value = data.textSize;
// Mettre à jour la couleur
const hexColor = rgbToHex(data.colorR, data.colorG, data.colorB);
document.getElementById('colorPicker').value = hexColor;
document.getElementById('colorPreview').style.backgroundColor = hexColor;
document.getElementById('countdown-preview').style.color = hexColor;
document.getElementById('colorR').value = data.colorR;
document.getElementById('colorG').value = data.colorG;
document.getElementById('colorB').value = data.colorB;
// Mettre à jour le preview
document.getElementById('countdown-preview').style.fontSize = (16 + (data.textSize * 6)) + 'px';
})
.catch(error => {
console.error('Erreur:', error);
});
};
</script>
</body>
</html>
)rawliteral";
// Note: La fonction display_update_enable() a été intégrée dans setup()
// pour une meilleure gestion avec FreeRTOS
// Calcul du temps restant
void updateCountdown(int &days, int &hours, int &minutes, int &seconds) {
DateTime now = rtc.now();
// Vérifier si le countdown est expiré
if (now >= countdownTarget) {
countdownExpired = true;
days = hours = minutes = 0;
seconds = 0;
return;
}
countdownExpired = false;
// Calculer la différence
TimeSpan diff = countdownTarget - now;
long totalSeconds = diff.totalseconds();
days = totalSeconds / 86400;
hours = (totalSeconds % 86400) / 3600;
minutes = (totalSeconds % 3600) / 60;
seconds = totalSeconds % 60;
// Activer le clignotement pour les 10 dernières secondes
blinkLastSeconds = (days == 0 && hours == 0 && minutes == 0 && seconds <= 10);
}
// Détermine le format d'affichage en fonction du temps restant
void updateDisplayFormat(int days, int hours, int minutes, int seconds) {
if (days > 0) {
displayFormat = 0; // Format jours
} else if (hours > 0) {
displayFormat = 1; // Format heures
} else if (minutes > 0) {
displayFormat = 2; // Format minutes
} else {
displayFormat = 3; // Format secondes uniquement
}
}
// Fonction pour obtenir la largeur du texte
uint16_t getTextWidth(const char* text, const GFXfont* font = NULL) {
int16_t x1, y1;
uint16_t w, h;
if (font) display.setFont(font);
display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
display.setFont(); // Reset to default font
return w;
}
// Dessine les deux points du séparateur d'horloge
void drawColon(int16_t x, int16_t y, uint16_t colonColor) {
display.fillRect(x, y, 2, 2, colonColor);
display.fillRect(x, y+4, 2, 2, colonColor);
}
// Obtenir la police en fonction de l'index
const GFXfont* getFontByIndex(int index) {
switch (index) {
case 1:
return &FreeSans9pt7b;
case 2:
return &FreeSansBold9pt7b;
case 3:
return &FreeMono9pt7b;
default:
return NULL; // Police par défaut
}
}
// Affichage du compte à rebours en plein écran
void displayFullscreenCountdown(int days, int hours, int minutes, int seconds) {
// Protection contre concurrence avec l'ISR d'affichage
portENTER_CRITICAL(&timerMux);
display.clearDisplay();
// Choisir la couleur
uint16_t displayColor = countdownColor;
// Si on est dans les 10 dernières secondes et qu'il faut clignoter
if (blinkLastSeconds) {
unsigned long currentTime = millis();
if (currentTime - lastBlinkTime >= BLINK_INTERVAL) {
lastBlinkTime = currentTime;
blinkState = !blinkState;
}
if (!blinkState) {
displayColor = myBLACK;
} else {
displayColor = myRED;
}
}
// Si le countdown est expiré
if (countdownExpired) {
const GFXfont *font = getFontByIndex(fontIndex);
display.setFont(font); // peut être NULL => police par défaut
// Calcul largeur avec police choisie
const char* endMsg = countdownTitle;
int maxFittingSize = 1;
// Pour police par défaut seulement on applique TextSize scaling; pour GFX font on garde size=1
if (!font) {
for (int s = 1; s <= 5; s++) {
display.setTextSize(s);
int w = getTextWidth(endMsg, font);
int h = 8 * s;
if (w < TOTAL_WIDTH && h < TOTAL_HEIGHT) maxFittingSize = s;
}
// Adapter selon préférence utilisateur (countdownTextSize 1..3)
int desiredTier = countdownTextSize; // 1 petit, 2 moyen, 3 grand
int chosen = maxFittingSize; // base grand
if (desiredTier == 1) {
chosen = max(1, maxFittingSize - 2);
} else if (desiredTier == 2) {
chosen = max(1, maxFittingSize - 1);
}
display.setTextSize(chosen);
// Calcul précis via getTextBounds
int16_t x1, y1; uint16_t w, h;
display.getTextBounds(endMsg, 0, 0, &x1, &y1, &w, &h);
int textX = (TOTAL_WIDTH - w) / 2 - x1;
int textY = (TOTAL_HEIGHT - h) / 2 - y1; // y1 souvent négatif
display.setCursor(textX, textY);
} else {
// GFX font: pas de variation de setTextSize fiable -> centrer simplement
display.setTextSize(1);
int16_t x1, y1; uint16_t w, h;
display.getTextBounds(endMsg, 0, 0, &x1, &y1, &w, &h);
int16_t textX = (TOTAL_WIDTH - w) / 2 - x1;
int16_t textY = (TOTAL_HEIGHT - h) / 2 - y1;
display.setCursor(textX, textY);
}
display.setTextColor(myRED);
display.print(endMsg);
portEXIT_CRITICAL(&timerMux);
return;
}
char countdownText[20];
int16_t textWidth, textX, textY;
int maxSize = 1;
// Déterminer le texte à afficher
switch (displayFormat) {
case 0:
sprintf(countdownText, "%dd %02d:%02d", days, hours, minutes);
break;
case 1:
sprintf(countdownText, "%02d:%02d:%02d", hours, minutes, seconds);
break;
case 2:
sprintf(countdownText, "%02d:%02d", minutes, seconds);
break;
case 3:
sprintf(countdownText, "%02d", seconds);
break;
}
const GFXfont *font = getFontByIndex(fontIndex);
display.setFont(font);
// Déterminer taille maximale (seulement pour police par défaut)
int maxFittingSize = 1;
if (!font) {
for (int s = 1; s <= 5; s++) {
display.setTextSize(s);
int w = getTextWidth(countdownText, font);
int h = 8 * s;
if (w < TOTAL_WIDTH && h < TOTAL_HEIGHT) maxFittingSize = s;
}
int desiredTier = countdownTextSize; // 1..3
int chosen = maxFittingSize;
if (desiredTier == 1) chosen = max(1, maxFittingSize - 2);
else if (desiredTier == 2) chosen = max(1, maxFittingSize - 1);
display.setTextSize(chosen);
int16_t x1, y1; uint16_t w, h;
display.getTextBounds(countdownText, 0, 0, &x1, &y1, &w, &h);
textX = (TOTAL_WIDTH - w) / 2 - x1;
textY = (TOTAL_HEIGHT - h) / 2 - y1;
} else {
display.setTextSize(1); // scaling custom font souvent ignoré
int16_t x1, y1; uint16_t w, h;
display.getTextBounds(countdownText, 0, 0, &x1, &y1, &w, &h);
textX = (TOTAL_WIDTH - w) / 2 - x1;
textY = (TOTAL_HEIGHT - h) / 2 - y1;
}
display.setTextColor(displayColor);
display.setCursor(textX, textY);
display.print(countdownText);
// Le titre n'est plus affiché pendant le compte à rebours
portEXIT_CRITICAL(&timerMux);
}
// Chargement des paramètres depuis la mémoire flash (version thread-safe)
void loadSettings() {
// Utiliser un timeout pour éviter les blocages
if (preferencesMutex == NULL || xSemaphoreTake(preferencesMutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
Serial.println("Failed to acquire preferences mutex - using defaults");
return;
}
// Prendre aussi le mutex countdown pour cohérence des paramètres
if (countdownMutex == NULL || xSemaphoreTake(countdownMutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
Serial.println("Failed to acquire countdown mutex - using defaults");
xSemaphoreGive(preferencesMutex);
return;
}
bool success = false;
do {
if (!preferences.begin("countdown", true)) {
Serial.println("Failed to begin preferences for reading");
break;
}
// Paramètres du countdown
countdownYear = preferences.getInt("cd_Year", 2025);
countdownMonth = preferences.getInt("cd_Month", 12);
countdownDay = preferences.getInt("cd_Day", 31);
countdownHour = preferences.getInt("cd_Hour", 23);
countdownMinute = preferences.getInt("cd_Minute", 59);
countdownSecond = preferences.getInt("cd_Second", 0);
// Paramètres d'affichage
countdownTextSize = preferences.getInt("textSize", 2);
countdownColorR = preferences.getInt("colorR", 0);
countdownColorG = preferences.getInt("colorG", 255);
countdownColorB = preferences.getInt("colorB", 0);
fontIndex = preferences.getInt("fontIndex", 0);
String savedTitle = preferences.getString("cd_Title", "COUNTDOWN");
strcpy(countdownTitle, savedTitle.c_str());
preferences.end();
success = true;
} while(false);
// Libérer le mutex
xSemaphoreGive(preferencesMutex);
xSemaphoreGive(countdownMutex);
if (success) {
Serial.println("Settings loaded successfully");
} else {
Serial.println("Using default settings");
}
// Mise à jour de la couleur
countdownColor = display.color565(countdownColorR, countdownColorG, countdownColorB);
// Mise à jour de la date cible
countdownTarget = DateTime(countdownYear, countdownMonth, countdownDay,
countdownHour, countdownMinute, countdownSecond);
}
// Sauvegarde des paramètres dans la mémoire flash (version thread-safe)
void saveSettings() {
// Utiliser un timeout pour éviter les blocages
if (preferencesMutex == NULL || xSemaphoreTake(preferencesMutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
Serial.println("Failed to acquire preferences mutex - settings not saved");
return;
}
if (countdownMutex == NULL || xSemaphoreTake(countdownMutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
Serial.println("Failed to acquire countdown mutex - settings not saved");
xSemaphoreGive(preferencesMutex);
return;
}
// Utiliser un try-catch style pour s'assurer que le mutex est libéré
bool success = false;
do {
if (!preferences.begin("countdown", false)) {
Serial.println("Failed to begin preferences");
break;
}
// Paramètres du countdown
preferences.putInt("cd_Year", countdownYear);
preferences.putInt("cd_Month", countdownMonth);
preferences.putInt("cd_Day", countdownDay);
preferences.putInt("cd_Hour", countdownHour);
preferences.putInt("cd_Minute", countdownMinute);
preferences.putInt("cd_Second", countdownSecond);
// Paramètres d'affichage
preferences.putInt("textSize", countdownTextSize);
preferences.putInt("colorR", countdownColorR);
preferences.putInt("colorG", countdownColorG);
preferences.putInt("colorB", countdownColorB);
preferences.putInt("fontIndex", fontIndex);
preferences.putString("cd_Title", String(countdownTitle));
preferences.end();
success = true;
} while(false);
// Libérer le mutex
xSemaphoreGive(preferencesMutex);
xSemaphoreGive(countdownMutex);
if (success) {
Serial.println("Settings saved successfully");
// Mise à jour de la couleur
countdownColor = display.color565(countdownColorR, countdownColorG, countdownColorB);
// Mise à jour de la date cible
countdownTarget = DateTime(countdownYear, countdownMonth, countdownDay,
countdownHour, countdownMinute, countdownSecond);
} else {
Serial.println("Failed to save settings");
}
}
// Connexion WiFi
void connecting_To_WiFi() {
Serial.println("\n-------------WIFI mode");
Serial.println("WIFI mode : STA");
WiFi.mode(WIFI_STA);
Serial.println("-------------");
delay(1000);
Serial.println("\n-------------Connection");
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
int connecting_process_timed_out = 40; // 20 secondes timeout
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
if(connecting_process_timed_out > 0) connecting_process_timed_out--;
if(connecting_process_timed_out == 0) {
Serial.println("\nFailed to connect to WiFi. Switching to AP mode.");
useStationMode = false;
break;
}
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected");
Serial.print("Successfully connected to : ");
Serial.println(ssid);
Serial.print("IP address : ");
Serial.println(WiFi.localIP());
}
Serial.println("-------------");
}
// Configuration du point d'accès
void set_ESP32_Access_Point() {
Serial.println("\n-------------");
Serial.println("WIFI mode : AP");
WiFi.mode(WIFI_AP);
Serial.println("-------------");
delay(1000);
Serial.println("\n-------------");
Serial.println("Setting up ESP32 to be an Access Point.");
WiFi.softAP(ap_ssid, ap_password);
delay(1000);
IPAddress local_ip(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
WiFi.softAPConfig(local_ip, gateway, subnet);
Serial.println("-------------");
Serial.print("SSID name : ");
Serial.println(ap_ssid);
Serial.print("IP address : ");
Serial.println(WiFi.softAPIP());
delay(1000);
}
// Gestionnaire de la page principale
void handleRoot() {
server.send(200, "text/html", MAIN_page);
}
// Gestionnaire des paramètres actuels en JSON
void handleGetSettings() {
String json = "{";
json += "\"title\":\"" + String(countdownTitle) + "\",";
json += "\"year\":" + String(countdownYear) + ",";
json += "\"month\":" + String(countdownMonth) + ",";
json += "\"day\":" + String(countdownDay) + ",";
json += "\"hour\":" + String(countdownHour) + ",";
json += "\"minute\":" + String(countdownMinute) + ",";
json += "\"second\":" + String(countdownSecond) + ",";
json += "\"textSize\":" + String(countdownTextSize) + ",";
json += "\"colorR\":" + String(countdownColorR) + ",";
json += "\"colorG\":" + String(countdownColorG) + ",";
json += "\"colorB\":" + String(countdownColorB) + ",";
json += "\"fontIndex\":" + String(fontIndex);
json += "}";
server.send(200, "application/json", json);
}
// Gestionnaire des paramètres
void handleSettings() {
Serial.println("\n-------------Settings");
// Récupérer les valeurs de la requête
String title = server.hasArg("title") ? server.arg("title") : String("");
String dateStr = server.hasArg("date") ? server.arg("date") : String("");
String timeStr = server.hasArg("time") ? server.arg("time") : String("");
if (dateStr.length() < 10 || timeStr.length() < 5) {
server.send(400, "text/plain", "Parametres invalides");
return;
}
// Parser la date (format YYYY-MM-DD)
// Protéger modifications
if (countdownMutex != NULL && xSemaphoreTake(countdownMutex, pdMS_TO_TICKS(200))) {
countdownYear = dateStr.substring(0, 4).toInt();
countdownMonth = dateStr.substring(5, 7).toInt();
countdownDay = dateStr.substring(8, 10).toInt();
// Parser l'heure (format HH:MM:SS)
countdownHour = timeStr.substring(0, 2).toInt();
countdownMinute = timeStr.substring(3, 5).toInt();
countdownSecond = timeStr.length() > 5 ? timeStr.substring(6, 8).toInt() : 0;
// Récupérer les autres paramètres
countdownTextSize = server.hasArg("textSize") ? server.arg("textSize").toInt() : countdownTextSize;
countdownColorR = server.hasArg("colorR") ? server.arg("colorR").toInt() : countdownColorR;
countdownColorG = server.hasArg("colorG") ? server.arg("colorG").toInt() : countdownColorG;
countdownColorB = server.hasArg("colorB") ? server.arg("colorB").toInt() : countdownColorB;
fontIndex = server.hasArg("fontIndex") ? server.arg("fontIndex").toInt() : fontIndex;
// Limiter les valeurs
if (countdownTextSize < 1) countdownTextSize = 1;
if (countdownTextSize > 3) countdownTextSize = 3;
if (fontIndex < 0) fontIndex = 0;
if (fontIndex > 3) fontIndex = 3;
// Mettre à jour le titre
if (title.length() > 0) {
utf8SafeCopyTruncate(title, countdownTitle, sizeof(countdownTitle), 50);
}
// Valider la date
if (countdownMonth < 1) countdownMonth = 1;
if (countdownMonth > 12) countdownMonth = 12;
if (countdownDay < 1) countdownDay = 1;
if (countdownDay > 31) countdownDay = 31;
// Valider l'heure
if (countdownHour > 23) countdownHour = 23;
if (countdownMinute > 59) countdownMinute = 59;
if (countdownSecond > 59) countdownSecond = 59;
// Recalculer couleur immédiate (affichage plus réactif)
countdownColor = display.color565(countdownColorR, countdownColorG, countdownColorB);
// Mettre à jour cible localement pour affichage avant sauvegarde
countdownTarget = DateTime(countdownYear, countdownMonth, countdownDay,
countdownHour, countdownMinute, countdownSecond);
xSemaphoreGive(countdownMutex);
} else {
Serial.println("Could not get countdown mutex in handleSettings");
}
// Sauvegarder les paramètres (demander la sauvegarde plutôt que de la faire directement)
saveRequested = true;
saveRequestTime = millis();
Serial.println("Settings updated:");
Serial.printf("Target: %d-%02d-%02d %02d:%02d:%02d\n",
countdownYear, countdownMonth, countdownDay,
countdownHour, countdownMinute, countdownSecond);
Serial.printf("Title: %s\n", countdownTitle);
Serial.printf("Text Size: %d, Font: %d\n", countdownTextSize, fontIndex);
Serial.printf("Color (RGB): %d,%d,%d\n", countdownColorR, countdownColorG, countdownColorB);
// Répondre avec une redirection vers la page principale
server.sendHeader("Location", "/", true);
server.send(302, "text/plain", "");
}
// Gestionnaire de reset
void handleReset() {
// Vérification clé supprimée
// Réinitialiser aux valeurs par défaut
if (countdownMutex != NULL && xSemaphoreTake(countdownMutex, pdMS_TO_TICKS(200))) {
countdownYear = 2025;
countdownMonth = 12;
countdownDay = 31;
countdownHour = 23;
countdownMinute = 59;
countdownSecond = 0;
strcpy(countdownTitle, "COUNTDOWN");
countdownTextSize = 2;
countdownColorR = 0;
countdownColorG = 255;
countdownColorB = 0;
fontIndex = 0;
countdownColor = display.color565(countdownColorR, countdownColorG, countdownColorB);
countdownTarget = DateTime(countdownYear, countdownMonth, countdownDay,
countdownHour, countdownMinute, countdownSecond);
xSemaphoreGive(countdownMutex);
}
// Sauvegarder les paramètres (demander la sauvegarde plutôt que de la faire directement)
saveRequested = true;
saveRequestTime = millis();
Serial.println("Settings reset to defaults");
// Répondre avec une redirection vers la page principale
server.sendHeader("Location", "/", true);
server.send(302, "text/plain", "");
}
// Configuration et démarrage du serveur
void prepare_and_start_The_Server() {
// Démarrage du serveur DNS pour le portail captif
dnsServer.start(DNS_PORT, "*", WiFi.softAPIP());
server.on("/", handleRoot);
server.on("/settings", HTTP_POST, handleSettings);
server.on("/getSettings", handleGetSettings);
server.on("/reset", HTTP_POST, handleReset);
// Rediriger toutes les autres requêtes HTTP vers la page principale (portail captif)
server.onNotFound([]() {
server.sendHeader("Location", "/", true);
server.send(302, "text/plain", "");
});
delay(500);
server.begin();
Serial.println("\nHTTP server started");
if (useStationMode) {
Serial.print("Open http://");
Serial.print(WiFi.localIP());
Serial.println(" in your browser");
} else {
Serial.print("Connect to WiFi network: ");
Serial.println(ap_ssid);
Serial.print("Then open http://");
Serial.print(WiFi.softAPIP());
Serial.println(" in your browser");
}
Serial.println("Use key: " KEY_TXT);
delay(500);
}
// Fonction de callback pour le timer d'affichage
void IRAM_ATTR display_updater() {
portENTER_CRITICAL_ISR(&timerMux);
display.display(display_draw_time);
portEXIT_CRITICAL_ISR(&timerMux);
}
// Activation/désactivation du timer d'affichage
void display_update_enable(bool is_enable) {
if (is_enable) {
if (timer == nullptr) {
timer = timerBegin(0, 80, true);
if (timer != nullptr) {
timerAttachInterrupt(timer, &display_updater, true);
timerAlarmWrite(timer, 4000, true);
timerAlarmEnable(timer);
Serial.println("Display timer enabled");
} else {
Serial.println("Failed to initialize display timer");
}
} else {
Serial.println("Display timer already enabled");
}
} else {
if (timer != nullptr) {
timerAlarmDisable(timer);
timerDetachInterrupt(timer);
timerEnd(timer);
timer = nullptr;
Serial.println("Display timer disabled");
}
}
}
void setup() {
delay(1000);
Serial.begin(115200);
Serial.println("\n=== ESP32 P10 RGB FULLSCREEN COUNTDOWN ===");
Serial.printf("Configuration: %dx%d panels (%dx%d total resolution)\n",
MATRIX_PANELS_X, MATRIX_PANELS_Y, TOTAL_WIDTH, TOTAL_HEIGHT);
// Création des mutex
displayMutex = xSemaphoreCreateMutex();
countdownMutex = xSemaphoreCreateMutex();
preferencesMutex = xSemaphoreCreateMutex();
if (displayMutex == NULL || countdownMutex == NULL || preferencesMutex == NULL) {
Serial.println("Error creating mutexes");
while(1);
}
// NE PAS activer le timer ici - attendre après le WiFi et les tâches
// Initialisation du RTC
Serial.println("Initializing RTC...");
if (!rtc.begin()) {
Serial.println("Couldn't find RTC");
while (1) delay(10);
}
Serial.println("RTC initialized successfully");
// Initialisation de l'affichage avec configuration P10 optimisée
display.begin(4); // 1/8 scan pour P10
display.setScanPattern(ZAGZIG);
display.setMuxPattern(BINARY);
const int muxdelay = 10; // Délai de multiplexage
display.setMuxDelay(muxdelay, muxdelay, muxdelay, muxdelay, muxdelay);
delay(100);
// Luminosité adaptée au nombre de panneaux
int brightness = 150;
if (MATRIX_PANELS_X > 2) brightness = 100;
if (MATRIX_PANELS_X > 4) brightness = 80;
if (MATRIX_PANELS_X > 6) brightness = 60;
display.setBrightness(brightness);
Serial.printf("Brightness set to: %d\n", brightness);
display.setTextWrap(false);
display.setRotation(0);
// Chargement des paramètres
loadSettings();
// Message de démarrage
display.clearDisplay();
display.setTextColor(countdownColor);
display.setTextSize(1);
display.setCursor(0, 0);
display.print("COUNTDOWN");
display.setCursor(0, 8);
display.print("INTERFACE WEB");
delay(2000);
display.clearDisplay();
// Configuration WiFi initiale
if (useStationMode) {
connecting_To_WiFi();
if (!useStationMode) {
set_ESP32_Access_Point();
}
} else {
set_ESP32_Access_Point();
}
// Démarrage du serveur web
prepare_and_start_The_Server();
Serial.println("Creating FreeRTOS tasks...");
// Création des tâches FreeRTOS avec une meilleure distribution
BaseType_t result;
// Tâche d'affichage sur le Core 1 (isolé du WiFi)
result = xTaskCreatePinnedToCore(
DisplayTask,
"DisplayTask",
TASK_DISPLAY_STACK,
NULL,
TASK_DISPLAY_PRIORITY,
&displayTaskHandle,
1 // Core 1
);
if (result != pdPASS) Serial.println("Failed to create Display task");
// Tâches réseau sur le Core 0 (avec le WiFi)
result = xTaskCreatePinnedToCore(
NetworkTask,
"NetworkTask",
TASK_NETWORK_STACK,
NULL,
TASK_NETWORK_PRIORITY,
&networkTaskHandle,
0 // Core 0
);
if (result != pdPASS) Serial.println("Failed to create Network task");
result = xTaskCreatePinnedToCore(
WebServerTask,
"WebServerTask",
TASK_WEBSERVER_STACK,
NULL,
TASK_WEBSERVER_PRIORITY,
&webServerTaskHandle,
0 // Core 0
);
if (result != pdPASS) Serial.println("Failed to create WebServer task");
// Tâche de calcul sur le Core 0
result = xTaskCreatePinnedToCore(
CountdownTask,
"CountdownTask",
TASK_COUNTDOWN_STACK,
NULL,
TASK_COUNTDOWN_PRIORITY,
&countdownTaskHandle,
0 // Core 0
);
if (result != pdPASS) Serial.println("Failed to create Countdown task");
Serial.println("FreeRTOS tasks created successfully");
Serial.println("Starting fullscreen countdown...");
// ACTIVATION DU TIMER D'AFFICHAGE EN DERNIÈRE ÉTAPE
Serial.println("Activating display timer...");
display_update_enable(true);
delay(200); // Attendre que le timer soit stable
}
// Tâche d'affichage
void DisplayTask(void * parameter) {
// Attendre que le système soit prêt
vTaskDelay(pdMS_TO_TICKS(1000));
Serial.println("Display task started on core " + String(xPortGetCoreID()));
int prevSeconds = -1, prevMinutes = -1, prevHours = -1, prevDays = -1;
bool prevExpired = false;
for(;;) {
if(displayMutex != NULL && xSemaphoreTake(displayMutex, pdMS_TO_TICKS(10))) {
int days=0, hours=0, minutes=0, seconds=0;
if(countdownMutex != NULL && xSemaphoreTake(countdownMutex, pdMS_TO_TICKS(10))) {
updateCountdown(days, hours, minutes, seconds);
updateDisplayFormat(days, hours, minutes, seconds);
prevExpired = countdownExpired; // snapshot
xSemaphoreGive(countdownMutex);
}
bool secondChanged = (seconds != prevSeconds) || (minutes != prevMinutes) || (hours != prevHours) || (days != prevDays);
bool needDraw = secondChanged || blinkLastSeconds || countdownExpired != prevExpired;
if (needDraw) {
displayFullscreenCountdown(days, hours, minutes, seconds);
prevSeconds = seconds; prevMinutes = minutes; prevHours = hours; prevDays = days; prevExpired = countdownExpired;
}
xSemaphoreGive(displayMutex);
}
vTaskDelay(pdMS_TO_TICKS(blinkLastSeconds ? 50 : 150)); // Moins de rafraîchissements hors clignotement
}
}
// Tâche du serveur web
void WebServerTask(void * parameter) {
// Attendre que le WiFi soit configuré
vTaskDelay(pdMS_TO_TICKS(2000));
Serial.println("WebServer task started on core " + String(xPortGetCoreID()));
for(;;) {
server.handleClient();
vTaskDelay(pdMS_TO_TICKS(1)); // Délai minimal
}
}
// Tâche de gestion du compte à rebours
void CountdownTask(void * parameter) {
// Attendre que le RTC soit prêt
vTaskDelay(pdMS_TO_TICKS(1500));
Serial.println("Countdown task started on core " + String(xPortGetCoreID()));
for(;;) {
// Vérifier s'il faut sauvegarder les paramètres (avec délai de sécurité)
if (saveRequested && (millis() - saveRequestTime) > 1500) { // Débounce 1.5s après dernière modif
saveRequested = false;
// Attendre un peu plus pour s'assurer que le contexte web est terminé
vTaskDelay(pdMS_TO_TICKS(50));
saveSettings();
Serial.println("Settings saved from CountdownTask");
}
if(countdownMutex != NULL && xSemaphoreTake(countdownMutex, pdMS_TO_TICKS(100))) {
DateTime now = rtc.now();
if (now.isValid()) { // Vérification de la validité de la date/heure
if (now >= countdownTarget) {
countdownExpired = true;
}
} else {
Serial.println("RTC read error!");
}
xSemaphoreGive(countdownMutex);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// Tâche de gestion réseau
void NetworkTask(void * parameter) {
// Attendre que le système soit stabilisé
vTaskDelay(pdMS_TO_TICKS(500));
Serial.println("Network task started on core " + String(xPortGetCoreID()));
for(;;) {
dnsServer.processNextRequest();
// Vérification et gestion de la connexion WiFi
if(useStationMode) {
if(WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi connection lost, trying to reconnect...");
WiFi.reconnect();
vTaskDelay(pdMS_TO_TICKS(5000)); // Attendre 5 secondes avant la prochaine tentative
}
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}
void loop() {
// Pause pour éviter les problèmes de watchdog
// Les tâches FreeRTOS gèrent tout le travail
vTaskDelay(pdMS_TO_TICKS(1000));
}