Hacking the Harmony RF Remote


For my home automation system I’ve been looking for a sleek universal remote. Some of the requirements are:

  • RF (my A/V cabinet is in the garage)
  • Great ergonomics
  • Inexpensive (<$100)
  • Not too many buttons
  • Use regular batteries so I don’t have to put it back in a charging station after use
  • High WAF (Wife Approval Factor)
  • Ability to integrate it to my DIY home automation system

I finally found it, the Logitech Harmony which is part of their Ultimate package ($130+), but you can buy it as an add-on for $30 (http://www.amazon.com/Logitech-Harmony-Companion-Ultimate-915-000245/dp/B00LTKGFDQ). After some research I figured out that it’s using the popular NRF24LE1 chip (NRF24L01+ radio plus a 8051 MCU). That is totally hackable!

My ultimate goal is to attach a cheap NRF24L01+ chip (http://www.amazon.com/nRF24L01-Wireless-Transceiver-Arduino-Compatible/dp/B00E594ZX0) to my home automation system (based on Raspberry Pi) and use this remote to control my A/V equipment.

I figured there are two phases to this goal, phase one is to determine the RF protocol and ids used to send packets between remote and hub. Phase two would be to understand the packet protocol and see if there are any software-based frequency hopping, encryption, etc.

What I know


  • Nordic Semiconductors NRF24LE1H (http://www.nordicsemi.com/eng/Products/2.4GHz-RF/nRF24LE1)
  • No USB or reset button (so I assume no firmware upgrades)
  • Radio packets around 2.446 GHz, i.e. channel 46 in the NRF24 chip (not confirmed yet)
  • Seems to use the auto-repeat and ack functionality of the chip (Enhanced ShockBurst™)


NRF24xxx chip

  • 2.4 GHz ISM band operation
  • 250 kbps, 1 Mbps, or 2 Mbps data rate
  • 126 RF channels (2.400 GHz to 2.525 GHz)
  • No promiscuous mode (network id, 3-5 bytes, is stripped from SPI output)
  • GFSK modulation


As I was researching the remote and trying to find people who have hacked it, or hacked the NRF24 chip its using, I figured there are four approaches to accomplish the first phase in the ultimate goal:

  1. Request the source code from Logitech for their hub since it’s based on Linux (GPL license)
  2. Sniff the data traffic using another NRF24L01 chip
  3. Reverse engineer the raw RF signal
  4. Reverse engineer the firmware in either the remote or the hub

All it (should) take is to find the answer to these parameters:

  • RF channel
  • Data rate (250 kbps, 1 Mbps, 2 Mbps)
  • 3-5 byte network id

Easy Route

I found out that Logitech had published the source code to some of their other Linux-based products at https://opensource.logitech.com, but as of April 14 2015 they had not posted the source for the Harmony Hub there. But now it’s there! Maybe my email to them helped! Here’s the link: https://opensource.logitech.com/opensource/index.php/Logitech_Harmony_Hubs

I haven’t been able to actually find anything useful, I was hoping they would include the driver for the CC2544 chip, but at least they comply with the license so that’s nice.

Brute Force

Since there are only a handful of parameters I figured if I try a couple of settings I may get luck. I tried some of the default/sample settings and also the (limited) promiscuous mode that someone has had success with (http://goodfet.sourceforge.net/clients/goodfetnrf/). The main problem is the 3-5 byte network id, too many combinations. A friend suggested to try all bytes in the remote firmware (a few kB), I haven’t tried that yet, good suggestion. One of the issues is that it takes a button click to generate a packet and I can only handle so many clicks per second. Also it seems that the firmware rate-limits the clicks, even if I bang on a button the amount of (RF) packets getting generates doesn’t increase. I assume it’s waiting to get its packets acknowledged by the hub.

Reverse Engineer RF

I had read about the HackRF One (https://greatscottgadgets.com/hackrf/), a really nice SDR (Software Defined Radio) with 6 GHz bandwidth, 20 M samples/sec. But at $340 it was more than I was willing to spend for this project. I borrowed a TI CC dev kit, but it was the previous generation chipset so it didn’t support 1 Mbps-2 Mbps. Then I found this article: http://blog.cyberexplorer.me/2014/01/sniffing-and-decoding-nrf24l01-and.html, and it was exactly what I was looking for. I ordered the SDR from Amazon and the MMDS down converter from AliExpress. Super-excited when I received it all I managed to get… nothing out of it. My guess is that either the down converter isn’t good enough, I don’t have the parameters for rtl_fm correct (frequency and gain is very sensitive for the fm demodulation to work, or it needs a lot more packets than I can generate from clicking the buttons on the remote. The author is using a wireless mouse to generate the traffic. There is also another article (http://hackaday.com/2014/08/05/sniffing-nrf24l01-traffic-with-wireshark/), but it’s used to decode the traffic once the network id is known.

One issue with using rtl_fm is that there’s no visual indication what’s going on. When I used SDR# (http://sdrsharp.com/) I was able to see the signal from the Harmony remote in the waterfall, but when I tried the same settings in rtl_fm I didn’t get any decoded output. I also learned about GnuRadio (http://gnuradio.org/redmine/projects/gnuradio/wiki), where I again could see the waterfall, but when I tried to create a low-pass filter and FM demodulator, I just got garbage out. I’m also the first to acknowledge that I’m lacking the fundamental skills to operate GnuRadio…

I did manage to record a sample from GnuRadio that include a packet from the remote. I used a tool called Baudline to visualize it and it had an export function that saves a selection of the sample to a wav file. I’ve linked to it here if anyone knows how to decode GFSK from it. It’s about 10 MB and the sample rate is 2M samples/sec (fastest the SDR-dongle could support), and 2 channels (I and Q): https://www.wetransfer.com/downloads/d83525804cd38e26711ac8151e7c4a0320150416000759/12bafc7cff9c31db37e7a1c46e6c814420150416000759/456bf0

Another tool that looks promising is this: http://nutsaboutnets.com/, but again a little pricey.

Reverse Engineer Code

My last approach was to disassemble the firmware from either the remote or the hub. I was surprised to learn that Logitech had left the protection bits turned off on the flash memory of the remote so with a little soldering I was able to hook up a USB-SPI interface and download the flash image from the embedded flash of the NRF24LE1. After several hours of tracing function calls in my favorite disassembler IDA I gave up this approach, too many variables and registers. I found the code that actually sets the network id, rf channel, etc, but the values came from memory variables and I was unable to find the code that populated it. The code may still come handy for phase two though.

Remote with SPI wires attached

Next steps

After my presentation at the Hardware Hackers meeting I got some good tips that I’ll try:

  • Send and capture the NRF24 signal from a known source (NRF24L01+ module attached to Arduino) and compare that to the waveform of the remote. That should confirm the data rate and RF channel.
  • With the RF channel and data rate known I could try all bytes in the flash memory as it’s likely they are stored there (v.s. calculated at start up).
  • See if it’s possible to borrow a newer TI CC dev kit as it has a better promiscuous mode.
  • Capture a better sample using a different SDR (HackRF, etc). Not sure I want to spend the money though.

To be continued…


, ,

  1. #1 by Stephane on March 21, 2016 - 11:07 am

    Hi Hakan,

    I came quite to the same “Why” conclusion, not to forget the above mentioned WAF which is in this area not a detail… Was quite surprised to find someone having same concern…

    Walking around www I found this.

    Sniffing the frames seems to be possible. Being not a radio specialist, I haven’t tried to follow the described process. I understand this is not unreachable for electronics enthusiasts whereby not direct…

    Anyway did you go further in your investigations?



    • #2 by hakanl on March 21, 2016 - 11:10 am

      I never managed to decipher the traffic (it goes into an encrypted channel hopping protocol once it’s paired so makes it very complicated), but I learned that the Harmony remote emulates a keyboard with a Logitech Unifying receiver, so I’m going that route instead.

  2. #3 by Stephane on August 15, 2016 - 2:25 am

    Hi Hakan again,

    Thanks to your indication I tried to pair the Harmony Remote with the Logitech Unifying Receiver, on a regular Windows PC (with the Logitech Unifying SW).
    It works!

    Then I leverage on the great work of Julien Danjou to sniff the exchanged data, on each button press. The code is given above. I run it on a Raspberry Pi under Linux Ubuntu.
    It works also!

    So that you’re able to receive Harmony Remote key press on any computer (should work on Windows too but I didn’t try it out).

    Hope it helps in your hobbies ;-).


    // Name        : HarmonyInput.cpp
    // Author      : Watea
    // Version     :
    #include <linux/hid.h>
    #include <libusb-1.0/libusb.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <iostream>
    // HARMONY USB interface, thanks to USB sniffer
    #define HARMONY_VENDOR_ID			0x46d
    #define HARMONY_PRODUCT_ID			0xc52b
    #define HARMONY_TIMEOUT				10000
    #define HARMONY_MAX_MSG_LENGTH		100
    #define HARMONY_KEY_POSITION		3 // Found by sniffing
    void outputFrame(unsigned char* _frame, int _size) {
    	std::cout << std::hex;
    	std::cout << "Frame => ";
    	for (int i = 0; i < _size; i++)
    		std::cout << (unsigned short) _frame[i] << "/";
    	std::cout << std::endl;
    	std::cout << std::dec;
    void outputKey(unsigned char* _frame, int _size) {
    	std::cout << std::hex;
    	std::cout << "KEY => " << (unsigned short) (_frame[HARMONY_KEY_POSITION] << 8) + _frame[HARMONY_KEY_POSITION + 1] << std::endl;
    	std::cout << std::dec;
    int main() {
    	libusb_context *ctx;
    	struct libusb_device_descriptor desc;
    	struct libusb_config_descriptor *config;
    	std::cout << "Starting keys sniffer..." << std::endl;
    	libusb_set_debug(ctx, LIBUSB_LOG_LEVEL_WARNING);
    	// Look at the keyboard based on vendor and device id
    	libusb_device_handle *device_handle = libusb_open_device_with_vid_pid(ctx, HARMONY_VENDOR_ID, HARMONY_PRODUCT_ID);
    	std::cout << std::hex;
    	std::cout << "Found Harmony Device: " << device_handle << std::endl << std::endl;
    	// Get interface
    	libusb_device *device = libusb_get_device(device_handle);
    	libusb_get_device_descriptor(device, &desc);
    	libusb_get_config_descriptor(device, HARMONY_CONFIG_INDEX, &config);
    	const struct libusb_interface *iface = &config->interface[HARMONY_INTERFACE_INDEX];
    	const struct libusb_interface_descriptor *iface_desc = &iface->altsetting[HARMONY_ALT_SETTING_INDEX];
    	// Detach & claim interface from kernel driver
    	libusb_detach_kernel_driver(device_handle, HARMONY_INTERFACE_INDEX);
    	libusb_claim_interface(device_handle, HARMONY_INTERFACE_INDEX);
    	// Bench for reading HARMONY Keys (Button Press)
    	unsigned char data[HARMONY_MAX_MSG_LENGTH];
    	int actual_length = 0;
    	int index = 0;
    	char next = 'y';
    	do {
    		if (next == 'y')
    			std::cout << "Press a Key" << std::endl;
    		// Key Value received (Key Release ignored here)
    		// Note: HARMONY REMOTE seems to send over data; some false positive happen, to ignore in this bench
    		if ((actual_length > 0) &&
    			(data[HARMONY_KEY_POSITION] + data[HARMONY_KEY_POSITION + 1] != 0)) {
    			std::cout << std::endl;
    			std::cout << "Index:" << index++ << std::endl;
    			std::cout << "Length:" << actual_length << std::endl;
    			// Key Frame
    			outputFrame(data, actual_length);
    			// Button value
    			outputKey(data, actual_length);
    			std::cout << std::endl;
    			// Continue?
    			std::cout << "Next y or ~y?" << std::endl;
    			std::cin >> next;
    		// Not a Key Value Frame
    			next = 'c'; // Continue
    	} while ((next == 'y') || (next == 'c'));
    	// Leave a clean environment
    	libusb_release_interface(device_handle, HARMONY_INTERFACE_INDEX);
    	libusb_attach_kernel_driver(device_handle, HARMONY_INTERFACE_INDEX);
    	return 0;
  3. #4 by hakanl on August 15, 2016 - 10:10 am

    Very nice, thanks!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: