CircuitPython

With CircuitPython you can easily read IR sensor pulses from Python code.  Built-in to CircuitPython is a special pulseio module which actually does most of the work of reading fast IR receiver pulses for you.  Even better with Python code you can very easily store and manipulate large lists of pulse lengths.  There's even a handy Adafruit CircuitPython IRRemote module which simplifies some of the processing logic for reading generic remote controls.  CircuitPython makes it very easy to read IR signals!

Hardware & Setup

To read raw IR signals you'll need to connect an IR sensor to your board as shown on the previous pages.  In this example we'll assume the sensor output is connected to pin D2 on your board.

As mentioned you'll also need to install the Adafruit CircuitPython IRRemote library on your CircuitPython board.

First make sure you are running the latest version of Adafruit CircuitPython for your board.

Next you'll need to install the necessary libraries to use the hardware--carefully follow the steps to find and install these libraries from Adafruit's CircuitPython library bundle.  Our introduction guide has a great page on how to install the library bundle for both express and non-express boards.

Remember for non-express boards like the, you'll need to manually install the necessary libraries from the bundle:

  • adafruit_irremote.mpy

Or download the file from the latest release on the Adafruit CircuitPython IRRemote releases page

Before continuing make sure your board's lib folder or root filesystem has the adafruit_irremote.mpy module copied over.

Usage

Next connect to the board's serial REPL so you are at the CircuitPython >>> prompt.

Then import the necessary board and pulseio modules:

import board
import pulseio

Now create an instance of the PulseIn class which reads pulses from the IR sensor's output.  A pulse is simply a change from high to low or vice-versa and the PulseIn class will record the microsecond duration of each pulse.  Let's create a pulse input that can remember the duration of up to 200 pulses (enough to record most remote control codes):

pulses = pulseio.PulseIn(board.D2, maxlen=200, idle_state=True)

Let's break down all the parameters passed in to the PulseIn initializer:

  • Board pin - This is a required parameter which indicates which pin is connected to the output of the IR receiver.
  • maxlen - This specifies the number of pulse durations to record.  For most remote controls a value of 200 will be more than enough pulse durations to store.  If you set this too high you might use more memory than your board has available so be careful with what value you pick.
  • idle_state - This is a boolean that indicates the 'default' or idle state of the pulse pin.  For IR receivers they typically idle in a high logic or True state so setting the idle_state to True indicates the normal state is high logic level.

Once you have a pulse input object you can interact with it as if it were a list of duration values.  Internally the PulseIn class is always listening for pulses from the pin (i.e. a change from the current high/low logic level to the opposite level) and saving the duration of the pulse.  You can list then number of received pulses just like reading the length of a list:

len(pulses)

A value of zero means the sensor hasn't yet received a pulse.  Try pointing a remote control at the sensor and pressing a button.  Then read the pulse length again:

len(pulses)

Now we have some pulse durations to investigate!  First let's tell the pulse class to temporarily stop listening for pulses.  This is useful so that you can operate on the last seen pulse without other pulses adding more noise or artifacts:

pulses.pause()

Now investigate some of the pulse durations by reading values as if the pulse object were a list.  For example to read the first three durations:

pulses[0]
pulses[1]
pulses[2]

Each duration is the time in milliseconds that the pulse was at a specific logic level.  The very first pulse is a maximum value of 65535 because it represents the amount of time the sensor was waiting for the pulse to start (i.e. how long it was in the default high logic level idle state).  Just like with the Arduino code on the previous page you can ignore this first value.

The next two values are interesting, the next pulse value shows the sensor received a pulse that was about 9 milliseconds long (or ~9000 microseconds).  Then the sensor received no pulse for about 4 milliseconds.  This pair of values represents a single pulse and the start of the remote control signal.  It's good to see a value of ~9ms on and ~4m off as that's a common preamble or start for IR codes!

It turns out these pairs of pulses are so common between different remote controls that many of them can be read with similar code.  The Adafruit CircuitPython IRRemote library is a very simple IR remote control decoding library that simplifies much of the pulse and remote decoding logic.  Let's use this module to simplify our pulse analysis, first import it and then create a remote decoder:

import adafruit_irremote
decoder = adafruit_irremote.GenericDecode()

The decoder class allows you to easily wait for and read a list of pulses from a remote control press.  Before you use it lets turn the pulse input back on (remember it's currently paused) and clear its previous input:

pulses.clear()
pulses.resume()

Now we're ready to use the decoder to wait for and return pulses.  Run this code and notice the REPL stops and waits for further input:

pulse = decoder.read_pulses(pulses)

Aim your remote control at the receiver and press a button.  You should see the REPL return to normal operation.  This means the decoder was able to detect an IR remote signal and returned the raw list of pulse values.

This list of pulses is an array which contains the length in microseconds of each high and low pulse from the receiver.  For example you can check how many pulse changes were detected and see their lengths by using the standard array length and printing operations:

len(pulse)
pulse

One very useful thing the decoder is doing internally is detecting and ignoring noise or extraneous pulse widths, like a long starting pulse width before the remote control is detected.  This is very useful as it simplifies your IR processing code--you can focus on just looking at the 'cleaned up' pulse lengths!

Try recording a second pulse:

pulse2 = decoder.read_pulses(pulses)

Remember the read_pulses function will wait for a remote control press to be detected (or if one had previously happened and not been processed it will grab it instead).  Press the same button on the remote to generate a similar pulse as the first press:

Now let's compare the first and second pulse list to see if they match.  A simple comparison might be to check every single value in each list and verify they're the same.  Let's try it with a simple Python function we define:

def simple_pulse_compare(pulse1, pulse2):
    if len(pulse1) != len(pulse2):
        return False
    for i in range(len(pulse1)):
        if pulse1[i] != pulse2[i]:
            return False
    return True

simple_pulse_compare(pulse, pulse2)

Oh no, the comparison failed and returned false!  What happened, wasn't the same button pressed?  It turns out the timing between pulses can vary in small ways.  If you look at the individual pulse lengths of each array you'll see they're close but not exactly the same.  If you compare raw pulses you need to add a 'fuzzyness' that compares values that are close but not exactly the same.

Let's make a new fuzzy compare function that will check for pulses that are close to each other (within 20% of one another for example):

def fuzzy_pulse_compare(pulse1, pulse2, fuzzyness=0.2):
    if len(pulse1) != len(pulse2):
        return False
    for i in range(len(pulse1)):
        threshold = int(pulse1[i] * fuzzyness)
        if abs(pulse1[i] - pulse2[i]) > threshold:
            return False
    return True

fuzzy_pulse_compare(pulse, pulse2)

Success!  Both pulses appear to be the same when using a fuzzy comparison.  By default the comparison will consider pulses the same if they're within 20% of each other, but you can change that fuzzyness by setting the fuzzyness keyword to a different value.  The fuzzyness value is a percentage from 0 to 1.0 (or 0 to 100%) where the pulses must be within that percent of each other's timing.  Lower values are stricter and require more similar pulses, whereas higher values are less strict and might allow noise or incorrect pulses to appear the same.  In general stick with the 20% fuzzyness unless you run into more problematic IR signals.

Let's tie everything together by making a complete program that waits for the button above to be pressed and prints a message.  You can use the recorded pulse list in your program to remember the previously recorded pulse and compare new ones against it.  To detect a different key press just record it with the steps above and update the pulse list in the code. 

Change the pulse list at the top in the code below to the value you recorded (just copy and paste it from the REPL) and save it as a main.py on your board:

import board
import pulseio

import adafruit_irremote


IR_PIN = board.D2  # Pin connected to IR receiver.

# Expected pulse, pasted in from previous recording REPL session:
pulse = [9144, 4480, 602, 535, 600, 540, 595, 536, 599, 537, 600, 536, 596, 540, 595, 544, 591, 539, 596, 1668, 592, 1676, 593, 1667, 593, 1674, 596, 1670, 590, 1674, 595, 535, 590, 1673, 597, 541, 595, 536, 597, 538, 597, 538, 597, 1666, 594, 541, 594, 541, 594, 540, 595, 1668, 596, 1673, 592, 1668, 592, 1672, 601, 540, 592, 1669, 590, 1672, 598, 1667, 593]

print('IR listener')
# Fuzzy pulse comparison function:
def fuzzy_pulse_compare(pulse1, pulse2, fuzzyness=0.2):
    if len(pulse1) != len(pulse2):
        return False
    for i in range(len(pulse1)):
        threshold = int(pulse1[i] * fuzzyness)
        if abs(pulse1[i] - pulse2[i]) > threshold:
            return False
    return True

# Create pulse input and IR decoder.
pulses = pulseio.PulseIn(IR_PIN, maxlen=200, idle_state=True)
decoder = adafruit_irremote.GenericDecode()
pulses.clear()
pulses.resume()
# Loop waiting to receive pulses.
while True:
    # Wait for a pulse to be detected.
    detected = decoder.read_pulses(pulses)
    print('got a pulse...')
    # Got a pulse, now compare.
    if fuzzy_pulse_compare(pulse, detected):
        print('Received correct remote control press!')

Now when you press the remote control button you should see a message printed at the REPL!  That's all there is to basic raw IR pulse detection and comparison with CircuitPython!  

The code on this page can be handy for basic or unknown remote control protocol detection.  However be aware that remote controls are actually quite advanced and sometimes don't behave the way you expect--like pressing a button multiple times might not actually send the full code each time, instead the remote might send a shorter repeat code!  This means the basic raw IR detection shown here could fail because it doesn't expect a repeat code when one is seen.  It turns out general IR remote detection is so advanced it's best handled by a separate library which can decode repeat codes and more.  For CircuitPython check out the IRLibCP module from Chris Young, it has much more full featured IR remote decoding support!

This guide was first published on Jul 29, 2012. It was last updated on Oct 21, 2018. This page (CircuitPython) was last updated on Jan 27, 2018.