#include <Wire.h>
#include <RTClib.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <SPI.h>
#include <SD.h>
#include <WiFi.h>
#include <Adafruit_NeoPixel.h>
#include <math.h>
// ===== PINS =====
#define PIN_TENSION 34 // Potentiomètre → niveau tension (0–260V simulé)
#define PIN_BTN_BRUIT 12 // Bouton bleu → injecte du bruit
#define PIN_BTN_HARM 13 // Bouton vert → injecte des harmoniques
#define LED_ALERTE 2
#define PIN_BUZZER 25
#define PIN_NEO 26
#define SD_CS 5
#define NUM_LEDS 1
// ===== SEUILS ELECTRIQUES (EN230V) =====
#define V_NOM 230.0f // Nominale
#define V_MAX_NOM 253.0f // +10 %
#define V_MIN_NOM 207.0f // -10 %
#define V_SAG_LOW 23.0f // 10 % → limite basse creux
#define V_MICRO 2.3f // 1 % → micro/coupure
#define V_SWELL_EXT 280.0f // surtension extrême
// ===== ECHANTILLONNAGE =====
#define N_SAMPLES 32
#define SEUIL_BRUIT 50.0f // variance → bruit
#define SEUIL_HARM 20.0f // variance périodique → harmonique
#define SEUIL_FLICKER 5.0f // variance faible → flicker
// ===== TYPES DE PERTURBATION =====
enum Perturbation : uint8_t {
NORMAL = 0,
TRANSITOIRE, // < 20 ms, V ≈ 0
MICROCOUPURE, // 20–100 ms, V ≈ 0
COUPURE, // > 100 ms, V ≈ 0
CREUX, // 10–90 % Vnom, < 1 min
SOUS_TENSION, // < 90 % Vnom, > 1 min
SURTENSION, // 110–120 % Vnom
SURTENSION_EXT, // > 120 % Vnom
BRUIT, // variance haute (btn1)
HARMONIQUE, // distorsion périodique (btn2)
FLICKER // fluctuation rapide (pot oscillant)
};
// ── Noms affichés ──
const char* NOM_PERT[] = {
"NORMAL", "TRANSITOIRE", "MICROCOUPURE", "COUPURE",
"CREUX", "SOUS-TENSION", "SURTENSION", "SURTENSION EXT",
"BRUIT", "HARMONIQUE", "FLICKER"
};
// ── Couleurs NeoPixel [R,G,B] ──
const uint8_t COULEURS[][3] = {
{ 0, 200, 0}, // NORMAL vert
{255, 255, 0}, // TRANSITOIRE jaune
{255, 0, 0}, // MICROCOUPURE rouge
{120, 0, 0}, // COUPURE rouge foncé
{255, 140, 0}, // CREUX orange
{200, 70, 0}, // SOUS-TENSION orange foncé
{150, 0, 150}, // SURTENSION violet
{255, 0, 255}, // SURTENS EXT magenta
{ 0, 200, 200}, // BRUIT cyan
{ 0, 80, 255}, // HARMONIQUE bleu
{220, 220, 220}, // FLICKER blanc
};
// ── Fréquences buzzer (Hz) ──
const int FREQ_BUZZ[] = {
0, 1800, 1000, 500, 800, 600, 1500, 2200, 1200, 900, 700
};
// ===== OBJETS =====
RTC_DS1307 rtc;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
Adafruit_NeoPixel strip(NUM_LEDS, PIN_NEO, NEO_GRB + NEO_KHZ800);
WiFiServer server(80);
// ===== VARIABLES GLOBALES =====
float buf[N_SAMPLES] = {0};
int bufIdx = 0;
float vRMS = 230.0f;
float variance = 0.0f;
float harmPhase = 0.0f;
bool injectBruit = false;
bool injectHarm = false;
Perturbation pertNow = NORMAL;
Perturbation pertPrev = NORMAL;
unsigned long debutPert = 0;
int compteurs[11] = {0};
unsigned long durMin = ULONG_MAX;
unsigned long durMax = 0;
unsigned long durTotal = 0;
int nbEvents = 0;
// Graphe OLED
#define GW 84
#define GH 12
uint8_t graphe[GW] = {0};
const char* ssid = "Wokwi-GUEST";
const char* password = "";
// ===== PROTOTYPES =====
float lireVoltage();
float moyBuf();
float varBuf();
Perturbation detecter(float v, float var, unsigned long dureeMs);
void majNeoPixel(Perturbation p);
void majBuzzer(Perturbation curr, Perturbation prev);
void majOLED(float v, Perturbation p, DateTime& now);
void webServeur(float v, Perturbation p, DateTime& now);
void logSD(Perturbation p, DateTime& t, unsigned long duree);
// =====================================================================
// SETUP
// =====================================================================
void setup() {
Serial.begin(115200);
pinMode(LED_ALERTE, OUTPUT);
pinMode(PIN_BTN_BRUIT, INPUT_PULLUP);
pinMode(PIN_BTN_HARM, INPUT_PULLUP);
// NeoPixel → vert au démarrage
strip.begin(); strip.setBrightness(90);
strip.setPixelColor(0, strip.Color(0, 200, 0)); strip.show();
Wire.begin(21, 22);
rtc.begin();
if (!rtc.isrunning()) rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay(); display.setTextColor(WHITE);
Serial.println(SD.begin(SD_CS) ? "SD OK" : "SD erreur");
WiFi.begin(ssid, password);
Serial.print("WiFi");
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println("\nIP : " + WiFi.localIP().toString());
server.begin();
}
// =====================================================================
// LOOP
// =====================================================================
void loop() {
DateTime now = rtc.now();
// Lecture boutons injection
injectBruit = !digitalRead(PIN_BTN_BRUIT);
injectHarm = !digitalRead(PIN_BTN_HARM);
// Remplir le buffer d'échantillons
float v = lireVoltage();
buf[bufIdx] = v;
bufIdx = (bufIdx + 1) % N_SAMPLES;
vRMS = moyBuf();
variance = varBuf();
// Durée active de la perturbation courante
unsigned long dureeActive = (pertNow != NORMAL) ? millis() - debutPert : 0;
// ── Détection ──
Perturbation pDetect = detecter(vRMS, variance, dureeActive);
// ── Machine à états perturbation ──
if (pDetect != NORMAL && pertNow == NORMAL) {
// Début perturbation
pertNow = pDetect;
debutPert = millis();
compteurs[pDetect]++;
Serial.printf("[START] %s | %.1fV | var:%.1f\n", NOM_PERT[pDetect], vRMS, variance);
}
else if (pDetect == NORMAL && pertNow != NORMAL) {
// Fin perturbation
unsigned long dur = millis() - debutPert;
if (dur < durMin) durMin = dur;
if (dur > durMax) durMax = dur;
durTotal += dur; nbEvents++;
logSD(pertNow, now, dur);
Serial.printf("[END] %s | dur: %lums\n", NOM_PERT[pertNow], dur);
pertNow = NORMAL;
}
else if (pDetect != NORMAL && pDetect != pertNow) {
// Changement de type pendant une perturbation
unsigned long dur = millis() - debutPert;
logSD(pertNow, now, dur);
pertNow = pDetect;
debutPert = millis();
compteurs[pDetect]++;
}
// ── Actionneurs ──
Perturbation pAff = (pertNow != NORMAL) ? pertNow : pDetect;
digitalWrite(LED_ALERTE, pAff != NORMAL ? HIGH : LOW);
majNeoPixel(pAff);
majBuzzer(pAff, pertPrev);
pertPrev = pAff;
// ── Graphe OLED ──
for (int i = 0; i < GW - 1; i++) graphe[i] = graphe[i + 1];
graphe[GW - 1] = (uint8_t)constrain(vRMS * GH / 260.0f, 0, GH);
// ── OLED + Web ──
majOLED(vRMS, pAff, now);
webServeur(vRMS, pAff, now);
delay(50);
}
// =====================================================================
// LECTURE TENSION AVEC INJECTIONS
// =====================================================================
float lireVoltage() {
int raw = analogRead(PIN_TENSION);
float v = raw * (260.0f / 4095.0f); // 0 → 260 V
if (injectHarm) {
harmPhase += 0.35f; // 3ème harmonique simulée
v += 28.0f * sinf(harmPhase * 3.0f); // THD ≈ 12 %
}
if (injectBruit) {
v += (random(-1000, 1000) / 1000.0f) * 40.0f; // ±40 V aléatoire
}
return constrain(v, 0.0f, 300.0f);
}
// ===== STATS BUFFER =====
float moyBuf() {
float s = 0;
for (int i = 0; i < N_SAMPLES; i++) s += buf[i];
return s / N_SAMPLES;
}
float varBuf() {
float m = moyBuf(), s = 0;
for (int i = 0; i < N_SAMPLES; i++) s += (buf[i] - m) * (buf[i] - m);
return s / N_SAMPLES;
}
// =====================================================================
// DETECTION
// =====================================================================
Perturbation detecter(float v, float var, unsigned long durMs) {
// 1. Bruit (variance très haute + injection active)
if (injectBruit && var > SEUIL_BRUIT) return BRUIT;
// 2. Harmonique (variance moyenne + injection active + tension présente)
if (injectHarm && var > SEUIL_HARM && v > V_MIN_NOM * 0.6f) return HARMONIQUE;
// 3. Flicker (variance faible, tension nominale, pas d'injection)
if (!injectBruit && !injectHarm
&& var > SEUIL_FLICKER && var < SEUIL_BRUIT
&& v > V_MIN_NOM && v < V_MAX_NOM) return FLICKER;
// 4. Coupure / Microcoupure / Transitoire (V ≈ 0)
if (v < V_MICRO) {
if (durMs < 20) return TRANSITOIRE;
if (durMs < 100) return MICROCOUPURE;
return COUPURE;
}
// 5. Zone 1–10 % Vnom → coupure profonde
if (v < V_SAG_LOW) return COUPURE;
// 6. Creux / Sous-tension (10–90 % Vnom)
if (v < V_MIN_NOM) {
if (durMs > 60000) return SOUS_TENSION;
return CREUX;
}
// 7. Zone normale
if (v <= V_MAX_NOM) return NORMAL;
// 8. Surtensions
if (v < V_SWELL_EXT) return SURTENSION;
return SURTENSION_EXT;
}
// =====================================================================
// NEOPIXEL
// =====================================================================
void majNeoPixel(Perturbation p) {
static unsigned long lastBlink = 0;
static bool blinkOn = true;
bool doBlink = (p == MICROCOUPURE || p == COUPURE ||
p == SURTENSION_EXT || p == FLICKER || p == TRANSITOIRE);
if (doBlink && millis() - lastBlink > 250) {
blinkOn = !blinkOn;
lastBlink = millis();
}
if (doBlink && !blinkOn) {
strip.setPixelColor(0, 0); strip.show(); return;
}
strip.setPixelColor(0, strip.Color(COULEURS[p][0], COULEURS[p][1], COULEURS[p][2]));
strip.show();
}
// =====================================================================
// BUZZER
// =====================================================================
void majBuzzer(Perturbation curr, Perturbation prev) {
if (curr == prev) return;
if (curr == NORMAL) { noTone(PIN_BUZZER); return; }
int dur = (curr == COUPURE || curr == SURTENSION_EXT) ? 900 : 250;
tone(PIN_BUZZER, FREQ_BUZZ[curr], dur);
}
// =====================================================================
// OLED
// =====================================================================
void majOLED(float v, Perturbation p, DateTime& now) {
display.clearDisplay();
display.setTextSize(1);
// L0 – Tension + type (max 10 car)
display.setCursor(0, 0);
display.print(v, 0); display.print("V ");
String nom = String(NOM_PERT[p]);
display.print(nom.length() > 9 ? nom.substring(0, 9) : nom);
// L1 – Variance + injection
display.setCursor(0, 12);
display.print("Var:"); display.print(variance, 0);
if (injectBruit) display.print(" BR");
if (injectHarm) display.print(" HM");
// L2 – Compteurs MC / CX / SV
display.setCursor(0, 24);
display.print("MC:"); display.print(compteurs[MICROCOUPURE]);
display.print(" CX:"); display.print(compteurs[CREUX]);
display.print(" SV:"); display.print(compteurs[SURTENSION]);
// L3 – Heure + durée max
display.setCursor(0, 36);
if (now.hour() < 10) display.print("0"); display.print(now.hour()); display.print(":");
if (now.minute() < 10) display.print("0"); display.print(now.minute()); display.print(":");
if (now.second() < 10) display.print("0"); display.print(now.second());
display.print(" mx:");
display.print(durMax > 0 ? String(durMax / 1000.0f, 1) + "s" : "-");
// L4 – Graphe tension
display.setCursor(0, 50);
display.print("V:");
display.drawRect(16, 49, GW + 4, GH + 4, WHITE);
for (int i = 0; i < GW; i++) {
int h = graphe[i];
if (h > 0) display.drawFastVLine(18 + i, 49 + GH + 2 - h, h, WHITE);
else display.drawPixel(18 + i, 49 + GH + 1, WHITE);
}
display.display();
}
// =====================================================================
// LOG SD
// =====================================================================
void logSD(Perturbation p, DateTime& t, unsigned long duree) {
File f = SD.open("/log.txt", FILE_APPEND);
if (!f) return;
f.print(NOM_PERT[p]); f.print(" - ");
if (t.hour() < 10) f.print("0"); f.print(t.hour()); f.print(":");
if (t.minute() < 10) f.print("0"); f.print(t.minute()); f.print(":");
if (t.second() < 10) f.print("0"); f.print(t.second());
f.print(" | "); f.print(duree); f.println("ms");
f.close();
}
// =====================================================================
// SERVEUR WEB
// =====================================================================
void webServeur(float v, Perturbation p, DateTime& now) {
WiFiClient client = server.available();
if (!client) return;
String req = "";
unsigned long t0 = millis();
while (client.connected() && millis() - t0 < 2000) {
if (client.available()) { req += (char)client.read(); if (req.endsWith("\r\n\r\n")) break; }
}
client.println("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n");
// ── HEAD ──
client.println("<!DOCTYPE html><html><head><meta charset='utf-8'>");
client.println("<meta http-equiv='refresh' content='2'>");
client.println("<style>");
client.println("*{box-sizing:border-box}");
client.println("body{font-family:Arial;background:#0a0a0a;color:#ddd;padding:20px;margin:0}");
client.println("h1,h2{color:#fff;border-bottom:1px solid #222;padding-bottom:6px;margin-top:24px}");
client.println(".top{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:4px}");
client.println(".badge-big{font-size:1em;padding:4px 14px;border-radius:20px;font-weight:bold}");
client.println(".cards{display:flex;flex-wrap:wrap;gap:10px;margin:12px 0}");
client.println(".card{background:#141414;border-radius:10px;padding:12px 16px;min-width:100px;flex:1;text-align:center;border-top:3px solid #333}");
client.println(".val{font-size:1.7em;font-weight:bold;margin:4px 0}");
client.println(".lbl{color:#555;font-size:.78em;text-transform:uppercase}");
client.println("table{width:100%;border-collapse:collapse;font-size:.84em;margin-top:8px}");
client.println("th{background:#141414;padding:8px 10px;text-align:left;color:#888;font-weight:400}");
client.println("td{padding:7px 10px;border-bottom:1px solid #161616}");
client.println("tr:hover td{background:#111}");
client.println(".chip{display:inline-block;padding:2px 9px;border-radius:12px;font-size:.77em;font-weight:bold}");
client.println(".alert-box{border-radius:8px;padding:10px 14px;margin:8px 0;font-size:.9em}");
client.println("</style></head><body>");
// ── TITRE ──
client.println("<div class='top'><h1 style='margin:0;border:none'>Analyseur de perturbations</h1>");
client.print("<span>Heure : ");
char buf2[9]; sprintf(buf2, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
client.print(buf2); client.println("</span></div>");
// ── ETAT COURANT ──
const char* hexColors[] = {
"#4caf50","#ffeb3b","#f44336","#8b0000",
"#ff9800","#e65100","#9c27b0","#e040fb",
"#00bcd4","#2196f3","#eeeeee"
};
client.print("<div class='alert-box' style='background:");
client.print(hexColors[p]); client.print("18;border:1px solid ");
client.print(hexColors[p]); client.println("'>");
client.print("<b style='color:"); client.print(hexColors[p]); client.print("'>");
client.print(NOM_PERT[p]); client.println("</b>");
client.print(" | Tension : <b>"); client.print(v, 1); client.print(" V</b>");
client.print(" | Variance : <b>"); client.print(variance, 1); client.println("</b>");
if (injectBruit) client.print(" <span class='chip' style='background:#00bcd422;color:#00bcd4'>BRUIT actif</span>");
if (injectHarm) client.print(" <span class='chip' style='background:#2196f322;color:#2196f3'>HARMONIQUE actif</span>");
client.println("</div>");
// ── CARTES COMPTEURS ──
client.println("<h2>Compteurs par type</h2><div class='cards'>");
for (int i = 1; i <= 10; i++) {
if (compteurs[i] == 0) continue;
client.print("<div class='card' style='border-top-color:");
client.print(hexColors[i]); client.println("'>");
client.print("<div class='val' style='color:"); client.print(hexColors[i]); client.print("'>");
client.print(compteurs[i]); client.println("</div>");
client.print("<div class='lbl'>"); client.print(NOM_PERT[i]); client.println("</div></div>");
}
client.println("</div>");
// ── STATS ──
client.println("<h2>Statistiques globales</h2><div class='cards'>");
client.print("<div class='card'><div class='val'>");
client.print(durMin == ULONG_MAX ? 0 : (int)durMin);
client.println(" ms</div><div class='lbl'>Duree min</div></div>");
client.print("<div class='card'><div class='val'>"); client.print((int)durMax);
client.println(" ms</div><div class='lbl'>Duree max</div></div>");
client.print("<div class='card'><div class='val'>");
client.print(nbEvents > 0 ? (int)(durTotal / nbEvents) : 0);
client.println(" ms</div><div class='lbl'>Duree moy</div></div>");
int total = 0; for (int i = 1; i <= 10; i++) total += compteurs[i];
client.print("<div class='card'><div class='val'>"); client.print(total);
client.println("</div><div class='lbl'>Total events</div></div>");
client.println("</div>");
// ── HISTORIQUE SD ──
client.println("<h2>Historique (SD)</h2>");
client.println("<table><tr><th>Type</th><th>Heure</th><th>Duree</th></tr>");
File file = SD.open("/log.txt", FILE_READ);
if (file) {
int rows = 0;
while (file.available() && rows < 60) {
String ligne = file.readStringUntil('\n'); ligne.trim();
if (!ligne.length()) continue; rows++;
int s1 = ligne.indexOf(" - "), s2 = ligne.indexOf(" | ");
String type = (s1 > 0) ? ligne.substring(0, s1) : ligne;
String heure = (s1 > 0 && s2 > 0) ? ligne.substring(s1 + 3, s2) : "-";
String dur = (s2 > 0) ? ligne.substring(s2 + 3) : "-";
// Couleur selon type
String col = "#4caf50";
if (type.indexOf("MICRO") >= 0) col = "#f44336";
else if (type.indexOf("COUPURE") >= 0) col = "#8b0000";
else if (type.indexOf("TRANS") >= 0) col = "#ffeb3b";
else if (type.indexOf("CREUX") >= 0) col = "#ff9800";
else if (type.indexOf("SOUS") >= 0) col = "#e65100";
else if (type.indexOf("EXT") >= 0) col = "#e040fb";
else if (type.indexOf("SURTEN") >= 0) col = "#9c27b0";
else if (type.indexOf("BRUIT") >= 0) col = "#00bcd4";
else if (type.indexOf("HARM") >= 0) col = "#2196f3";
else if (type.indexOf("FLICKER") >= 0) col = "#eeeeee";
client.print("<tr><td><span class='chip' style='background:");
client.print(col); client.print("18;color:"); client.print(col); client.print("'>");
client.print(type); client.print("</span></td><td>"); client.print(heure);
client.print("</td><td>"); client.print(dur); client.println("</td></tr>");
}
file.close();
} else {
client.println("<tr><td colspan='3' style='color:#333;text-align:center;padding:18px'>Aucun log</td></tr>");
}
client.println("</table>");
client.println("<p style='color:#222;font-size:.72em;margin-top:24px'>Auto-refresh 2s</p>");
client.println("</body></html>");
client.stop();
}