Introduction: Reverse Engineer RF Remote Controller for IoT!

Picture of Reverse Engineer RF Remote Controller for IoT!

In this instructable, you will learn how to reverse engineer Radio Frequency (RF) remotes and how to implement in a very cheap WiFi enabled computer, the Node MCU. Using this technique you can IoT enable older appliances and devices!

I purchased this Harbor Breeze Kingsbury fan a while back. While it’s a nice looking fan, and quite large, I was disappointed to find that it is controlled with a remote. Main reason being, it is not possible to control the fan without a remote. And the problem with a remote is that it inevitably gets lost and/or broken. So, I decided to undertake a project to replace the remote.

I break down this project into a couple of challenges:

  1. Capture what the remote control is sending to the fan
  2. Analyze the signal and determine pattern
  3. Figure out how to recreate the pattern
  4. Transmit corresponding signal with Radio Frequency (RF) Transmitter (TX)
  5. Make it easy to have this control available on my WiFi network
  6. Put a nice UI on top of it so it’s very user friendly.

Step 1: Hardware

Picture of Hardware

Step 2: Software

I am on OSX but most of this software (or functional equivalent) is available cross platform:

  • Gqrx Software Defined Radio (SDR)
  • rtl_fm - was installed with Gqrx, but needed to create a symbolic link to use it easily from command line:
sudo ln -s /Applications/Gqrx.app/Contents/MacOS/rtl_fm /usr/local/bin/rtl_fm 
brew install sox

Step 3: Capture What the Remote Control Is Sending to the Fan.

Picture of Capture What the Remote Control Is Sending to the Fan.

First, we want to figure out what frequency the device is broadcasting on. In this case, I disabled the remote and found the transmitter and was able to identify it as 315 Mhz - the transmitter module actually had 315 printed on it.

Let’s fire up the Software Defined Radio (Gqrx) and have a look around 315 Mhz frequency, and start pressing buttons on the remote, to see what we can see.

Pressing the buttons and watching for patterns in the waterfall view will help you dial in the exact frequency that the remote is broadcasting on. Also if your squelch is set correctly, you can even hear the signal as you push the button. We have our frequency, so now let’s capture all of the buttons into a wav file so we can analyze.
Rtl_fm will capture the AM broadcast at the given frequency (315.03 Mhz) at a high sample rate, so we can look at exact timings of the signal to decode them. Sox will take the output from rtl_fm and put it into a wav format to analyze in a sound editor.

rtl_fm -M am -f 315030000 -s 2000000 - | sox -t raw -r 2000000 -e signed-integer -b 16 -c 1 -V1 - fancontrol.wav

You will need to quit entirely from Gqrx so that the USB device is released. So, run the above command, and give it a moment to identify your RTL SDR card. Once it is capturing, press each button on the remote in a specified order (write it down), giving a few seconds between button presses. When you have run through all of the buttons, press Ctrl-C to stop the program.

Step 4: Analyze the Signal and Determine Pattern.

Picture of Analyze the Signal and Determine Pattern.

Now find your fancontrol.wav file and open it in Audacity.

This shows Audacity with all of the button presses in a single view. Let’s zoom into the first set to get a better view.

Step 5: Zooming In...

Picture of Zooming In...

So it appears that there are 6 bursts in a transmission. Are they 6 repeats of a single code, or are they all different? We’ll have to decode a few of them to find out. Let’s zoom in on the first message in the set.

Step 6: Zooming in Some More...

Picture of Zooming in Some More...

There are 16 On/Off pairs that look the same, then a few that look different. Let’s see if we can find any regular intervals or timing patterns. Zooming in again…

Step 7: Analysis of Signals & Figure Out How to Recreate the Pattern.

Picture of Analysis of Signals & Figure Out How to Recreate the Pattern.

There are 4 distinct timings in play here, and by a lot of looking around and sampling, we can determine there’s not much more going on but patterns of Short ON, Short OFF, Long ON, and Long OFF, comprising the basic alphabet of the radio communication signal. Using Audacity, selecting an interval and subtracting end position from start position of that selection, we can determine the duration of each of the signals:

  • Short ON: 400 μs (micro seconds)
  • Short OFF: 500 μs
  • Long ON: 850 μs
  • Long OFF: 950 μs

Further analysis also revealed a few other interesting facts:

  1. A single Transmission contains six Bursts, each burst has 50 Symbols in the Message.
  2. The bursts are repeats, there is nothing unique between them.
  3. There are six bursts of the same message in every transmission. There is a break or REST between the bursts, duration of 10,000 μs.
  4. There are patterns which are common among every message. Each message contains a “preamble” of 16 pairs of “Short ON, Long OFF”. This alerts the receiver that a message (Payload) is going to arrive.
  5. The Payload of the message is the next 16 symbols. There is always an ON followed by an OFF. The message symbol pairs then can be represented by SS (Short ON, Short OFF), LS (Long ON, Short OFF), SL (Short ON, Long OFF), and LL (Long On, Long OFF).
  6. The “postamble” is a single Short ON, followed by the REST (if between bursts).

Now that we know the timings, we just need to figure out the payloads so that we can reproduce in our code.

To document this, I isolated the payload in each Transmission, and documented the symbols used. You will see all of this in the code shortly.

Step 8: Transmit Corresponding Signal With Radio Frequency (RF) Transmitter (TX)

Picture of Transmit Corresponding Signal With Radio Frequency (RF) Transmitter (TX)

I’m not going to duplicate information that’s easily available, so get PlatformIO working with your NodeMCU. For my system I had to search for and install CH304G drivers. Once they are installed you should see /dev/cu.wchusbserial1420 in your file system, when the NodeMCU is plugged in the USB port.

My platformio.ini settings are the following, so I can use Arduino code and libraries on NodeMCU:

[env:nodemcuv2]
platform = espressif framework = arduino board = nodemcuv2

Connect the NodeMCU to the 315 Mhz transmitter according to the diagram.

Step 9: Code and UI

Picture of Code and UI

The code itself is pretty simple. The device fires up and joins your wireless network. If you haven't configured it for your network yet, it spins up as its own "fancontrol" SSID and you can join it, navigate to 192.168.4.1, and configure the device to join your home WiFi network.

The device will now wait patently until someone goes to http://fancontrol.local -- if you are on windows you will want to install Bonjour for Windows. Otherwise on OSX and Linux you will be able to address the device in this way already.

The web interface is created with jQuery Mobile, and is hosted on the NodeMCU in SPIFFS image.

From the web based UI you can operate the fan, as long as the NodeMCU is in range - for me 15 feet is no issue.

You can also bookmark the site so it appears as an app on your IOS device.

Code can all be found at my Bitbucket Repository.

Step 10: Conclusion

Using these tools and techniques, it should be possible to reverse engineer many 315 Mhz and 433 Mhz remote controls.

Controlling these with cheap computers with REST and Web interfaces allows you to bring these things into Internet of Things realm, where they can be remotely controlled, monitored, and potentially exploited -- well, that’s another topic.

Thanks to Samy Kamkar for some ideas on the approach.

Comments

jamied_uk (author)2017-11-11

I use windows and Linux not mac!

JasonR273 made it! (author)2017-10-07

Just want to say thank you, thank you, thank you!!! I've been trying to automate my fan for a while with no success. I have a Hook Smarthome device that learns and broadcasts RF using a web interface that works awesome, but it runs on 433Mhz so would not work with the 315Mhz.

I reached out to the team at Hook and found they are beta testing a "build your own Hook" method using a particle photon. It didn't initially work with this due to the pulse width required, but they are modifying the code to make it work now. In the mean time, I modified your code to work with a particle photon. So below is the code to control the ceiling fan without the Hook software.

In the process of testing your payloads, I found that the HB remote sends discrete commands for the 6 speeds in BOTH forward and reverse directions. I think your payloads only did reverse. I captured all of the codes below:

When the crew at Hook are done updating software, I'll blow away this code and use their flash. This will allow me to have a fully functional learning remote that works on the 315Mhz range.

[CODE]/********************************************************************************************************************
* Jryan1776
* 10/7/2017
*
* Code to control Harbor breeze Saratoga ceiling fan using a Particle Photon. Code should work for and Harbor
* Breeze fan with the 6 speed remote <https://images-na.ssl-images-amazon.com/images/I/41GdQZ4%2BDwL.jpg>
*
* Components used:
* Particle photon: https://www.amazon.com/gp/product/B016YNU1A0/ref=...
* RF transmitter (315Mhz):https://www.amazon.com/gp/product/B00LNADJS6/ref=...
*
* Device wiring:
* https://gethookblog.wordpress.com/2015/10/30/hook...
*
* All credit goes to the following:
* veggiebenz @ https://www.instructables.com/id/Reverse-Engineer-...
* veggiebenz @ https://bitbucket.org/veggiebenz/fancontrol/src
* Rahil from Hook Smarthome
*
* To control the fan:
* 1. Go to a command prompt that talks to the photon (in my case windows command prompt
* 2. Type the command without parenthesis
* particle call (device id or device name) Transmit (command)
* 3. Commands include light, power, 1f (speed 1 forward), 5r (speed 5 reverse), etc
*
* To control from the internet using IFTTT just setup a trigger and look for "particle"
* IFTTT will connect to your particle easily and will run the Transmit function. Just
* define which command you want to send. I use google home to voice activate the fan
* this way.
*
**********************************************************************************************************************/
const int TX_PIN = D3; // Particle photon pin 3
typedef struct {
boolean power;
int duration; // micro seconds
} signal;
const signal SHORT_ON { true, 400 };
const signal SHORT_OFF { false, 500 };
const signal LONG_ON { true, 850 };
const signal LONG_OFF { false, 950 };
const int REST = 10000;
const char* host = "fancontrol";
signal sig_preamble [] { // 50 items total
SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF, SHORT_ON, LONG_OFF
};
signal sig_light [] {
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_power [] {
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_1f [] {
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_2f [] {
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_3f [] {
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_4f [] {
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_5f [] {
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_6f [] {
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_1r [] {
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_2r [] {
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_3r [] {
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_4r [] {
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_5r [] {
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_6r [] {
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_breeze [] {
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_house [] {
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_delay [] {
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_2h [] {
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_4h [] {
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_8h [] {
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_summer [] {
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_winter [] {
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
// This routine executes transmission of the code out to the RF transmitter
// Signal sent in 3 pcs PREAMBLE + SIGNAL + POSTAMBLE
void doTransmission(signal array[16]) {
Serial.println("Doing transmission...");
for (int burst = 1; burst <= 6; burst++) {
for (int pre = 0; pre < 32; ++pre) { // transmits the Preamble
digitalWrite(TX_PIN, sig_preamble[pre].power);
delayMicroseconds (sig_preamble[pre].duration);
}
for (int idx = 0; idx < 16; ++idx ) { // transmits the payload (signal command)
digitalWrite(TX_PIN, array[idx].power);
delayMicroseconds( array[idx].duration );
}
digitalWrite(TX_PIN, SHORT_ON.power); // transmits the postamble - is that a word?
delayMicroseconds(SHORT_ON.duration);
digitalWrite(TX_PIN, SHORT_OFF.power);
delayMicroseconds(REST); // rest between bursts
}
}
// This routine is called from the windows command line using the following string (leave out the < and > symbols)
// particle call <device_id> Transmit <command in yellow below>
int Transmit(String cmd) {
if (cmd == "light"){
doTransmission(sig_light);}
else if (cmd == "power"){
doTransmission(sig_power);}
else if (cmd == "1f"){
doTransmission(sig_1f);}
else if (cmd == "2f"){
doTransmission(sig_2f);}
else if (cmd == "3f"){
doTransmission(sig_3f);}
else if (cmd == "4f"){
doTransmission(sig_4f);}
else if (cmd == "5f"){
doTransmission(sig_5f);}
else if (cmd == "6f"){
doTransmission(sig_6f);}
else if (cmd == "1r"){
doTransmission(sig_1r);}
else if (cmd == "2r"){
doTransmission(sig_2r);}
else if (cmd == "3r"){
doTransmission(sig_3r);}
else if (cmd == "4r"){
doTransmission(sig_4r);}
else if (cmd == "5r"){
doTransmission(sig_5r);}
else if (cmd == "6r"){
doTransmission(sig_6r);}
else if (cmd == "breeze"){
doTransmission(sig_breeze);}
else if (cmd == "house"){
doTransmission(sig_house);}
else if (cmd == "delay"){
doTransmission(sig_delay);}
else if (cmd == "2h"){
doTransmission(sig_2h);}
else if (cmd == "4h"){
doTransmission(sig_4h);}
else if (cmd == "8h"){
doTransmission(sig_8h);}
else if (cmd == "summer" || cmd == "forward"){ // respond to either summer or forward command
doTransmission(sig_summer);}
else if (cmd == "winter" || cmd == "reverse"){ // respond to either winter or reverse command
doTransmission(sig_winter);}
else {
return -1;} // returns -1 if unrecognized command was sent
return 1; // returns 1 for a successful command
}
void loop() {
// nothing to loop
}
void setup() {
pinMode(TX_PIN, OUTPUT); // required for output, deleting will cost you hours of pain...
Particle.function("Transmit", Transmit); // defines the transmit function
}[/CODE]
veggiebenz (author)JasonR2732017-10-17

Jason, this is super cool -- glad you found this useful.. I had suspected that I missed some codes, thanks for working those out!

JasonR273 (author)2017-10-07

Turns out the breeze command also has discrete forward and reverse...

[CODE]signal sig_breeze [] { //forward direction
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
signal sig_breeze_reverse [] { //reverse direction
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF,
SHORT_ON, LONG_OFF,
SHORT_ON, SHORT_OFF,
LONG_ON, LONG_OFF
};
[CODE]
JasonR273 (author)2017-09-19

This is a fantastic write up. Can you let me know if after the build, it would be possible to send simple web commands to control the fan instead of using the software interface? I would like to use IFTTT or Homeseer to control the fan. Thanks!

WilliamB211 (author)2017-03-21

Friggin' awesome, deud!

Most excellent method! Audacity.

Whooda thunk it. Damn creative!

SteveL206 (author)2016-12-19

I'm working on running through this for my own fan. One method that helps shorten up the process a bit - you can search the FCC database for the FCC ID for the remote. It won't get you detail on the protocol but it will give you the frequency of the transmitter. Easier than taking the remote apart. :)

Marbots (author)2016-06-04

Very Nice. Thanks for sharing g.
Thanks for introducing me to Gqrx.
M.

gm280 (author)2016-05-29

Okay, seem you went through a long process to "break the code" to be able to control your new ceiling fan in the event you loose your remote. And I certainly applaud your efforts and ideas. Very well investigated and solved the code issue I'd say. But if you wanted to control the fan via typical wall switches, you could have bypassed the receiver circuitry and wired direct for the fan and a light, if that was an option. Bravo either way.

About This Instructable

14,010views

208favorites

License:

More by veggiebenz:Reverse Engineer RF Remote Controller for IoT!iSPRESSO:  Remote Controlled, Raspberry Pi Powered Espresso Machine
Add instructable to: