← Retour

Contrôle d'état de LEDs avec un ESP32

2026-01-24
Contrôle d'état de LEDs avec un ESP32

Contexte

Mon patron m'avait offert un ESP32 (un petit microcontrôleur avec Wi-Fi intégré) pour que je puisse le découvrir et m'amuser à tester divers projets dessus. Il m'arrivait aussi souvent de rater des notifications car elles n'étaient pas dans mon champ de vision. J'ai donc eu l'idée de faire un système de leds qui s'allument avec cet esp32.

Étapes:

-> 1 - Achat de tous les éléments manquants

-> 2 - Montage du breadboard

-> 3 - Conception du code

-> 4 - Écriture de scripts exécutés depuis mon PC pour allumer ou éteindre les LEDs de l'ESP

Achat des éléments manquants

Quand j'étais plus petite, j'avais reçu un kit d'essai aux circuits et mes parents m'avaient acheté une boite de LEDs que j'ai gardée depuis.

Je n'ai donc pas eu à acheter les LEDs qui m'auraient coûté 2€.

Il me fallait quand même des résistances pour éviter de griller mes LEDs, des câbles pour tout brancher et un "breadboard" pour faire mes branchements dessus.

Le total était d'environ 10€.

Résistances

Image résistance de 220 ohms

Pour choisir les bonnes résistances, il a fallu diviser (Tension d'alimentation - Tension directe de la LED) par le courant souhaité:

  • Tension d'alimentation: un ESP32 sort du 3,3V sur ses broches GPIO (broches permettant de brancher les LEDs ici)
  • Tension directe de la LED: 2V
  • Courant souhaité: 6mA (soit 0,006 A)

-> (3,3 - 2) / 0,006 = 1,3/0,006 = 216,6 ohms

Au moment de l'achat, je prends la valeur la plus proche juste au dessus: 220 ohms.

Câbles et breadboard

Pour les câbles et le breadboard, j'ai acheté des standards sans spécificités particulières.

Montage des éléments

Voilà une image du montage sur le breadboard avec des chemins exemples de courant pour la LED rouge et la LED bleue:

Image résistance de 220 ohms

Code de l'ESP

Voilà le code en C++ flashé dans l'esp:

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

// codes Wi-Fi
const char* ssid = "Mon-Wifi";
const char* password = "Mon-mdp";

ESP8266WebServer server(80);

// Pins pour les 5 LEDs
int leds[] = {5, 4, 14, 12, 13}; // D1, D2, D5, D6, D7

void handleControl() {
  if (server.hasArg("led") && server.hasArg("state")) {
    // On enlève 1 au numéro pour les index de liste
    int ledIndex = server.arg("led").toInt() - 1;
    String state = server.arg("state");

    if (ledIndex >= 0 && ledIndex < 5) {
      digitalWrite(leds[ledIndex], (state == "on") ? HIGH : LOW);
      server.send(200, "text/plain", "LED " + String(ledIndex + 1) + " is " + state);
    } else {
      server.send(400, "text/plain", "Index LED invalide");
    }
  } else {
    server.send(400, "text/plain", "Arguments manquants");
  }
}

void setup() {
  Serial.begin(115200);

  // Initialisation des pins
  for (int i = 0; i < 5; i++) {
    pinMode(leds[i], OUTPUT);
    digitalWrite(leds[i], LOW);
  }

  // Connexion au Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }

  Serial.println("");
  Serial.println("Connecté ! Adresse IP : ");
  Serial.println(WiFi.localIP());

  // Routes de l'API
  server.on("/set", handleControl);
  server.begin();
}

void loop() {
  server.handleClient();
}

Ce code permet à l'ESP de servir de mini serveur web auquel on peut envoyer des requêtes pour qu'il allume et éteigne les LEDs.

Les URLs valables sont sous cette forme: https://<IP>//set?led=1&state=on

Scripts pour contrôler les LEDs

Pour pouvoir allumer les LEDs en fonction des différents évenements, il a fallu que je fasse tourner des scripts sur mon PC debian pour qu'il puisse envoyer les requêtes à l'ESP sur le même réseau.

Script pour allumer les LEDS

Pour ce script, j'ai choisi de ne faire s'allumer les LEDs que pour les notifications de certaines applications.

#!/bin/bash

IP="192.168.1.13"

# Applications pour lesquelles je veux allumer la LED
TARGET_APP="discord|Vesktop|Telegram Desktop|notify-send|Microsoft Teams|Signal"

LAST_APP=""
LAST_TIME=0

echo "--- App Watcher Active ---"
echo "En attente de notifications venant de : $TARGET_APP"

dbus-monitor --session "interface='org.freedesktop.Notifications',member='Notify'" | while read -r line; do

    # Dans DBus, le premier 'string' après 'Notify' est souvent le nom de l'application
    if [[ "$line" == *"string"* ]]; then
        APP_NAME=$(echo "$line" | cut -d '"' -f 2)

        # On vérifie si c'est l'app qu'on cherche
        if echo "$APP_NAME" | grep -Eiq "$TARGET_APP"; then
            CURRENT_TIME=$(date +%s)

            # Anti-spam (évite de trigger 2 fois pour la même notif)
            if [[ "$APP_NAME" == "$LAST_APP" ]] && (( CURRENT_TIME - LAST_TIME < 2 )); then
                continue
            fi

            LAST_APP="$APP_NAME"
            LAST_TIME=$CURRENT_TIME

            echo -e "\n📱 App Détectée : $APP_NAME"

            # Activation de la LED
            curl -s "http://$IP/set?led=2&state=on" > /dev/null
            sleep 0.5
            curl -s "http://$IP/set?led=2&state=off" > /dev/null

            echo "Signal envoyé !"
        else
            echo "App: $APP_NAME"
            echo "$LAST_APP"
        fi
    fi
done

Script qui détecte ma voix

Pendant mes longues sessions d'appels avec mes amis il arrivait que mon micro ne soit pas détecté correctement.

Sauf que l'application d'appels était rarement au premier plan. Donc une led qui s'allume quand ma voix est détectée est le projet parfait.

import pyaudio
import numpy as np
import requests
import time

# --- CONFIGURATION ---
IP = "192.168.1.13"
ESP_URL_ON = "http://"+IP+"/set?led=1&state=on"
ESP_URL_OFF = "http://"+IP+"/set?led=1&state=off"
THRESHOLD = 400  # Permet de choisir l'intensité à laquelle ma voix est détectée
CHUNK = 1024
RATE = 48000
DELAY_OFF = 0.3

p = pyaudio.PyAudio()

try:
    stream = p.open(
        format=pyaudio.paInt16, 
        channels=1, 
        rate=RATE, 
        input=True, 
        frames_per_buffer=CHUNK,
        input_device_index=DEVICE_INDEX
    )
except Exception as e:
    print(f"Erreur ouverture micro : {e}")
    exit()

print("Script lancé... ")

is_on = False
last_speech_time = 0

try:
    while True:
        # Lecture du flux audio
        raw_data = stream.read(CHUNK, exception_on_overflow=False)
        data = np.frombuffer(raw_data, dtype=np.int16)
        peak = np.max(np.abs(data))

        # Pour voir le volume :
        # print(f"Volume: {peak}", end='\r')

        if peak > THRESHOLD:
            last_speech_time = time.time()
            if not is_on:
                try:
                    requests.get(ESP_URL_ON, timeout=0.5)
                    is_on = True
                    print("\n[ON] Voix détectée")
                except:
                    print("\nErreur : Impossible de joindre l'ESP")

        elif is_on and (time.time() - last_speech_time > DELAY_OFF):
            try:
                requests.get(ESP_URL_OFF, timeout=0.5)
                is_on = False
                print("\n[OFF] Silence")
            except:
                pass

except KeyboardInterrupt:
    print("\nArrêt...")
finally:
    stream.stop_stream()
    stream.close()
    p.terminate()

Page web Pomodoro

J'ai eu des problèmes de concentration où je finissais souvent par procrastiner au lieu de réviser.

Après qu'on m'ait parlé de la méthode pomodoro, j'ai voulu la tester et les LEDs étaient une excellente manière de voir dans quelle "période" j'étais.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>LED Pomodoro Control</title>
    <style>
        body { font-family: 'Segoe UI', sans-serif; background: #121212; color: white; text-align: center; padding: 50px; }
        .timer { font-size: 5rem; font-weight: bold; margin: 20px 0; font-variant-numeric: tabular-nums; }
        .status { font-size: 1.5rem; text-transform: uppercase; letter-spacing: 2px; color: #bbb; }
        .controls button { padding: 15px 30px; font-size: 1.2rem; cursor: pointer; border: none; border-radius: 5px; margin: 5px; transition: 0.3s; }
        .start { background: #27ae60; color: white; }
        .stop { background: #c0392b; color: white; }
        .start:hover { background: #2ecc71; }
        .stop:hover { background: #e74c3c; }
    </style>
</head>
<body>

    <div class="status" id="phase-name">Ready to Grind</div>
    <div class="timer" id="display">20:00</div>

    <div class="controls">
        <button class="start" onclick="startPomodoro()">START</button>
        <button class="stop" onclick="resetTimer()">RESET</button>
    </div>

    <script>
        const IP = "192.168.1.4";
        let timeLeft;
        let timerId = null;
        let currentPhase = 0; // 0: Work, 1: Relax, 2: Prepare

        const PHASES = [
            { name: "WORK", duration: 20 * 60, led: 3, color: "#ff4d4d" },
            { name: "RELAX", duration: 4 * 60 + 50, led: 5, color: "#2ecc71" },
            { name: "PREPARE", duration: 10, led: 4, color: "#f1c40f" }
        ];

        async function triggerLed(ledPin, state) {
            try {
                await fetch(`http://${IP}/set?led=${ledPin}&state=${state}`, { mode: 'no-cors' });
            } catch (e) {
                console.log("LED Request failed: ", e);
            }
        }

        function updateDisplay() {
            const mins = Math.floor(timeLeft / 60).toString().padStart(2, '0');
            const secs = (timeLeft % 60).toString().padStart(2, '0');
            document.getElementById('display').innerText = `${mins}:${secs}`;
            document.getElementById('display').style.color = PHASES[currentPhase].color;
            document.getElementById('phase-name').innerText = PHASES[currentPhase].name;
        }

        async function startPhase(index) {
            currentPhase = index;
            timeLeft = PHASES[currentPhase].duration;

            // Éteindre toutes les lEDs du pomodoro qui ont pu rester allumées.
            [3, 4, 5].forEach(pin => triggerLed(pin, "off"));
            // Allumer la LED de la bonne "période"
            triggerLed(PHASES[currentPhase].led, "on");

            updateDisplay();
        }

        function startPomodoro() {
            if (timerId) return;
            startPhase(0);

            timerId = setInterval(async () => {
                timeLeft--;
                if (timeLeft < 0) {
                    currentPhase = (currentPhase + 1) % 3;
                    await startPhase(currentPhase);
                }
                updateDisplay();
            }, 1000);
        }

        function resetTimer() {
            clearInterval(timerId);
            timerId = null;
            [3, 4, 5].forEach(pin => triggerLed(pin, "off"));
            document.getElementById('display').innerText = "20:00";
            document.getElementById('display').style.color = "white";
            document.getElementById('phase-name').innerText = "Ready to Grind";
        }
    </script>
</body>
</html>

Conclusion

Ce projet m'a permis de découvrir le fonctionnement des ESP et d'apprendre comment les "GPIO" fonctionnent.

Il m'a aussi permis de bidouiller des librairies python que je ne connaissais pas et de me servir de tous ces scripts pour me faciliter la vie en ayant toujours mes LEDs visibles sur mon bureau.