In this post I’m going to demonstrate code to record the pulse-width timings coming from an IR sensor and use that to record a button-press from a TV remote. Then I’ll turn that around and use it to identify that same button-press later on, using an interrupt to make sure your robot gets your command. If you’re a TL;DR sort of person, consult the ir_capture.ino sketch to capture the pattern for a given button, and the ir_identify.ino sketch to respond when that specific pattern is detected.

More after the jump, if you want the full story.


Disclaimer: A lot of this is experimental and behavioral. It’s entirely possible I made assumptions that I shouldn’t — and my C is very rusty, so there are definitely bugs lurking in here. But it ‘works well enough’ for me to move on to the next phase of the project, so I’m sticking a pin in it. In my exploration, I found a few sites with some code, but I ended up redoing a bunch of it, so here it is. If you spot a big problem, certainly leave a comment and let me know, however I’m intentionally leaving things a little simple here for illustration purposes. In my own project, I’ll be optimizing this and it’ll probably be less readable.


Introduction

I’ve messed around with the Arduino in the past, and I was particularly interested in getting into robotics when I had a chance, so I hopped on the Pi-Bot bandwagon and was very lucky to be early on the delivery list, and got to spend a great weekend building and troubleshooting my own robot.

Hell yeah.

Hell yeah.

In an ominous turn, my very first task went awry when I reversed the inequality in a test and transformed my meek robot – who approaches but stops obediently a foot away – into a hunter that waits until something is close and then lunges. The irony, it burns.

A later experiment had the little guy go into an uncontrolled spin that was hard to rescue it from. It was growing apparent that I needed a way to shut it down from a distance.

By chance, I had just been at Vetco (if you’re in the Seattle area anywhere, it’s worth stopping in, you’ll get lost for hours) and picked up an IR Detector. I didn’t really have a need for it, but it was $3 and for that much I was happy to have it in the parts box until something came up. Usually it takes a couple years and a few rounds of self-doubt before my pack-ratting comes to fruition, but it was only a couple days this time. Score!

The product packaging pointed me toward the Adafruit site which got me started. It’s not a terribly complicated setup. Here’s the circuit:

So, yeah, not very complicated.

So, yeah, not very complicated.

Red and black go to 5V and GND as usual, and the white lead goes to the Arduino’s digital pin 2 for the single input pin. The trick is that the bits aren’t encoded as 1 = HIGH & 0 = LOW on that pin. Instead it uses Pulse Width Modulation to encode the data. Further, it turns out there are a few different standards, this is one of them:

Maybe another time.

Maybe another time.

I was hoping for a quick hack so I could keep going, and this was starting to look like a lot of work to figure it all out… and for what? I don’t really need to know what info is encoded in the flickering IR light (I am curious, though), I just need to recognize that pattern when I see it again and to make that take precedence over anything else currently going on in Number 5’s head.

Making it Interrupt Driven

When you’re giving your robot directions, you typically want the effect to happen immediately, and probably without concern for what’s otherwise going on. That’s what interrupts were made for. When they fire, everything else is put on hold while your interrupt handler is called. The Arduino has two or more interrupts (check your model). I’m using an Uno, and I’m going to use Interrupt 0 (which is connected to digital pin #2) and I’m going to call a special handler when it sees pin D2 go HIGH (which it will when it sees the IR message).

attachInterrupt( 0, captureIR, RISING );

It turns out that’s all you have to do. Well, you have to be careful in how you write your interrupt handler, in particular how to handle it when the second IR pulse comes in and wants to run the interrupt again! I tried a number of things, it turns out all it took was this:

unsigned long last_time = 0;

void captureIR() {
  
  // If the previous call came in less than 0.25 seconds ago, just ignore it.
  unsigned long now = micros();
  if (now - last_time < 250000)
    return;
  last_time = now;
 
  // ... the rest coming soon
}

Remembering the message

Capturing/recording the message turns out to not be that hard. The details of the message — the individual bits — might not be decoded, but the pulse-width message is essential a set of timings. How long does the line stay high for? how long does it go low for? high? low? Repeat until it’s time to stop, in this case if you see the line go dead for a long time (relatively speaking).

There tends to be 10-20 of these ‘pulse pairs’. Rather than leave them as raw uS measurements I change them to ‘cycles’ according to the 36Khz standard which seems a median value. I leave room for up to 50 pulse pairs, though, and as I get the lengths of the pulses, I store them in an array:

//  the maximum pulse we'll listen for
#define MAX_PULSE 30000
 
// the largest message we expect to receive (in bits)
#define MAX_MSG_SIZE 50
 
//  These variables hold the info for the incoming IR message.  If we have received
//  a message, then these will hold the message length and the pulse width values.
//  We'll store up to 50 pulse pairs (high and low), which is more than enough. 
volatile uint8_t ir_msg_len = 0;
volatile uint16_t ir_msg[MAX_MSG_SIZE*2]; // pair is high and low pulse

unsigned long last_time = 0;
const float cycle_time = (1.0/36000.0)*1000000;   // 27.7778 uS per cycle


void captureIR() {
  
  // If the previous call came in less than 0.25 seconds ago, just ignore it.
  unsigned long now = micros();
  if (now - last_time < 250000)
    return;
  last_time = now;
 
 
  // Track the pin as it rises and falls and record the time it spends in HIGH
  // then LOW state.  If we pass MAX_PULSE uS, the message is over.
  unsigned long hightime, lowtime;
  ir_msg_len = 0;
  
  while (ir_msg_len < MAX_MSG_SIZE*2) {
    hightime = pulseIn(IR_PIN, HIGH, MAX_PULSE);
    lowtime = pulseIn(IR_PIN, LOW, MAX_PULSE);
    if ( (hightime == 0) || (lowtime == 0) ) {
        // Finished with message
        return;
    }
 
    ir_msg[ir_msg_len++] = round(hightime/cycle_time);
    ir_msg[ir_msg_len++] = round(lowtime/cycle_time);
  }
}
 

The loop function is going to just wait until it sees there’s a message waiting, then respond by printing it out to the serial port in a format that will let you just paste it in in the next step:

void loop() {
  if (ir_msg_len > 0) {
    // print the message
    Serial.println("message caught:");
    print_message();
    
    // reset to 'no message' state when finished.
    ir_msg_len = 0;
  }
}
 
void print_message(void) {
  Serial.print("uint16_t target_seq[");
  Serial.print( int(ir_msg_len/2) );
  Serial.print("*2] = { ");
  for (uint8_t i = 0; i < ir_msg_len; i+=2) {
    Serial.print( ir_msg[i] );
    Serial.print(",");
    Serial.print( ir_msg[i+1] );
 
    if (i != ir_msg_len-2) {
      Serial.print(",  ");
    }
  }
  Serial.println(" };");
}  

Go ahead, give it a try. Grab the full text of the ir_capture.ino sketch and save it, compile it, and push it to your Arduino. Open the Serial monitor, and when it says it’s ready, press a button on any old remote you’ve got lying around. You’ll see something like:

message caught:
uint16_t target_seq[16*2] = { 16,15,  16,15,  16,15,  16,15,  16,15,  16,15,  16,15,  48,15,  16,15,  16,15,  48,15,  48,15,  48,15,  48,15,  48,15,  16,15 };

That’s the set of timings for the button you pressed. We’ll use those next, so save that somewhere.

Recognizing it again

Once you’ve captured the pattern, you just have to recognize it when you see it later. Turns out that’s a bit tricky. We’re not just comparing a sequence of bits, that would be easy. Instead it’s a series of timings, how many cycles pass while the line is HIGH or LOW. Sometimes 16 or 17 will pass, sometimes only 15, and sometimes 25. How do you match a pattern and still allow for some fuzziness?

I tried a few different ways and by the time I found one that worked, I realized that I had just recreated the concept of ‘variance‘, which is basically the sum of error (distance from expected value to actual value) squared. Because the difference is squared, larger differences are amplified and make for very large variances while small differences remain small(ish).

The equation looks like this, where Oi is the ith Observed timing, and Ei is the ith Expected timing.

{Var}(X) = \frac{1}{n} \sum\limits_i (O_i - E_i)^2

I’m going to assume that each timing measurement could be off by up to 3 cycles. There’s no good reason for picking that figure except that it seems to work. If the variance is less than 3*3 = 9, I’m going to call that good and assume it’s the message we expected. If it’s more than that, then we have the wrong message and there’s no match.

The function to test for that match is pretty simple:

//  Replace vv-- this line here --vv to change what message to listen for.
uint16_t target_seq[16*2] = { 14,17,  15,17,  15,17,  15,17,  15,17,  15,16,  16,16,  48,16,  15,16,  16,16,  47,16,  48,16,  47,15,  47,16,  47,16,  16,16 };

boolean is_match() {
  uint32_t sum_of_squares = 0;
   
  // start at two because the first pair can vary wildly based on initial  
  // timing, but it's possible I should be starting with 1.
  for (uint8_t i = 2; i < ir_msg_len; i++) {
    int16_t delta = ir_msg[i] - target_seq[i];
    sum_of_squares += (delta*delta);
  }
  
  // the variance is the sum of the squares of the error, divided by the
  // size of the input.
  uint16_t variance = sum_of_squares/(sizeof(target_seq)/sizeof(target_seq[0]));
    
  Serial.print("variance: ");
  Serial.println(variance);
    
  // Call it a match if the average difference per reading is 3uS or less (squared). 
  return variance < 3*3;
}}

Notice that I’m skipping over the first pulse pair, and in particular the first part where the line stays HIGH. This seems to be a ‘wake up!’ indicator and the time it stays high can vary, which can throw our variance way off. It doesn’t seem to actually have any informational content except that a message is coming, so I felt safe throwing away the first reading. If this is how the robot uprising starts, my apologies to humanity.

The grimy instrument of your doom

The grimy instrument of your doom

Once you know how to compare the messages, you just need to add that to the main loop. The IR detection is always standing by, ready to fire as an interrupt. Once it does, ir_msg_len will hold the length of a waiting message, and you can pick it up next time through the loop:

void loop() {
  
  // ... do your own thing ...
  
  if (ir_msg_len > 0) {
    if (is_match()) {
      Serial.println("You pressed the right button!");
    }
    else {
      Serial.println("You pressed some other button");
    }
    
    ir_msg_len = 0;
  }
}

And that’s about it. Go ahead and grab the ir_identify.ino sketch, paste in your own message from the previous part, and fire it up. Once it’s started, point your remote at it, press the button, and with any luck you’ll get this happy message:

variance: 0
You pressed the right button!

Variance should be a small number. Often it’ll be zero (an exact match), but don’t be surprised if you see a variance of 1 or 2 — that just means the timing was a little different than we expected, but it’s still probably the right button. Press the wrong button and get sad trombone message.

Yay!

So now you can control your Arduino with a remote. You can add multiple target sequences if you want to have multiple commands — make sure you clearly label the button that turns on the coffee maker and the one that controls the laser defense grid.

I wanted it to use as a remote shut-off for my Pi-Bot antics, so next time I’m going to show off my schedule-driven framework for controlling the robot, and show how this fits in to provide a ‘stand-by’ mode (with a cool glowing light and everything!)

5 thoughts on “Take (IR) Remote Control of your Arduino

  1. This is really cool. I’ve always liked code that writes code.

    There is, as you mentioned, a least one bug in your code, though. First obvious thing: you write “uint16_t delta = ir_msg[i] – target_seq[i];”, but the actual delta might be positive or negative, so storing it in a uint is not the best idea. This bug is likely suppressed when you square the value, but without further testing, it would be hard to be sure it was doing what you expected, so why take the chance? Changing delta to be a signed value is enough to fix the problem correctly.

    On a more stylistic note, you determine the size of an array with “sizeof(target_seq)/sizeof(uint16_t)”, but the more robust recipe is slightly different: “sizeof(target_seq)/sizeof(target_seq[0])”. This style allows you to change the array type later on without inadvertently breaking your code.

    Overall, though, this looks great, and should help to save your ankles from vicious robot attacks in the future. Cheers!

    • Thank you Josh! Good eyes, and it’s nice to know that someone actually looked at the code. Those were good catches and I’ve made those changes.

Leave a Reply

Your email address will not be published. Required fields are marked *