ESP32 Tutorial: Infrared Remote Control Webserver with ESP32 and KY-005
Abstract
This article guides you through creating a self-hosted web application on an ESP32 microcontroller. This application serves a simple webpage with virtual remote control keys (Power, Volume, Channel, etc.) and translates key presses into Infrared (IR) signals using the KY-005 IR Transmitter module.
1. Introduction: Infrared Remote Control Webserver with ESP32 and KY-005
In the modern connected home, traditional Infrared (IR) remote controls often represent a gap between legacy appliances (like TVs, stereos, and air conditioners) and the convenience of network-based control. This project bridges that gap using the powerful ESP32 microcontroller as a Smart Home Gateway.
This tutorial will guide you through creating a self-hosted, full-featured Web Server on the ESP32. This server hosts a sleek, responsive web page—essentially a virtual remote control—that can be accessed and operated from any web browser on your local network (e.g., your smartphone or laptop).
The core function of the application is to translate digital input (a button click on the web page) into a physical, modulated Infrared signal using the affordable KY-005 IR Transmitter module and the specialized IRremote library. By mapping specific Universal Resource Identifiers (URIs) on the web server to corresponding IR hex codes (like those for Power, Volume, and Channel), you will learn how to send commands that can instantly control any IR-compatible device, bringing your older electronics into the age of Wi-Fi control. This project provides a practical foundation for understanding how to integrate network communication and physical I/O control in a single IoT device.
2. Prerequisites and Wiring
2.1 Hardware
- ESP32 Development Board (any model, e.g., ESP32-WROOM-32)
- KY-005 IR Transmitter Module (or any 3-pin IR LED module)
- Breadboard and Jumper Wires
- Micro-USB cable for power and programming
2.2 Software
- Arduino IDE (or VS Code with PlatformIO)
- ESP32 Board Support Package installed in the Arduino IDE.
- Required Libraries:
- ESP WebServer: For creating the HTTP web server on the ESP32.
- IRremote: A powerful library for sending and receiving IR signals that works with several boards, including the ESP devices.
To install the libraries, open the Arduino IDE, go to Sketch > Include Library > Manage Libraries…, and search for the names above.
2.3 Wiring the KY-005 IR Transmitter
The KY-005 module typically has three pins: Signal (S), VCC (+), and GND (-).
KY-005 Pin | ESP32 Pin | Note |
S (Signal) | GPIO 4 | This is the dedicated IR output pin in the code below. |
+ (VCC) | 5V | Power supply for the module. |
– (GND) | GND | Ground connection. |
Important Note: GPIO 4 is used as a standard example. You can change the IR_SEND_PIN in the code to any suitable ESP32 GPIO pin.
Add a 100-220 Ohm resistor at R1 position on KY-005.
3. The ESP32 Arduino Sketch
The code has three main parts: WiFi & Server Setup, the HTML webpage, and the IR code sending logic. This example will rely on a Samsung TV, but other models and patterns can be used, just edit the sendIrCommand() function.
3.1 Define WiFi and IR Settings
First, include the necessary libraries and define your network credentials and the GPIO pin for the IR LED.
#include
#include
#include
#include "image_data.h"
// --- USER CONFIGURATION ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const int IR_TX_PIN = 4; // KY-005 Signal pin connected to GPIO 4
// --------------------------
// --- IR CODES (NEC PROTOCOL EXAMPLE) ---
// --- IR CODES (NEC PROTOCOL) ---
// Codes extracted from your switch-case block
#define POWER_TOGGLE_CODE 0xFD020707 // power toggle: Universal Remote
#define VOL_UP_CODE 0xF8070707 // volume up
#define VOL_DOWN_CODE 0xF40B0707 // volume down
#define MUTE_CODE 0xF00F0707 // mute / vol push
#define CH_UP_CODE 0xED120707 // channel up - universal
#define CH_DOWN_CODE 0xEF100707 // channel down - universal
#define CH_LIST_CODE 0x946B0707 // channel list (using the 4-byte version)
#define UP_CODE 0x9F600707 // up - Universal
#define DOWN_CODE 0x9E610707 // down - Universal
#define LEFT_CODE 0x9A650707 // left - Universal
#define RIGHT_CODE 0x9D620707 // right - Universal
#define OK_ENTER_CODE 0x97680707 // ok / enter - universal
#define MENU_CODE 0xE51A0707 // menu - universal
#define RETURN_GO_BACK_CODE 0xEC130707 // return / go back - universal (using the 4-byte version)
#define EXIT_CODE 0xD22D0707 // exit - universal
#define INPUT_CODE 0xFE010707 // input - universal
#define DIGITAL_1_CODE 0xFB040707 // digital 1
#define DIGITAL_2_CODE 0xFA050707 // digital 2
#define DIGITAL_3_CODE 0xF9060707 // digital 3
#define DIGITAL_4_CODE 0xF7080707 // digital 4
#define DIGITAL_5_CODE 0xF6090707 // digital 5
#define DIGITAL_6_CODE 0xF50A0707 // digital 6
#define DIGITAL_7_CODE 0xF30C0707 // digital 7
#define DIGITAL_8_CODE 0xF20D0707 // digital 8
#define DIGITAL_9_CODE 0xF10E0707 // digital 9
#define DIGITAL_0_CODE 0xEE110707 // digital 0
// Server object on port 80 (standard HTTP)
WebServer server(80);
3.2 The Webpage HTML (handleRoot())
This function generates the main webpage with the remote control buttons. Each button is an <a> tag that links to a specific URI (e.g., /power), which the server will intercept.
// Function to send a specific IR command
void sendIrCommand(uint32_t code, const char* name) {
// Extract the 16-bit command (upper 16 bits of the 32-bit code)
uint16_t command = (uint16_t)(code >> 16);
// Extract the 16-bit address (lower 16 bits of the 32-bit code)
uint16_t address = (uint16_t)(code & 0xFFFF);
// Send the code using the Samsung protocol
// The address and command are sent once (1 repeat)
IrSender.sendSamsung(address, command, 1);
// Print a more informative message for debugging
Serial.printf("Sent Samsung IR Code - Address: 0x%X, Command: 0x%X for command: %s\n",
(unsigned int)address, (unsigned int)command, name);
}
// HTML content for the web server
String get_html_page() {
String html = R"rawliteral(
ESP32 Hacker Remote
ESP32 Hacker Remote
Navigation
Number Pad
)rawliteral";
return html;
}
// Handler for the root path ("/")
void handleRoot() {
server.send(200, "text/html", get_html_page());
}
3.3 Server Handlers and Main Logic
These functions define what happens when the server receives a request for a specific command URI (e.g., /vol_up).
// --- Handlers for remote control keys ---
// Note: Each handler calls handleRoot() at the end to refresh the page after sending the command.
// Power and Volume/Channel
void handlePower() { sendIrCommand(POWER_TOGGLE_CODE, "POWER"); handleRoot(); }
void handleVolUp() { sendIrCommand(VOL_UP_CODE, "VOL UP"); handleRoot(); }
void handleVolDown() { sendIrCommand(VOL_DOWN_CODE, "VOL DOWN"); handleRoot(); }
void handleMute() { sendIrCommand(MUTE_CODE, "MUTE"); handleRoot(); }
void handleChUp() { sendIrCommand(CH_UP_CODE, "CH UP"); handleRoot(); }
void handleChDown() { sendIrCommand(CH_DOWN_CODE, "CH DOWN"); handleRoot(); }
// Navigation
void handleUp() { sendIrCommand(UP_CODE, "UP"); handleRoot(); }
void handleDown() { sendIrCommand(DOWN_CODE, "DOWN"); handleRoot(); }
void handleLeft() { sendIrCommand(LEFT_CODE, "LEFT"); handleRoot(); }
void handleRight() { sendIrCommand(RIGHT_CODE, "RIGHT"); handleRoot(); }
void handleOk() { sendIrCommand(OK_ENTER_CODE, "OK/ENTER"); handleRoot(); }
// Function Buttons
void handleMenu() { sendIrCommand(MENU_CODE, "MENU"); handleRoot(); }
void handleReturn() { sendIrCommand(RETURN_GO_BACK_CODE, "RETURN/BACK"); handleRoot(); }
void handleExit() { sendIrCommand(EXIT_CODE, "EXIT"); handleRoot(); }
void handleInput() { sendIrCommand(INPUT_CODE, "INPUT"); handleRoot(); }
// Number Pad
void handleD0() { sendIrCommand(DIGITAL_0_CODE, "0"); handleRoot(); }
void handleD1() { sendIrCommand(DIGITAL_1_CODE, "1"); handleRoot(); }
void handleD2() { sendIrCommand(DIGITAL_2_CODE, "2"); handleRoot(); }
void handleD3() { sendIrCommand(DIGITAL_3_CODE, "3"); handleRoot(); }
void handleD4() { sendIrCommand(DIGITAL_4_CODE, "4"); handleRoot(); }
void handleD5() { sendIrCommand(DIGITAL_5_CODE, "5"); handleRoot(); }
void handleD6() { sendIrCommand(DIGITAL_6_CODE, "6"); handleRoot(); }
void handleD7() { sendIrCommand(DIGITAL_7_CODE, "7"); handleRoot(); }
void handleD8() { sendIrCommand(DIGITAL_8_CODE, "8"); handleRoot(); }
void handleD9() { sendIrCommand(DIGITAL_9_CODE, "9"); handleRoot(); }
void setup() {
Serial.begin(115200);
IrSender.begin(IR_TX_PIN);
// 1. Connect to WiFi
Serial.print("Connecting to WiFi..");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected.");
Serial.print("Web Server IP: ");
Serial.println(WiFi.localIP());
// Serve the logo image
server.on("/logo.png", HTTP_GET, []() {
server.send_P(200, "image/png", (const char*) logo_hacker_png, logo_hacker_png_len);
});
// 2. Setup Server Routes
server.on("/", handleRoot);
server.on("/power", handlePower);
server.on("/vol_up", handleVolUp);
server.on("/vol_down", handleVolDown);
server.on("/mute", handleMute);
server.on("/ch_up", handleChUp);
server.on("/ch_down", handleChDown);
// Navigation
server.on("/up", handleUp);
server.on("/down", handleDown);
server.on("/left", handleLeft);
server.on("/right", handleRight);
server.on("/ok", handleOk);
// Function Buttons
server.on("/menu", handleMenu);
server.on("/return", handleReturn);
server.on("/exit", handleExit);
server.on("/input", handleInput);
// Number Pad
server.on("/d0", handleD0);
server.on("/d1", handleD1);
server.on("/d2", handleD2);
server.on("/d3", handleD3);
server.on("/d4", handleD4);
server.on("/d5", handleD5);
server.on("/d6", handleD6);
server.on("/d7", handleD7);
server.on("/d8", handleD8);
server.on("/d9", handleD9);
// 3. Start the Server
server.begin();
Serial.println("HTTP Server started.");
}
void loop() {
// Main server loop to handle client requests
server.handleClient();
4. Source Code
The entire code for an easier copy and paste:
#include
#include
#include
#include "image_data.h"
// --- USER CONFIGURATION ---
const char* ssid = "SSDI";
const char* password = "PASSWORD";
const int IR_TX_PIN = 4; // KY-005 Signal pin connected to GPIO 4
// --------------------------
// --- IR CODES (NEC PROTOCOL) ---
// Codes extracted from your switch-case block
#define POWER_TOGGLE_CODE 0xFD020707 // power toggle: Universal Remote
#define VOL_UP_CODE 0xF8070707 // volume up
#define VOL_DOWN_CODE 0xF40B0707 // volume down
#define MUTE_CODE 0xF00F0707 // mute / vol push
#define CH_UP_CODE 0xED120707 // channel up - universal
#define CH_DOWN_CODE 0xEF100707 // channel down - universal
#define CH_LIST_CODE 0x946B0707 // channel list (using the 4-byte version)
#define UP_CODE 0x9F600707 // up - Universal
#define DOWN_CODE 0x9E610707 // down - Universal
#define LEFT_CODE 0x9A650707 // left - Universal
#define RIGHT_CODE 0x9D620707 // right - Universal
#define OK_ENTER_CODE 0x97680707 // ok / enter - universal
#define MENU_CODE 0xE51A0707 // menu - universal
#define RETURN_GO_BACK_CODE 0xEC130707 // return / go back - universal (using the 4-byte version)
#define EXIT_CODE 0xD22D0707 // exit - universal
#define INPUT_CODE 0xFE010707 // input - universal
#define DIGITAL_1_CODE 0xFB040707 // digital 1
#define DIGITAL_2_CODE 0xFA050707 // digital 2
#define DIGITAL_3_CODE 0xF9060707 // digital 3
#define DIGITAL_4_CODE 0xF7080707 // digital 4
#define DIGITAL_5_CODE 0xF6090707 // digital 5
#define DIGITAL_6_CODE 0xF50A0707 // digital 6
#define DIGITAL_7_CODE 0xF30C0707 // digital 7
#define DIGITAL_8_CODE 0xF20D0707 // digital 8
#define DIGITAL_9_CODE 0xF10E0707 // digital 9
#define DIGITAL_0_CODE 0xEE110707 // digital 0
// Server object on port 80 (standard HTTP)
WebServer server(80);
// Function to send a specific IR command
void sendIrCommand(uint32_t code, const char* name) {
// Extract the 16-bit command (upper 16 bits of the 32-bit code)
uint16_t command = (uint16_t)(code >> 16);
// Extract the 16-bit address (lower 16 bits of the 32-bit code)
uint16_t address = (uint16_t)(code & 0xFFFF);
// Send the code using the Samsung protocol
// The address and command are sent once (1 repeat)
IrSender.sendSamsung(address, command, 1);
// Print a more informative message for debugging
Serial.printf("Sent Samsung IR Code - Address: 0x%X, Command: 0x%X for command: %s\n",
(unsigned int)address, (unsigned int)command, name);
}
// HTML content for the web server
String get_html_page() {
String html = R"rawliteral(
ESP32 Hacker Remote
ESP32 Hacker Remote
Navigation
Number Pad
)rawliteral";
return html;
}
// Handler for the root path ("/")
void handleRoot() {
server.send(200, "text/html", get_html_page());
}
// --- Handlers for remote control keys ---
// Note: Each handler calls handleRoot() at the end to refresh the page after sending the command.
// Power and Volume/Channel
void handlePower() { sendIrCommand(POWER_TOGGLE_CODE, "POWER"); handleRoot(); }
void handleVolUp() { sendIrCommand(VOL_UP_CODE, "VOL UP"); handleRoot(); }
void handleVolDown() { sendIrCommand(VOL_DOWN_CODE, "VOL DOWN"); handleRoot(); }
void handleMute() { sendIrCommand(MUTE_CODE, "MUTE"); handleRoot(); }
void handleChUp() { sendIrCommand(CH_UP_CODE, "CH UP"); handleRoot(); }
void handleChDown() { sendIrCommand(CH_DOWN_CODE, "CH DOWN"); handleRoot(); }
// Navigation
void handleUp() { sendIrCommand(UP_CODE, "UP"); handleRoot(); }
void handleDown() { sendIrCommand(DOWN_CODE, "DOWN"); handleRoot(); }
void handleLeft() { sendIrCommand(LEFT_CODE, "LEFT"); handleRoot(); }
void handleRight() { sendIrCommand(RIGHT_CODE, "RIGHT"); handleRoot(); }
void handleOk() { sendIrCommand(OK_ENTER_CODE, "OK/ENTER"); handleRoot(); }
// Function Buttons
void handleMenu() { sendIrCommand(MENU_CODE, "MENU"); handleRoot(); }
void handleReturn() { sendIrCommand(RETURN_GO_BACK_CODE, "RETURN/BACK"); handleRoot(); }
void handleExit() { sendIrCommand(EXIT_CODE, "EXIT"); handleRoot(); }
void handleInput() { sendIrCommand(INPUT_CODE, "INPUT"); handleRoot(); }
// Number Pad
void handleD0() { sendIrCommand(DIGITAL_0_CODE, "0"); handleRoot(); }
void handleD1() { sendIrCommand(DIGITAL_1_CODE, "1"); handleRoot(); }
void handleD2() { sendIrCommand(DIGITAL_2_CODE, "2"); handleRoot(); }
void handleD3() { sendIrCommand(DIGITAL_3_CODE, "3"); handleRoot(); }
void handleD4() { sendIrCommand(DIGITAL_4_CODE, "4"); handleRoot(); }
void handleD5() { sendIrCommand(DIGITAL_5_CODE, "5"); handleRoot(); }
void handleD6() { sendIrCommand(DIGITAL_6_CODE, "6"); handleRoot(); }
void handleD7() { sendIrCommand(DIGITAL_7_CODE, "7"); handleRoot(); }
void handleD8() { sendIrCommand(DIGITAL_8_CODE, "8"); handleRoot(); }
void handleD9() { sendIrCommand(DIGITAL_9_CODE, "9"); handleRoot(); }
void setup() {
Serial.begin(115200);
IrSender.begin(IR_TX_PIN);
// 1. Connect to WiFi
Serial.print("Connecting to WiFi..");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected.");
Serial.print("Web Server IP: ");
Serial.println(WiFi.localIP());
// Serve the logo image
server.on("/logo.png", HTTP_GET, []() {
server.send_P(200, "image/png", (const char*) logo_hacker_png, logo_hacker_png_len);
});
// 2. Setup Server Routes
server.on("/", handleRoot);
server.on("/power", handlePower);
server.on("/vol_up", handleVolUp);
server.on("/vol_down", handleVolDown);
server.on("/mute", handleMute);
server.on("/ch_up", handleChUp);
server.on("/ch_down", handleChDown);
// Navigation
server.on("/up", handleUp);
server.on("/down", handleDown);
server.on("/left", handleLeft);
server.on("/right", handleRight);
server.on("/ok", handleOk);
// Function Buttons
server.on("/menu", handleMenu);
server.on("/return", handleReturn);
server.on("/exit", handleExit);
server.on("/input", handleInput);
// Number Pad
server.on("/d0", handleD0);
server.on("/d1", handleD1);
server.on("/d2", handleD2);
server.on("/d3", handleD3);
server.on("/d4", handleD4);
server.on("/d5", handleD5);
server.on("/d6", handleD6);
server.on("/d7", handleD7);
server.on("/d8", handleD8);
server.on("/d9", handleD9);
// 3. Start the Server
server.begin();
Serial.println("HTTP Server started.");
}
void loop() {
// Main server loop to handle client requests
server.handleClient();
}
4.1 Code Explanation: IR Remote Webserver
This project transforms the ESP32 into a Smart Home Bridge that converts HTTP requests (button clicks on a web page) into Infrared (IR) signals via the KY-005 module.
4.2 Core Mechanism
The ESP32 runs an HTTP server hosting a page that looks like a remote control. Each button on the page is a link to a unique URL (a URI, e.g., /power). When a user clicks a button, the ESP32 intercepts the request, maps the URI to a specific IR Hex Code, and transmits that code using the IR LED.
4.3 Key Components and Logic
| Component/Function | Role in the Code | Details |
IR Codes (#define ...) | Protocol Definition | Defines hexadecimal constants for every remote function (e.g., POWER_TOGGLE_CODE). These codes are based on a specific protocol (e.g., Samsung) and must be custom-validated by the user. |
IR_TX_PIN (4) | Physical Output | Defines the GPIO 4 pin used to connect the KY-005 module. The IrSender.begin(IR_TX_PIN); command initializes the IRremote library for output on this pin. |
get_html_page() | Presentation Layer | Generates the entire HTML/CSS of the virtual remote control. The key element is the use of <a> tags where the href attribute links to the command URIs (e.g., <a href="/power" class="btn power">POWER</a>). |
server.on("/power", handlePower); | URI Mapping (Routing) | In setup(), this line tells the web server: “If you receive an HTTP request for the path /power, execute the function handlePower().” This creates a REST-like endpoint for every button. |
handlePower() (and others) | Execution Layer | These are the specific handler functions. When executed, they call sendIrCommand(...) with the relevant code and then immediately refresh the page using handleRoot(). |
sendIrCommand() | Physical Control | Takes the 32-bit IR code, splits it into the 16-bit Address and 16-bit Command (as required by the protocol), and calls IrSender.sendSamsung(address, command, 1); to physically pulse the IR LED, sending the signal to the target device. |
server.handleClient() | Main Loop | Placed in loop(), this ensures the ESP32 is continuously listening for incoming HTTP requests from the connected browser, making the system responsive. |
4.4 Execution Flow
Start: ESP32 connects to Wi-Fi and prints its IP address.
User Action: A user navigates a smartphone browser to the ESP32’s IP address and sees the remote control page.
Command Sent: The user clicks “VOL+”. The browser requests the URI:
http://[ESP32_IP_ADDRESS]/vol_up.Server Action:
server.handleClient()detects the request and executeshandleVolUp().IR Transmitted:
handleVolUp()callssendIrCommand(VOL_UP_CODE, "VOL UP"). The IRremote library pulses the LED on GPIO 4 with the correct Samsung protocol pattern.Page Refresh:
handleVolUp()finishes by callinghandleRoot(), which immediately sends the HTML page back to the browser, confirming the action visually.
5. Usage Instructions
- Find Your Codes: The most critical step is replacing the placeholder IR codes (e.g., POWER_CODE, VOL_UP_CODE) with the actual SAMSUNG, NEC or other protocol codes for your target device (TV, stereo, etc.).
- Use the IRrecvDumpV2 example from the IRremoteESP8266 library with an IR Receiver Module (like the KY-022) to capture the codes from your original remote.
- Upload the Code: Update the ssid and password variables, then upload the complete sketch to your ESP32 board.
- Access the Webpage: Open the Serial Monitor in the Arduino IDE to find the IP address printed by the ESP32 (e.g., Web Server IP: 192.168.1.100).
- Remote Control: Navigate to that IP address in any web browser on the same network (e.g., your smartphone or computer). You will see the web remote interface.
- Control Your Device: Click the buttons on the webpage. Each click will send the corresponding IR signal via the KY-005 module, controlling your IR-compatible device.
- The terminal will display the command sent
6. Lab Recap
This project successfully integrates three core IoT concepts: Networking, Web Services, and Physical Control.
6.1 The Three-Layer Architecture
Layer | Component | Role | Key Technology |
Presentation (The UI) | Web Page (HTML/CSS) | Provides a user-friendly interface for control. | HTTP (HyperText Transfer Protocol) |
Logic (The Brain) | ESP32 Sketch | Manages the WiFi connection, runs the web server, and maps URIs to functions. | WiFi.h & WebServer.h |
Physical (The Output) | KY-005 IR Transmitter | Converts the digital command into a physical, modulated infrared light signal. Remember to add a 110-220 Ohm resistor at R1. | IRremoteESP8266.h |
6.2 Key Takeaways from the Lab
- Event-Driven Nature of the Server: The ESP32 doesn’t constantly check for button presses. It only acts when the server.handleClient() function in the loop() detects an incoming HTTP request (a click on a link like /power).
- URI to Function Mapping: The server.on(“/command”, handlerFunction) structure is critical. It defines a route where a specific URL path (/command) is tied directly to a specific action (the handlerFunction), creating a REST-like endpoint for each key.
- IR Protocol Importance: The project relies on knowing the exact IR protocol (e.g., NEC, RC5) and the corresponding hex code for the device. Sending the wrong protocol or code, even with the right hardware, will fail to control the target device. This is the main point of customization for the user.
- Signal Repetition: Notice that irsend.sendNEC(code, 32, 3); sends the code three times. This repetition is a standard practice in IR protocols to ensure the receiving device reliably captures the signal, especially across distance or line-of-sight variations.
This complete loop, from a smartphone click to an IR pulse, demonstrates the ESP32’s powerful capability as a Smart Home bridge between IP networks and legacy physical control systems.




