ESP32 IR Learn & Replay Remote Codes (2026)

This project turns an ESP32 into a simple learning IR remote. An IR receiver captures a button press from a TV, amplifier, fan or other infrared remote, the ESP32 identifies the protocol, address and command, stores the code in memory, and an IR LED transmits the same command again when a pushbutton is pressed. The current Arduino-IRremote library provides dedicated IrReceiver and IrSender objects and can even print the exact send command needed to reproduce a received signal.

What is an IR learning remote?

A normal infrared remote contains a list of commands programmed by the manufacturer.

Press:

Volume Up

and the remote transmits a specific infrared pattern.

Press:

Power

and it sends another.

A learning remote does not need to know those codes in advance.

Instead, it:

  1. listens to the original remote
  2. decodes the received IR message
  3. stores it
  4. transmits the same message later

That is exactly what the ESP32 will do in this project.

The official Arduino-IRremote library includes a ReceiveAndSend example specifically designed to record the last received IR signal and play it back later.

What can this project control?

Infrared is still widely used for:

  • televisions
  • AV receivers
  • soundbars
  • projectors
  • LED controllers
  • fans
  • set-top boxes
  • some air conditioners
  • older home appliances

Once the ESP32 learns a command, it can eventually be triggered by:

  • a physical button
  • Wi-Fi
  • MQTT
  • Home Assistant
  • a web page
  • a timer
  • a sensor
  • voice assistants

This tutorial starts with the simplest version:

Remote → ESP32 learns → pushbutton → ESP32 replays

Once that works, adding Home Assistant is straightforward.

Parts required

A basic build needs:

  • ESP32 development board
  • IR receiver module
  • IR LED
  • NPN transistor such as 2N2222 or BC337
  • transistor base resistor
  • IR LED current-limiting resistor
  • momentary pushbutton
  • breadboard and jumper wires

A common demodulated IR receiver designed for consumer remote-control frequencies is easiest because it outputs a clean digital signal instead of requiring the ESP32 to process the raw infrared carrier directly.

Recommended GPIOs

For a classic ESP32 DevKit, this guide uses:

FunctionGPIO
IR receiverGPIO27
IR transmitterGPIO25
Replay buttonGPIO33

These are ordinary GPIOs that avoid the classic ESP32 boot-strapping pins.

Other suitable pins can be used if the project already needs GPIO25, GPIO27 or GPIO33.

IR receiver wiring

Most IR receiver modules have three connections:

VCC
GND
OUT

Typical wiring:

IR receiverESP32
VCC3.3V
GNDGND
OUTGPIO27

Check the pin order on the actual receiver module because different packages can arrange VCC, GND and OUT differently.

The Arduino-IRremote library allows the receive input to be placed on essentially any suitable ESP32 GPIO. On ESP32, the library uses an hw_timer_t timer for IR reception.

IR transmitter wiring

An IR LED can technically be driven at low current directly from a GPIO, but the range is usually disappointing.

A much better arrangement uses a transistor.

                     +5V
                      │
                Current-limiting
                   resistor
                      │
                  IR LED
                      │
                   Collector
                      │
ESP32 GPIO25 ─1kΩ─ Base
                    2N2222
                      │
                   Emitter
                      │
                     GND

The ESP32 controls only the transistor.

The transistor supplies the pulsed LED current.

This gives substantially better IR range than asking the ESP32 GPIO itself to drive a high-current infrared LED.

The exact LED resistor depends on:

  • supply voltage
  • IR LED forward voltage
  • desired pulse current
  • transistor
  • IR LED current rating

Values around 100–220 Ω are commonly practical for a modest 5 V transmitter, but the correct resistor should be chosen from the specifications of the actual IR LED.

Always share ground between the ESP32 and IR LED power supply.

Why an IR LED does not simply stay on

Infrared remote-control signals are modulated.

A typical remote does not transmit:

LED ON for 100 ms

Instead, it rapidly switches the IR LED using a carrier frequency, commonly around 38 kHz, while encoding the actual command as bursts and gaps.

The Arduino-IRremote library generates this carrier automatically when transmitting supported protocols. On ESP32 it uses the platform’s PWM facilities, and the library can use arbitrary suitable send pins.

So there is no need to manually generate a 38 kHz signal using digitalWrite().

Install the Arduino-IRremote library

In Arduino IDE:

Sketch
→ Include Library
→ Manage Libraries

Search for:

IRremote

Install:

Arduino-IRremote

Modern examples use:

#include <IRremote.hpp>

rather than the old:

#include <IRremote.h>

The 4.x API also uses global objects:

IrReceiver
IrSender

instead of older tutorials that manually create IRrecv and IRsend objects.

First test: receive an IR command

Before trying to replay anything, confirm that the ESP32 can read the original remote.

Use:

#include <IRremote.hpp>

#define IR_RECEIVE_PIN 27

void setup() {
  Serial.begin(115200);

  IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);

  Serial.println("IR receiver ready");
}

void loop() {
  if (IrReceiver.decode()) {

    IrReceiver.printIRResultShort(&Serial);
    IrReceiver.printIRSendUsage(&Serial);

    IrReceiver.resume();
  }
}

Open Serial Monitor at:

115200 baud

Then point the original remote toward the IR receiver and press a button.

What the Serial Monitor tells you

For a recognised protocol, output can look similar to:

Protocol=NEC
Address=0xF1
Command=0x76
Raw-Data=0x89760EF1

The library can then print something similar to:

IrSender.sendNEC(0xF1, 0x76, <numberOfRepeats>);

This is one of the most useful features of the modern Arduino-IRremote library: printIRSendUsage() tells you how the detected signal can be transmitted again.

The exact protocol may instead be:

  • NEC
  • Samsung
  • Sony
  • RC5
  • RC6
  • Panasonic/Kaseikyo
  • LG
  • JVC
  • another supported format

The library handles many common consumer IR protocols.

Protocol, address and command explained

A decoded IR message commonly contains several pieces of information.

Protocol

This defines the signalling format.

Example:

NEC

Address

This usually identifies the device or device family.

Example:

0xF1

Command

This identifies the button function.

Example:

0x76

So two buttons on the same remote might look like:

Power
Address = 0xF1
Command = 0x10

and:

Volume Up
Address = 0xF1
Command = 0x11

The address remains the same while the command changes.

The Arduino-IRremote documentation notes that addresses are often constant for a device while commands are grouped according to function.

The complete simple learn-and-replay sketch

The following example learns the most recently received recognised protocol, stores it in RAM and sends it again whenever a local button is pressed.

#include <IRremote.hpp>

#define IR_RECEIVE_PIN 27
#define IR_SEND_PIN    25
#define REPLAY_BUTTON  33

IRData storedIRData;

bool codeStored = false;
bool lastButtonState = HIGH;

void setup() {
  Serial.begin(115200);

  pinMode(REPLAY_BUTTON, INPUT_PULLUP);

  // Start IR receiver
  IrReceiver.begin(IR_RECEIVE_PIN, DISABLE_LED_FEEDBACK);

  // Start IR transmitter
  IrSender.begin(IR_SEND_PIN);

  Serial.println();
  Serial.println("ESP32 IR Learn-and-Replay");
  Serial.println("Point remote at receiver and press a button.");
}

void loop() {

  // Learn a new IR code
  if (IrReceiver.decode()) {

    if (!(IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT)) {

      if (IrReceiver.decodedIRData.protocol != UNKNOWN) {

        storedIRData = IrReceiver.decodedIRData;

        // Clear repeat flags before later transmission
        storedIRData.flags = 0;

        codeStored = true;

        Serial.println();
        Serial.println("IR code learned:");

        IrReceiver.printIRResultShort(&Serial);
        IrReceiver.printIRSendUsage(&Serial);

        Serial.println("Press the replay button to transmit it.");
      }
      else {
        Serial.println("Unknown protocol - not stored by this simple example.");
      }
    }

    IrReceiver.resume();
  }

  // Read local replay button
  bool buttonState = digitalRead(REPLAY_BUTTON);

  // Send once when button is pressed
  if (buttonState == LOW && lastButtonState == HIGH) {

    if (codeStored) {

      Serial.println("Replaying stored IR command...");

      // Prevent receiver from detecting our own transmission
      IrReceiver.stop();

      delay(10);

      IrSender.write(&storedIRData);

      delay(50);

      IrReceiver.start();

      Serial.println("Done.");
    }
    else {
      Serial.println("No IR command has been learned yet.");
    }

    delay(50); // simple button debounce
  }

  lastButtonState = buttonState;
}

This simplified approach follows the same architecture used by Arduino-IRremote’s official ReceiveAndSend example: copy the decoded IRData, stop reception before transmitting, use IrSender.write() to send the stored protocol, then restart the receiver.

What happens when a remote button is pressed?

This line checks whether a full infrared message has arrived:

if (IrReceiver.decode())

When decoding succeeds, information about the signal becomes available through:

IrReceiver.decodedIRData

The library’s IRData structure includes fields such as protocol, address, command, raw data and flags.

The sketch then copies that structure:

storedIRData = IrReceiver.decodedIRData;

Now the ESP32 has remembered the command.

Why repeat frames are ignored when learning

Holding a remote button often does not resend the complete command every single time.

Many protocols transmit:

Full command
Repeat
Repeat
Repeat

The library marks these using:

IRDATA_FLAGS_IS_REPEAT

The official ReceiveDemo and ReceiveAndSend examples specifically detect repeat frames separately.

For a simple learning remote, it is better to store the original full command rather than accidentally storing a repeat frame.

That is why the sketch checks:

if (!(IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT))

Replaying the command

The important transmit line is:

IrSender.write(&storedIRData);

The Arduino-IRremote library examines the stored protocol and selects the appropriate sender internally. The official ReceiveAndSend example uses exactly this technique for known protocols.

So the sketch does not need a huge block like:

if NEC...
else if Sony...
else if Samsung...

The library handles the protocol selection.

Why stop the receiver while transmitting?

The IR LED and receiver are normally only a few centimetres apart.

If the receiver remains enabled while the ESP32 sends a command, it can detect its own transmission.

That can create confusing behaviour:

Transmit command
→ receiver sees command
→ command is learned again
→ unexpected repeat behaviour

The official Arduino-IRremote ReceiveAndSend example stops the receiver before transmitting and restarts it afterward.

That is why the example uses:

IrReceiver.stop();

IrSender.write(&storedIRData);

IrReceiver.start();

Why the code disappears after reboot

The simple sketch stores the learned command in RAM.

That means:

Power removed
→ learned command lost

This is intentional for a beginner example.

The next upgrade is storing learned commands in:

  • ESP32 Preferences/NVS
  • EEPROM emulation
  • LittleFS
  • SPIFFS

Then commands can survive a reboot or power failure.

Learning several remote buttons

Once one command works, the same idea can be expanded.

Instead of:

IRData storedIRData;

use several storage locations:

IRData powerCode;
IRData volumeUpCode;
IRData volumeDownCode;
IRData sourceCode;

The ESP32 can then become a programmable universal remote.

For example:

Button 1 → TV Power
Button 2 → Volume Up
Button 3 → Volume Down
Button 4 → HDMI Input

Or those commands could later become Home Assistant buttons.

What if the protocol says UNKNOWN?

Not every infrared signal can be decoded into a recognised protocol.

When Arduino-IRremote reports:

Protocol=UNKNOWN

the waveform can still potentially be replayed using its raw mark/space timings.

The official ReceiveAndSend example handles unknown and generic pulse-width/pulse-distance formats by copying the raw timing buffer and later using:

IrSender.sendRaw(...)

It assumes a 38 kHz carrier for that fallback example.

This is useful for unusual remotes that the decoder does not understand.

Why raw replay is more complicated

A recognised protocol might be represented compactly as:

Protocol = NEC
Address = 0x10
Command = 0x34

Raw mode instead stores something like:

9000
4500
560
560
560
1690
560
560
...

These numbers represent the timing of the infrared marks and spaces.

The buffer is therefore much larger.

Arduino-IRremote includes the official ReceiveAndSend example specifically for projects that need to store and replay both recognised and raw signals.

For a first project, begin with recognised protocols.

Use ReceiveDemo when troubleshooting

Arduino IDE includes:

File
→ Examples
→ IRremote
→ ReceiveDemo

The project documentation recommends ReceiveDemo when learning codes from an existing remote because it prints both the decoded information and the appropriate transmit statement.

This should be the first diagnostic tool whenever:

  • a remote isn’t recognised
  • addresses appear strange
  • commands don’t replay correctly
  • repeats behave unexpectedly

Example: NEC remote

Suppose Serial Monitor displays:

Protocol=NEC
Address=0x2
Command=0x34

and then:

IrSender.sendNEC(0x2, 0x34, <numberOfRepeats>);

The same command can be transmitted manually with:

IrSender.sendNEC(0x2, 0x34, 0);

The Arduino-IRremote project’s own sample logs show this same address/command approach for NEC messages.

Using:

0 repeats

sends one normal command frame.

How many repeats should be transmitted?

Some devices respond perfectly with no repeats.

Others expect the command to be held slightly longer.

Arduino-IRremote’s documentation suggests three repeats as a practical starting point when the correct repeat count is unknown, then reducing it if fewer repeats work reliably.

For a normal button press, start with:

0 or 1 repeats

and increase only if necessary.

Receiver works but transmitter does nothing

This is one of the most common IR projects to fail halfway.

The ESP32 successfully prints:

Protocol=NEC
Address=...
Command=...

but the television does nothing when replay is pressed.

Check the transmitter hardware first.

Common causes include:

  • IR LED connected backwards
  • no current-limiting resistor
  • transistor wired incorrectly
  • no common ground
  • IR LED pointed away from appliance
  • wrong send GPIO
  • IR LED current too low
  • wrong protocol/repeat count

A phone camera can sometimes show infrared LEDs as a faint flashing purple/white light, making it a useful quick check that the transmitter is actually emitting.

The IR LED works only from a few centimetres away

The likely cause is insufficient LED current.

Driving an IR LED directly from an ESP32 GPIO is fine for testing but usually provides poor range.

Use:

ESP32
→ transistor
→ IR LED

for a practical remote-control transmitter.

The Arduino-IRremote documentation also discusses transmitter current and carrier duty cycle because IR receiver range depends strongly on peak emitter intensity rather than simply average power.

The remote code changes when holding the button

This can be completely normal.

A protocol can transmit:

Initial command
Repeat frame
Repeat frame
Repeat frame

or send slightly different repeat information.

Use:

IrReceiver.printIRResultShort(&Serial);

and inspect the flags.

The official library’s examples explicitly distinguish new commands from repeat frames.

Old tutorials won’t compile

Many older IRremote tutorials contain code such as:

#include <IRremote.h>

IRrecv irrecv(RECV_PIN);
decode_results results;

and:

irrecv.enableIRIn();

That is the old API.

Current Arduino-IRremote 4.x uses:

#include <IRremote.hpp>

and:

IrReceiver.begin(...)

with received information stored in:

IrReceiver.decodedIRData

The project’s migration documentation explicitly explains this change.

When following older ESP32 IR tutorials, this is one of the most common reasons code fails to compile.

Can the same ESP32 receive and send?

Yes.

Arduino-IRremote explicitly supports both receiving and transmitting in the same application, and its official examples include ReceiveAndSend and SendAndReceive.

The practical trick is simply to avoid receiving your own transmission by temporarily stopping the receiver during replay.

Can one ESP32 learn an entire remote?

Yes.

A more advanced version can provide a learning mode:

Select "TV Power"
→ press original Power button
→ save command

Select "Volume Up"
→ press original Volume Up
→ save command

Select "Volume Down"
→ press original Volume Down
→ save command

The ESP32 can save each learned command into non-volatile storage.

At that point, the device behaves like a proper programmable IR bridge.

Adding Home Assistant later

Once the send side works, each stored IR command can become a Home Assistant button.

For example:

TV Power
Volume Up
Volume Down
Mute
Input

Home Assistant can then trigger those functions through:

  • dashboards
  • automations
  • scenes
  • voice control

That creates a DIY alternative to a commercial Wi-Fi IR blaster.

The important part is to first prove the remote code can be learned and replayed locally before adding Wi-Fi, MQTT or Home Assistant complexity.

Air conditioners are more complicated

TV remotes commonly transmit relatively simple button commands.

Many air-conditioner remotes work differently.

Instead of:

Temperature Up

the remote may transmit the entire AC state:

Power = ON
Mode = COOL
Temperature = 23°C
Fan = AUTO
Swing = ON

in one long frame.

This creates much longer and more complicated IR packets.

Arduino-IRremote supports numerous protocols and raw capture, but AC remotes may require larger buffers or a library specifically designed around HVAC protocols. The Arduino-IRremote ReceiveDemo source itself notes that some air-conditioner protocols require a larger record gap to capture the complete frame.

For a first learn-and-replay project, test with a TV or simple appliance remote.

Best way to develop the project

Build it in stages.

Start with:

IR receiver
↓
Serial Monitor

Confirm that codes are decoded.

Then add:

IR transmitter
↓
Send known code

Confirm that the appliance responds.

Then combine them:

Original Remote
      ↓
 IR Receiver
      ↓
    ESP32
   stores code
      ↓
 Replay Button
      ↓
   IR LED
      ↓
 TV / Device

Only after this works reliably should additional features such as Home Assistant or persistent storage be added.

Final recommendation

An ESP32 is an excellent platform for building a simple IR learning remote because one board can both receive and transmit infrared while also providing Wi-Fi for later smart-home integration.

The key steps are:

  • connect an IR receiver to a safe GPIO
  • use the modern IRremote.hpp API
  • call IrReceiver.decode() to capture a signal
  • inspect protocol, address and command
  • copy IrReceiver.decodedIRData
  • ignore repeat frames while learning
  • drive the IR LED through a transistor for useful range
  • stop the receiver before replaying
  • send the stored command using IrSender.write()
  • restart the receiver after transmitting

Arduino-IRremote already contains nearly all of the difficult protocol handling required. Its official ReceiveDemo can show how a received command should be sent, while ReceiveAndSend demonstrates the full record-and-playback pattern used in this project.

Once one button can be learned and replayed reliably, the same idea can grow into a complete ESP32 universal IR remote for Home Assistant.

Share your love