ESP32: How to create a Web Server to control RGB LED

ESP32 Tutorial: RGB LED Control with Web Server

Abstract

This tutorial integrates several key ESP32 features: GPIO Interrupts, PWM (Pulse Width Modulation), and a Web Server. The ESP32 controls a KY-016 RGB LED module (likely a Common Cathode type) via three PWM channels to set the color. Two external push buttons are monitored using Interrupts to quickly adjust the brightness of the LED. A web server provides a full dashboard for setting the RGB color using three individual slider controls and displaying the current brightness level, all styled with a modern dark theme.

1. Introduction to RGB LED Control

Creating a dynamic lighting controller requires mastering two fundamental aspects of microcontroller programming: Pulse Width Modulation (PWM) for color intensity and network communication for remote control. This intermediate ESP32 tutorial integrates these concepts to build a sophisticated lighting system using the KY-016 RGB LED Module.

The ESP32 is uniquely suited for this task, utilizing its dedicated LEDC (LED Control) peripheral to generate the three high-frequency PWM signals necessary for the Red, Green, and Blue channels. This approach ensures smooth, flicker-free color mixing.

Beyond simple local control, the ESP32 hosts a Web Server, serving a modern, dark-themed HTML dashboard. Users can remotely adjust the full 24-bit color spectrum using three individual slider controls and an additional slider for overall brightness. The core value of this project lies in creating a unified system where the digital, network-based color settings are smoothly combined with the efficiency of the ESP32’s hardware-based PWM, delivering a powerful and customizable lighting controller.

2. Prerequisites and Wiring

2.1 Hardware and Libraries

  • Hardware: ESP32 Dev Board, KY-016 RGB LED Module (Common Cathode), Jumper Wires.
  • Libraries: Standard Arduino libraries (h, WebServer.h). The RGB control uses built-in ESP32 PWM functions.

2.2 ESP32-to-Components Wiring

The KY-016 RGB LED is typically Common Cathode (shared pin connects to GND), requiring a current-limiting resistor on each color pin.

Component

Function

ESP32 GPIO Pin

RGB LED (R)

Red Channel

GPIO 25

RGB LED (G)

Green Channel

GPIO 26

RGB LED (B)

Blue Channel

GPIO 27

 

3. The ESP32 Arduino Sketch

The ESP32’s LED Control (LEDC) peripheral handles PWM for the RGB channels, and the attachInterrupt() function handles the buttons.

3.1 Global Variables and State

Note: Considering ESP32 API 3.x

				
					#include <WiFi.h>
#include <WebServer.h>
// Replace with your network credentials
const char* ssid = "SSID";
const char* password = "PASSWORD";
WebServer server(80);
// --- RGB PWM Setup ---
#define LED_R_PIN 25
#define LED_G_PIN 26
#define LED_B_PIN 27
// PWM configuration
#define PWM_FREQ 5000     // 5 kHz
#define PWM_RESOLUTION 8  // 8-bit resolution (0-255)
// PWM Channels (0-15 available)
const int R_CHANNEL = LED_R_PIN;
const int G_CHANNEL = LED_G_PIN;
const int B_CHANNEL = LED_B_PIN;
// RGB Color Values (0-255)
volatile int currentRed = 255;
volatile int currentGreen = 0;
volatile int currentBlue = 0;
// Overall Brightness Multiplier (1-10, percentage of max intensity)
volatile int overallBrightness = 10;

				
			

3.2 PWM Configuration Function

Note: Considering ESP32 API 3.x

				
					void setupPWM() {
  // Configure LEDC channels
void setupPWM() {
  // Configure LEDC channels
  ledcAttach(R_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
  ledcAttach(G_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
  ledcAttach(B_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
  // Set initial color and brightness
  ledcWrite(R_CHANNEL, currentRed);
  ledcWrite(G_CHANNEL, currentGreen);
  ledcWrite(B_CHANNEL, currentBlue);
}

				
			

3.3 Color Calculation and PWM Update

				
					void updateLEDColor() {
  // Calculate the scaled duty cycle for each color component
  // Duty cycle is scaled by the overall brightness (1 to 10) / 10
  int r_duty = (int)(((float)currentRed / 255.0) * (float)overallBrightness / 10.0 * 255.0);
  int g_duty = (int)(((float)currentGreen / 255.0) * (float)overallBrightness / 10.0 * 255.0);
  int b_duty = (int)(((float)currentBlue / 255.0) * (float)overallBrightness / 10.0 * 255.0);

  // Write the new PWM duty cycle
  ledcWrite(R_CHANNEL, r_duty);
  ledcWrite(G_CHANNEL, g_duty);
  ledcWrite(B_CHANNEL, b_duty);
}

				
			

3.4 Setup and Main Loop

				
					void setup() {

 Serial.begin(115200);
 // 1. Setup PWM
 setupPWM();


 // 2. Connect to Wi-Fi
 WiFi.begin(ssid, password);
 Serial.print("Connecting to WiFi...");
 while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.print(".");
 }

 Serial.println("\nWiFi connected.");
 Serial.print("Web Server IP: ");
 Serial.println(WiFi.localIP());

 // 3. Setup Web Server Routes
 server.on("/", HTTP_GET, handleRoot);
 server.on("/color", HTTP_POST, handleColorPost);
 server.begin();
}

void loop() {
 // Handle web client requests
 server.handleClient();
 // The continuous updateLEDColor() is now used ONLY for applying
 // the last set web values, which is efficient as it only runs

 // if the loop is not blocked.

 updateLEDColor();
 delay(10);

}
				
			

3.5 HTML Page 

				
					String getHtmlPage() {
 // Get the current color state in hex format for the color preview
 String hexCode = rgbToHex(currentRed, currentGreen, currentBlue);
 
 String html = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
 <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
  /* --- COLOR PALETTE --- */
  :root {
   --color-primary: #00f5a0;  /* Bright Green/Teal */
   --color-background: #121212; /* Dark/Black */
   --color-container-bg: #1e1e1e; /* Slightly lighter dark for container */
   --color-text-light: #d1d5db; /* Light gray for general text */
   --color-text-dark: #121212; /* Dark text for inside bright boxes */
  }

  body {
   font-family: sans-serif;
   text-align: center;
   margin-top: 20px;
   background-color: var(--color-background);
   color: var(--color-text-light);
  }
  .container {
   background-color: var(--color-container-bg);
   padding: 30px;
   border-radius: 10px;
   box-shadow: 0 4px 15px rgba(0,0,0,0.5);
   display: inline-block;
   width: 90%; /* Responsive width */
   max-width: 450px;
  }
  h1 {
   color: var(--color-primary);
   margin-top: 0;
  }
 
  /* --- DISPLAY BOX --- */
  .display-box {
   border: 1px solid var(--color-primary);
   padding: 20px;
   margin: 20px 0;
   border-radius: 5px;
   background-color: #282828; /* Dark box background */
   display: flex;
   align-items: center;
   justify-content: space-around;
   text-align: left;
  }
  .color-preview {
   width: 60px;
   height: 60px;
   border-radius: 50%;
   margin-right: 15px;
   border: 3px solid var(--color-text-light);
   flex-shrink: 0;
  }
  .current-values strong {
   color: var(--color-primary);
  }
  .brightness-display {
   font-size: 1.5em;
   margin-top: 10px;
   color: var(--color-primary);
   font-weight: bold;
  }
 
  /* --- SLIDERS --- */
  form {
   margin-top: 20px;
  }
  .slider-group {
   margin-bottom: 25px;
   display: flex;
   flex-direction: column;
   align-items: flex-start;
   padding: 0 10px;
  }
  .slider-label {
   font-weight: bold;
   margin-bottom: 5px;
   font-size: 1.1em;
  }
  input[type="range"] {
   width: 100%;
   -webkit-appearance: none;
   background: transparent;
   margin: 5px 0;
  }
  input[type="range"]::-webkit-slider-thumb {
   -webkit-appearance: none;
   height: 20px;
   width: 20px;
   border-radius: 50%;
   cursor: pointer;
   margin-top: -8px;
   border: 1px solid var(--color-text-light);
  }

  /* Custom tracks for color sliders */
  #red_slider::-webkit-slider-runnable-track { background: linear-gradient(to right, #121212, #FF0000); height: 5px; border-radius: 3px; }
  #red_slider::-webkit-slider-thumb { background: #FF0000; }
 
  #green_slider::-webkit-slider-runnable-track { background: linear-gradient(to right, #121212, #00FF00); height: 5px; border-radius: 3px; }
  #green_slider::-webkit-slider-thumb { background: #00FF00; }
 
  #blue_slider::-webkit-slider-runnable-track { background: linear-gradient(to right, #121212, #0000FF); height: 5px; border-radius: 3px; }
  #blue_slider::-webkit-slider-thumb { background: #0000FF; }

    /* Brightness slider style */
    #brightness_slider::-webkit-slider-runnable-track { background: linear-gradient(to right, #282828, var(--color-primary)); height: 5px; border-radius: 3px; }
    #brightness_slider::-webkit-slider-thumb { background: var(--color-primary); }

  input[type="submit"] {
   background-color: var(--color-primary);
   color: var(--color-text-dark);
   padding: 12px 25px;
   border: none;
   border-radius: 5px;
   cursor: pointer;
   margin-top: 15px;
   font-size: 1.2em;
   font-weight: bold;
   transition: background-color 0.3s;
  }
  input[type="submit"]:hover {
    background-color: #00c77d; /* Slightly darker primary on hover */
  }
  .note {
   margin-top: 30px;
   color: var(--color-text-light);
   font-size: 0.9em;
  }
    /* Added JavaScript for live value update */
    .slider-value-display {
        display: inline-block;
        min-width: 30px; /* Prevent text jump */
        text-align: right;
    }
 </style>
 <title>Hacker Embedded RGB LED Controller</title>
  <script>
    function updateValues() {
        document.getElementById('r_val').innerText = document.getElementById('red_slider').value;
        document.getElementById('g_val').innerText = document.getElementById('green_slider').value;
        document.getElementById('b_val').innerText = document.getElementById('blue_slider').value;
        document.getElementById('b_val_pc').innerText = document.getElementById('brightness_slider').value * 10;
       
        // Update color preview in real-time
        var r = document.getElementById('red_slider').value;
        var g = document.getElementById('green_slider').value;
        var b = document.getElementById('blue_slider').value;
        var hex = "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
        document.getElementById('color_preview').style.backgroundColor = hex;
       
    }
    document.addEventListener('DOMContentLoaded', function() {
        // Attach listener to all relevant sliders
        document.getElementById('red_slider').addEventListener('input', updateValues);
        document.getElementById('green_slider').addEventListener('input', updateValues);
        document.getElementById('blue_slider').addEventListener('input', updateValues);
        document.getElementById('brightness_slider').addEventListener('input', updateValues);
    });
  </script>
</head>
<body>
 <div class="container">
  <h1><img decoding="async" src="https://www.github.com/hackerembedded/STM32/blob/main/Logo%20Hacker%20Embedded.png?raw=true" alt="Logo" style="width: 100px; vertical-align: middle; margin-right: 10px;">RGB LED Control</h1>
 
  <div class="display-box">
   <div id="color_preview" class="color-preview" style="background-color: CURRENT_HEX_CODE;"></div>
   <div class="current-values">
    <strong>Color:</strong> R: <span id="r_static">CURRENT_RED</span> G: <span id="g_static">CURRENT_GREEN</span> B: <span id="b_static">CURRENT_BLUE</span><br>
    <div class="brightness-display">Brightness: <span id="b_pc_static">BRIGHTNESS_PERCENT</span>%</div>
   </div>
  </div>
 
  <form method="POST" action="/color">
   <div class="slider-group">
    <label for="red_slider" class="slider-label" style="color:#FF6666;">Red: <span id="r_val" class="slider-value-display">CURRENT_RED</span></label>
    <input type="range" id="red_slider" name="red_value" min="0" max="255" value="CURRENT_RED">
   </div>
   
   <div class="slider-group">
    <label for="green_slider" class="slider-label" style="color:#66FF66;">Green: <span id="g_val" class="slider-value-display">CURRENT_GREEN</span></label>
    <input type="range" id="green_slider" name="green_value" min="0" max="255" value="CURRENT_GREEN">
   </div>
   
   <div class="slider-group">
    <label for="blue_slider" class="slider-label" style="color:#6666FF;">Blue: <span id="b_val" class="slider-value-display">CURRENT_BLUE</span></label>
    <input type="range" id="blue_slider" name="blue_value" min="0" max="255" value="CURRENT_BLUE">
   </div>
   
         <div class="slider-group">
    <label for="brightness_slider" class="slider-label" style="color:var(--color-primary);">Brightness (10% to 100%): <span id="b_val_pc" class="slider-value-display">BRIGHTNESS_PERCENT</span>%</label>
    <input type="range" id="brightness_slider" name="brightness_value" min="1" max="10" value="BRIGHTNESS_LEVEL">
   </div>
         <input type="submit" value="Set Color & Brightness">
  </form>
 
 </div>
 <p class="note">ESP32 IP: IP_ADDRESS</p>
</body>
</html>
)rawliteral";

 // Replace placeholders (unchanged from previous version)
 html.replace("CURRENT_HEX_CODE", hexCode);
 html.replace("CURRENT_RED", String(currentRed));
 html.replace("CURRENT_GREEN", String(currentGreen));
 html.replace("CURRENT_BLUE", String(currentBlue));
  // The brightness level is 1-10, which we need for the slider 'value'
  html.replace("BRIGHTNESS_LEVEL", String(overallBrightness));
  // The brightness percentage is (1-10) * 10, which we show the user
 html.replace("BRIGHTNESS_PERCENT", String(overallBrightness * 10));
 html.replace("IP_ADDRESS", WiFi.localIP().toString());

 return html;
}

				
			

The web server allows for full RGB control via HTML slider inputs (<input type=”range”>).

3.6 Web Handlers (Updated for Sliders)

				
					// Handler for the root web page (dashboard)
void handleRoot() {
 server.send(200, "text/html", getHtmlPage());
}

// --- HANDLER TO READ BRIGHTNESS ---
void handleColorPost() {
 // Check for R, G, B, and the new brightness slider argument
 if (server.hasArg("red_value") && server.hasArg("green_value") && server.hasArg("blue_value") && server.hasArg("brightness_value")) {
 
  // Read and update RGB values
  currentRed  = server.arg("red_value").toInt();
  currentGreen = server.arg("green_value").toInt();
  currentBlue = server.arg("blue_value").toInt();
   
    // Read and update the brightness value (1-10)
    overallBrightness = server.arg("brightness_value").toInt();
 
  // Apply the new color and brightness immediately
  updateLEDColor();

  // Redirect back to the main dashboard
  server.sendHeader("Location", "/");
  server.send(303);
 } else {
  server.send(400, "text/plain", "Invalid request. Missing one or more RGB/Brightness arguments.");
 }
}
// ------------------------------------------

// Helper to convert R, G, B values to a #RRGGBB hex string for HTML
String rgbToHex(int r, int g, int b) {
 String hex = "#";
 if (r < 16) hex += "0";
 hex += String(r, HEX);
 if (g < 16) hex += "0";
 hex += String(g, HEX);
 if (b < 16) hex += "0";
 hex += String(b, HEX);
 return hex;
}

				
			

4. Source Code

The entire code for an easier copy and paste:

				
					#include <WiFi.h>
#include <WebServer.h>

// Replace with your network credentials
const char* ssid = "SSID";
const char* password = "PASSWORD";

WebServer server(80);

// --- RGB PWM Setup ---
#define LED_R_PIN 25
#define LED_G_PIN 26
#define LED_B_PIN 27

// PWM configuration
#define PWM_FREQ 5000   // 5 kHz
#define PWM_RESOLUTION 8 // 8-bit resolution (0-255)

// PWM Channels (0-15 available)
const int R_CHANNEL = LED_R_PIN;
const int G_CHANNEL = LED_G_PIN;
const int B_CHANNEL = LED_B_PIN;

// RGB Color Values (0-255)
int currentRed = 255; 
int currentGreen = 0;
int currentBlue = 0;

// Overall Brightness Multiplier (1-10, percentage of max intensity)
int overallBrightness = 10; 

void setupPWM() {
 // Configure LEDC channels and attach pins
 ledcAttach(R_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
 ledcAttach(G_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
 ledcAttach(B_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
  
 // Set initial color and brightness
 ledcWrite(R_CHANNEL, currentRed);
 ledcWrite(G_CHANNEL, currentGreen);
 ledcWrite(B_CHANNEL, currentBlue);
}

void updateLEDColor() {
 // Calculate the scaled duty cycle for each color component
 // Duty cycle is scaled by the overall brightness (1 to 10) / 10
 int r_duty = (int)(((float)currentRed / 255.0) * (float)overallBrightness / 10.0 * 255.0);
 int g_duty = (int)(((float)currentGreen / 255.0) * (float)overallBrightness / 10.0 * 255.0);
 int b_duty = (int)(((float)currentBlue / 255.0) * (float)overallBrightness / 10.0 * 255.0);

 // Write the new PWM duty cycle
 ledcWrite(R_CHANNEL, r_duty);
 ledcWrite(G_CHANNEL, g_duty);
 ledcWrite(B_CHANNEL, b_duty);
}

// Handler for the root web page (dashboard)
void handleRoot() {
 server.send(200, "text/html", getHtmlPage());
}

// --- HANDLER TO READ BRIGHTNESS ---
void handleColorPost() {
 // Check for R, G, B, and the new brightness slider argument
 if (server.hasArg("red_value") && server.hasArg("green_value") && server.hasArg("blue_value") && server.hasArg("brightness_value")) {
  
  // Read and update RGB values
  currentRed  = server.arg("red_value").toInt();
  currentGreen = server.arg("green_value").toInt();
  currentBlue = server.arg("blue_value").toInt();
    
    // Read and update the brightness value (1-10)
    overallBrightness = server.arg("brightness_value").toInt();
  
  // Apply the new color and brightness immediately
  updateLEDColor();

  // Redirect back to the main dashboard
  server.sendHeader("Location", "/");
  server.send(303);
 } else {
  server.send(400, "text/plain", "Invalid request. Missing one or more RGB/Brightness arguments.");
 }
}
// ------------------------------------------

// Helper to convert R, G, B values to a #RRGGBB hex string for HTML
String rgbToHex(int r, int g, int b) {
 String hex = "#";
 if (r < 16) hex += "0";
 hex += String(r, HEX);
 if (g < 16) hex += "0";
 hex += String(g, HEX);
 if (b < 16) hex += "0";
 hex += String(b, HEX);
 return hex;
}

String getHtmlPage() {
 // Get the current color state in hex format for the color preview
 String hexCode = rgbToHex(currentRed, currentGreen, currentBlue);
 
 String html = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
 <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
  /* --- COLOR PALETTE --- */
  :root {
   --color-primary: #00f5a0;  /* Bright Green/Teal */
   --color-background: #121212; /* Dark/Black */
   --color-container-bg: #1e1e1e; /* Slightly lighter dark for container */
   --color-text-light: #d1d5db; /* Light gray for general text */
   --color-text-dark: #121212; /* Dark text for inside bright boxes */
  }

  body { 
   font-family: sans-serif; 
   text-align: center; 
   margin-top: 20px; 
   background-color: var(--color-background);
   color: var(--color-text-light);
  }
  .container { 
   background-color: var(--color-container-bg); 
   padding: 30px; 
   border-radius: 10px; 
   box-shadow: 0 4px 15px rgba(0,0,0,0.5); 
   display: inline-block; 
   width: 90%; /* Responsive width */
   max-width: 450px;
  }
  h1 { 
   color: var(--color-primary); 
   margin-top: 0;
  }
  
  /* --- DISPLAY BOX --- */
  .display-box { 
   border: 1px solid var(--color-primary); 
   padding: 20px; 
   margin: 20px 0; 
   border-radius: 5px; 
   background-color: #282828; /* Dark box background */
   display: flex;
   align-items: center;
   justify-content: space-around;
   text-align: left;
  }
  .color-preview { 
   width: 60px; 
   height: 60px; 
   border-radius: 50%; 
   margin-right: 15px; 
   border: 3px solid var(--color-text-light);
   flex-shrink: 0;
  }
  .current-values strong { 
   color: var(--color-primary); 
  }
  .brightness-display { 
   font-size: 1.5em; 
   margin-top: 10px; 
   color: var(--color-primary); 
   font-weight: bold; 
  }
  
  /* --- SLIDERS --- */
  form { 
   margin-top: 20px; 
  }
  .slider-group {
   margin-bottom: 25px;
   display: flex;
   flex-direction: column;
   align-items: flex-start;
   padding: 0 10px;
  }
  .slider-label {
   font-weight: bold;
   margin-bottom: 5px;
   font-size: 1.1em;
  }
  input[type="range"] { 
   width: 100%; 
   -webkit-appearance: none;
   background: transparent;
   margin: 5px 0;
  }
  input[type="range"]::-webkit-slider-thumb {
   -webkit-appearance: none;
   height: 20px;
   width: 20px;
   border-radius: 50%;
   cursor: pointer;
   margin-top: -8px; 
   border: 1px solid var(--color-text-light);
  }

  /* Custom tracks for color sliders */
  #red_slider::-webkit-slider-runnable-track { background: linear-gradient(to right, #121212, #FF0000); height: 5px; border-radius: 3px; }
  #red_slider::-webkit-slider-thumb { background: #FF0000; }
  
  #green_slider::-webkit-slider-runnable-track { background: linear-gradient(to right, #121212, #00FF00); height: 5px; border-radius: 3px; }
  #green_slider::-webkit-slider-thumb { background: #00FF00; }
  
  #blue_slider::-webkit-slider-runnable-track { background: linear-gradient(to right, #121212, #0000FF); height: 5px; border-radius: 3px; }
  #blue_slider::-webkit-slider-thumb { background: #0000FF; }

    /* Brightness slider style */
    #brightness_slider::-webkit-slider-runnable-track { background: linear-gradient(to right, #282828, var(--color-primary)); height: 5px; border-radius: 3px; }
    #brightness_slider::-webkit-slider-thumb { background: var(--color-primary); }

  input[type="submit"] { 
   background-color: var(--color-primary); 
   color: var(--color-text-dark); 
   padding: 12px 25px; 
   border: none; 
   border-radius: 5px; 
   cursor: pointer; 
   margin-top: 15px; 
   font-size: 1.2em; 
   font-weight: bold;
   transition: background-color 0.3s;
  }
  input[type="submit"]:hover {
    background-color: #00c77d; /* Slightly darker primary on hover */
  }
  .note {
   margin-top: 30px; 
   color: var(--color-text-light);
   font-size: 0.9em;
  }
    /* Added JavaScript for live value update */
    .slider-value-display { 
        display: inline-block; 
        min-width: 30px; /* Prevent text jump */
        text-align: right;
    }
 </style>
 <title>Hacker Embedded RGB LED Controller</title>
  <script>
    function updateValues() {
        document.getElementById('r_val').innerText = document.getElementById('red_slider').value;
        document.getElementById('g_val').innerText = document.getElementById('green_slider').value;
        document.getElementById('b_val').innerText = document.getElementById('blue_slider').value;
        document.getElementById('b_val_pc').innerText = document.getElementById('brightness_slider').value * 10;
        
        // Update color preview in real-time
        var r = document.getElementById('red_slider').value;
        var g = document.getElementById('green_slider').value;
        var b = document.getElementById('blue_slider').value;
        var hex = "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
        document.getElementById('color_preview').style.backgroundColor = hex;
        
    }
    document.addEventListener('DOMContentLoaded', function() {
        // Attach listener to all relevant sliders
        document.getElementById('red_slider').addEventListener('input', updateValues);
        document.getElementById('green_slider').addEventListener('input', updateValues);
        document.getElementById('blue_slider').addEventListener('input', updateValues);
        document.getElementById('brightness_slider').addEventListener('input', updateValues);
    });
  </script>
</head>
<body>
 <div class="container">
  <h1><img decoding="async" src="https://www.github.com/hackerembedded/STM32/blob/main/Logo%20Hacker%20Embedded.png?raw=true" alt="Logo" style="width: 100px; vertical-align: middle; margin-right: 10px;">RGB LED Control</h1>
  
  <div class="display-box">
   <div id="color_preview" class="color-preview" style="background-color: CURRENT_HEX_CODE;"></div>
   <div class="current-values">
    <strong>Color:</strong> R: <span id="r_static">CURRENT_RED</span> G: <span id="g_static">CURRENT_GREEN</span> B: <span id="b_static">CURRENT_BLUE</span><br>
    <div class="brightness-display">Brightness: <span id="b_pc_static">BRIGHTNESS_PERCENT</span>%</div>
   </div>
  </div>
  
  <form method="POST" action="/color">
   <div class="slider-group">
    <label for="red_slider" class="slider-label" style="color:#FF6666;">Red: <span id="r_val" class="slider-value-display">CURRENT_RED</span></label>
    <input type="range" id="red_slider" name="red_value" min="0" max="255" value="CURRENT_RED">
   </div>
   
   <div class="slider-group">
    <label for="green_slider" class="slider-label" style="color:#66FF66;">Green: <span id="g_val" class="slider-value-display">CURRENT_GREEN</span></label>
    <input type="range" id="green_slider" name="green_value" min="0" max="255" value="CURRENT_GREEN">
   </div>
   
   <div class="slider-group">
    <label for="blue_slider" class="slider-label" style="color:#6666FF;">Blue: <span id="b_val" class="slider-value-display">CURRENT_BLUE</span></label>
    <input type="range" id="blue_slider" name="blue_value" min="0" max="255" value="CURRENT_BLUE">
   </div>
   
         <div class="slider-group">
    <label for="brightness_slider" class="slider-label" style="color:var(--color-primary);">Brightness (10% to 100%): <span id="b_val_pc" class="slider-value-display">BRIGHTNESS_PERCENT</span>%</label>
    <input type="range" id="brightness_slider" name="brightness_value" min="1" max="10" value="BRIGHTNESS_LEVEL">
   </div>
         <input type="submit" value="Set Color & Brightness">
  </form>
  
 </div>
 <p class="note">ESP32 IP: IP_ADDRESS</p>
</body>
</html>
)rawliteral";

 // Replace placeholders (unchanged from previous version)
 html.replace("CURRENT_HEX_CODE", hexCode);
 html.replace("CURRENT_RED", String(currentRed));
 html.replace("CURRENT_GREEN", String(currentGreen));
 html.replace("CURRENT_BLUE", String(currentBlue));
  // The brightness level is 1-10, which we need for the slider 'value'
  html.replace("BRIGHTNESS_LEVEL", String(overallBrightness)); 
  // The brightness percentage is (1-10) * 10, which we show the user
 html.replace("BRIGHTNESS_PERCENT", String(overallBrightness * 10));
 html.replace("IP_ADDRESS", WiFi.localIP().toString()); 

 return html;
}

void setup() {
 Serial.begin(115200);
 
 // 1. Setup PWM
 setupPWM();

 // 2. Connect to Wi-Fi
 WiFi.begin(ssid, password);
 Serial.print("Connecting to WiFi...");
 while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.print(".");
 }
 Serial.println("\nWiFi connected.");
 Serial.print("Web Server IP: ");
 Serial.println(WiFi.localIP());

 // 3. Setup Web Server Routes
 server.on("/", HTTP_GET, handleRoot);
 server.on("/color", HTTP_POST, handleColorPost); 
 server.begin();
}

void loop() {
 // Handle web client requests
 server.handleClient();
 
 // The continuous updateLEDColor() is now used ONLY for applying
 // the last set web values, which is efficient as it only runs
 // if the loop is not blocked.
 updateLEDColor(); 
 
 delay(10); 
}
				
			

4.1 Code Explanation and Logic Breakdown

The code’s complexity stems from integrating the LEDC PWM peripheral for hardware control and the WebServer for handling multiple slider inputs simultaneously.

4.1.1 PWM Configuration (setupPWM)

Instead of the standard Arduino analogWrite(), the ESP32 uses its dedicated hardware-accelerated LEDC peripheral for PWM generation, which is configured in setupPWM().

  • Constants: PWM_FREQ (5 kHz) and PWM_RESOLUTION (8-bit, 0-255) are defined globally to ensure smooth color transitions and maximum color depth.

  • ledcAttach(channel, frequency, resolution): This function maps the chosen GPIO pins (25, 26, 27) to unique LEDC channels, setting the frequency and resolution for each.

  • ledcWrite(channel, duty_cycle): This is the function used later to actually set the intensity (duty cycle) for each R, G, or B color.

4.1.2 Color Calculation and Update (updateLEDColor)

This function is the core logic that combines the user’s color selection with the current brightness level.

				
					void updateLEDColor() {
    // ...
    int r_duty = (int)(((float)currentRed / 255.0) * (float)overallBrightness / 10.0 * 255.0);
    // ...
    ledcWrite(R_CHANNEL, r_duty);
    // ...
}
				
			
  • Scaling: The raw 8-bit color values (currentRed, 0-255) are first normalized to a float (0.0 to 1.0) by dividing by 255.0.

  • Brightness Multiplier: This normalized value is then scaled by the overallBrightness (which ranges from 1 to 10) divided by 10.0. This makes the overallBrightness act as a global intensity percentage (10% to 100%).

  • Final Duty Cycle: The result is multiplied by 255.0 and cast back to an integer to get the final duty cycle for the 8-bit LEDC write.

4.1.3 Web POST Handler (handleColorPost)

This handler receives all color and brightness data simultaneously via an HTTP POST request from the HTML form.

				
					void handleColorPost() {
    if (server.hasArg("red_value") && /* ... and other args ... */) {
        currentRed = server.arg("red_value").toInt();
        // ... update currentGreen, currentBlue
        overallBrightness = server.arg("brightness_value").toInt();
        
        updateLEDColor(); // Apply changes immediately
        server.sendHeader("Location", "/");
        server.send(303); // Redirect back to refresh
    }
}
				
			
  • Bulk Reading: It checks for the existence of all four required arguments (red_value, green_value, blue_value, brightness_value) before proceeding.

  • .toInt(): The server.arg() method returns a String, so .toInt() is crucial for converting the text input from the HTML sliders into usable integer values.

  • Synchronization: Calling updateLEDColor() immediately after updating the global variables ensures the physical LED reacts instantly.

  • Redirection: The HTTP 303 (See Other) response forces the client browser to reload the root page, which refreshes the dashboard to show the newly set values.

4.1.4 Loop Execution

The loop() function primarily calls server.handleClient(), which is responsible for processing incoming web requests. The separate updateLEDColor() call inside the loop ensures that the LED is constantly updated with the latest values, providing a stable output.

5. Hands-On Lab Recap

This project effectively demonstrates the power of concurrent processing on the ESP32:

  • PWM Control: The ESP32’s LEDC (LED Control) peripheral is used to generate 8-bit PWM signals on the R, G, and B pins of the KY-016 module.
  • Interrupts: GPIO Interrupts are attached to the buttons for immediate, non-blocking reading, enabling instant brightness changes without delay.
  • Web Control: The web server hosts an HTML form with RGB sliders to allow users to set the raw color values. The data is sent via an HTTP POST request, which is handled by the handleColorPost()
  • Connect to the IP shown in the terminal and play with the RGB
  • Hybrid Control: The final color output is a combination of the web-set raw RGB values and the interrupt-set overall brightness multiplier, demonstrating effective simultaneous control from both physical and network interfaces.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top