0

empowering creative people

Infrared (IR) Controlled Lights with CircuitPython: Adafruit Circuit Playground Express

The latest Circuit Playground board, the Adafruit Circuit Playground Express comes equipped with an onboard IR transmitter and receiver! This can be used to communicate with TVs and other household devices. You can also use infrared remote controls to control your Circuit Playground Express. You can use another Circuit Playground Express as a remote to control your CPX, and you can even send simple messages between Circuit Playgrounds. By measuring the raw reflected IR light bouncing off of nearby objects you can make a simple proximity sensor!


For this tutorial, we will be utilizing the infrared transmitter and receiver to control two different light animations on our Circuit Playground Express. We will put the same program on two Circuit Playgrounds, so both can act as both a remote and a receiver. The transmit and receive on the Circuit Playground Express works reliably up to three meters. If you are only receiving signals with the Circuit Playground and controlling using a TV remote, you should have longer range.

Adafruit Circuit Playground Express IR

There are two parts for infrared. The transmitter is the clear LED to the left of center on the board marked with TX, its connected to pin #29. The receiver is the darker LED to the right of center marked with RX. The receiver also has a decoder chip that receives the 28KHz signals and demodulates them. Demodulated output from the receiver is available on pin #39.

If you want to use the infrared transmitter and receiver as a proximity sensor, then you need to use the raw analog value from the receiver. The direct analog value is available on pin A10.


The Code

# IR Controlled Lights for the Adafruit Circuit Playground Express
# Written for core-electronics.com.au
# For use on two Circuit Playgrounds, pressing button A or B on one board
# turns on a short light animation on the receiving board.

from adafruit_circuitplayground.express import cpx
import board
import neopixel
import random
import time
import pulseio
import array 

# Create IR input, maximum of 59 bits. 
pulseIn = pulseio.PulseIn(board.IR_RX, maxlen=59, idle_state=True)
# Clears any artifacts
pulseIn.clear()
pulseIn.resume() 

# Creates IR output pulse
pwm = pulseio.PWMOut(board.IR_TX, frequency=38000, duty_cycle=2 ** 15)
pulse = pulseio.PulseOut(pwm)

# Array for button A pulse, this is the pulse output when the button is pressed
# Inputs are compared against this same array
# array.array('H', [x]) must be used for IR pulse arrays when using pulseio
# indented to multiple lines so its easier to see
pulse_A = array.array('H', [1000, 3800, 65000, 950, 300, 200, 300, 1000, 350, 175,
    275, 215, 275, 250, 275, 215, 275, 225, 275, 215, 275, 1000, 300, 225, 275,
    950, 300, 950, 300, 1000, 300, 950, 300, 250, 275, 700, 300, 950, 300, 450, 
    300, 475, 300, 215, 275, 725, 300, 950, 300, 200, 300, 715, 325, 900, 315,
    425, 315, 1000, 65000])
pulse_B = array.array('H', [1000, 3800, 65000, 960, 300, 200, 300, 950, 350, 190,
    215, 245, 300, 225, 275, 225, 275, 215, 275, 200, 300, 700, 300, 200, 300,
    700, 300, 1000, 315, 675, 300, 1000, 300, 200, 300, 700, 300, 950, 300,
    950, 300, 700, 300, 700, 300, 450, 300, 475, 275, 715, 300, 225, 275, 450,
    300, 450, 300, 1000, 65000])

# Fuzzy pulse comparison function. Fuzzyness is % error
def fuzzy_pulse_compare(pulse1, pulse2, fuzzyness=0.5):
    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
    
# Initializes NeoPixel ring
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=.2)
pixels.fill((0, 0, 0))
pixels.show() 

def wheel(pos):
    # Input a value 0 to 255 to get a color value.
    # The colours are a transition r - g - b - back to r.
    if pos < 85:
        return (int(pos*3), int(255 - (pos*3)), 0)
    elif pos < 170:
        pos -= 85
        return (int(255 - (pos*3)), 0, int(pos*3))
    else:
        pos -= 170
        return (0, int(pos*3), int(255 - pos*3))

# neopixel animation for random white sparkles
def sparkles(wait):  # Random sparkles - lights just one LED at a time
    i = random.randint(0, len(pixels) - 1)  # Choose random pixel
    pixels[i] = ((255, 255, 255))  # Set it to current color
    pixels.write()  # Refresh LED states
# Set same pixel to "off" color now but DON'T refresh...
# it stays on for now...bot this and the next random
# pixel will be refreshed on the next pass.
    pixels[i] = [0, 0, 0]
    time.sleep(0.008)  # 8 millisecond delay

# NeoPixel animation to create a rotating rainbow 
def rainbow_cycle(wait):
    for j in range(30):
        for i in range(len(pixels)):
            idx = int((i * 256 / len(pixels)) + j*20)
            pixels[i] = wheel(idx & 255)
        pixels.show()
        time.sleep(wait)
    pixels.fill((0, 0, 0,))

# serial print once when activated    
print('IR Activated!')
    
while True:
# when button is pressed, send IR pulse
# detection is paused then cleared and resumed after a short pause
# this prevents reflected detection of own IR
while cpx.button_a:
pulseIn.pause() # pauses IR detection
pulse.send(pulse_A) # sends IR pulse
time.sleep(.2) # wait so pulses dont run together
pulseIn.clear() # clear any detected pulses to remove partial artifacts
pulseIn.resume() # resumes IR detection
while cpx.button_b:
pulseIn.pause()
pulse.send(pulse_B)
time.sleep(.2)
pulseIn.clear()
pulseIn.resume()
# Wait for a pulse to be detected of desired length while len(pulseIn) >= 59: # our array is 59 bytes so anything shorter ignore pulseIn.pause() # converts pulseIn raw data into useable array detected = array.array('H', [pulseIn[x] for x in range(len(pulseIn))]) # print(len(pulseIn)) # print(detected) # Got a pulse, now compare against stored pulse_A and pulse_B if fuzzy_pulse_compare(pulse_A, detected): print('Received correct Button A control press!') t_end = time.monotonic() + 2 # saves time 2 seconds in the future while time.monotonic() < t_end: # plays sparkels until time is up sparkles(.001) if fuzzy_pulse_compare(pulse_B, detected): print('Received correct Button B control press!') t_end = time.monotonic() + 2 while time.monotonic() < t_end: rainbow_cycle(.001) time.sleep(.1) pulseIn.clear() pulseIn.resume()

The Breakdown

Let’s break down what’s going on within the code piece by piece. 

from adafruit_circuitplayground.express import cpx
import board
import neopixel
import random
import time
import pulseio
import array

# Create IR input, maximum of 59 bits.
pulseIn = pulseio.PulseIn(board.IR_RX, maxlen=59, idle_state=True)
# Clears any artifacts
pulseIn.clear()
pulseIn.resume()

By now you should be familiar with CircuitPython libraries. The library that is critical for infrared is pulseio. When we are reading an IR signal it is actually a series of active and idle pulses. The pulsed signal consists of timed active and idle periods. We need to make our Circuit Playground Express recognize the precisely timed pulses and pulse length. All this happens in a fraction of a second!
The next part of our code initializes our IR input. I have chosen the variable pulseIn for our IR received. The parameters of the PulseIn initializer are as follows:

  • board pin - This is the parameter that identifies where our IR receiver is connected. On the Circuit Playground Express, we have chosen the onboard IR receiver by using board.IR_RX
  • maxlen - This specifies the number of pulse durations to record. Most remote controls will be under 200. If you need to save memory you can make this smaller. In our sketch, we are receiving and transmitting only 59 pulse durations, so setting it at 59 auto filters out some extraneous readings.
  • idle_state - This is a Boolean that indicates the default or idle state of the pulse pin. IR receivers typically idle in a high logic or True state. Setting it to True indicates the normal state is high logic level.

pulse_A = array.array('H', [1000, 3800, 65000, 950, 300, 200, 300, 1000, 350, 175,
275, 215, 275, 250, 275, 215, 275, 225, 275, 215, 275, 1000, 300, 225, 275,
950, 300, 950, 300, 1000, 300, 950, 300, 250, 275, 700, 300, 950, 300, 450,
300, 475, 300, 215, 275, 725, 300, 950, 300, 200, 300, 715, 325, 900, 315,
425, 315, 1000, 65000])
pulse_B = array.array('H', [1000, 3800, 65000, 960, 300, 200, 300, 950, 350, 190,
215, 245, 300, 225, 275, 225, 275, 215, 275, 200, 300, 700, 300, 200, 300,
700, 300, 1000, 315, 675, 300, 1000, 300, 200, 300, 700, 300, 950, 300,
950, 300, 700, 300, 700, 300, 450, 300, 475, 275, 715, 300, 225, 275, 450,
300, 450, 300, 1000, 65000])

These are the pulse arrays that we will both send and receive from our Circuit Playground Express. This can be any length and complexity, I arrived at these numbers by reading what the IR transmitter output when making the same program using MakeCode. I have since modified them slightly because its good to start and end your blocks with longer pulses. To send a pulse it must be an array.array with data type ‘H’ for unsigned halfword (two bytes). This method waits until the whole array of pulses has been sent and ensures the signal is off afterward. 

# Fuzzy pulse comparison function. Fuzzyness is % error
def fuzzy_pulse_compare(pulse1, pulse2, fuzzyness=0.5):
    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

This block of code compares received pulses against stored pulses. There is a surprising amount of error in each detected pulse even at a short distance, so this code is designed to return true any value that falls within a certain margin of error. I have it set to 50% because I am only looking for two input values and they are very dissimilar. This gives me a high acceptance rate of received pulses, and so far I have never returned a false positive. If you have many different pulses you are detecting you may want to turn the fuzziness down to 20% or .2.

# Initializes NeoPixel ring
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=.2)
pixels.fill((0, 0, 0))
pixels.show() 

def wheel(pos):
    # Input a value 0 to 255 to get a color value.
    # The colours are a transition r - g - b - back to r.
    if pos < 85:
        return (int(pos*3), int(255 - (pos*3)), 0)
    elif pos < 170:
        pos -= 85
        return (int(255 - (pos*3)), 0, int(pos*3))
    else:
        pos -= 170
        return (0, int(pos*3), int(255 - pos*3))

# neopixel animation for random white sparkles
def sparkles(wait):  # Random sparkles - lights just one LED at a time
    i = random.randint(0, len(pixels) - 1)  # Choose random pixel
    pixels[i] = ((255, 255, 255))  # Set it to current color
    pixels.write()  # Refresh LED states
# Set same pixel to "off" color now but DON'T refresh...
# it stays on for now...bot this and the next random
# pixel will be refreshed on the next pass.
    pixels[i] = [0, 0, 0]
    time.sleep(0.008)  # 8 millisecond delay

# NeoPixel animation to create a rotating rainbow 
def rainbow_cycle(wait):
    for j in range(30):
        for i in range(len(pixels)):
            idx = int((i * 256 / len(pixels)) + j*20)
            pixels[i] = wheel(idx & 255)
        pixels.show()
        time.sleep(wait)
    pixels.fill((0, 0, 0,))

This portion of the code is where we initialize our NeoPixels and create the animations that play when a signal is received.

while True:
# when button is pressed, send IR pulse
# detection is paused then cleared and resumed after a short pause
# this prevents reflected detection of own IR
while cpx.button_a:
pulseIn.pause() # pauses IR detection
pulse.send(pulse_A) # sends IR pulse
time.sleep(.2) # wait so pulses dont run together
pulseIn.clear() # clear any detected pulses to remove partial artifacts
pulseIn.resume() # resumes IR detection
while cpx.button_b:
pulseIn.pause()
pulse.send(pulse_B)
time.sleep(.2)
pulseIn.clear()
pulseIn.resume()

This is the part of the program that sends pulse signals out when a button is pressed. Since we are transmitting the same signals as we are hoping to receive, it's important to pause the IR receiver when transmitting. We also have a short wait after each pulse sent so the pulses don't run together in one long string. We then clear any detected pulses in case we paused in the middle of a received pulse, so our next received pulse won't be corrupted.

# Wait for a pulse to be detected of desired length
    while len(pulseIn) >= 59:  # our array is 59 bytes so anything shorter ignore
        pulseIn.pause()  
# converts pulseIn raw data into useable array
        detected = array.array('H', [pulseIn[x] for x in range(len(pulseIn))])  
#        print(len(pulseIn))
#        print(detected) 
        
    # Got a pulse, now compare against stored pulse_A and pulse_B
        if fuzzy_pulse_compare(pulse_A, detected):  
            print('Received correct Button A control press!')
            t_end = time.monotonic() + 2  # saves time 2 seconds in the future
            while time.monotonic() < t_end: # plays sparkels until time is up
                sparkles(.001)
        
        if fuzzy_pulse_compare(pulse_B, detected):
            print('Received correct Button B control press!')
            t_end = time.monotonic() + 2
            while time.monotonic() < t_end:
                rainbow_cycle(.001)

        time.sleep(.1)      
        pulseIn.clear()      
        pulseIn.resume()

Now we need to receive pulses sent and check them to see if they match. The first part checks to make sure anything received is the correct length, anything else is cleared until the next go around. We know that our pulse will have 59 pulse durations, so there is no need to check to see that anything shorter matches. Once we have a reading of the correct length we pause our listening for more signals while we check to see if we have a match. The first thing we need to do is convert our “raw” received data to an array.array variable. This is done with “detected = array.array('H', [pulseIn[x] for x in range(len(pulseIn))]) “. We then compare the detected pulse against our stored pulses. If one matches within the acceptable margin of error then a NeoPixel animation is displayed for two seconds.


Reading and Recording IR Signals

If you want to use an IR remote rather than another Circuit Playground Express to transmit, then you need to be able to learn the pulses that the IR remote uses. We can do this pretty easily using the REPL. There is a REPL available in the Mu editor, it’s the same place where your serial printouts show up, just hit any key after opening the REPL with a Circuit Playground Express connected to enter. We will be doing some live programming, and enter our program straight into the REPL with no .py file needed.

We start by entering the following, hit enter after each line.

>>> import board
>>> import pulseio
>>> ir_read = pulseio.PulseIn(board.IR_RX, maxlen=100, idle_state=True)
>>> len(ir_read)
 

When you enter len(ir_read) you should have a value of 0 returned. This means that you have not received any signals. Right now the Circuit Playground is waiting for an IR signal. Point your IR remote at the Circuit Playground and hit a button. Type in len(ir_read) again, you should have a number returned this time. If it is still 0 check your remote and try again. If you accidentally press multiple buttons it will save them all up to the 100 pulse max length we set earlier. We only want to read one button press at a time, if you made an error enter ir_read.clear() and ir_read.resume() to start over.

Continue by typing the following, press enter at the end of each line.

>>> len(ir_read)
59
>>> import array
>>> on_command = array.array('H', [ir_read[x] for x in range(len(ir_read))])
>>> on_command
array('H', [1018, 3796, 65030, 894, 368, 133, 363, 952, 385, 142, 307, 167, 332, 194, 333, 167, 306, 194, 316, 157, 337, 951, 333, 194, 305, 908, 357, 877, 355, 961, 353, 881, 362, 192, 338, 635, 363, 899, 305, 432, 361, 428, 336, 165, 335, 664, 365, 898, 334, 168, 336, 663, 382, 853, 360, 377, 356, 959, 65023])
>>>

Notice that now when I entered len(ir_read) a value of 59 is returned. That is the number of pulse durations detected. We then import the array library, and convert the len(ir_read) into an array.array. The new array.array I've named on_command. We then print on_command by entering it in. There it is! Our detected pulse can now be copied and pasted into our code! You can repeat this process for the rest of the keys without starting over by entering.

>>>ir_read.clear()
>>>ir.read.resume()
>>># press button on IR remote now
>>>len(ir_read)
59
>>>ir_read.pause()
>>> on_command = array.array('H', [ir_read[x] for x in range(len(ir_read))])
>>> on_command

There are a lot of fun possibilities with the infrared transmitter and receiver. A great project could be making a laser tag game with two Circuit playgrounds or a tree ornament that changes animations remotely. You could use the infrared to send messages to another Circuit Playground Express, or make a robot controlled by infrared. There are a lot of possibilities, so get tinkering! Be sure to share your projects on our forum! There are many other tutorials for the Adafruit Circuit Playground Express if you are looking for direction or inspiration.

The latest Circuit Playground board, the Adafruit Circuit Playground Express comes equipped with an onboard IR transmitter an...

Have a question? Ask the Author of this guide today!