A TV remote talks to the TV with flashes of infrared light that you can't see. In this lesson the ESP32 learns those flashes. Press "Learn" next to a button on a web page, point your TV remote at the IR receiver and press the real button: the ESP32 stores the code. From then on, tapping "Send" on your phone makes the IR emitter flash the same code, and the TV obeys. The codes are saved, so they're still there after a restart.
What you'll learn
- How infrared remotes send codes with flashes of light
- How the IRremote library decodes and repeats a code
- How to build a web page with buttons that call the ESP32
- How to save small pieces of data in the ESP32's flash memory
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- IR receiver module (45-in-1 Sensor Kit)
- IR emitter module (45-in-1 Sensor Kit)
- A TV or other device with an infrared remote control
- A 2.4 GHz Wi-Fi network
- The printed parts from the 3D printing box below (IR blaster stand), or the cardboard version described there
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Wiring

| From | To |
|---|---|
| IR receiver module S | ESP32 GPIO 35 |
| IR emitter module S | ESP32 GPIO 4 |
| Both modules + and − | 3V3 and GND |
Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.
The emitter in the kit is a small LED, so it reaches about 1 to 2 metres. Point it straight at the TV's receiver window, usually at the bottom of the screen.
The sketch
In the Arduino IDE, open Library Manager and install the IRremote library by shirriff, z3t0 and ArminJo.
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <IRremote.hpp>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const int RECEIVE_PIN = 35;
const int SEND_PIN = 4;
const int BUTTONS = 6;
const char* NAMES[BUTTONS] = {"Power", "Volume +", "Volume -", "Channel +", "Channel -", "Mute"};
struct Code { // everything needed to repeat a button
uint8_t protocol;
uint16_t address;
uint16_t command;
};
Code codes[BUTTONS];
WebServer server(80);
Preferences prefs;
int learning = -1; // which button is being learned, or -1
unsigned long learnStart = 0;
void handleRoot() {
String html = "<!doctype html><meta name=viewport content='width=device-width'><title>Remote</title>"
"<style>body{font-family:sans-serif;max-width:420px;margin:24px auto;padding:0 16px}"
"a{display:inline-block;padding:14px 18px;margin:4px;border-radius:12px;background:#1B3A7B;color:#fff;text-decoration:none}"
"a.l{background:#eee;color:#333}</style><h1>Remote</h1>";
if (learning >= 0) html += "<p><b>Point your remote at the receiver and press " + String(NAMES[learning]) + " now...</b></p><meta http-equiv=refresh content=3>";
for (int i = 0; i < BUTTONS; i++) {
html += "<p>";
if (codes[i].protocol != UNKNOWN) html += "<a href='/send?b=" + String(i) + "'>" + NAMES[i] + "</a>";
else html += "<span>" + String(NAMES[i]) + ": not learned yet</span>";
html += " <a class=l href='/learn?b=" + String(i) + "'>Learn</a></p>";
}
server.send(200, "text/html", html);
}
void back() {
server.sendHeader("Location", "/");
server.send(303);
}
void handleSend() {
int b = server.arg("b").toInt();
if (b >= 0 && b < BUTTONS && codes[b].protocol != UNKNOWN) {
IrReceiver.stop(); // don't hear our own flashes
IrSender.write((decode_type_t)codes[b].protocol, codes[b].address, codes[b].command, 1);
IrReceiver.start();
Serial.printf("Sent %s\n", NAMES[b]);
}
back();
}
void handleLearn() {
learning = constrain(server.arg("b").toInt(), 0, BUTTONS - 1);
learnStart = millis();
back();
}
void setup() {
Serial.begin(115200);
prefs.begin("remote");
for (int i = 0; i < BUTTONS; i++) {
codes[i].protocol = UNKNOWN;
prefs.getBytes(("b" + String(i)).c_str(), &codes[i], sizeof(Code));
}
IrReceiver.begin(RECEIVE_PIN, DISABLE_LED_FEEDBACK);
IrSender.begin(SEND_PIN);
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print(" connected. Open http://");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/send", handleSend);
server.on("/learn", handleLearn);
server.begin();
}
void loop() {
server.handleClient();
if (IrReceiver.decode()) {
IRData& d = IrReceiver.decodedIRData;
bool repeat = d.flags & IRDATA_FLAGS_IS_REPEAT;
if (d.protocol != UNKNOWN && !repeat) {
Serial.printf("Heard %s, address 0x%X, command 0x%X\n", getProtocolString(d.protocol), d.address, d.command);
if (learning >= 0) {
codes[learning] = {(uint8_t)d.protocol, d.address, d.command};
prefs.putBytes(("b" + String(learning)).c_str(), &codes[learning], sizeof(Code));
Serial.printf("Learned %s\n", NAMES[learning]);
learning = -1;
}
}
IrReceiver.resume();
}
if (learning >= 0 && millis() - learnStart > 15000) learning = -1; // gave up after 15 s
}
Teach it your remote
Open the Serial Monitor at 115200 baud: after connecting, the ESP32 prints its address, such as http://192.168.1.42. Open it in a browser on a phone or computer in the same Wi-Fi.
- Tap Learn next to Power. Within 15 seconds, point your TV remote at the receiver from about 30 cm and press its power button once.
- The Serial Monitor shows "Learned Power" and the page now has a blue Power button.
- Learn the other buttons the same way, then point the stand at the TV and try them from your phone.
How it works
An infrared remote blinks its LED about 38,000 times a second in short bursts. The pattern of long and short bursts spells out a code, and different makers use different patterns, called protocols (NEC, Sony, Samsung, RC5 and others). The IR receiver module only reacts to light blinking at 38 kHz, so sunlight and lamps don't confuse it.
The IRremote library recognises the protocol and splits the code into an address (which device) and a command (which button). Those three numbers are all the ESP32 needs to send the same code again, so the sketch stores them in a small struct and saves its bytes in Preferences. Repeat codes (sent while a button is held) are ignored when learning.
While sending, the receiver is stopped, so the ESP32 doesn't hear and learn its own flashes.
Try this next
- Add a "Good night" button that sends Mute and then Power, one second apart.
- Learn the buttons of a second device, like a sound bar or an LED strip.
- Switch the TV off automatically at a set time, using internet time.
Troubleshooting
- The Serial Monitor shows nothing when you press a remote button: check the receiver on GPIO 35, and press from closer up.
- A button is "heard" but never learned: some remotes use protocols the library can't repeat. Try another remote or another button.
- The TV doesn't react: move the stand closer (under 2 m) and point the emitter straight at the TV's receiver window.
- It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz.
- A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.