How to Build an Arduino-Based Learning Universal Remote Part 2 Software
2021-02-24 | By Maker.io Staff
License: General Public License Arduino
The first article in this series focused on the hardware required to build an Arduino-based IR capture and replicate device.
This article focuses on the software side of the Arduino-based learning IR remote control project introduced in a previous article. The project consists of only a few parts and one simple circuit. The simplicity of the hardware is possible because the software running on the Arduino will do the heavy lifting for us!
A Few Things to Brush-Up On
This project builds upon the IRremote Arduino library, which handles capturing, decoding, and sending data over an infrared LED. Before going on with this article, we recommend taking a look at this article that discusses how the library works. It’s also a good idea to take a look at the IRremote API and documentation in the official GitHub wiki.
It’s worth noting that the IRremote library officially supports most Arduino boards. The code for this article, however, was optimized and restructured to work on an Arduino Uno. That particular Arduino, however, only supports interrupts on two digital pins, which leads to a less convenient button handling. These problems are something to keep in mind when choosing an Arduino to employ in this project.
The Firmware
#include <IRremote.h>
#define RECORD_BTN 2
#define REPLICATE1 5
#define REPLICATE2 6
#define REPLICATE3 7
#define IR_RECEIVE_PIN 4
#define RECORD_TIMEOUT 5000
#define BTN_DEBOUNCE 100
#define STATUS_LED 13
// These variables control the internal state of the Arduino
bool recording = false; // The record button was pressed and the operation did not time out yet
bool replicate = false; // One of the replication buttons was pressed
// These arrays contain data related to the buttons
// !!! IMPORTANT !!!
// The lengths of these arrays must match in order for this project to function
bool pressedButtons[3] = {true, true, true}; // All true because we use a pull-up (i.e. the button states are high when not pressed and get pulled low when pressed)
int buttons[3] = {REPLICATE1, REPLICATE2, REPLICATE3};
long lastPressTimes[3];
// These arrays contain the captured IR data
// !!! IMPORTANT !!!
// The lengths of these arrays must match in order for this project to function
// Furthermore, the lengths must also match the length of the buttons array from above
uint32_t data[3] = {0xFFE01F, 0xFFE01F, 0xFFE01F};
uint8_t len[3] = {32, 32, 32};
// The next captured index will be assigned to this index
// Note that the starting value is -1 because the variable will be incremented before assigning a new value
int nextIndex = -1;
int numberOfButtons = 0;
int pattern = -1;
long recordingStarted = -1;
IRrecv receiver(IR_RECEIVE_PIN);
IRsend sender;
void setup()
{
Serial.begin(9600);
// Wait for the serial port to open
while(!Serial)
{ }
// Calculate the number of entries in the buttons array
numberOfButtons = sizeof(buttons) / sizeof(int);
// Initialize the digital I/O pins
pinMode(RECORD_BTN, INPUT_PULLUP);
pinMode(REPLICATE1, INPUT_PULLUP);
pinMode(REPLICATE2, INPUT_PULLUP);
pinMode(REPLICATE3, INPUT_PULLUP);
pinMode(STATUS_LED, OUTPUT);
// Attach an interrupt that will fire when the record button is pressed
attachInterrupt(digitalPinToInterrupt(RECORD_BTN), recordPressed, FALLING);
}
// Returns true when the push button with the given index is pressed
bool buttonPressed(int index)
{
if(index < numberOfButtons && index >= 0)
{
bool currentState = digitalRead(buttons[index]);
bool oldState = pressedButtons[index];
if(currentState != oldState && millis() > lastPressTimes[index] + BTN_DEBOUNCE)
{
pressedButtons[index] = !pressedButtons[index];
lastPressTimes[index] = millis();
return (oldState == true && currentState == false);
}
}
return false;
}
void loop()
{
for(int i = 0; i < numberOfButtons; i++)
{
if(buttonPressed(i))
{
replicatePressed(i + 1);
break;
}
}
if(!replicate && recording && millis() < (recordingStarted + RECORD_TIMEOUT))
{
decode_results results;
if (receiver.decode(&results))
{
int i = (++nextIndex) % numberOfButtons;
data[i] = results.value;
len[i] = results.bits;
Serial.println("RECORDING DONE!");
Serial.print("CAPTURED ");
Serial.print(results.bits);
Serial.print(": ");
Serial.print(results.value, HEX);
Serial.print(" ON REPLICATE BUTTON ");
Serial.println(i+1);
recording = false;
}
}
else if(recording)
{
Serial.println("RECORD TIMEOUT. BUTTON ASSIGNMENT UNCHANGED!");
recording = false;
}
digitalWrite(STATUS_LED, recording);
}
// Replicate a previously stored IR signal using the NEC protocol
void replicatePressed(int number)
{
if(!recording && !replicate && number > 0 && number <= numberOfButtons)
{
replicate = true;
sender.sendNEC(data[number - 1], len[number - 1]);
Serial.print("REPLICATED ");
Serial.println(data[number - 1], HEX);
replicate = false;
}
}
// Callback for when the record button is pressed
void recordPressed()
{
if(!recording && !replicate)
{
Serial.println("RECORD PRESSED");
// Enable the IR receiver
// This will also reset it, so that it's ready to receive another code right away
receiver.enableIRIn();
recording = true;
recordingStarted = millis();
}
}
As mentioned, the code for this project is rather long (as it does most of the work). The beginning of the sketch defines the digitals pins that are used for connecting the buttons to the Arduino. This section also includes the pins that the IR receiver and sender module are attached to. After that come a few variables. The most important ones are the arrays: the first set of arrays contain information about the buttons and their individual state and when that state changed the last time. It’s crucial that the lengths of these arrays match. The next set of arrays stores the captured IR signals. The length of these, again, must match the length of the previous arrays for the program to work.
The start method contains nothing unusual. It opens a serial port for debugging and then initializes the I/O pins of the Arduino. Note that the input pins use the INPUT_PULLUP value so that the input pins are not left floating. The last line attaches an interrupt to the pin connected to the record button. An interrupt configures the Arduino to call a given method whenever a certain thing happens. In this case, pressing the record button will issue a call to the recordPressed() method.
The interrupt handler (the recordPressed() method) is the last function in the code. It first checks whether the Arduino is already recording a signal or if it’s currently replicating one. If it does neither, the sketch proceeds to enable the IR receiver functionality of the library and sets the recording flag to true. It, furthermore, stores the current time in milliseconds so that the Arduino can detect a time out if it doesn't receive an IR signal for a set amount of time.
The loop method does the main work. It first loops over all the replicate buttons until it has either checked all of them or finds a button that the user presses. The buttonPressed function performs that check. It does so by looking at the array that stores the previous state of each button. If the current state differs from the previously stored one, the button’s position changed. If it changed from HIGH to LOW, the button got pressed down. Otherwise, the button is released.
Note that this behavior is the opposite of what would be intuitive due to the earlier use of the internal pull-up resistors. When a user is not pressing the button, the input pin is pulled high. The buttonPressed function also makes use of the built-in millis() method to read the current time to prevent imprecisions from causing incorrectly identified button presses. Two consecutive button presses are only detected as such if they are 100 milliseconds apart.
If the Arduino detects a button press, the buttonPressed function returns true, and the replicatePressed method gets called. This method first checks whether the supplied number is valid and whether the program is currently waiting for an IR input. If it’s not waiting, the requested previously-stored IR signal gets replicated.
The loop method then proceeds to check what the internal state of the program is. If it is in the record state because the record button was pressed earlier, and the record operation did not time out, then the Arduino checks whether the IRremote library received and decoded any IR signals. If it did, the captured signal gets stored in the next array position. The last line of the loop method turns the built-in LED on while the program is waiting for an IR signal to store.
Note how the IR capture and replication are strictly separated in the program because the IRremote library does not support reading and sending IR signals simultaneously (and the Arduino would lock up in this situation).
A Look Back at the Hardware and Software for this Project
This part of the learning Arduino IR remote control build discussed the software component of the project. The code might look long and intimidating at first, but it’s easily possible to break it down into more manageable parts. The setup function initializes the I/O ports, and the loop method contains the main program logic. The buttonPressed function checks whether the state of a single push button has changed since the last call. It, furthermore, contains simple de-bounce logic to prevent the incorrect detection of a button press where there was none. The replicatePressed sends out a previously captured IR signal, and the recordPressed callback handler gets called whenever the user presses the record button.
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum