A Smarter Remote

Universal infrared remote controlled through voice commands

5 minute read

5 min read

I replaced all of the remotes in my house with a microcontroller and an infrared LED.

Controlling it through Google Home is helpful when you can’t find the standard remote or it’s too far away!

The Inspiration

I bought a used Kenwood audio receiver but it didn’t come with a remote so I’ve using a generic universal remote to control it. This works pretty well for power and volume but it didn’t have the correct codes to switch inputs or mute!

This got me thinking: if this universal remote can store a bunch of common IR codes I bet I could find or capture all of the codes that control my specific devices and make a smart universal remote with a Particle Photon. This would give me the ability to send signals from my phone or with my voice and transmit batch commands (like “turn all the power off” or “go to specific input sources on each device”).

How it works

IR communication works by pulsing an LED with a 930–950nm wavelength at 38kHz. The time the LED is on is called a “mark” (no relation to yours truly), and when the LED is off it’s called a “space.” The pattern used to represent bits of data depends on the protocol used by the manufacturer.

Timing of NEC bits.
Timing of NEC bits.

The most common IR communication protocol is called NEC and it uses the following pattern to transmit 32 bits of data.

A full NEC transmission.
A full NEC transmission.

A full transmitted NEC code has the following components:

  1. A 9ms mark followed by a 4.5ms space to signal the receiving device. Essentially saying:

    HEY I WANT TO TELL YOU SOMETHING!

  2. After that, the 8 bit address (AKA, the name of the intended recipient) is sent. The address is sent again (8 more bits) in inverse to ensure data integrity:

    10011010 01100101

  3. Our command comes last: 8 bits of data that represent power, volume up, or menu, etc. Of course, the inverse of the command is sent as well for redundancy and verification:

    01101000 10010111

In total, the duration of a NEC transmission is 9 + 4.5 + (32 * ((2.25 + 1.125) / 2)) = 67.5ms.

I found these images and most of this information here. Check it out if you’d like to learn more!

Capturing All of My Remote Codes

I wasn’t sure what protocol my devices use so I got the raw marks and spaces emitted by my remotes using an infrared remote library for Arduino.

#include "IRremote.h"

#define RECV_PIN D0

IRrecv irrecv(RECV_PIN);

decode_results results;

void setup() {
    Serial.begin(9600);
    irrecv.enableIRIn(); // Start the receiver
}

void loop() {
    if (irrecv.decode(&results)) {
        Serial.print("RAW: ");
        
        int count = results.rawlen;
        for (int i = 1; i < count; i++) {
            if (i & 1) {
                Serial.print(results.rawbuf[i]*USECPERTICK, DEC);
            } else {
                Serial.print((unsigned long) results.rawbuf[i]*USECPERTICK, DEC);
            }
            Serial.print(" ");
        }
        Serial.println("");
        
        Serial.print("HEX: ");
        Serial.println(results.value, HEX);
        Serial.println("");

        irrecv.resume(); // Receive the next value
    }
}

Here’s an action shot:

Thanks for the hand, lego man.

So I pressed every button I wanted to be able to mimic and I recorded the codes as they printed out to my terminal.

$ screen /dev/tty.usbmodem14101 9600
RAW: 8800 4450 500 600 500 1700 550 1650 550 550 550 550 550 550 550 600 500 1700 550 1650 550 550 550 1650 550 550 550 600 500 600 500 600 500 600 500 1700 550 1650 550 1700 500 1700 550 550 550 550 550 550 550 550 550 550 550 550 550 550 550 550 550 1700 500 1700 500 1700 550 1700 500
HEX: 61A0F00F

Partly due to the receiving library’s granularity of 50µs, the durations of the marks and spaces didn’t line up perfectly with any of the protocol specifications I was able to find. But after looking at the pattern, it appeared to be NEC: an AGC burst of ~8800µs (vs 9000µs) then a ~4450µs space (vs 4500µs). It clearly contained 32 bits of data with ~550µs (vs 560µs) marks and ~1700µs/~550µs (vs 1690µs/560µs) ones and zeros.

Transmitting all of my remote codes

Now that I’ve captured all of the codes I’d like to emit, I used another library to easily emit the IR codes.

#include <IRTransmitter/IRTransmitter.h>
#include "Codes.h"

#define IR_PIN D6
#define LED_PIN D7
                        
IRTransmitter transmitter(IR_PIN, LED_PIN);

void setup() {
    Particle.function("transmit", transmit);
}

void loop() {
}

int transmit(String args) {
    // Sanitize possible voice to text input
    args.toLowerCase();
    args.replace(" ", "_");

    if (args == "all_power") {
        transmitter.Transmit(Codes::tv_power, sizeof(Codes::tv_power) / sizeof(Codes::tv_power[0]));
        delay(200);
        transmitter.Transmit(Codes::aux_power, sizeof(Codes::aux_power) / sizeof(Codes::aux_power[0]));
    } else if (args == "tv_power") {
        transmitter.Transmit(Codes::tv_power, sizeof(Codes::tv_power) / sizeof(Codes::tv_power[0]));
    } else if (args == "tv_input_hdmi") {
        transmitter.Transmit(Codes::tv_input_hdmi, sizeof(Codes::tv_input_hdmi) / sizeof(Codes::tv_input_hdmi[0]));
    } else if (args == "tv_input_video") {
        transmitter.Transmit(Codes::tv_input_video, sizeof(Codes::tv_input_video) / sizeof(Codes::tv_input_video[0]));
    } else if (args == "tv_input_tv") {
        transmitter.Transmit(Codes::tv_input_tv, sizeof(Codes::tv_input_tv) / sizeof(Codes::tv_input_tv[0]));
    } else if (args == "tv_vol_up") {
        transmitter.Transmit(Codes::tv_vol_up, sizeof(Codes::tv_vol_up) / sizeof(Codes::tv_vol_up[0]));
    } else if (args == "tv_vol_down") {
        transmitter.Transmit(Codes::tv_vol_down, sizeof(Codes::tv_vol_down) / sizeof(Codes::tv_vol_down[0]));
    } else if (args == "tv_vol_mute") {
        // ...etc, etc
    } else {
        return -1;
    }
    
    return 1;
}

IFTTT connection

You can call the code on your Particle as a IFTTT Actions.

So here we’re going to create an Alexa Phrase Trigger on IFTTT which will then pass a command to the transmit function.

After that I wanted to put the device out of sight but make it visible to every device it’s programmed to control.

Demo time!

There’s only one thing left. Let’s give it a try!