Browse Source

Appels enphase redirigés sur un coeur dédié

Ajout API /api/stats
master
scayac 2 months ago
parent
commit
54b94ce7c5
  1. 69
      README.md
  2. 147
      src/main.cpp

69
README.md

@ -24,6 +24,7 @@ Application ESP32 pour contrôler intelligemment un chauffe-eau avec l'excédent @@ -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) @@ -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 : @@ -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) @@ -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 = @@ -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. @@ -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

147
src/main.cpp

@ -9,6 +9,8 @@ @@ -9,6 +9,8 @@
#include <ArduinoJson.h>
#include <Update.h>
#include <ArduinoOTA.h>
#include <esp_heap_caps.h>
#include <freertos/task.h>
// Configuration WiFi
String wifiSSID;
@ -79,6 +81,11 @@ volatile unsigned long pwmOnTime = 0; // Temps ON en ms pour le cycle actuel @@ -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); @@ -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() { @@ -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() { @@ -1304,7 +1423,7 @@ void setup() {
return;
}
if (doc.containsKey("mode")) {
if (doc["mode"].is<int>()) {
int newMode = doc["mode"].as<int>();
// Valider le mode (0-31 pour 5 bits)
@ -1478,6 +1597,21 @@ void setup() { @@ -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() { @@ -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

Loading…
Cancel
Save