Skip to content

OOK_Example

Travis Goodspeed edited this page Aug 7, 2018 · 6 revisions

Cloning an OOK Remote Relay with the CC430 GoodWatch

Howdy y'all,

This article is a quick description of how the On-Off-Keyed (OOK) example works in the GoodWatch. OOK modulation in various forms is used by many cheap remote controls, and it is a nice easy target with which to learn radio reverse engineering.

You should read along with the commented source code, to be found in firmware/apps/ook.c and ook.h.

73 from Pizza Rat City,

--Travis KK4VCZ

The Target

Our target for today is a generic remote relay control, purchased as a kit from Seattle's neighborhood international book conglomerate for $16 USD. It is listed as ``INSMA 433Mhz Wireless RF Switch 328ft Long Range DC 12V 4CH Channel Wireless Remote Control Switch, DC12V Relay Receiver Module, Transmitter Toggle Switch RF Relay (2 Transmitter & 1 Receiver)'', but most 433MHz models would have worked.

The remote is labeled as 433.92MHz, and by using GQRX on my Ettus B200 software defined radio, I found the expected signal just a bit higher at 433.96MHz. With a bit of care about signal strength and a bit less bandwidth, you might as well use an RTL SDR, which will be a lot cheaper and for this application, just as effective.

The Signal

In the OOK world, the transmitter is switched on for each 1 bit and off for each 0 bit. This is also called ASK, or Amplitude Shift Keying, because from the transmitter's perspective, the off mode might as well be just a lower power level.

To program this mode into the radio, we need to know a few parameters:

  1. What are the bits of the packets, for each button of the remote?

  2. What is the width of one bit? (The reciprocal of this is the symbol rate.)

  3. What is the center frequency for the transmission?

We already know the frequency from GQRX, so let's fire up Universal Radio Hacker (URH) to record a raw signal and measure a bit. URH either expects a .complex file as input, or it will record one directly from the radio.

Inside of URH, we can measure the width of a bit by selecting ASK modulation and then setting the Signal View to "Demodulated." When you have a clean recording, you ought to see bursts for every packet, and zooming in on one should show you clear square waves.

As you can see, the symbols of this transmission last 341µs. Four of these symbols form one bit of the message, but it's far easier to think about them as if they were bits, since we are only interested in replaying transmissions and not in creating new ones.

Once we tell URH that the symbol width is 341 samples in my 1MHz recording, it correctly identifies the contents of the packets. Even if it is a bit awkward to use, we now have a working packet sniffer!

This then provides us with the last piece of the protocol puzzle, as we have a packet! Pressing all four buttons on the remote in sequence quickly shows that these are the four packets, and that each packet is repeated so long as the packet is transmitted.

A: 0000 e8e8ee88e88ee888eee8888e80 00
B: 0000 e8e8ee88e88ee888eee888e880 00
C: 0000 e8e8ee88e88ee888eee88e8880 00
D: 0000 e8e8ee88e88ee888eee8e88880 00

(Zero indicates an off transmitter, so the zeroes here are just for padding. By drawing it out on paper, you can see that E's are long pulses and 8's are short pulses, which might be handy if you needed to enumerate all addresses or sniff for a transmission.)

Transmitting One Packet

So now we know the protocol: Each button sends a steady stream of one of four packets, with a symbol period of 341µs.

At this stage it's tempting to dive directly into writing watch firmware, but it's far better to move slowly, rigging up a single packet transmission from the Python monitor and then viewing that packet in URH to verify the parameters are right.

So how do we inform the radio of the symbol rate, OOK mode, and similar parameters? Well, the GoodWatch's CPU is a CC430F6137 chip that contains a module much like the old CC1101 chip as its radio. We'll use a combination of SmartRF Studio from Texas Instruments and a novel technique of reading the documentation!

For documentation, you should grab cc430family.pdf and cc1101.pdf in the datasheets/ directory of the GoodWatch's Git repo. I keep a paper copy of the register regions of the CC1101 guide, while searing a PDF of the CC430 Family Guide.

SmartRF Studio is a Windows application that parses XML definitions of TI radio chips, allowing you to quickly try out different modulations without modifying firmware. It also has a feature for connecting to a real chip, but I prefer to run it under WINE in the offline mode.

I told SmartRF to use OOK mode with a symbol rate of 2.93207 kiloBaud, which is the reciprocal of the 341µs symbol duration. It provides me with a set of registers, and then I tore through the CC1101 datasheet's interpretation of them to come up with these, slightly different radio settings.

In particular, I disabled the sync field in MDMCFG2 to prevent additional bits from being added before my packets and zeroed PKTCTRL0 so that the chip will not try to append a checksum to the end of each packet.

# Example configuration from a cheap 4-button keychain remote. 
ookconfig=[
    MDMCFG4,  0x86,     #  Modem Configuration
    MDMCFG3,  0xD9,     #  Modem Configuration
    MDMCFG2,  0x30,     #  Modem Configuration, no sync
    FREND0 ,  0x11,     #  Front End TX Configuration
    FSCAL3 ,  0xE9,     #  Frequency Synthesizer Calibration
    FSCAL2 ,  0x2A,     #  Frequency Synthesizer Calibration
    FSCAL1 ,  0x00,     #  Frequency Synthesizer Calibration
    FSCAL0 ,  0x1F,     #  Frequency Synthesizer Calibration
    PKTCTRL0, 0x00,     #  Packet automation control, fixed length without CRC.
    PKTLEN,   32,       #  PKTLEN    Packet length.
    0, 0 
];

Code to transmit the message is simple enough. Note that the packet length disagrees with the data; elsewhere in the Python code, the packet is padded with zeroes to match the right length.

# Might be unique to Travis's set.
ookpackets=[
    "0000e8e8ee88e88ee888eee8888e8000", #A
    "0000e8e8ee88e88ee888eee888e88000", #B
    "0000e8e8ee88e88ee888eee88e888000", #C
    "0000e8e8ee88e88ee888eee8e8888000"  #D
];

...

    if args.ook!=None:
        print "Turning radio on.";
        time.sleep(1);
        goodwatch.radioonoff(1);
        print "Configuring radio.";
        goodwatch.radioconfig(beaconconfig);
        goodwatch.radioconfig(ookconfig);
        goodwatch.radiofreq(433.920);
        while 1:
            print "Transmitting packet %d" % int(args.ook);
            goodwatch.radiotx(ookpackets[int(args.ook)].decode('hex'));
            time.sleep(1);

A lot of the frustration that people feel with this chip comes from slight register misconfigurations, and the only good way to avoid this frustration is by using tools like URH to view the signal in the raw. For example, at one point I was sending packets that were too long, overflowing the transmit buffer. This resulted in packets like the following, which were of varying length, but because I could see them visually, I knew that my modulation, bit timing, power, and other settings were correct.

At this point, URH was showing me that my development board was transmitting correct packets of the correct length, with the correct symbol timing, on the correct frequency. Now I could finally move on to writing a watch applet for a transmitter.

Creating an Applet Skeleton

Before we can add radio code, we need an applet to contain it.

The GoodWatch firmware consists of a number of applets in two structs. Those in apps[] are always available, cycled by pressing the mode button on the right side of the watch. The final entry in this struct is a submenu selector, where one applet from the subapps[] structure is selected, so we add an entry for our configuration.

#ifdef OOK_APP
  //OOK
  {.name="OOK",
   .init=ook_init, .draw=ook_draw, .exit=ook_exit,
   .packetrx=ook_packetrx, .packettx=ook_packettx,
   .keypress=ook_keypress
  },
#endif

Here, various functions in the ook applet are called for entering, exiting, and drawing the application. .packettx is a function pointer that will be called /after/ a packet has successfully been transmitted, and .keypress is called once when a key is pressed, then again when it is released. .draw is called four times per second to render the screen.

We also need to create our module as apps/ook.c and its headers as apps/ook.h. Read through these briefly on your own machine to see how they work, but I'll highlight a few parts.

Our packets are defined as a constant array of constant byte arrays, and we need the const keyword twice to keep everything in Flash memory. They are static to avoid polluting the namespace of other modules.

//! Array of keys for button pressing.
static const char * const button_array[] = {
  "\x00\x00\xe8\xe8\xee\x88\xe8\x8e\xe8\x88\xee\xe8\x88\x8e\x80\x00", //A
  "\x00\x00\xe8\xe8\xee\x88\xe8\x8e\xe8\x88\xee\xe8\x88\xe8\x80\x00", //B
  "\x00\x00\xe8\xe8\xee\x88\xe8\x8e\xe8\x88\xee\xe8\x8e\x88\x80\x00", //C
  "\x00\x00\xe8\xe8\xee\x88\xe8\x8e\xe8\x88\xee\xe8\xe8\x88\x80\x00"  //D
};

Whenever a packet has been transmitted, we transmit another packet if any of the important buttons is still held down. getchar() performs a scan of the keyboard, and a simple switch() sends the right entry. We only do this if the radio is in the idle state, which it ought to be as the callback is called.

//! Called after a transmission, or on a button press.
void ook_packettx(){
  char c=getchar();
  
  if(radio_getstate()==1){
    /* Schedule next packet if the right key is being held.
       
       Buttons for A,B,C,D are 0,1,2,3 or 0,1,4,7.
     */
    switch(c){
    case '0':
      packet_tx((uint8_t*) button_array[0],
		LEN);
      break;
    case '1':
      packet_tx((uint8_t*) button_array[1],
		LEN);
      break;
    case '2': case '4':
      packet_tx((uint8_t*) button_array[2],
		LEN);
      break;
    case '3': case '7':
      packet_tx((uint8_t*) button_array[3],
		LEN);
      break;
      
    }
  }
}

On a down keypress, we call this function directly, kicking off a flurry of packet transmissions that match those of the original remote.

//! Keypress handler for the ook applet.
int ook_keypress(char ch){
  if(ch)
    ook_packettx();
  return 0;
}

It's also nice to provide some user interaction, so we write the word "TRANSMIT" to the LCD if the radio is not in the idle state.

//! Draw the OOK screen.
void ook_draw(){
  int state=radio_getstate();

  if(state==1){
    lcd_string("     OOK");
  }else{
    lcd_string("TRANSMIT");
  }
}

One last consideration is the CPU clock frequency. For power management reasons, the watch runs with a main system clock of 32kHz, which is just barely fast enough to update the LCD with the time. Our application draws more power, of course, but it must directly request a faster clock in order to send the packets quickly.

We do this by calling ucs_fast() to enter fast mode on entry and ucs_slow() to drop the frequency on exit. Similarly, we turn the radio on and off and bail out to the next application if this watch has no radio.

//! Enter the OOK transmitter application.
void ook_init(){
  /* This enters the application.
     We ignore the codeplug frequency and set our own.
   */
  if(has_radio){
    //Faster processing time, for rapid packet succession.
    ucs_fast();
    
    radio_on();
    radio_writesettings(ook_settings);
    radio_writepower(0x25);
  
    //Set a frequency manually rather than using the codeplug.
    radio_setfreq(433960000);
  }else{
    app_next();
  }
}

//! Exit the OOK application.
int ook_exit(){
  //Cut the radio off.
  radio_off();
  ucs_slow();
  //Allow the exit.
  return 0;
}

Conclusion

That's all that's needed for a quick little OOK remote control emulator! I can now switch relays in my apartment from my watch without leaving the couch, and that's not bad for a day's worth of tinkering.

2-FSK and 4-FSK radio protocols can also be implemented with a little bit of work. I hope to add tools for working with DMR, P25, and DStar in the near future, but pull requests are welcome if you'd care to implement it first. (Be sure to have FREND0 use item 1 of the power table when transmitting, as the first element of that table is zeroed in the GoodWatch.)

It would also be handy to receive OOK packets, so that we can clone remotes without recompiling firmware or reflashing the watch. See Michael Ossmann's old IMME code for examples of doing this on-device.

Postscript

After writing this article, I used preprocessor declarations in config.h to make the settings rapidly reconfigurable. Simply calculate MDMCFG4 and MDMCFG3 to match your bitrate, then define the buttons as appropriate. Many receivers use clock recovery on the data signal and allow for wildly varying bit periods, allowing you to combine signals that use different bitrates.

//Settings for my Magicfly doorbell, 200us/bit OOK
#define OOKMDMCFG4 0xF7
#define OOKMDMCFG3 0x93
#define OOKBUTTONA "\x00\xe8\xe8\x88\x88\xee\x88\x8e\x88\xee\x8e\x88\xee\x80\x00\x00"

/*
//Settings for a DC relay, 341us/bit OOK
#define OOKMDMCFG4 0x86
#define OOKMDMCFG3 0xd9
#define OOKBUTTONA "\x00\x00\xe8\xe8\xee\x88\xe8\x8e\xe8\x88\xee\xe8\x88\x8e\x80\x00"
#define OOKBUTTONB "\x00\x00\xe8\xe8\xee\x88\xe8\x8e\xe8\x88\xee\xe8\x88\xe8\x80\x00"
#define OOKBUTTONC "\x00\x00\xe8\xe8\xee\x88\xe8\x8e\xe8\x88\xee\xe8\x8e\x88\x80\x00"
#define OOKBUTTOND "\x00\x00\xe8\xe8\xee\x88\xe8\x8e\xe8\x88\xee\xe8\xe8\x88\x80\x00"
*/

For an example of raw packet recordings and the resulting GoodWatch configuration, see this repository.