ESP32 - Color Sensor via Web

In this entertaining project, you will connect a TCS3200D/TCS230 color sensor to an ESP32 and stream the detected colors to a web browser in real-time. The web page features a playful animated Minion character whose skin color updates live based on what the sensor sees. To simplify building the web interface and managing WebSocket communication, this project uses the DIYables ESP32 WebApps library.

ESP32 TCS3200 TCS230 color sensor web Minion

Here is a summary of what happens:

A step-by-step video walkthrough is also available at the bottom of this tutorial.

Hardware Used In This Tutorial

1×ESP-WROOM-32 Dev Module
1×USB Cable Type-A to Type-C (for USB-A PC)
1×USB Cable Type-C to Type-C (for USB-C PC)
1×TCS3200D/TCS230 Color Recognition Sensor Module
1×Jumper Wires
1×Recommended: Screw Terminal Expansion Board for ESP32
1×Recommended: Breakout Expansion Board for ESP32
1×Recommended: Power Splitter for ESP32

Or you can buy the following kits:

1×DIYables ESP32 Starter Kit (ESP32 included)
1×DIYables Sensor Kit (30 sensors/displays)
1×DIYables Sensor Kit (18 sensors/displays)
Disclosure: Some of the links in this section are Amazon affiliate links, meaning we may earn a commission at no additional cost to you if you make a purchase through them. Additionally, some links direct you to products from our own brand, DIYables .

Prerequisites

If you are new to the TCS3200D/TCS230 color sensor or DIYables ESP32 WebApps, the following tutorials will help you get up to speed:

Wiring Diagram

The diagram below shows how to wire the TCS3200 color sensor to an ESP32:

TCS3200 Color SensorESP32
VCC5V (VIN)
GNDGND
S0GPIO 17
S1GPIO 16
S2GPIO 18
S3GPIO 5
OUTGPIO 19
ESP32 and TCS3200 color sensor wiring diagram

This image is created using Fritzing. Click to enlarge image

If you're unfamiliar with how to supply power to the ESP32 and other components, you can find guidance in the following tutorial: The best way to Power ESP32 and sensors/displays.

How It Works

Here is the step-by-step flow of this project:

  1. Every second, the ESP32 reads the color sensor by switching between the red, green, and blue filters using the S2/S3 control pins, and measuring the pulse width on the OUT pin.
  2. The raw pulse width values are converted to 0–255 RGB values using calibration data (obtained from the sensor calibration step).
  3. The RGB values are formatted into a HEX color string such as #FF8000.
  4. This color string is broadcast to all connected web browsers via WebSocket through the DIYables ESP32 WebApps library.
  5. On the web page, JavaScript receives the color and instantly applies it to the Minion character's body, arms, and eyelids.

ESP32 Code - Color Sensor Minion Web App

This project consists of 4 files:

  • ColorSensorESP32.ino - Main sketch: initializes the sensor, reads colors, and sends them to the web page
  • CustomWebApp.h - Header file: declares the custom web app page class
  • CustomWebApp.cpp - Implementation file: manages WebSocket messaging using the "Color sensor:" identifier
  • custom_page_html.h - Web page: the animated Minion built with HTML/CSS/JavaScript that reacts to incoming colors

ColorSensorESP32.ino

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-color-sensor-via-web */ #include <DIYables_ESP32_Platform.h> #include <DIYablesWebApps.h> #include "CustomWebApp.h" // CHANGE THESE TO YOUR WIFI DETAILS const char WIFI_SSID[] = "YOUR_WIFI_SSID"; const char WIFI_PASSWORD[] = "YOUR_WIFI_PASSWORD"; // Configure TCS3200 pins for ESP32 const int S0 = 17; const int S1 = 16; const int S2 = 18; const int S3 = 5; const int sensorOut = 19; // Create server and pages 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..."); // Initialize TCS3200 pins pinMode(S0, OUTPUT); pinMode(S1, OUTPUT); pinMode(S2, OUTPUT); pinMode(S3, OUTPUT); pinMode(sensorOut, INPUT); // Set frequency scaling to 20% digitalWrite(S0, HIGH); digitalWrite(S1, LOW); // Add pages to server webAppsServer.addApp(&homePage); webAppsServer.addApp(&customPage); // Start WiFi and web server 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() { // Handle web server webAppsServer.loop(); // Send sensor data every 1 second if (millis() - lastColorRead > 1000) { // Read Red color digitalWrite(S2, LOW); digitalWrite(S3, LOW); int r = map(pulseIn(sensorOut, LOW), 31, 150, 255, 0); // Read Green color digitalWrite(S2, HIGH); digitalWrite(S3, HIGH); int g = map(pulseIn(sensorOut, LOW), 35, 180, 255, 0); // Read Blue color digitalWrite(S2, LOW); digitalWrite(S3, HIGH); int b = map(pulseIn(sensorOut, LOW), 30, 150, 255, 0); // Convert to HEX color and send to Web 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

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-color-sensor-via-web */ #ifndef CUSTOM_WEBAPP_H #define CUSTOM_WEBAPP_H #include <DIYablesWebApps.h> /** * Simple Custom WebApp Page * * This is a template for creating your own custom web applications. * It provides basic controls like buttons and sliders that communicate * with your Arduino in real-time. */ class CustomWebAppPage : public DIYablesWebAppPageBase { private: // WebSocket message identifier for this custom app static const String APP_IDENTIFIER; public: CustomWebAppPage(); // ======================================== // REQUIRED METHODS - USED BY LIBRARY - DON'T CHANGE THESE! // ======================================== 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; // ======================================== // YOUR METHODS - USE THESE IN YOUR CODE! // ======================================== void onCustomMessageReceived(void (*callback)(const String& payload)); void sendToWeb(const String& message); }; #endif

CustomWebApp.cpp

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-color-sensor-via-web */ #include "CustomWebApp.h" #include "custom_page_html.h" // Define the static member - WebSocket message identifier for this custom app const String CustomWebAppPage::APP_IDENTIFIER = "Color sensor:"; // Callback function for handling messages from web browser void (*customMessageCallback)(const String& payload) = nullptr; CustomWebAppPage::CustomWebAppPage() : DIYablesWebAppPageBase("/custom") { } void CustomWebAppPage::handleHTTPRequest(IWebClient& client) { // Send the HTML page to web browser 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); // Only handle messages that start with our app identifier if (messageStr.startsWith(APP_IDENTIFIER)) { String payload = messageStr.substring(APP_IDENTIFIER.length()); // Remove identifier // Call your callback function with the payload if (customMessageCallback) { customMessageCallback(payload); } } } void CustomWebAppPage::onCustomMessageReceived(void (*callback)(const String& payload)) { customMessageCallback = callback; } void CustomWebAppPage::sendToWeb(const String& message) { // Send message to web browser with app identifier 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

/* * This ESP32 code is created by esp32io.com * * This ESP32 code is released in the public domain * * For more detail (instruction and wiring diagram), visit https://esp32io.com/tutorials/esp32-color-sensor-via-web */ #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> /* GIỮ NGUYÊN TOÀN BỘ CSS GỐC CỦA BẠN */ 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> // LOGIC KẾT NỐI WEBSOCKET const APP_IDENTIFIER = 'Color sensor:'; let ws = null; function connectWebSocket() { ws = new WebSocket('ws://' + location.hostname + ':81'); 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); // Cập nhật màu cho thân, tay và mí mắt 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; } }; } // GIỮ NGUYÊN LOGIC RESIZE & MẮT GỐC 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

Quick Instructions

Follow these steps to get the project running:

  • If this is your first time working with ESP32, check the tutorial on setting up the ESP32 development environment in Arduino IDE.
  • Run the calibration first using the TCS3200D/TCS230 calibration guide for ESP32. Write down your calibration results (redMin, redMax, greenMin, greenMax, blueMin, blueMax).
  • Connect the hardware as shown in the wiring diagram above.
  • Plug the ESP32 board into your computer with a USB cable.
  • Open the Arduino IDE.
  • Choose the correct ESP32 board (e.g., ESP32 Dev Module) and the correct COM port.
  • Go to the Libraries icon on the left sidebar of the Arduino IDE.
  • Search for "DIYables ESP32 WebApps" and locate the library by DIYables.
  • Click Install to install it.
  • When prompted about additional dependencies, click Install All.
DIYables ESP32 WebApps library
DIYables ESP32 WebApps dependency
  • Create a new sketch in Arduino IDE and name it ColorSensorESP32.
  • Copy all 4 files listed above into the project. Your Arduino IDE should show 4 tabs like this:
ESP32 Color Sensor Web App project files in Arduino IDE
  • In ColorSensorESP32.ino, replace the Wi-Fi credentials with your own network details:
const char WIFI_SSID[] = "YOUR_WIFI_NAME"; const char WIFI_PASSWORD[] = "YOUR_WIFI_PASSWORD";
  • Replace the calibration values in the map() calls inside loop() with the numbers you recorded during calibration. For instance, if your calibration produced redMin = 42, redMax = 210, greenMin = 55, greenMax = 185, blueMin = 60, blueMax = 172, update the lines to:
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);
  • Click the Upload button to flash the code to the ESP32.
  • Open the Serial Monitor. You should see something like:
COM6
Send
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
Autoscroll Show timestamp
Clear output
9600 baud  
Newline  
  • If nothing appears, try pressing the reset button on the ESP32.
  • Copy the IP address shown in the Serial Monitor and open it in a web browser on your phone or computer.
  • For example: http://192.168.0.5
  • On the home page, tap the Color sensor WebApp card to open the Minion page.
  • Alternatively, go directly to http://[IP_ADDRESS]/custom.
  • You will see the animated laughing Minion on your screen.
  • Hold a colored object near the TCS3200 sensor — the Minion's skin color will update instantly to reflect the detected color!

You can follow the step-by-step video guide below.

Understanding the Code

ESP32 Side (ColorSensorESP32.ino)

The main sketch performs these tasks:

  • Sets up the TCS3200 sensor: Configures S0/S1 for 20% frequency scaling and prepares S2/S3 for filter selection.
  • Samples color once per second: Inside loop(), the ESP32 cycles through the red, green, and blue color filters, measures the pulse width with pulseIn(), and converts each reading to a 0–255 value using map() with your calibration data.
  • Formats as HEX: The three RGB values are combined into a HEX string (e.g., #FF8000) using sprintf() and constrain().
  • Broadcasts to browsers: The HEX color is transmitted to every connected web client through customPage.sendToWeb().

Web Page Side (custom_page_html.h)

The HTML file contains:

  • A CSS-only animated Minion: The character features blinking eyes, a laughing mouth with a wagging tongue, and pupils that move randomly — all powered by CSS animations and a small JavaScript interval.
  • WebSocket listener: JavaScript opens a persistent connection to the ESP32's WebSocket server on port 81 and processes incoming color messages.
  • Live color application: Each received HEX color is smoothly applied to the Minion's body, arms, and eyelids using a CSS transition for a fluid visual effect.
  • Automatic reconnection: If the WebSocket disconnects, the page retries the connection every 2 seconds without user intervention.
  • Responsive layout: The Minion automatically scales to fit any screen size, from phones to desktops.

Message Protocol

This project follows the DIYables ESP32 WebApps custom app framework. Messages are tagged with the identifier "Color sensor:":

  • ESP32 sends: Color sensor:#FF8000 (identifier prefix + HEX color value)
  • Browser receives: JavaScript strips the Color sensor: prefix and applies the remaining #FF8000 to the Minion

To learn more about this communication pattern and how to build your own custom apps, visit the DIYables ESP32 WebApps Custom WebApp tutorial.

※ OUR MESSAGES