Sunday, 25 November 2018

Reverse-engineering a 433MHz remote-controlled power socket for use with Arduino

Overview

The 433 MHz ISM band is a RF band often used for low-power, short range wireless communication in devices such as garage door openers, wireless weather stations, and remote-controlled power sockets, like this device:



The device is an Olsent RCS-5A which I picked up from a local clearance shop for the bargain price of $9, and three additional slave units at $6 each.

The end goal of this project is to reverse-engineer the protocol this device uses to communicate, and interface this with a dirt-cheap 433 MHz transmitter module attached to an Aduino Nano (and fully portable to ESP8266) to enable custom control of attached devices.



There's a couple of approaches that can be taken to decoding the signal.  I've previously used an Arduino Mega as a logic analyser with an IR receiver module for decoding infra-red; the same could be done here with the 433 MHz receiver module.  Instead, I've opted to use an RTL-SDR together with Universal Radio Hacker, which is designed to exact and decode this exact kind of thing.

Everything required:

Not a lot to this one at all - it almost takes some of the fun out of it!

  • Software-Defined Radio dongle
  • Arduino Nano (any Atmel-based model will work)
  • 433 MHz RX / TX modules (only the TX is needed)
  • A copy of Universal Radio Hacker (https://github.com/jopohl/urh)

Assuming you've already got the SDR, you're up for less than $5 in parts.

Capturing and decoding the signal

If you're playing along at home, fire up URH and click on "Record Signal", select your model of SDR, enter the frequency (usually written on the device) and click the start button.  Trigger your device (try a few different durations to be safe), click stop and save your captured signal, and if you're end up with something like this:


This modulation is a form of Amplitude Shift Keying, or ASK, known as On-Off Keying (OOK), the same type as the 433 MHz module implements.  Zooming in a bit, you should be able to understand how it works:


Pulses lengths are precisely timed to within a few microseconds.  If the signal is present for a pre-defined duration (in the above sample, around 350 microseconds) a 1 is recorded, and a 0 if absent.  In the above example, a sequence of 16 bits is highlighted.

URH did a pretty good job of auto detecting the parameters, and with some mild fine-tuning of the bit length, I was able to obtain consistent results, such as the following sequence:

1000100010001000100010001000111010001110100010001000111010001000100011101000111010001110100011101

That's exactly 100 bits... but there's a pattern here.  You'll notice that each 4-bit sequence is either 1000 or 1110, which means that the 25th bit is actually the start of a 1000 sequence - but because the transmitter's last three pulses were "off" , we don't know they're there.

Assuming 1000 is a 1 and 1110 is a 0, we can compress this down to the following sequence:

1111111010111011101010101

Which represented as hexadecimal, becomes:

1FD7755

Which looks like complete crap in ASCII... let's stick with hex.

Repeating the above for each of the 8 buttons, we get the following codes:

Button On Sequence Off Sequence
1 1FD7755 1FD7757
2 1FD5F55 1FD5F57
3 1FDD755 1FDD757
4 1FF5755 1FF5757

Hex is a great way to display data that's aligned on 4 or 8-bit boundaries - this probably isn't.  My best guess is that at the bit level, the data is split into an initial 7 bits (all high), an 8-bit device address, an 8-bit constant and a 2-bit command, like so:

‭1111111/11010101/11010101/01‬
‭1111111/01010111/11010101/11‬

To make the data a bit easier to work when using C/C++, we can break this up a bit differently.

By discarding the first bit and automatically sending it at the start of every sequence, we can split the remaining 24 bits into 16-bit address and 8-bit command.  For any given button, we simply need to send a initial "1" bit (which you'll remember was one high pulse and three low pulses) , followed by the device address (e.g., FDD7) and finally the command (55 for on or 57 for off).

Building the circuit

This is about as easy as an Arduino circuit gets:

To drive the 433 MHz module, you simply need to use the digitalWrite() function; as long as the pin is held high, a constant signal will be sent from the device.  If we modulate the signal by timing our digitalWrite() calls accordingly, we've got ourselves a pretty good approximation of the original OOK-modulated signal sent by the transmitter unit.

For the antenna, I'm using a 1/4-wave monopole, which is a 172mm wire attached to the antenna pin.  Ideally, a 1/2 wave would perform much better, but at 344mm is somewhat impractical.

Writing the code

I could use one of the many libraries that are available for the 433 MHz units, but there's no fun to be had that way!

Rolling-your-own code for these kind of implementations really makes you think about how they work at a logical level, and often saves precious flash space by only implementing the functions your code uses.
Implementing this in code can be broken down into two main tasks:
  1. Extracting the bits of the 'compressed' version of the sequence
  2. Sending the bits as their relevant sequence of 4-bit pulses
Here's how I've gone about it:

#include <Arduino.h>

#define HIGH_PULSE 0x8  // 4-bit pulse sequence representing a logical 1
#define LOW_PULSE 0xE   // 4-bit pulse sequence representing a logical 0
#define BIT_LEN 360     // length of individual bit pulses

#define SEQ_LEN 24      // total number of bits in sequence
#define SEQ_RPT 4       // number of times to repeat full sequence (requires a minimum of 3 to register)
#define SEQ_PAUSE 11500 // time in microseconds to pause between repeated sequences

#define CMD_LEN 8       // total number of bits in command
#define CMD_ON 0x55     // on command sequence
#define CMD_OFF 0x57    // sequence for off command

#define DEV_1 0xFD77   // device 1 address sequence
#define DEV_2 0xFD5F   // device 2 address sequence
#define DEV_3 0xFDD7   // device 3 address sequence
#define DEV_4 0xFF57   // device 4 address sequence

#define TX_PIN 2        // physical pin to which 433MHz module is connected

/* send 4-bit pulse sequence representing logical 1 or 0 */
void send_pulse(bool state) {
  uint8_t pulse = state ? HIGH_PULSE : LOW_PULSE;  // determine pulse sequence based on state

  for (uint8_t j = 4; j > 0; j--) {
    bool bit = pulse >> (j - 1) & 1;               // extract individual bits from pulse from most to least significant bit
    digitalWrite(TX_PIN, bit);                     // set pin state based on pulse bit
    delayMicroseconds(BIT_LEN);                    // hold pin state for the bit length time
    digitalWrite(TX_PIN, LOW);                     // pull pin low once bit sent
  }
}

/* send command to specified device address */
void send_command(uint8_t chan, bool state) {
  uint16_t addrs[] = {DEV_1, DEV_2, DEV_3, DEV_4}; // load device addresses into array
  uint8_t command = state ? CMD_ON : CMD_OFF;      // determine command sequence based on state
  uint32_t addr = addrs[chan - 1];                 // determine address of device by index
  uint32_t seq = addr << CMD_LEN | command;        // concatenate address and command sequences

  for (uint8_t i = 0; i < SEQ_RPT; i++) {          // repeat command for specified count; several sequences required for command to register on reciever
    send_pulse(1);                                 // send initial bit pulse sequence

    for (uint8_t j = SEQ_LEN; j > 0; j--) {
      bool bit = seq >> (j - 1) & 1;               // extract individual bits from sequence from MSB to LSB
      send_pulse(bit);                             // send pulse sequence of extracted bit
    }

    delayMicroseconds(SEQ_PAUSE);                  // delay between repeated sequences
  }
}

void setup() {
  pinMode(TX_PIN, OUTPUT);
}

void loop() {
  send_command(1, 1);                              // turn device 1 on
  delay(1000);                                     // wait one second
  send_command(1, 0);                              // turn device 1 off
  delay(1000);                                     // wait one second
}

I've commented the code pretty heavily - most of it should be pretty self explanatory.  For the sake of demonstration, this just flicks receiver #1 on and off with a delay in between.  By changing the first parameter of the  send_command() function for another address, any of the four devices can be controlled - no libraries needed!

Testing it all 

For whatever reason, the device won't respond to a single pulse - I fired up URH again and sure enough, if I press the button on the original remote quickly enough to trigger only a single pulse, the device doesn't respond.  Further testing showed that at least four pulses were needed for the device to trigger consistently.  This is the purpose of the #define SEQ_RPT 4 macro.

So after a quick change to the code, everything works as expected; I can control any of the four devices from almost anywhere in my house.

Here's the output from my device, captured using Universal Radio Hacker:



So, what now?

I've got a few ideas in mind for this; this is most ideal for switching circuits that are mains powered for home automation purposes- such as lights or some appliances.  This isn't necessary for most devices that use step down or switching power supplies - we could use a simple low-voltage relay to interrupt the low-voltage side of the supply for these.

It's worth noting that it's only a small jump from here to interface with Blynk to enable you to even build your own mobile apps - that's one for another post in itself!

Thursday, 19 July 2018

The perfect multi-button input resistor ladder

A resistor ladder is a brilliant but super simple way to attach multiple buttons to a single analog to digital converter (ADC) pin on an Arduino or other microcontroller.  By expanding on the concept of the voltage divider, the resistor ladder offers a way to create various reference voltages by tapping into each 'rung' of the ladder.  Consider the following schematic:


By attaching our analog pin between the first and subsequent series resistors and a button to earth at each 'rung', we're effectively creating a variable voltage divider.  If we measure or calculate the values read by our analog pin, we can interpret each reading as a different button being pressed with a few lines of code:

#define INPUT_PIN A0

void setup() {
  Serial.begin(9600);
  pinMode(INPUT_PIN, INPUT);
}

void loop() {
  int result = readAnalogButton();
  Serial.println(result);
}

int readAnalogButton() {
  int button = analogRead(INPUT_PIN);
  if (button > 921) return 0;
  if (button < 256) return 1;
  if (button < 598) return 2;
  if (button < 726) return 3;
  if (button < 794) return 4;
  if (button < 921) return 5;
}

Rather than telling our code to look for the exact values we expect the analog pin to read, we set a range of values that can be interpreted as belonging to a specific button.  We do this by calculating the points halfway between each expected ADC reading and set these as as the boundaries of our ranges.  Just like that, we've got 5 buttons on the one pin!  We're not done just yet though, because this is just about the worst possible way we could implement this.

Assuming perfect resistors, the voltage readings for each button should be around 0.0V, 2.5V, 3.33V, 3.75V and 4.0V, with corresponding ADC readings of around 0, 512, 683, 768 and 819.  You might have already noticed a trend by this point...

If we add another 5 switches each with a 1kΩ resistor, the subsequent voltages will be 4.17V, 4.29V, 4.38V, 4.44V  and 4.5V, and will continue to grow logarithmically with each 1kΩ resistor we add:


This might work for a project with a small number of buttons if you measure each value, and calculate and hard-code your ADC boundaries for each button - but magic numbers are bad, and you should feel bad.

If we change the voltage scaling from logarithmic to linear, each adjacent ADC value will become roughly equidistant.  This means that we can dynamically calculate the corresponding button for any given reading, rather than hard-coding fixed values.  This allows us to use the same piece of code for any number of buttons and as a bonus, uses the full range of our ADC - in the above example, we jump straight from 0 to 512 between the first two buttons - that's half our resolution!

So, how do we know what resistors we should use?

For the following example, I've used a 47k Ω resistor for R1.  I've chosen this value as it allows us to operate the circuit at a super low current (by Ohm's law, 5V / 47kΩ = 0.11 mA) and still use a wide-range of commonly available resistor values.  We'll use 5 buttons total, and calculate the value for button 1 (which is actually the second button, like a zero-indexed array).

Since we have a fixed value of R1 we can work out the value for R2 using the following formula:


Rbutton = R1 / (1 - (Bindex / Btotal)) - Rtotal


Where Bindex is the index of the button in question, Btotal is the total number of buttons and Rtotal is the combined value of all resistors in the circuit prior to the current (in this case, it's just the 47kΩ for button 0).  So from the above, we have 47000 / (1 - (1 / 5)) - 47000 = 11750Ω.  We can use a lookup table to find our closest equivalent value, which in this instance is 12kΩ.

If we do this for the remaining buttons, we get values of 20k, 29k and 120k, which should give us corresponding ADC readings of around 0, 208, 415, 616 and 822.

That's still a bit of a process to work out - especially if you've got more than a handful of buttons, which is why I made this Google Sheets spreadsheet to do all the heavy lifting!  Just enter the number of buttons you want to use and the tolerance of your resistors (which is only a consideration with a very large number of buttons - otherwise you can leave this at the default 2%.)


The column you're most interested in is "Closest R" - this performs a lookup of the closest common resistor value to the calculated required value.  The "Req'd" columns indicate the exact values we're looking for, while the "Actual" columns indicate the voltages for the calculated closest available resistor.  "Variance" simply tells us the difference between the current and previous ADC reading - if the result here is greater than the specified resistor tolerance, this will be indicated by the "In Tolerance" column.

Great - so we've got our resistors all worked out - all that's left now is the code!

#define BUTTONS 5
#define RESOLUTION 1023

int readAnalogButton() {
  float avg = RESOLUTION / float(BUTTONS);
  int val = analogRead(ANALOG_PIN);

  if (val > (BUTTONS - 0.5) * avg) { 
    return 0; 
  }

  for (int i = 0; i < BUTTONS; i++) {
    if (val < round((i + 0.5) * avg)) { 
      return i + 1; 
    }
  }

  return 0;
}

We start by determining the average difference between adjacent ADC readings, enabling us to dynamically calculate each button's expected ADC reading.  To test for an open circuit - the most likely result - we check if the measured value is less than halfway between 0 and our dynamically-calculated reading for the first button.  If this evaluates to false, we begin by testing for each button in turn, comparing our ADC reading against the range boundaries that lie between adjacent readings, until we find a match.

Note that because "no button pressed" is a valid result in addition to each individual button press, we treat a zero value as an open circuit, and commence indexing our buttons from 1, as opposed to the zero index used during calculation.

With the above code, we can scale up and down by simply changing the value defined by the macro #define BUTTONS 5, with the number of buttons only limited by the tolerance of your resistors and resolution of your microcontroller's ADC. We'll see just how far we can push this in a future post!

Wednesday, 30 May 2018

Postcards from the ISS

A few years ago, I learned of slow-scan TV transmissions from the International Space Station.  SSTV is essentially a form of narrowband television - a single frame, taking anywhere from a few seconds to a few minutes to transmit, used mainly in amateur radio applications.

The SSTV images from the ISS are transmitted by ARISS (http://www.ariss.org) to commemorate various events and anniversaries.  Usually, a series of 12 images will be transmitted over the period of a few days, allowing anyone with the right equipment to have a chance at capturing and decoding the images as the ISS passes overhead.

Most recently, a series of images celebrating the 40th anniversary of the Interkosmos Soviet space program were transmitted between the 11th and 14th of April.  These were transmitted in PD120 format, which take 120 seconds for a full frame at 640x496.

I recently got my hands on an RTL-SDR dongle (https://www.rtl-sdr.com) and a 25-1300 MHz discone antenna and was keen to see what I could receive.

The first step was to determine when the ISS would be passing overhead using Heavens Above (http://www.heavens-above.com).  Make sure you select all passes (not just visible only) - this will show all passes of the ISS that are 10 degrees or higher above the horizon:


Next, I set up VB-Cable (https://www.vb-audio.com/cable) to route audio output back to input, and configured MMSSTV (http://hamsoft.ca/pages/mmsstv.php) to decode the images.  Both programs require only a minimum of configuration and work largely out-of-the-box.

A few minutes before the ISS was above the horizon, I used SDR Sharp (https://airspy.com/download) to tune the SDR to 145.800 MHz with a gain of around 33dB, and soon started to receive the transmission:


You'll see above that I'm tuned slightly off the centre frequency.  This is due to Doppler shift as the ISS approaches, causing the signal to appear slightly higher than transmitted - as it crosses overhead and moves away from the antenna, the frequency slightly and gradually decreases.

With everything in place, I had several attempts over the course of a few days with some moderate success:

...and quite a few failures:
Even though I'm reasonably happy with the result, I was able to learn plenty that should help me achieve even better results next time.

Receiver gain can be both your friend and enemy!  Depending on if the ISS is directly overhead or approaching the horizon, it can be anything from just over 400km to a few thousand kilometres away, requiring the receiver gain to be set to a reasonably high level to receive a clear signal.  ISS transmissions sit in the 2-meter band which in Australia, and particularly major cities, is adjacent to the band heavily used for POCSAG pager traffic.  POCSAG transmissions are very high power, which results in interference in adjacent bands when the receiver gain is increased. This results in artefacts in the received images (visible in some of the above images as horizontal lines of interference.)  A notch filter may be effective in filtering these to some extent.

Before the next series of transmissions occurs, I'll build myself a tape measure Yagi antenna and maybe move somewhere a bit more radio-silent.  The directivity of the antenna will increase the gain of the received signal and the narrow bandwidth of the Yagi decrease the gain of interfering neighbouring frequencies (at least on paper!)

You can listen to one of the better transmissions below.  If you're setting this up for yourself, you can use this to test that VB-Cable and MMSSTV are configured correctly.



With any luck, we'll see another series of transmissions before the end of the year.