In this Project we will explain how to remote control air conditioners (i.e. turing it on\off, set temperature or fan speed).
The objective of this post is to explain basics of IR protocols of remote controls and to learn how to remote control an air conditioner using an IR Led with an ESP8266 NodeMcu.
About Air Conditioning IR protocols
IR transmitter and IR receiver
A typical infrared communication system requires an IR transmitter and an IR receiver. The transmitter looks just like a standard LED, except it produces light in the IR spectrum instead of the visible spectrum. If you have a look at the front of a TV remote, you’ll see the IR transmitter LED. IR remotes use an IR LED to transmit signal from remote to the receiver by rapidly switching the LED on and off. But many other light sources produce IR light as well. So, to make our signal special, a PWM signal is used at a certain frequency (modulation).
IR Signal Modulation
IR light is emitted by the sun, light bulbs, and anything else that produces heat. That means there is a lot of IR light noise all around us. To prevent this noise from interfering with the IR signal, a signal modulation technique is used.
In IR signal modulation, an encoder on the IR remote converts a binary signal into a modulated electrical signal. This electrical signal is sent to the transmitting LED. The transmitting LED converts the modulated electrical signal into a modulated IR light signal. The IR receiver then demodulates the IR light signal and converts it back to binary before passing on the information to a microcontroller.
Frequencies used in almost all IR remotes are 30khz, 33khz, 36khz, 38khz, 40khz and 56khz. Most common ones though, are 38khz and 40khz.

Image: IR signal modulation by the transceiver and demodulation by the receiver
In TVs, air conditioners and other devices that are ir remote controlled, a receiver module - ( TSOP module) demodulates the carrier signal(eg. 38khz) to a more suitable TTL logic of GND and VCC. These modules are integrated circuits composed of a LED receiver diode and an analog circuit that amplifies certain signal patterns, filters out noise and, converts the signal to a more usable and less frequent on and off signal. The duration of HIGH of LOW logic denotes bit '1' or '0' . The duration varies by every remote protocol.
To understand IR protocol in detail, you can refer to THIS document.
IR Protocols
The pattern in which the modulated IR signal is converted to binary is defined by a transmission protocol. There are many IR transmission protocols. Sony, Matsushita, NEC, and RC5 are some of the more common protocols.
Unlike traditional remotes of nearly all electronics devices (say a TV, garage door opener) where only one button's information is sent at the time, In AC remotes all of the parameters are encoded and sent at once. Hence, It can be a little bit tricky to decode the signal from a microcontroller.

Image: Signal pattern that is sent through the IR transceiver
Each time a button is pressed on the remote control, a unique hexadecimal code is generated. This is the information that is modulated and sent over IR to the receiver. In order to decipher which key is pressed, the receiving microcontroller needs to know which code corresponds to each key on the remote.
Different remotes send different codes for the keypresses, so it is necessary to determine the code generated for each key on your particular remote. If you can find the datasheet, the IR key codes should be listed. If not though, there is a simple Arduino - ESP8266 sketch that will read most of the popular remote controls and print the hexadecimal codes to the serial monitor when you press a key.
Materials Needed
For the sake of simplicity, this project is demonstrated in its most basic form in such that features and materials are kept at a minimum. Following materials are needed for this project:
- 1x Jumper wires pack
- 1x NodeMCU ESP8266
- 1x Infrared LED
- 1x BC547 NPN Transistor
- 1x Resistor: 50 Ohm
- 1x Resistor: 150 Ohm
- 1x Breadboard
- Optional: power jack connector
Wiring
In this example power will be supplied through a powerjack. The voltage should be between 5 - 10 V.We will have one ESP8266 NodeMcu pin (D2) connected in series with a 150 Ohm resistor to the base of a BC547 NPN transistor. The transistor is in a open collector configuration with the infrared LED connected to the transistor's collector in series with a 50 Ohm resistor. The emitter is connected to the common ground.

Image: Schematics of the IR remote consisting of a transistor, resistors, IR LED and the ESP8266 NodeMcu
By doing so, we ensure that the IR LED is supplied with enough current to transmit the IR signal over larger distances. In IR remote controls, IR LEDs are turned on and off in short bursts, thus allowing the current (>1 A) to exceed the rated value without damaging the LED. However, the NodeMcu GPIO pins can't supply a current higher than 12 mA. That is why it is necessary to use a transistor in this setup.
Connectthe components as follows:
- D2 of NodeMcu connected to base of BC547 transistor with 50 Ohm resistor between
- Transistor emitter connected to ground, ground connected to GND of NodeMcu and powerjack (-)
- Transistor (collector) connected to cathode of IR LED,
- Anode connected to VCC with 50 Ohm resistor between
- VCC connected to Vin of NodeMcu and powerjack (+)
- Connect the fourth pin of the DHT22 to ground of ESP8266.
ESP8266 Code
There are several libraries for known protocols of certain air conditioning systems brands available, so that it is not necessary to start from scratch to decode and understand the IR signal code. In this example we use the IRremoteESP8266 library, in order to control an air conditioner. This library can be installed via Arduino IDE library manager.

Image: Install the IRremoteESP8266 library from the library manager of the Arduino IDE
If your Air Conditoner is not supported by the library
If your brand is not supported by the library, you have the option to 'raw read' the signal by using the IRrecvDumpV2 example. As explained here, you need to build a circuit usin a TSOP ir receiver module which is also used in TVs and other devices that are IR remote controlled.

Image: IR receiver circuit with TSOP module to read raw IR code from your remote control
Upload the IRrecvDumpV2.ino code, restart the ESP8266 and point your remote control towards the receiver module. As soon as you push a button, a series of hexadecimal numbers will be displayed on the serial monitor. These numbers represent the length of the 'dots' and 'dashes', i.e. the duration of each high of the signal.
IRrecvDumpV2.ino - used to get the raw IR code in case your AC is not supported by this library
/* * IRremoteESP8266: IRrecvDumpV2 - dump details of IR codes with IRrecv * An IR detector/demodulator must be connected to the input kRecvPin. * * Copyright 2009 Ken Shirriff, http://arcfn.com * Copyright 2017 David Conran * * Example circuit diagram: * https://github.com/markszabo/IRremoteESP8266/wiki#ir-receiving * * Changes: * Version 0.4 July, 2018 * - Minor improvements and more A/C unit support. * Version 0.3 November, 2017 * - Support for A/C decoding for some protcols. * Version 0.2 April, 2017 * - Decode from a copy of the data so we can start capturing faster thus * reduce the likelihood of miscaptures. * Based on Ken Shirriff's IrsendDemo Version 0.1 July, 2009, */ #ifndef UNIT_TEST #include <IRrecv.h> #include <IRremoteESP8266.h> #include <IRutils.h> // The following are only needed for extended decoding of A/C Messages #include <ir_Coolix.h> #include <ir_Daikin.h> #include <ir_Fujitsu.h> #include <ir_Gree.h> #include <ir_Haier.h> #include <ir_Hitachi.h> #include <ir_Kelvinator.h> #include <ir_Midea.h> #include <ir_Mitsubishi.h> #include <ir_Panasonic.h> #include <ir_Samsung.h> #include <ir_Tcl.h> #include <ir_Teco.h> #include <ir_Toshiba.h> #include <ir_Vestel.h> #include <ir_Whirlpool.h> // ==================== start of TUNEABLE PARAMETERS ==================== // An IR detector/demodulator is connected to GPIO pin 14 // e.g. D5 on a NodeMCU board. const uint16_t kRecvPin = 14; // The Serial connection baud rate. // i.e. Status message will be sent to the PC at this baud rate. // Try to avoid slow speeds like 9600, as you will miss messages and // cause other problems. 115200 (or faster) is recommended. // NOTE: Make sure you set your Serial Monitor to the same speed. const uint32_t kBaudRate = 115200; // As this program is a special purpose capture/decoder, let us use a larger // than normal buffer so we can handle Air Conditioner remote codes. const uint16_t kCaptureBufferSize = 1024; // kTimeout is the Nr. of milli-Seconds of no-more-data before we consider a // message ended. // This parameter is an interesting trade-off. The longer the timeout, the more // complex a message it can capture. e.g. Some device protocols will send // multiple message packets in quick succession, like Air Conditioner remotes. // Air Coniditioner protocols often have a considerable gap (20-40+ms) between // packets. // The downside of a large timeout value is a lot of less complex protocols // send multiple messages when the remote's button is held down. The gap between // them is often also around 20+ms. This can result in the raw data be 2-3+ // times larger than needed as it has captured 2-3+ messages in a single // capture. Setting a low timeout value can resolve this. // So, choosing the best kTimeout value for your use particular case is // quite nuanced. Good luck and happy hunting. // NOTE: Don't exceed kMaxTimeoutMs. Typically 130ms. #if DECODE_AC // Some A/C units have gaps in their protocols of ~40ms. e.g. Kelvinator // A value this large may swallow repeats of some protocols const uint8_t kTimeout = 50; #else // DECODE_AC // Suits most messages, while not swallowing many repeats. const uint8_t kTimeout = 15; #endif // DECODE_AC // Alternatives: // const uint8_t kTimeout = 90; // Suits messages with big gaps like XMP-1 & some aircon units, but can // accidentally swallow repeated messages in the rawData[] output. // // const uint8_t kTimeout = kMaxTimeoutMs; // This will set it to our currently allowed maximum. // Values this high are problematic because it is roughly the typical boundary // where most messages repeat. // e.g. It will stop decoding a message and start sending it to serial at // precisely the time when the next message is likely to be transmitted, // and may miss it. // Set the smallest sized "UNKNOWN" message packets we actually care about. // This value helps reduce the false-positive detection rate of IR background // noise as real messages. The chances of background IR noise getting detected // as a message increases with the length of the kTimeout value. (See above) // The downside of setting this message too large is you can miss some valid // short messages for protocols that this library doesn't yet decode. // // Set higher if you get lots of random short UNKNOWN messages when nothing // should be sending a message. // Set lower if you are sure your setup is working, but it doesn't see messages // from your device. (e.g. Other IR remotes work.) // NOTE: Set this value very high to effectively turn off UNKNOWN detection. const uint16_t kMinUnknownSize = 12; // ==================== end of TUNEABLE PARAMETERS ==================== // Use turn on the save buffer feature for more complete capture coverage. IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true); decode_results results; // Somewhere to store the results // Display the human readable state of an A/C message if we can. void dumpACInfo(decode_results *results) { String description = ""; #if DECODE_DAIKIN if (results->decode_type == DAIKIN) { IRDaikinESP ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_DAIKIN #if DECODE_DAIKIN2 if (results->decode_type == DAIKIN2) { IRDaikin2 ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_DAIKIN2 #if DECODE_FUJITSU_AC if (results->decode_type == FUJITSU_AC) { IRFujitsuAC ac(0); ac.setRaw(results->state, results->bits / 8); description = ac.toString(); } #endif // DECODE_FUJITSU_AC #if DECODE_KELVINATOR if (results->decode_type == KELVINATOR) { IRKelvinatorAC ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_KELVINATOR #if DECODE_MITSUBISHI_AC if (results->decode_type == MITSUBISHI_AC) { IRMitsubishiAC ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_MITSUBISHI_AC #if DECODE_TOSHIBA_AC if (results->decode_type == TOSHIBA_AC) { IRToshibaAC ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_TOSHIBA_AC #if DECODE_GREE if (results->decode_type == GREE) { IRGreeAC ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_GREE #if DECODE_MIDEA if (results->decode_type == MIDEA) { IRMideaAC ac(0); ac.setRaw(results->value); // Midea uses value instead of state. description = ac.toString(); } #endif // DECODE_MIDEA #if DECODE_HAIER_AC if (results->decode_type == HAIER_AC) { IRHaierAC ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_HAIER_AC #if DECODE_HAIER_AC_YRW02 if (results->decode_type == HAIER_AC_YRW02) { IRHaierACYRW02 ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_HAIER_AC_YRW02 #if DECODE_SAMSUNG_AC if (results->decode_type == SAMSUNG_AC) { IRSamsungAc ac(0); ac.setRaw(results->state, results->bits / 8); description = ac.toString(); } #endif // DECODE_SAMSUNG_AC #if DECODE_COOLIX if (results->decode_type == COOLIX) { IRCoolixAC ac(0); ac.setRaw(results->value); // Coolix uses value instead of state. description = ac.toString(); } #endif // DECODE_COOLIX #if DECODE_PANASONIC_AC if (results->decode_type == PANASONIC_AC && results->bits > kPanasonicAcShortBits) { IRPanasonicAc ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_PANASONIC_AC #if DECODE_HITACHI_AC if (results->decode_type == HITACHI_AC) { IRHitachiAc ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_HITACHI_AC #if DECODE_WHIRLPOOL_AC if (results->decode_type == WHIRLPOOL_AC) { IRWhirlpoolAc ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_WHIRLPOOL_AC #if DECODE_VESTEL_AC if (results->decode_type == VESTEL_AC) { IRVestelAc ac(0); ac.setRaw(results->value); // Like Coolix, use value instead of state. description = ac.toString(); } #endif // DECODE_VESTEL_AC #if DECODE_TECO if (results->decode_type == TECO) { IRTecoAc ac(0); ac.setRaw(results->value); // Like Coolix, use value instead of state. description = ac.toString(); } #endif // DECODE_TECO #if DECODE_TCL112AC if (results->decode_type == TCL112AC) { IRTcl112Ac ac(0); ac.setRaw(results->state); description = ac.toString(); } #endif // DECODE_TCL112AC // If we got a human-readable description of the message, display it. if (description != "") Serial.println("Mesg Desc.: " + description); } // The section of code run only once at start-up. void setup() { Serial.begin(kBaudRate, SERIAL_8N1, SERIAL_TX_ONLY); while (!Serial) // Wait for the serial connection to be establised. delay(50); Serial.println(); Serial.print("IRrecvDumpV2 is now running and waiting for IR input on Pin "); Serial.println(kRecvPin); #if DECODE_HASH // Ignore messages with less than minimum on or off pulses. irrecv.setUnknownThreshold(kMinUnknownSize); #endif // DECODE_HASH irrecv.enableIRIn(); // Start the receiver } // The repeating section of the code // void loop() { // Check if the IR code has been received. if (irrecv.decode(&results)) { // Display a crude timestamp. uint32_t now = millis(); Serial.printf("Timestamp : %06u.%03u\n", now / 1000, now % 1000); if (results.overflow) Serial.printf( "WARNING: IR code is too big for buffer (>= %d). " "This result shouldn't be trusted until this is resolved. " "Edit & increase kCaptureBufferSize.\n", kCaptureBufferSize); // Display the basic output of what we found. Serial.print(resultToHumanReadableBasic(&results)); dumpACInfo(&results); // Display any extra A/C info if we have it. yield(); // Feed the WDT as the text output can take a while to print. // Display the library version the message was captured with. Serial.print("Library : v"); Serial.println(_IRREMOTEESP8266_VERSION_); Serial.println(); // Output RAW timing info of the result. Serial.println(resultToTimingInfo(&results)); yield(); // Feed the WDT (again) // Output the results as source code Serial.println(resultToSourceCode(&results)); Serial.println(""); // Blank line between entries yield(); // Feed the WDT (again) } }
The series of hexadecimal numbers that are displayed on the serial output must be copied and pasted in place of the rawData array in the IRsendDemo example below.
IRsendDemo - used to send raw IR code in case your AC is not supported by this library
/* IRremoteESP8266: IRsendDemo - demonstrates sending IR codes with IRsend. * * Version 1.0 April, 2017 * Based on Ken Shirriff's IrsendDemo Version 0.1 July, 2009, * Copyright 2009 Ken Shirriff, http://arcfn.com * * An IR LED circuit *MUST* be connected to the ESP8266 on a pin * as specified by kIrLed below. * * TL;DR: The IR LED needs to be driven by a transistor for a good result. * * Suggested circuit: * https://github.com/markszabo/IRremoteESP8266/wiki#ir-sending * * Common mistakes & tips: * * Don't just connect the IR LED directly to the pin, it won't * have enough current to drive the IR LED effectively. * * Make sure you have the IR LED polarity correct. * See: https://learn.sparkfun.com/tutorials/polarity/diode-and-led-polarity * * Typical digital camera/phones can be used to see if the IR LED is flashed. * Replace the IR LED with a normal LED if you don't have a digital camera * when debugging. * * Avoid using the following pins unless you really know what you are doing: * * Pin 0/D3: Can interfere with the boot/program mode & support circuits. * * Pin 1/TX/TXD0: Any serial transmissions from the ESP8266 will interfere. * * Pin 3/RX/RXD0: Any serial transmissions to the ESP8266 will interfere. * * ESP-01 modules are tricky. We suggest you use a module with more GPIOs * for your first time. e.g. ESP-12 etc. */ #ifndef UNIT_TEST #include <Arduino.h> #endif #include <IRremoteESP8266.h> #include <IRsend.h> const uint16_t kIrLed = 4; // ESP8266 GPIO pin to use. Recommended: 4 (D2). IRsend irsend(kIrLed); // Set the GPIO to be used to sending the message. // Example of data captured by IRrecvDumpV2.ino replace the array below with the numbers captured by IRrecvDumpV2.ino from the serial monitor uint16_t rawData[67] = {9000, 4500, 650, 550, 650, 1650, 600, 550, 650, 550, 600, 1650, 650, 550, 600, 1650, 650, 1650, 650, 1650, 600, 550, 650, 1650, 650, 1650, 650, 550, 600, 1650, 650, 1650, 650, 550, 650, 550, 650, 1650, 650, 550, 650, 550, 650, 550, 600, 550, 650, 550, 650, 550, 650, 1650, 600, 550, 650, 1650, 650, 1650, 650, 1650, 650, 1650, 650, 1650, 650, 1650, 600}; void setup() { irsend.begin(); Serial.begin(115200, SERIAL_8N1, SERIAL_TX_ONLY); } void loop() { #if SEND_RAW Serial.println("a rawData capture from IRrecvDumpV2"); irsend.sendRaw(rawData, 67, 38); // Send a raw data capture at 38kHz. #endif // SEND_RAW delay(2000); }
If your Air Conditioner is supported
In case your air conditioner is supported by this library , simply select the respective library as shown below and change the parameters according to your settings.

Image: If your AC is supported, you can use the respective library to control your device
All libraries use the same methods to turn the AC on/off, set the temperature, mode, fanspeed and timer.
ac.on(): Sets the AC on
ac.off(); Sets the AC onff
ac.setFan(1); Sets the fan speed (here: fanspeed is set to 1)
ac.setMode(kToshibaAcCool); Sets the mode (here: cooling for Toshiba devices)
ac.setTemp(26); Sets the temperature in degrees celsius
TurnOnToshibaAC.ino
#ifndef UNIT_TEST #include <Arduino.h> #endif #include <IRremoteESP8266.h> #include <IRsend.h> #include <ir_Toshiba.h> #define IR_LED 4 // ESP8266 GPIO pin to use. Recommended: 4 (D2). IRToshibaAC toshibair(IR_LED); // Set the GPIO to be used for sending messages. void printState() { // Display the settings. Serial.println("Toshiba A/C remote is in the following state:"); Serial.printf(" Power: %d, Mode: %d, Temp: %dC, Fan Speed: %d\n", toshibair.getPower(), toshibair.getMode(), toshibair.getTemp(), toshibair.getFan()); // Display the encoded IR sequence. unsigned char* ir_code = toshibair.getRaw(); Serial.print("IR Code: 0x"); for (uint8_t i = 0; i < TOSHIBA_AC_STATE_LENGTH; i++) Serial.printf("%02X", ir_code[i]); Serial.println(); } void setup() { toshibair.begin(); Serial.begin(115200); delay(200); // Set up what we want to send. See ir_Toshiba.cpp for all the options. Serial.println("Default state of the remote."); printState(); Serial.println("Setting desired state for A/C."); toshibair.on(); toshibair.setFan(1); toshibair.setMode(TOSHIBA_AC_COOL); toshibair.setTemp(26); } void loop() { // Now send the IR signal. #if SEND_TOSHIBA_AC Serial.println("Sending IR command to A/C ..."); toshibair.send(); #endif // SEND_TOSHIBA_AC printState(); delay(5000); }
In the example above, we use the Toshiba Ac example. We set following IR code to the AC receiver:
- Temperature: 26 degrees celsius
- Fan Speed: 1
- Mode: Cooling
- On/Off: On
Test the Setup
After uploading the respective air condition control sketch (for example: TurnOnToshibaAC.ino for Toshiba devices or IRsendDemo.ino for unsupported devices) to the board, the ESP8266 NodeMcu will repeatedly send the IR signal in certain intervals (default: 5000ms). To test the setup, simply connect the power jack to a DC voltage source with 4.5 - 10 V. This can be an adapter or 3-5 AA batteries in series.
In some cases, the IR signal from the LED is too weak, due to wrong resistor values or too low voltage. Thus, it is best to hold the setup as close as possible to the receiver, to rule out the signal weakness and distance as a possible cause of error. If the air conditioner reacts to the signal, you can move slowly away from the receiver and test again. This will be repeated until the maximum distance is found within which the A/C will receive commands from the ESP8266.
If the A/C doesn't show any reaction to the IR signal, you can check the IR LED with a smartphone camera (cameras can 'see' infrared) whether it is blinking (transmitting the IR signal).

Image: Smartphone cameras can detect IR signals from remote controls
Possible error causes can be:
- wrong wiring of transistor
- wrong polarity of IR LED
- wrong pin on ESP8266
- wrong sketch on ESP8266
- power supply not sufficient

Image: Test setup to send IR control signals to the air conditioning device
In case, everything works fine, you should hear a beep every 5 seconds from the AC. This indicates that a command has been received by the A/C receiver. It will not matter if the signal has been received once or is being received repeatedly, as the signal in this example contains the same command every time it is send.
Wrap-Up
We connected the ESP8266 with an IR LED, and uploaded a library to transmit IR control signals. For air conditioners that are not supported by the library, we explained how to work around this issue by recording the raw IR signal and sending this captured raw signal for each command.
This is the most basic configuration and there are plenty other ways to add functionalities. The ESP8266 has WiFi capability, so that we are able to extend this IR remote control system to be controlled via the internet from any part of the world.
Also, similar to this project a remote control can be programmed to control other devices such as TVs, music systems, garage doors and many more.
About the author
I use code IRsendDemo.ino, but I donot know when the IR led turn on.
Follow the code, that mean the IR led turn on and delay (5000ms), and then turn on, and delay(5000 ms).
That mean the IR led turn on all the time, right??
how did you define the resistors?
Great, Thank you