Appendix B: Encoding: IR Codes
The goal is to simulate an Infra Red (IR) Remote Control so that we can use an Arduino like device to control a Settop box. Verizon has a really nice FIOS Mobile app that allows simulating remote control buttons from a tablet or phone. It works well, however, I needed to write my own interface. There were two alternatives:
This note is about the first alternative; I'll try the second later. The Arduino has IR libraries written in C++, but I couldn't find any decent documentation for them to be useful.
I used an oscilloscope and a TCRT5000L IR sensor to view the waveform emitted by the remote. The LED portion of the TCRT5000L will be used later. Turn off any bright light in the area. Aim the Remote at the sensor and press any button.
You should see a pattern like this on the scope for Remote button 3, @10 ms/div. We get very nice TTL signals with the simple setup. The signal is high when there is IR and low without the IR.
The first thing you notice in the picture above is that the signal peaks seem very noisy (circled). Let's zoom in. We see a nice waveform that does not seem like noise. Click on the screen shots below to expand.
A bit of research on Google explains that this is the commonly used 38kHz modulation. Many IRs use this technique to reduce interference from other IR sources (like fireplaces, warm bodies, etc.) Checking the wavelength of the signal above shows that it is pretty close to 38kHz.
Pressing each button on the Remote shows that they all have the general format as shown below. There are 2 pulse trains, separated by 100 ms. Here is the trace for button 3: a Button Start Marker, followed by data, followed by a Stop Marker.
I made up the terms Start and Stop Marker. The Marker pulse is wider than the data pulses. Here are some measurements:
It seems that the time between pulses indicates whether the data bit is a 0 or 1. Again Google comes to the rescue. The tutorial from Vishay discusses Phase encoding, Pulse distance encoding and Pulse width encoding. Clearly, Pulse width is not being used here because most of the pulses are narrow. Let's assume that Pulse length encoding is being used, since it is a bit easier to work with. (Turns out this is the correct assumption!) A picture on the Web describes this nicely, though the timings don't quite match ours.
Let's get some more waveforms to study.
Button 1 = 10000000 00001111
===========================
Button 2 = 01000000 00000111
===========================
Button 3 = 11000000 00001011
===========================
Button 4 = 10010000000000011
===========================
Based on the waveforms seen on the scope, I couldn't imagine how to have a bit stream ending with a 0 while using the Pulse distance encoding. Every button as seen would have to end with a 1 bit since there is a 30 ms Low between the last data bit and the Stop Marker. So I am guessing that the encoding is a bit different from the standard Pulse distance encoding described. I see it as being Low for a time, followed by a 0.5 ms High, i.e., the High pulse appears at the end of the data bit, not the beginning.
With the above assumption, it appears that the Stop and Start markers are as follows:
Using the decoding program in Appendix A, one can collect the codes transmitted for each button. Connect an Arduino digital input pin along with the Oscilloscope. No additional circuitry is necessary, however, you may need to 'de-bounce' the signal in software. The codes are perfectly repeatable. They are in the table below. The transmitted code doesn't make much sense until you realize that it is transmitted Least Significant Bit (LSB) first. So let's reverse it and display it in Hex. Ta da, the patterns jump out. The low order byte contains a command code, while the high order byte contains a check-sum of some sort.
Button |
Transmitted code |
Binary (Reversed) |
Hex |
Command (Dec) |
0 |
0000000000000000 |
00000000 00000000 |
0x0000 |
0 |
1 |
1000000000001111 |
11110000 00000001 |
0xF001 |
1 |
2 |
0100000000000111 |
11100000 00000010 |
0xE002 |
2 |
3 |
1100000000001011 |
11010000 00000011 |
0xD003 |
3 |
4 |
0010000000000011 |
11000000 00000100 |
0xC004 |
4 |
5 |
1010000000001101 |
10110000 00000101 |
0xB005 |
5 |
6 |
0110000000000101 |
10100000 00000110 |
0xA006 |
6 |
7 |
1110000000001001 |
10010000 00000111 |
0x9007 |
7 |
8 |
0001000000000001 |
10000000 00001000 |
0x8008 |
8 |
9 |
1001000000001110 |
01110000 00001001 |
0x7009 |
9 |
* |
0010001000000001 |
10000000 01000100 |
0x8044 |
68 |
# |
0000001000000011 |
11000000 01000000 |
0xC040 |
64 |
ok |
1000100000000111 |
11100000 00010001 |
0xE011 |
17 |
ch+ |
1101000000001010 |
01010000 00001011 |
0x500B |
11 |
ch- |
0011000000000010 |
01000000 00001100 |
0x400C |
12 |
Exit |
0100100000001011 |
11010000 00010010 |
0xD012 |
18 |
Opts |
0100001000000101 |
10100000 01000010 |
0xA042 |
66 |
Menu |
1001100000000110 |
01100000 00011001 |
0x6019 |
25 |
Guide |
0000110000001011 |
11010000 00110000 |
0xD030 |
48 |
Info |
1100110000000101 |
10100000 00110011 |
0xA033 |
51 |
STB |
0101000000000110 |
01100000 00001010 |
0x600A |
10 |
Up |
0010110000001001 |
10010000 00110100 |
0x9034 |
52 |
Down |
1010110000000001 |
10000000 00110101 |
0x8035 |
53 |
Left |
0110110000001110 |
01110000 00110110 |
0x7036 |
54 |
Right |
1110110000000110 |
01100000 00110111 |
0x6037 |
55 |
FIOS |
0111110000001111 |
11110000 00111110 |
0xF03E |
62 |
DVR |
1011110000000000 |
00000000 00111101 |
0x003D |
61 |
Star |
0101100000001010 |
01010000 00011010 |
0x501A |
26 |
Plus |
1100001000001001 |
10010000 01000011 |
0x9043 |
67 |
Heart |
1010100000000101 |
10100000 00010101 |
0xA015 |
21 |
Play |
1101100000000010 |
01000000 00011011 |
0x401B |
27 |
Pause |
1111100000000000 |
00000000 00011111 |
0x001F |
31 |
Stop |
0011100000001100 |
00110000 00011100 |
0x301C |
28 |
Rec |
1000110000000011 |
11000000 00110001 |
0xC031 |
49 |
Prev |
0011110000001000 |
00010000 00111100 |
0x103C |
60 |
Next |
1111110000000111 |
11100000 00111111 |
0xE03F |
63 |
Rwd |
0111100000001000 |
00010000 00011110 |
0x101E |
30 |
Fwd |
1011100000000100 |
00100000 00011101 |
0x201D |
29 |
A |
1110100000000001 |
10000000 00010111 |
0x8017 |
23 |
B |
1110100000000001 |
01110000 00100111 |
0x7027 |
39 |
C |
0001010000000110 |
01100000 00101000 |
0x6028 |
40 |
D |
1001010000001010 |
01010000 00101001 |
0x5029 |
41 |
PIP |
0100010000000011 |
11000000 00100010 |
0xC022 |
34 |
The Volume, Mute & A/V buttons depend on the type of TV selected.
When something appears so simple, we must be on the right track. All we need to do is to figure out the formula for the checksum. A Check byte is appended to the command byte. The two bytes are transmitted LSB first. The Check byte serves to verify that the data is received correctly. The Settop box will ignore a received command if received checksum does not match the expected value that it computes.
Just by looking at the many example values in the Hex column, we see that the hex digits always add up to 16 (or a multiple of it.) So, the Check digit seems to be the negative of sum of 2 hex digits of the command code. The result is moved to the high order nibble of the Check byte.
checkByte = (-((command & 0xF0) + (command << 4))) & 0xF0
There is a problem associated with the minimalist approach used for sensing the signal. The voltage at the photo transistor is an analog value that we are treating as a digital signal. While the signal looks fine on the scope, it will tend to confuse the program. Here is a nice explanation of the issue. It has to do with the voltage being in an "indeterminate" zone between High and Low while it is changing state. We could use an A to D converter to measure the voltage, but we just wanna to use the Digital pin. An electronics enthusiast would go nuts addressing the problem with op amps, but we can solve the problem quite easily in software. In the function waitForState(), we wait for 4 consecutive readings with the same value before we accept it. We are waiting for the signal to "settle" when it is changing. You may need to increase the number if you have a noisier situation.
The hard part is done. Generating the IR codes is relatively easy. Hook up an IR LED to the Digital Out pin of an Arduino. If you are using a 3.3V device, you may need to reduce the resistor to 47 Ω. However, the Settop receiver seems to be quite sensitive and 100 Ω should be fine.
The associated code for Arduinos can be downloaded from here. Since Arduinos are relatively slow, generating the 38 kHz modulation will be a challenge. I generated pulses and viewed the waveforms on the scope. The delay values in the function sendPulse() had to be adjusted by trial and error till the output came close to 38 kHz. On testing, it worked on the first attempt !!
Then I added WiFi support just because I could. The code was tested on an ESP-12, which is like an Arduino that supports WiFi. The delay values will need to be changed if some other device is used. If you don't have an oscilloscope to examine the output, try reducing or increasing the delay times in steps of 10% till it works. Note that adding print statements will affect timings.
My analysis didn't quite fit with the standards, but it fit the measured data.
The purpose of this educational note is just to show how something can be analyzed. Use it to annoy your parents by changing the channel to your favorite show every 5 mins even when you are not anywhere nearby. Any harmful or commercial use of this information is deprecated.
// IRRemoteScan
#include <arduino.h>
const int ird = 5; // IR input
unsigned long tWait, tLow, tHigh;
String pulses;
void setup() {
Serial.begin(9600);
pinMode(ird, INPUT);
Serial.println("\nStarting IRRemoteScan");
}
void loop() {
if (!getPulseTrain(ird, 200000)) // 200 ms
return;
Serial.print("\nCode:");
Serial.print(pulses);
Serial.print(" Reversed:");
Serial.print(reverse(pulses));
Serial.print(" Hex: 0x");
Serial.println(value(pulses), HEX);
}
/////////////////////////////////
// detect a sequence of bits after a marker
boolean getPulseTrain(int ird, long timeout) {
if (!getPulse(ird, timeout)) // on success, tLow & tHigh are set
return false; // timeout
// tHigh should be about 10 ms for Marker
if (tHigh < 8000 || tHigh > 12000)
return false;
// get the next data pulse
if (!getPulse(ird, timeout))
return false; // unexpected timeout
if (tLow < 4000) {
pulses = "STOP";
//Serial.println(" STOP");
return true;
}
pulses = "";
for (;;) {
// get the next data pulse
if (!getPulse(ird, 6000)) // timeout = 6 ms
return true; // done
if (tLow < 4000)
pulses += "0";
else
pulses += "1";
}
}
// returns tHigh and tLow (in uSec)
boolean getPulse(int ird, long timeout) {
// wait for signal to go HIGH
if (!waitForState(ird, HIGH, timeout))
return false;
tLow = tWait;
//Serial.print(" tLow=");
//Serial.print(tLow);
//if (tLow < 1500 || tLow > 6000)
// Serial.print(" !!!");
//Serial.print("; ");
// wait for signal to go LOW
if (!waitForState(ird, LOW, timeout))
return false;
tHigh = tWait;
//Serial.print(" tHigh=");
//Serial.print(tHigh);
//if (tHigh < 300 || tHigh > 600)
// Serial.print(" !!!");
//Serial.print("; ");
return true;
}
// returns duration (in ms) till pin is at requested state (HIGH or LOW)
// returns +ve on success, -ve on timeout
boolean waitForState(int pin, int state, long timeout) {
unsigned long start = micros();
int repeat = 0;
for (;;) {
tWait = micros() - start;
if (digitalRead(pin) == state) {
if (++repeat > 4) // need to see 4 consecutive readings to avoid noise
return true;
}
else
repeat = 0;
if (tWait > timeout)
return false;
yield();
}
}
// reverses input string
String reverse(String str) {
String rev = "";
while (str.length() > 0) {
rev = str.substring(0, 1) + rev;
str = str.substring(1); // pop
}
return rev;
}
// converts LSB-First bit string to int.
int value(String str) {
int val = 0;
int mask = 1;
while (str.length() > 0) {
if (str.substring(0, 1).equals("1"))
val |= mask;
// prepare for next bit
mask <<= 1;
str = str.substring(1); // pop
}
return val;
}
// IR Codes for VZ FIOS remote
struct IRCode {
String buttonName;
int buttonCode;
};
struct IRCode codes[] = {
{ "0", 0 },
{ "1", 1 },
{ "2", 2 },
{ "3", 3 },
{ "4", 4 },
{ "5", 5 },
{ "6", 6 },
{ "7", 7 },
{ "8", 8 },
{ "9", 9 },
{ "*", 68 },
{ "#", 64 },
{ "ok", 17 },
{ "ch+", 11 },
{ "ch-", 12 },
{ "Exit", 18 },
{ "Opts", 66 },
{ "Menu", 25 },
{ "Guide", 48 },
{ "Info", 51 },
{ "STB", 10 },
{ "Up", 52 },
{ "Down", 53 },
{ "Left", 54 },
{ "Right", 55 },
{ "FIOS", 62 },
{ "DVR", 61 },
{ "Star", 26 },
{ "Plus", 67 },
{ "Heart", 21 },
{ "Play", 27 },
{ "Pause", 31 },
{ "Stop", 28 },
{ "Rec", 49 },
{ "Prev", 60 },
{ "Next", 63 },
{ "Rwd", 30 },
{ "Fwd", 29 },
{ "A", 23 },
{ "B", 39 },
{ "C", 40 },
{ "D", 41 },
{ "PIP", 34 }
};
const int numButtons = (sizeof(codes)/sizeof(codes[0]));
// find button and return 16 bit value to be transmitted
int buttonValue(String button) {
for (int i = 0; i < numButtons; ++i)
if (codes[i].buttonName.equalsIgnoreCase(button))
return value(codes[i].buttonCode);
return -1;
}
// compute 16 bit value: checkDigit followed by command code
int value(int val) {
return (checkDigit(val) << 8) | val;
}
// compute checksum digit, 4 bit value in high order nibble
int checkDigit(int val) {
return (-((val & 0xF0) + (val << 4))) & 0xF0;
}
// IRRemoteSend
#include <arduino.h>
#include <ESP8266WiFi.h>
const int ird = 14; // Pin 14, LED output
WiFiServer server(80);
WiFiClient client;
const String http = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
void setup() {
Serial.begin(115200);
Serial.println("\nStarting IRRemoteSend");
pinMode(ird, OUTPUT);
WiFi.mode(WIFI_AP);
WiFi.softAP("remote", "", 1); // wide open AP, no password
Serial.print("AP 'remote' up at ");
Serial.println(WiFi.softAPIP());
server.begin(); // start TCP server
}
void loop() {
if (! client.connected())
client = server.available();
if (client && client.available())
handleWiFiClient(); // process request from browser
}
// process request from browser client
void handleWiFiClient() {
String req = client.readStringUntil('\r'); // get first line
client.flush(); // this is needed
Serial.print("\nFrom ");
Serial.print(client.remoteIP()); // debug
Serial.print(": ");
Serial.println(req); // debug
if (req.indexOf("favicon") < 0) {
String seq = req.substring(req.indexOf("/")+1);
seq = seq.substring(0, seq.indexOf(" "));
String resp = http+"<html>"+seq+"</html>\r\n";
client.print(resp); // send web page
sendButtonSeq(seq); // IR stuff
}
client.stop(); // cannot open another connection without closing this one
}
/////////////////////////////////
// input is a sequence of buttons separated by ','.
void sendButtonSeq(String seq) {
String button;
while (seq.length() > 0) {
int dlm = seq.indexOf(",");
if (dlm < 0) { // no more?
button = seq;
seq = "";
}
else { // pop off first cmd
button = seq.substring(0, dlm);
dlm++; // skip past ','
if (dlm < seq.length())
seq = seq.substring(dlm);
else
seq = ""; }
sendButton(button);
Serial.print(",");
Serial.print(button);
delay(500); // wait a bit between buttons, adds to suspense!
}
}
// send one button command
void sendButton(String button) {
int val = buttonValue(button); // get button code
if (val < 0) // invalid command ignored
return;
sendMarker(1); // send start
for (int i = 0; i < 16; ++i) // send 16 bits
sendBit((val >> i) & 1);
delay(30);
sendMarker(0); // send stop
}
// STOP=sendMarker(0) and START=sendMarker(1)
void sendMarker(int data) {
sendPulse(9000); // 9 ms
sendBit(data);
}
// send a data pulse
void sendBit(int data) {
delay((data == 0)? 2 : 5); // Low signal
sendPulse(500); // 500 us wide pulse
}
// send 38kHz modulated pulse for specified us
// the 2 delay values are tuned for ESP-12 by watching the output on a scope
// time for signal on is less than signal off, perhaps because of the resistor value chosen
void sendPulse(int us) {
unsigned long endTime = us + micros();
for (;;) {
digitalWrite(ird, HIGH);
delayMicroseconds(6); // ramps up faster, reason unknown
digitalWrite(ird, LOW);
delayMicroseconds(18); // ramps down slowly
if (micros() > endTime)
break; // end of pulse
}
}