Overview
In this beginner project you will connect a TSOP38238 IR receiver to the ESP32 and use the IRremote library to decode infrared signals from any remote control. Pointing a TV remote at the receiver and pressing buttons prints the protocol name, address, and command code to the Serial Monitor. You will identify and record the codes for your specific remote model.
Components
- 1× ESP32 DevKit V1
- 1× TSOP38238 IR receiver module — 38 kHz carrier; 3-pin (OUT, GND, VCC)
- 1× IR LED (940 nm) — For transmitting IR codes
- 1× 100 ohm resistor — IR LED current limiting (5 V drive)
- 1× Breadboard and jumper wires
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| TSOP38238 OUT | GPIO 15 | IR receiver data output |
| TSOP38238 VCC | 3.3 V | |
| TSOP38238 GND | GND | |
| IR LED anode | GPIO 4 | 100 ohm resistor in series; cathode to GND |
Arduino Code
// ESP32 IR Remote Control - Beginner
// Decode any IR remote and print protocol, address, command
#include <IRremote.hpp>
const int IR_RECV_PIN = 15;
const int IR_SEND_PIN = 4;
void setup() {
Serial.begin(115200);
IrReceiver.begin(IR_RECV_PIN, ENABLE_LED_FEEDBACK);
Serial.println("IR Receiver ready. Point remote and press any button.");
}
void loop() {
if (IrReceiver.decode()) {
Serial.println("===== IR SIGNAL RECEIVED =====");
Serial.printf("Protocol : %sn",
getProtocolString(IrReceiver.decodedIRData.protocol));
Serial.printf("Address : 0x%04Xn",
IrReceiver.decodedIRData.address);
Serial.printf("Command : 0x%02Xn",
IrReceiver.decodedIRData.command);
Serial.printf("Raw data : 0x%08lXn",
IrReceiver.decodedIRData.decodedRawData);
IrReceiver.printIRResultShort(&Serial);
Serial.println();
IrReceiver.resume(); // ready for next signal
}
}How It Works
IR Modulation and Demodulation: Infrared remotes transmit data by modulating an IR LED at 38 kHz. The TSOP38238 receiver contains a bandpass filter tuned to 38 kHz and a demodulator that strips the carrier, outputting a clean digital pulse pattern. The ESP32 measures pulse widths to decode the data.
IR Protocol Library: The IRremote library decodes over 40 IR protocols including NEC (most TVs), Samsung, Sony SIRC, Philips RC5/RC6, and DAIKIN (air conditioners). IrReceiver.decode() returns true when a complete frame is received and fills the decodedIRData structure with protocol, address, and command.
Protocol, Address, Command Structure: Most IR protocols encode a device address (identifies the specific appliance model) and a command byte (identifies the button pressed). The NEC protocol uses 8-bit address and 8-bit command with inverted bytes for error checking. Recording these three values is sufficient to replay any button.
IrReceiver.resume(): After decoding a signal, the receiver is paused to prevent re-decoding the same transmission. resume() must be called to re-arm the receiver for the next signal. Forgetting this call causes the receiver to appear non-functional after the first press.
Applications
- Record unknown remote codes for documentation
- Identify which IR protocol your TV uses before building a transmitter
- Audit all button codes on an air conditioner remote for full control
- Reverse-engineer IR codes for devices without publicly available documentation
Troubleshooting
No IR signal received even when pressing remote buttons
Aim the remote directly at the TSOP38238 within 30 cm for initial testing. Check the receiver pinout; many TSOP modules have OUT on the left, GND in the middle, and VCC on the right when the flat face is toward you.
Protocol shows UNKNOWN
Some devices use non-standard or proprietary protocols. Print the raw pulse timing using IrReceiver.printIRResultRawFormatted() to see the raw on/off durations and manually decode the protocol structure.
Same button decodes differently each press
Some protocols (NEC, RC5) use toggle bits that alternate between presses to distinguish held keys from repeated presses. Mask the toggle bit when comparing codes; use only address and command fields.
Upgrades
- Add an IR LED and replay a recorded code using IrSender.sendNEC(address, command, repeats)
- Store 10 codes in NVS keyed by button number and replay with a number pad
- Add an OLED showing the last decoded protocol and command
- Add a web page listing all stored codes as clickable buttons
FAQ
You need an ESP32 DevKit, TODO: sensor, TSOP38238 OUT, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
The intermediate build creates a universal remote cloner. First, a learning mode captures up to 10 button codes from any existing remote and stores them in NVS. Then a web interface served by the ESP32 displays 10 buttons labelled with device names (TV Power, Vol Up, HDMI 1, etc.). Pressing a button on the web page transmits the stored IR code via the IR LED. A physical reset button erases NVS and enters learning mode again.
Components
- 1× ESP32 DevKit V1
- 1× TSOP38238 IR receiver
- 1× IR LED 940 nm
- 1× 2N2222 NPN transistor — IR LED current driver
- 1× 100 ohm resistor (base)
- 1× 10 ohm resistor (collector) — IR LED limits to ~150 mA peak
- 1× Tactile button — Reset / learn mode trigger
- 1× Wi-Fi router — Web dashboard access
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| TSOP38238 OUT | GPIO 15 | |
| IR LED via 2N2222 | GPIO 4 (base via 100R) | Collector to IR LED anode; emitter to GND |
| Reset button | GPIO 0 to GND | Pull-up; hold 3 s to enter learn mode |
Arduino Code
// ESP32 Universal IR Remote Cloner - Intermediate
#include <IRremote.hpp>
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
WebServer server(80);
Preferences prefs;
const char* SSID="YourSSID", *PASS="YourPass";
const int IR_RECV=15, IR_SEND=4, BTN=0;
const int MAX_CODES=10;
struct IRCode { uint8_t protocol; uint16_t address; uint8_t command; };
IRCode codes[MAX_CODES];
String labels[MAX_CODES];
int numCodes=0;
bool learnMode=false;
int learnIndex=0;
void loadCodes(){
prefs.begin("ir",true);
numCodes=prefs.getInt("n",0);
for(int i=0;i<numCodes;i++){
String k=String(i);
codes[i].protocol=prefs.getUChar(("p"+k).c_str(),0);
codes[i].address=prefs.getUShort(("a"+k).c_str(),0);
codes[i].command=prefs.getUChar(("c"+k).c_str(),0);
labels[i]=prefs.getString(("l"+k).c_str(),"Button "+k);
}
prefs.end();
}
void saveCodes(){
prefs.begin("ir",false);
prefs.putInt("n",numCodes);
for(int i=0;i<numCodes;i++){
String k=String(i);
prefs.putUChar(("p"+k).c_str(),codes[i].protocol);
prefs.putUShort(("a"+k).c_str(),codes[i].address);
prefs.putUChar(("c"+k).c_str(),codes[i].command);
prefs.putString(("l"+k).c_str(),labels[i]);
}
prefs.end();
}
void serveRemote(){
String html="<html><body><h2>IR Remote</h2>";
if(learnMode) html+="<p style="color:red">LEARN MODE: point remote at sensor</p>";
for(int i=0;i<numCodes;i++){
html+="<form method=POST action=/send>"
"<input type=hidden name=i value="+String(i)+">"
"<button type=submit>"+labels[i]+"</button></form><br>";
}
html+="</body></html>";
server.send(200,"text/html",html);
}
void handleSend(){
int idx=server.arg("i").toInt();
if(idx<0||idx>=numCodes){ server.send(400,"text/plain","invalid"); return; }
IrSender.sendNEC(codes[idx].address,codes[idx].command,2);
server.send(200,"text/plain","Sent: "+labels[idx]);
}
void setup(){
Serial.begin(115200);
loadCodes();
IrReceiver.begin(IR_RECV,DISABLE_LED_FEEDBACK);
IrSender.begin(IR_SEND);
pinMode(BTN,INPUT_PULLUP);
WiFi.begin(SSID,PASS);
while(WiFi.status()!=WL_CONNECTED) delay(500);
server.on("/",serveRemote);
server.on("/send",HTTP_POST,handleSend);
server.begin();
Serial.printf("Remote dashboard: http://%s/n",WiFi.localIP().toString().c_str());
}
void loop(){
server.handleClient();
// Hold button for 3 s to enter learn mode
if(digitalRead(BTN)==LOW){
delay(3000);
if(digitalRead(BTN)==LOW){
learnMode=true; learnIndex=0; numCodes=0;
Serial.println("Learn mode: press button 1 on your remote");
}
}
if(learnMode && IrReceiver.decode()){
codes[learnIndex]={
(uint8_t)IrReceiver.decodedIRData.protocol,
IrReceiver.decodedIRData.address,
(uint8_t)IrReceiver.decodedIRData.command
};
labels[learnIndex]="Button "+String(learnIndex+1);
Serial.printf("Learned #%d: proto=%d addr=0x%04X cmd=0x%02Xn",
learnIndex+1,codes[learnIndex].protocol,
codes[learnIndex].address,codes[learnIndex].command);
learnIndex++;
numCodes=learnIndex;
IrReceiver.resume();
if(learnIndex>=MAX_CODES){ learnMode=false; saveCodes();
Serial.println("Learning complete. Saved."); }
else Serial.printf("Press button %dn",learnIndex+1);
}
}How It Works
Learning Mode Code Capture: Holding the physical button for 3 seconds enters learning mode. The ESP32 waits for IR signals and records each decoded protocol, address, and command into the codes[] array. After 10 presses the array is full and codes are saved to NVS. Labels default to "Button N" and can be renamed in NVS.
NVS Code Storage: Each stored code occupies three NVS keys: protocol (1 byte), address (2 bytes), command (1 byte), plus a string label. 10 codes use approximately 400 bytes of NVS space, well within the 8 KB available in the default NVS partition.
Transistor-Driven IR LED: The 2N2222 NPN transistor amplifies the 10-20 mA GPIO output to 150 mA through the IR LED, tripling the transmission power compared to direct GPIO drive. The base resistor (100 ohm) limits base current and the collector resistor (10 ohm) sets the LED peak current.
Web-Based Button Panel: The web page renders one HTML form per stored code. Each form POST to /send includes the code index. The server handler calls IrSender.sendNEC() with the stored address and command. With repeats=2, the signal is sent twice to handle receivers that require repeated frames.
Applications
- Universal remote for home entertainment system control
- IR blaster for home automation hub controlling legacy devices
- Shared TV remote backup accessible from any smartphone
- Automated IR command sender for testing IR receiver devices
Troubleshooting
Learned code does not control the device
Some protocols (Samsung, LG) require the exact repeat count and timing of the original remote. Try increasing the repeats parameter in sendNEC() from 2 to 5. Also verify the protocol matches; Samsung uses a different protocol than NEC even though the timings look similar.
Web page shows no buttons
No codes have been learned yet. Enter learn mode, capture 10 button presses, then refresh the page. If codes were previously saved, check that loadCodes() reads the correct NVS namespace.
Learn mode captures phantom presses
Ambient IR from sunlight, fluorescent lights, or other IR remotes in the room can trigger false captures. Learn codes in a dark room away from direct sunlight and other remotes. Alternatively add a 10 us IrReceiver.isIdle() check before treating a signal as valid.
Upgrades
- Add a /label endpoint to rename button labels via a web form without re-learning codes
- Add support for multi-protocol: learn Samsung, Sony, and NEC codes on the same remote
- Add a schedule: automatically send a code at a specific time (e.g. TV on at 19:00)
- Add MQTT publishing so each button press is also logged to a home automation hub
FAQ
You need an ESP32 DevKit, TODO: sensor, TSOP38238 OUT, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.
Overview
The advanced build is a Wi-Fi IR blaster controlled via MQTT. An MQTT message to ir/send with a JSON payload specifying protocol, address, and command triggers an IR transmission. Home Assistant and Node-RED can send IR commands to control any IR device as part of automated scenes and routines. A raw-code endpoint allows replaying pulse-duration arrays for non-standard protocols captured with a logic analyser.
Components
- 1× ESP32 DevKit V1
- 1× IR LED + 2N2222 driver
- 1× TSOP38238 IR receiver — For learning mode
- 1× MQTT broker (Mosquitto)
- 1× Home Assistant or Node-RED
Wiring
| Component Pin | ESP32 Pin | Notes |
|---|---|---|
| IR LED via 2N2222 | GPIO 4 | |
| TSOP38238 OUT | GPIO 15 |
Arduino Code
// ESP32 IR Blaster - Advanced (MQTT controlled)
// MQTT topic: ir/send Payload: {"protocol":"NEC","address":32,"command":2}
// MQTT topic: ir/learn Payload: "start" to enter learn mode
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <IRremote.hpp>
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
const char* SSID="YourSSID", *PASS="YourPass";
const char* MQTT_HOST="192.168.1.100";
const int IR_RECV=15, IR_SEND=4;
bool learnMode=false;
void mqttCallback(char* topic, byte* payload, unsigned int len){
String t(topic), msg((char*)payload,len);
if(t=="ir/learn" && msg=="start"){
learnMode=true;
IrReceiver.start();
mqtt.publish("ir/status","learn_mode");
return;
}
if(t=="ir/send"){
StaticJsonDocument<128> doc;
if(deserializeJson(doc,msg)) return;
uint16_t addr=doc["address"]|0;
uint8_t cmd =doc["command"]|0;
String proto=doc["protocol"]|"NEC";
if(proto=="NEC") IrSender.sendNEC(addr,cmd,2);
else if(proto=="SAMSUNG") IrSender.sendSamsung(addr,cmd,2);
else if(proto=="SONY") IrSender.sendSony(addr,cmd,2,SIRCS_12_PROTOCOL);
char buf[64];
snprintf(buf,64,"{"sent":"%s","addr":%u,"cmd":%u}",
proto.c_str(),addr,cmd);
mqtt.publish("ir/result",buf);
}
}
void setup(){
Serial.begin(115200);
IrReceiver.begin(IR_RECV,DISABLE_LED_FEEDBACK);
IrSender.begin(IR_SEND);
WiFi.begin(SSID,PASS);
while(WiFi.status()!=WL_CONNECTED) delay(500);
mqtt.setServer(MQTT_HOST,1883);
mqtt.setCallback(mqttCallback);
}
void loop(){
if(!mqtt.connected()){
mqtt.connect("IRBlaster");
mqtt.subscribe("ir/send");
mqtt.subscribe("ir/learn");
}
mqtt.loop();
if(learnMode && IrReceiver.decode()){
StaticJsonDocument<128> doc;
doc["protocol"]=getProtocolString(IrReceiver.decodedIRData.protocol);
doc["address"]=IrReceiver.decodedIRData.address;
doc["command"]=IrReceiver.decodedIRData.command;
char buf[128]; serializeJson(doc,buf);
mqtt.publish("ir/learned",buf);
IrReceiver.resume();
learnMode=false;
mqtt.publish("ir/status","ready");
}
}How It Works
MQTT Command Dispatch: The ESP32 subscribes to ir/send on startup. When a JSON payload arrives, the callback deserialises the protocol, address, and command fields and calls the appropriate IRremote send function. Results are published to ir/result for Home Assistant state tracking.
Multi-Protocol Support: The callback dispatches to different IrSender functions based on the protocol field. NEC covers most Asian brands; Samsung uses a Samsung-specific 32-bit format; Sony uses 12, 15, or 20-bit SIRC. Adding more protocols requires only an additional else-if branch.
MQTT Learn Mode: Publishing "start" to ir/learn activates the IR receiver for one code capture. The decoded result is published to ir/learned as a JSON object with protocol, address, and command. Home Assistant can then save this as a script that publishes back to ir/send to replay the code.
Home Assistant Integration: An HA MQTT integration entity subscribes to ir/result for state feedback. HA scripts send the JSON payload to ir/send. Scenes combine multiple IR commands (TV on, HDMI input 1, volume 20) into a single scene activation.
Applications
- Home Assistant IR blaster for complete home theatre control
- Node-RED automation triggering IR commands on schedules
- IR command library server for multi-room control
- Legacy appliance integration into modern smart home systems
Troubleshooting
MQTT payload causes deserialization error
Ensure the JSON payload uses double quotes and correct field names: {\"protocol\":\"NEC\",\"address\":32,\"command\":2}. Use the ArduinoJson assistant to validate the document capacity matches the payload size.
Home Assistant cannot send commands to the blaster
Verify the MQTT topic spelling matches exactly: ir/send (case-sensitive). In HA, use the MQTT developer tools to publish a test message and confirm the ESP32 receives and processes it.
Samsung TV does not respond to sendSamsung()
Some Samsung models require sendSamsung48() for 48-bit codes. Try decoding the original Samsung remote with the beginner sketch to identify the exact protocol variant and bit depth.
Upgrades
- Add a web UI listing all known codes with one-click send buttons
- Add a code database stored in SPIFFS JSON for persistent multi-device management
- Add raw pulse replay for exotic protocols: publish an array of pulse durations to ir/raw
- Add an IR receiver auto-discover mode that scans and publishes all received codes from any nearby remote
FAQ
You need an ESP32 DevKit, TODO: sensor, TSOP38238 OUT, a breadboard, jumper wires, and a USB cable for power and programming.
Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.
Start with Beginner if you are new to Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.