Dans ce projet divertissant, vous allez connecter un capteur de couleur TCS3200D/TCS230 à un ESP32 et diffuser les couleurs détectées vers un navigateur web en temps réel. La page web présente un personnage Minion animé et ludique dont la couleur de peau se met à jour en direct selon ce que le capteur détecte. Pour simplifier la création de l'interface web et la gestion de la communication WebSocket, ce projet utilise la Exemple de WebApp personnalisée pour ESP32 - Tutoriel d'interface Web simple pour les débutants..
Un tutoriel vidéo étape par étape est également disponible en bas de ce guide.
Le diagramme ci-dessous montre comment connecter le capteur de couleur TCS3200 à un ESP32 :
| Capteur de couleur TCS3200 | ESP32 |
| VCC | 5V (VIN) |
| GND | GND |
| S0 | GPIO 17 |
| S1 | GPIO 16 |
| S2 | GPIO 18 |
| S3 | GPIO 5 |
| OUT | GPIO 19 |

Cette image a été créée avec Fritzing. Cliquez pour agrandir l'image.
Si vous ne savez pas comment alimenter l'ESP32 et d'autres composants, consultez les instructions dans le tutoriel suivant : Comment alimenter l'ESP32..
Ce projet se compose de 4 fichiers :
ColorSensorESP32.ino - Sketch principal : initialise le capteur, lit les couleurs et les envoie à la page web
CustomWebApp.h - Fichier d'en-tête : déclare la classe de page d'application web personnalisée
CustomWebApp.cpp - Fichier d'implémentation : gère la messagerie WebSocket en utilisant l'identifiant "Color sensor:"
custom_page_html.h - Page web : le Minion animé construit avec HTML/CSS/JavaScript qui réagit aux couleurs reçues
ColorSensorESP32.ino
#include <DIYables_ESP32_Platform.h>
#include <DIYablesWebApps.h>
#include "CustomWebApp.h"
const char WIFI_SSID[] = "YOUR_WIFI_SSID";
const char WIFI_PASSWORD[] = "YOUR_WIFI_PASSWORD";
const int S0 = 17;
const int S1 = 16;
const int S2 = 18;
const int S3 = 5;
const int sensorOut = 19;
ESP32ServerFactory serverFactory;
DIYablesWebAppServer webAppsServer(serverFactory, 80, 81);
DIYablesHomePage homePage;
CustomWebAppPage customPage;
unsigned long lastColorRead = 0;
void setup() {
Serial.begin(9600);
delay(1000);
Serial.println("Starting Custom WebApp...");
pinMode(S0, OUTPUT);
pinMode(S1, OUTPUT);
pinMode(S2, OUTPUT);
pinMode(S3, OUTPUT);
pinMode(sensorOut, INPUT);
digitalWrite(S0, HIGH);
digitalWrite(S1, LOW);
webAppsServer.addApp(&homePage);
webAppsServer.addApp(&customPage);
if (!webAppsServer.begin(WIFI_SSID, WIFI_PASSWORD)) {
while (1) {
Serial.println("Failed to connect to WiFi!");
delay(1000);
}
}
Serial.println("Custom WebApp ready!");
customPage.sendToWeb("Arduino is ready!");
}
void loop() {
webAppsServer.loop();
if (millis() - lastColorRead > 1000) {
digitalWrite(S2, LOW);
digitalWrite(S3, LOW);
int r = map(pulseIn(sensorOut, LOW), 31, 150, 255, 0);
digitalWrite(S2, HIGH);
digitalWrite(S3, HIGH);
int g = map(pulseIn(sensorOut, LOW), 35, 180, 255, 0);
digitalWrite(S2, LOW);
digitalWrite(S3, HIGH);
int b = map(pulseIn(sensorOut, LOW), 30, 150, 255, 0);
char hexColor[8];
sprintf(hexColor, "#%02X%02X%02X", constrain(r, 0, 255), constrain(g, 0, 255), constrain(b, 0, 255));
customPage.sendToWeb(String(hexColor));
Serial.println("Sent to Minion: " + String(hexColor));
lastColorRead = millis();
}
}
CustomWebApp.h
#ifndef CUSTOM_WEBAPP_H
#define CUSTOM_WEBAPP_H
#include <DIYablesWebApps.h>
class CustomWebAppPage : public DIYablesWebAppPageBase {
private:
static const String APP_IDENTIFIER;
public:
CustomWebAppPage();
void handleHTTPRequest(IWebClient& client) override;
void handleWebSocketMessage(IWebSocket& ws, const char* message, uint16_t length) override;
const char* getPageInfo() const override;
String getNavigationInfo() const override;
void onCustomMessageReceived(void (*callback)(const String& payload));
void sendToWeb(const String& message);
};
#endif
CustomWebApp.cpp
#include "CustomWebApp.h"
#include "custom_page_html.h"
const String CustomWebAppPage::APP_IDENTIFIER = "Color sensor:";
void (*customMessageCallback)(const String& payload) = nullptr;
CustomWebAppPage::CustomWebAppPage() : DIYablesWebAppPageBase("/custom") {
}
void CustomWebAppPage::handleHTTPRequest(IWebClient& client) {
sendHTTPHeader(client);
client.print(CUSTOM_PAGE_HTML);
}
void CustomWebAppPage::handleWebSocketMessage(IWebSocket& ws, const char* message, uint16_t length) {
String messageStr = String(message, length);
Serial.print("Color sensor WebApp received: ");
Serial.println(messageStr);
if (messageStr.startsWith(APP_IDENTIFIER)) {
String payload = messageStr.substring(APP_IDENTIFIER.length());
if (customMessageCallback) {
customMessageCallback(payload);
}
}
}
void CustomWebAppPage::onCustomMessageReceived(void (*callback)(const String& payload)) {
customMessageCallback = callback;
}
void CustomWebAppPage::sendToWeb(const String& message) {
String fullMessage = APP_IDENTIFIER + message;
broadcastToAllClients(fullMessage.c_str());
}
const char* CustomWebAppPage::getPageInfo() const {
return "🔧 Color sensor WebApp";
}
String CustomWebAppPage::getNavigationInfo() const {
String result = "<a href=\"";
result += getPagePath();
result += "\" class=\"app-card custom\" style=\"background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);\">";
result += "<h3>🔧 Color sensor WebApp</h3>";
result += "<p>Simple template for your own apps</p>";
result += "</a>";
return result;
}
custom_page_html.h
#ifndef CUSTOM_PAGE_HTML_H
#define CUSTOM_PAGE_HTML_H
const char CUSTOM_PAGE_HTML[] PROGMEM = R"HTML_WRAPPER(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mobile Laughing Minion</title>
<style>
body { margin: 0; padding: 20px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: flex-start; align-items: center; background-color: #f0f8ff; font-family: sans-serif; overflow-x: hidden; }
.text { font-size: clamp(16px, 5vw, 24px); font-weight: bold; color: #333; margin-bottom: 20px; text-align: center; z-index: 10; }
.scale-wrapper { transform-origin: top center; display: flex; justify-content: center; align-items: flex-start; }
.minion-container { position: relative; width: 200px; height: 400px; }
.body { position: absolute; top: 20px; left: 25px; width: 150px; height: 300px; background-color: #FFD90F; border-radius: 75px; box-shadow: inset -10px -10px 20px rgba(0,0,0,0.1); overflow: hidden; z-index: 2; transition: background-color 0.5s; }
.overalls { position: absolute; bottom: 0; width: 100%; height: 90px; background-color: #225A94; border-radius: 0 0 75px 75px; box-shadow: inset -10px -10px 20px rgba(0,0,0,0.2); }
.pocket { position: absolute; bottom: 30px; left: 50px; width: 50px; height: 40px; background-color: #1A4674; border-radius: 10px 10px 20px 20px; border: 2px dashed #fce144; }
.strap { position: absolute; top: 65px; left: 0; width: 100%; height: 25px; background-color: #333; z-index: 1; }
.goggles-wrapper { position: absolute; top: 50px; left: -5px; width: 160px; display: flex; justify-content: center; z-index: 3; }
.goggle { position: relative; width: 50px; height: 50px; background-color: white; border: 12px solid #999; border-radius: 50%; box-shadow: 3px 3px 8px rgba(0,0,0,0.2), inset 3px 3px 8px rgba(0,0,0,0.1); margin: 0 -2px; overflow: hidden; }
.pupil { position: absolute; top: 50%; left: 50%; width: 20px; height: 20px; background-color: #4B3621; border-radius: 50%; transform: translate(-50%, -50%); transition: transform 0.2s ease-out; }
.pupil::after { content: ''; position: absolute; top: 4px; left: 4px; width: 6px; height: 6px; background-color: black; border-radius: 50%; }
.catchlight { position: absolute; top: 2px; right: 4px; width: 4px; height: 4px; background-color: white; border-radius: 50%; z-index: 4; }
.eyelid { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #FFD90F; border-bottom: 3px solid #D4B200; transform-origin: top; transform: scaleY(0); z-index: 5; animation: blink 4s infinite; }
.mouth { position: absolute; top: 145px; left: 35px; width: 80px; height: 45px; background-color: #3E2723; border-radius: 10px 10px 60px 60px; overflow: hidden; z-index: 3; box-shadow: inset 0 5px 10px rgba(0,0,0,0.5); animation: laugh 0.2s infinite alternate ease-in-out; }
.teeth { position: absolute; top: 0; left: 0; width: 100%; height: 14px; background-color: #fff; border-radius: 0 0 5px 5px; }
.tongue { position: absolute; bottom: -5px; left: 20px; width: 40px; height: 25px; background-color: #FF5252; border-radius: 50%; animation: wag 0.2s infinite alternate ease-in-out; }
.arm { position: absolute; top: 140px; width: 25px; height: 80px; background-color: #FFD90F; border-radius: 12px; z-index: 1; transition: background-color 0.5s; }
.arm.left { left: 10px; transform: rotate(35deg); }
.arm.right { right: 15px; transform: rotate(-35deg); }
.glove { position: absolute; bottom: -15px; left: -5px; width: 35px; height: 35px; background-color: #333; border-radius: 50%; }
.leg { position: absolute; bottom: 50px; width: 25px; height: 40px; background-color: #225A94; z-index: 1; }
.leg.left { left: 60px; }
.leg.right { left: 115px; }
.shoe { position: absolute; bottom: -15px; left: -10px; width: 45px; height: 20px; background-color: #222; border-radius: 20px 20px 5px 5px; border-bottom: 5px solid #111; }
@keyframes blink { 0%, 94%, 100% { transform: scaleY(0); } 97% { transform: scaleY(1); } }
@keyframes laugh { 0% { height: 40px; transform: scaleX(1); } 100% { height: 55px; transform: scaleX(1.05); } }
@keyframes wag { 0% { transform: translateY(0); } 100% { transform: translateY(-3px); } }
</style>
</head>
<body>
<div class="text" id="status-text">Just watch him look around! 👀</div>
<div class="scale-wrapper" id="minionWrapper">
<div class="minion-container">
<div class="arm left" id="armL"><div class="glove"></div></div>
<div class="arm right" id="armR"><div class="glove"></div></div>
<div class="leg left"><div class="shoe"></div></div>
<div class="leg right"><div class="shoe"></div></div>
<div class="body" id="minionBody">
<div class="overalls">
<div class="pocket"></div>
</div>
<div class="strap"></div>
<div class="goggles-wrapper">
<div class="goggle"><div class="pupil"><div class="catchlight"></div></div><div class="eyelid" id="eyelidL"></div></div>
<div class="goggle"><div class="pupil"><div class="catchlight"></div></div><div class="eyelid" id="eyelidR"></div></div>
</div>
<div class="mouth">
<div class="teeth"></div>
<div class="tongue"></div>
</div>
</div>
</div>
</div>
<script>
const APP_IDENTIFIER = 'Color sensor:';
let ws = null;
function connectWebSocket() {
ws = new WebSocket('ws:
ws.onopen = () => document.getElementById('status-text').textContent = "ESP32 - Color Sensor";
ws.onclose = () => setTimeout(connectWebSocket, 2000);
ws.onmessage = (event) => {
if (event.data.startsWith(APP_IDENTIFIER)) {
let color = event.data.substring(APP_IDENTIFIER.length);
document.getElementById('minionBody').style.backgroundColor = color;
document.getElementById('armL').style.backgroundColor = color;
document.getElementById('armR').style.backgroundColor = color;
document.getElementById('eyelidL').style.backgroundColor = color;
document.getElementById('eyelidR').style.backgroundColor = color;
document.getElementById('status-text').style.color = color;
}
};
}
function resizeMinion() {
const wrapper = document.getElementById('minionWrapper');
const availableWidth = window.innerWidth - 40;
const minionTrueWidth = 260;
const minionHeight = 400;
let scaleFactor = availableWidth / minionTrueWidth;
if (scaleFactor > 1.5) scaleFactor = 1.5;
wrapper.style.transform = `scale(${scaleFactor})`;
wrapper.style.height = `${minionHeight * scaleFactor}px`;
}
window.addEventListener('resize', resizeMinion);
resizeMinion();
connectWebSocket();
const pupils = document.querySelectorAll('.pupil');
function moveEyesAutomatically() {
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * 15;
const pupilX = Math.cos(angle) * distance;
const pupilY = Math.sin(angle) * distance;
pupils.forEach(pupil => {
pupil.style.transform = `translate(calc(-50% + ${pupilX}px), calc(-50% + ${pupilY}px))`;
});
}
setInterval(moveEyesAutomatically, 600);
</script>
</body>
</html>
)HTML_WRAPPER";
#endif
Suivez ces étapes pour faire fonctionner le projet :
Connectez le matériel comme indiqué dans le schéma de câblage ci-dessus.
Branchez la carte ESP32 sur votre ordinateur avec un câble USB.
Ouvrez l'IDE Arduino.
Choisissez la bonne carte ESP32 (par exemple, ESP32 Dev Module) et le bon port COM.
Allez à l'icône Bibliothèques dans la barre latérale gauche de l'IDE Arduino.
Recherchez "DIYables ESP32 WebApps" et localisez la bibliothèque par DIYables.
Cliquez sur Installer pour l'installer.
Quand on vous demande les dépendances supplémentaires, cliquez sur Tout installer.
const char WIFI_SSID[] = "VOTRE_NOM_WIFI";
const char WIFI_PASSWORD[] = "VOTRE_MOT_DE_PASSE_WIFI";
Remplacez les valeurs d'étalonnage dans les appels map() à l'intérieur de loop() par les nombres que vous avez notés pendant l'étalonnage. Par exemple, si votre étalonnage a produit redMin = 42, redMax = 210, greenMin = 55, greenMax = 185, blueMin = 60, blueMax = 172, mettez à jour les lignes ainsi :
int r = map(pulseIn(sensorOut, LOW), 42, 210, 255, 0);
int g = map(pulseIn(sensorOut, LOW), 55, 185, 255, 0);
int b = map(pulseIn(sensorOut, LOW), 60, 172, 255, 0);
Starting Custom WebApp...
Custom WebApp ready!
INFO: Added app /
INFO: Added app /custom
DIYables ESP32 WebApp Library
Network connected!
IP address: 192.168.0.5
HTTP server started on port 80
WebSocket server started on port 81
==========================================
DIYables WebApp Ready!
==========================================
📱 Web Interface: http://192.168.0.5
🔗 WebSocket: ws://192.168.0.5:81
📋 Available Applications:
🏠 Home Page: http://192.168.0.5/
🔧 Color sensor WebApp: http://192.168.0.5/custom
==========================================
Sent to Minion: #FFD200
Sent to Minion: #00C832
Sent to Minion: #0028FF
Si rien n'apparaît, essayez d'appuyer sur le bouton reset de l'ESP32.
Copiez l'adresse IP affichée dans le Moniteur série et ouvrez-la dans un navigateur web sur votre téléphone ou ordinateur.
Par exemple : http://192.168.0.5
Sur la page d'accueil, appuyez sur la carte Color sensor WebApp pour ouvrir la page Minion.
Alternativement, allez directement à http://[ADRESSE_IP]/custom.
Vous verrez le Minion animé qui rit sur votre écran.
Tenez un objet coloré près du capteur TCS3200 — la couleur de peau du Minion se mettra à jour instantanément pour refléter la couleur détectée !
Vous pouvez suivre le guide vidéo étape par étape ci-dessous.
Le sketch principal effectue ces tâches :
Configure le capteur TCS3200 : Configure S0/S1 pour une mise à l'échelle de fréquence de 20% et prépare S2/S3 pour la sélection de filtre.
Échantillonne la couleur une fois par seconde : À l'intérieur de loop(), l'ESP32 fait le cycle à travers les filtres de couleur rouge, vert et bleu, mesure la largeur d'impulsion avec pulseIn(), et convertit chaque lecture en une valeur de 0 à 255 en utilisant map() avec vos données d'étalonnage.
Formate en HEX : Les trois valeurs RGB sont combinées en une chaîne HEX (par exemple, #FF8000) en utilisant sprintf() et constrain().
Diffuse aux navigateurs : La couleur HEX est transmise à chaque client web connecté grâce à customPage.sendToWeb().
Le fichier HTML contient :
Un Minion animé en CSS uniquement : Le personnage présente des yeux qui clignent, une bouche qui rit avec une langue qui remue, et des pupilles qui bougent aléatoirement — le tout alimenté par des animations CSS et un petit intervalle JavaScript.
Écouteur WebSocket : JavaScript ouvre une connexion persistante au serveur WebSocket de l'ESP32 sur le port 81 et traite les messages de couleur entrants.
Application de couleur en direct : Chaque couleur HEX reçue est appliquée en douceur au corps, aux bras et aux paupières du Minion en utilisant une transition CSS pour un effet visuel fluide.
Reconnexion automatique : Si le WebSocket se déconnecte, la page retente la connexion toutes les 2 secondes sans intervention de l'utilisateur.
Disposition responsive : Le Minion se redimensionne automatiquement pour s'adapter à toute taille d'écran, des téléphones aux ordinateurs de bureau.
Ce projet suit le framework d'application personnalisée DIYables ESP32 WebApps. Les messages sont étiquetés avec l'identifiant "Color sensor:" :
Pour en savoir plus sur ce modèle de communication et comment construire vos propres applications personnalisées, visitez le Exemple de WebApp personnalisée pour ESP32 - Tutoriel d'interface Web simple pour les débutants..