diff --git a/README.md b/README.md index 7f6606c..48bc1e9 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Application ESP32 pour contrôler intelligemment un chauffe-eau avec l'excédent - ✅ Intégration passerelle Enphase pour données solaires en temps réel - ✅ Réception température via API ESPEasy - ✅ API REST complète pour contrôle externe +- ✅ Tâche FreeRTOS dédiée pour Enphase sur cœur 1 (cœur 0 libéré) ### Configuration @@ -199,6 +200,43 @@ Retourne toute la configuration (WiFi, système, Enphase) #### POST `/api/settings` - Modifier configuration Envoyer les paramètres à modifier (voir structure dans `config.json`) +#### GET `/api/stats` - Statistiques système +Retourne les informations système (RAM, CPU, uptime, tâches FreeRTOS) : +```json +{ + "memory": { + "total_bytes": 327680, + "free_bytes": 120000, + "used_bytes": 207680, + "usage_percent": 63.4 + }, + "psram": { + "total_bytes": 8388608, + "free_bytes": 7500000, + "used_bytes": 888608, + "usage_percent": 10.6 + }, + "system": { + "uptime_seconds": 3661, + "uptime_days": 0, + "uptime_hours": 1, + "uptime_minutes": 1, + "uptime_seconds_remainder": 1, + "cpu_freq_mhz": 240, + "task_count": 15, + "mac_address": "AA:BB:CC:DD:EE:FF" + }, + "wifi": { + "ssid": "MonWiFi", + "rssi_dbm": -65, + "signal_strength": "good" + }, + "tasks": { + "enphase_running": true + } +} +``` + ### Endpoints publics (sans authentification) #### GET `/api/espeasy?temperature=48.5` @@ -284,7 +322,25 @@ Pour envoyer la température de l'eau depuis un capteur DS18B20 sur ESPEasy : endon ``` -## Calcul des Modes +## Optimisation Multi-Cœur (FreeRTOS) + +L'ESP32 dispose de 2 cœurs. Cette application les utilise optimalement : + +- **Cœur 0 (principal)** : + - Boucle `loop()` Arduino + - Gestion PWM (timer hardware) + - Serveur web AsyncWebServer + - Calcul des modes de chauffe + - Calcul lever/coucher soleil + +- **Cœur 1 (dédié)** : + - Tâche FreeRTOS `EnphaseTask` + - Récupération données passerelle Enphase (HTTP HTTPS) + - Exécution non-bloquante via `xTaskNotifyGive()` + +**Avantage** : Le cœur 0 n'est jamais bloqué par les opérations réseau Enphase. Le web server reste réactif même pendant une mise à jour Enphase. + +**Communication inter-cœur** : Notification FreeRTOS (ultra-rapide, ~100µs) ### Mode JOUR Calcul progressif pour atteindre `max_water_temp` à l'heure cible : @@ -369,6 +425,7 @@ Exemple: JOUR+SOLEIL (6) = max(PuissanceJour, PuissanceSoleil) - **Contrôle** : GPIO12 (sortie digitale) - **Sécurité** : SHA-256 pour authentification, sessions avec timeout - **OTA** : Web + ArduinoOTA (port série et réseau) +- **Multi-cœur** : FreeRTOS avec tâche dédiée Enphase sur cœur 1 ## Bibliothèques utilisées @@ -382,10 +439,11 @@ lib_deps = ## Performances - **Mémoire** : ~150KB RAM utilisée (avec PSRAM disponible) -- **CPU** : <5% en idle, ~20% pendant fetch Enphase +- **CPU** : <5% en idle, ~20% pendant fetch Enphase (cœur 1) - **Réseau** : Latence <50ms pour API REST - **PWM** : Précision ±10ms (timer 100Hz) - **Update rate** : Enphase configurable (défaut 5s), calcul PWM 1s +- **Multi-cœur** : 2 cœurs optimisés - cœur 0 pour web/PWM, cœur 1 pour Enphase ## Contributeurs @@ -397,6 +455,11 @@ MIT License - Libre d'utilisation, modification et distribution. --- -**Version**: 1.0.0 +**Version**: 1.1.0 **Date**: Janvier 2026 **Compatibilité**: ESP32-S3, Arduino Framework, PlatformIO + +**Nouveautés v1.1.0**: +- Ajout API `/api/stats` pour monitoring système +- Optimisation multi-cœur avec FreeRTOS (Enphase sur cœur 1) +- Amélioration réactivité web server diff --git a/src/main.cpp b/src/main.cpp index 24da5c6..38144bf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include // Configuration WiFi String wifiSSID; @@ -79,6 +81,11 @@ volatile unsigned long pwmOnTime = 0; // Temps ON en ms pour le cycle actuel volatile unsigned long pwmElapsed = 0; // Temps écoulé dans le cycle actuel en ms volatile bool pwmNeedRecalc = true; // Flag pour indiquer qu'il faut recalculer la puissance +// Variables pour la gestion des tâches FreeRTOS (Enphase sur cœur 1) +TaskHandle_t enphaseTaskHandle = NULL; +unsigned long lastEnphaseUpdate = 0; +bool enphaseUpdateRequested = false; + #ifdef ESP32 hw_timer_t *timer = NULL; portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; @@ -90,6 +97,19 @@ AsyncWebServer server(80); void handleFileRequest(String path); void IRAM_ATTR onTimerISR(); void calculateHeaterPower(); +void fetchEnphaseData(); + +// Fonction wrapper pour exécuter fetchEnphaseData sur le cœur 1 +void enphaseTaskFunction(void *parameter) { + for (;;) { + // Attendre la notification de mise à jour + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + // Exécuter la récupération des données Enphase sur le cœur 1 + Serial.println("[ENPHASE] Mise à jour sur cœur 1"); + fetchEnphaseData(); + } +} // Fonction pour créer un fichier config.json par défaut (mode AP) bool createDefaultConfig() { @@ -1152,6 +1172,105 @@ void setup() { request->send(200, "application/json", response); }); + // API GET /api/stats - Informations système (CPU, RAM, temps de fonctionnement) + server.on("/api/stats", HTTP_GET, [](AsyncWebServerRequest *request){ + if (!checkAuthentication(request)) { + request->send(401, "text/plain", "Non autorisé"); + return; + } + + JsonDocument doc; + + // Informations de mémoire + multi_heap_info_t heap_info; + heap_caps_get_info(&heap_info, MALLOC_CAP_DEFAULT); + + size_t totalHeap = heap_caps_get_total_size(MALLOC_CAP_DEFAULT); + size_t freeHeap = heap_caps_get_free_size(MALLOC_CAP_DEFAULT); + size_t usedHeap = totalHeap - freeHeap; + float heapUsagePercent = (usedHeap * 100.0) / totalHeap; + + doc["memory"]["total_bytes"] = (int)totalHeap; + doc["memory"]["free_bytes"] = (int)freeHeap; + doc["memory"]["used_bytes"] = (int)usedHeap; + doc["memory"]["usage_percent"] = (float)heapUsagePercent; + + // PSRAM (si disponible) + size_t totalPsram = esp_spiram_get_size(); + if (totalPsram > 0) { + size_t freePsram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + size_t usedPsram = totalPsram - freePsram; + float psramUsagePercent = (usedPsram * 100.0) / totalPsram; + + doc["psram"]["total_bytes"] = (int)totalPsram; + doc["psram"]["free_bytes"] = (int)freePsram; + doc["psram"]["used_bytes"] = (int)usedPsram; + doc["psram"]["usage_percent"] = (float)psramUsagePercent; + } + + // Temps de fonctionnement + unsigned long uptimeSeconds = millis() / 1000; + unsigned long uptimeDays = uptimeSeconds / 86400; + unsigned long uptimeHours = (uptimeSeconds % 86400) / 3600; + unsigned long uptimeMinutes = (uptimeSeconds % 3600) / 60; + unsigned long uptimeSecs = uptimeSeconds % 60; + + doc["system"]["uptime_seconds"] = (unsigned long)uptimeSeconds; + doc["system"]["uptime_days"] = (unsigned long)uptimeDays; + doc["system"]["uptime_hours"] = (unsigned long)uptimeHours; + doc["system"]["uptime_minutes"] = (unsigned long)uptimeMinutes; + doc["system"]["uptime_seconds_remainder"] = (unsigned long)uptimeSecs; + + // Fréquence CPU + doc["system"]["cpu_freq_mhz"] = getCpuFrequencyMhz(); + + // Version du firmware + doc["system"]["esp_sdk_version"] = ESP.getSdkVersion(); + doc["system"]["sketch_md5"] = ESP.getSketchMD5(); + + // Nombre de tâches FreeRTOS + doc["system"]["task_count"] = uxTaskGetNumberOfTasks(); + + // Numéro d'expédition (chip ID) + doc["system"]["chip_revision"] = ESP.getChipRevision(); + + // Adresse MAC + uint8_t mac[6]; + WiFi.macAddress(mac); + char macStr[18]; + sprintf(macStr, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + doc["system"]["mac_address"] = macStr; + + // Signal WiFi si connecté + if (WiFi.status() == WL_CONNECTED) { + int rssi = WiFi.RSSI(); + doc["wifi"]["ssid"] = WiFi.SSID(); + doc["wifi"]["rssi_dbm"] = rssi; + doc["wifi"]["signal_strength"] = "excellent"; // Valeur par défaut + + if (rssi >= -50) { + doc["wifi"]["signal_strength"] = "excellent"; + } else if (rssi >= -60) { + doc["wifi"]["signal_strength"] = "very good"; + } else if (rssi >= -70) { + doc["wifi"]["signal_strength"] = "good"; + } else if (rssi >= -80) { + doc["wifi"]["signal_strength"] = "fair"; + } else { + doc["wifi"]["signal_strength"] = "weak"; + } + } else { + doc["wifi"]["connected"] = false; + } + + // Uptime des tâches importantes + doc["tasks"]["enphase_running"] = (enphaseTaskHandle != NULL); + + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); + }); + // API pour ESPEasy - Endpoint simplifié pour recevoir la température // ESPEasy peut envoyer avec: http://[IP]/api/espeasy?temperature=[value] server.on("/api/espeasy", HTTP_GET, [](AsyncWebServerRequest *request){ @@ -1304,7 +1423,7 @@ void setup() { return; } - if (doc.containsKey("mode")) { + if (doc["mode"].is()) { int newMode = doc["mode"].as(); // Valider le mode (0-31 pour 5 bits) @@ -1478,6 +1597,21 @@ void setup() { timerAlarmWrite(timer, 10000, true); timerAlarmEnable(timer); Serial.println("Timer PWM initialisé (100Hz)"); + + // Créer la tâche FreeRTOS pour Enphase sur le cœur 1 + // Priority: 2 (plus élevée que tskIDLE_PRIORITY=0 mais moins que le loop=1) + // StackSize: 4096 octets (environ 2KB pour les variables locales) + xTaskCreatePinnedToCore( + enphaseTaskFunction, // Fonction de la tâche + "EnphaseTask", // Nom de la tâche + 8192, // Taille de la pile (8KB pour HTTPClient) + NULL, // Paramètre + 2, // Priorité (tskIDLE_PRIORITY=0, loop=1) + &enphaseTaskHandle, // Handle de la tâche + 1 // Cœur 1 (0 = cœur principal, 1 = cœur libre) + ); + + Serial.println("Tâche Enphase créée sur cœur 1"); } void loop() { @@ -1493,13 +1627,18 @@ void loop() { // Calculer la puissance du chauffe-eau au début de chaque cycle PWM calculateHeaterPower(); - // Récupération des données Enphase - static unsigned long lastEnphaseUpdate = 0; + // Récupération des données Enphase (non-bloquant avec tâche sur cœur 1) unsigned long currentMillis = millis(); if (currentMillis - lastEnphaseUpdate >= (enphaseUpdateInterval * 1000)) { lastEnphaseUpdate = currentMillis; - fetchEnphaseData(); + + // Notifier la tâche Enphase pour mettre à jour les données + // Cette opération est non-bloquante et s'exécute sur le cœur 1 + if (enphaseTaskHandle != NULL) { + xTaskNotifyGive(enphaseTaskHandle); + Serial.printf("[LOOP] Mise à jour Enphase déléguée à cœur 1 (interval: %lus)\n", enphaseUpdateInterval); + } } // Recalculer lever/coucher du soleil chaque jour à minuit