Tuesday, August 16, 2016

CC1101 Atmega32u USB dongle + python = RFkitten

The goal

Write software to communicate with my first USB dongle - CC1101 Atmega32u

Usability

Mostly for fun, knowing better wireless devices and protocols, especially OOK.

Code


Hardware


Description


You probably wondering about the name "RFkitten"? Of course there is RFCat, which is great software/firmware for CC1111 EMK or YARD Stick One dongles. The second part of "RFcat" seems been taken from Linux cat program, it looks like an allegory of browsing in-the-air data just like regular files in Linux. Fun fact is that most of people know "cat" from printing files on console, but main function is to conCATenate files.

Why "RFkitten"? Because it's quite similar to "RFCat" but at the end it's not the same thing, it uses different wireless chip CC1101 instead of CC1111 and external atmega32u4 MCU instead of build-in one. This was the first call to go into the cat -> kitten abstraction, second was that RFCat is stable software (unfortunately I didn't have occasion to use it), my version where just born, hence "kitten".

I think CC1101+Atmega32u dongle has feature, mostly because of LUFA, there are many examples like ethernet cards, keyboards, joysticks and audio hosts/devices using it and I think it's nice that you can push more into hardware (dongle).

I plan to add wireless data transmitter (OOK) to my UNI-T digital multimeter, and on the other side I'll have this dongle working as virtual serial port. From PC side it will look like UNI-T is connected directly through serial cable, it will be transparent for logging software.

Device


I bought CC1101 ISM 433Mhz module and basically make an CC1101 Atmega32u USB dongle from it. It's also kind-of replica of busware's CC1101 USB Litle module V3, so in the future I could use busware's and my firmware.

CC1101 ISM 433Mhz module + atmega32u4 = ISM USB dongle

Software


I used python, because it's huge time-saving language when comes to prototyping, also RFCat is based on it. Maybe some day when I familiarize with RFCat sourses I could add CC1101+Atmega32u driver?. This would mean that my RFkitten grow up to be a RFCat, it would be nice to achieve this!

When you look into the RFkitten code, it uses couple of small functions to build CC1101 config. Main python script starts additional thread that reads data from virtual serial port, then decode and print on console, basically that's it. When you write something directly to serial port it will be send over the air, so it's kind of transparent. Config is send after DTR signal as you read in firmware section of CC1101 Atmega32u USB dongle, RFkitten just been born, hence it works only as OOK transmitter/decoder.

CC1101 config


Config is build as a dictionary and joined into one at join_config function. Register values are also binary OR-ed, since many registers use bits as flags for specified options. Config is pushed after DTR poke in push_config.
def join_config(*partial_cfg):
   cfg_f = {}
   for cfg in partial_cfg:
      for key in cfg:
         if key in cfg_f:
            cfg_f[key] |= cfg[key]
         else:
            cfg_f[key] = cfg[key]
   return cfg_f

def push_config(s, c):
   sleep(0.01)
   s.setDTR(0)
   s.setDTR(1)

   for reg, value in c.iteritems():
      s.write(bytearray([reg, value]))

   # end of config
   s.write(bytearray([0xff]))

   sleep(0.01)
Below you can find part of join and send.
   push_config(s, join_config(
                     get_cfg_init(),
                     modulation("ASK/OOK"),
                     manchaster(0),
                     base_frequency(433.92),
                     sensivity("27 dB"),
                     channel_bandwidth(100),
                     data_rate(9600),
                     packet_len(128),
                  )
               )
After sending initial config (above), you can also manipulate registers like below, but remember that PACKET_LEN register is 1 byte = 8 bits long, without additional flags that should be OR-ed, so it's save to do below.
      push_config(s, packet_len(128))

Decode OOK with regexp


I recommend you to read Hacking fixed key remotes, I don't want to double Andrew's descriptions here. I studied them before making my own decoder.

Actually it's possible to decode OOK and determine it's original speed using regular expressions. It means that you can set 9600 speed on CC1101, capture data and it will be decoded even if would be 4800 or 2400. Regexp determine divide factor: 1, 2, 4, 8 of captured signal.
from re import compile,sub,subn

# regexp for data rates
zero={}
zero[1] = compile("1{3}0")
zero[2] = compile("1{6}0")
zero[4] = compile("1{12}0")
zero[8] = compile("1{24}0")

one={}
one[1]  = compile("10{3,}")
one[2]  = compile("10{6,}")
one[4]  = compile("10{12,}")
one[8]  = compile("10{24,}")

# list of know keys
know_keys = [
   ('doorbell_button_1_first',  "10001011111110100000010"),
   ('doorbell_button_1',       "110001011111110101010010"),
   ('doorbell_button_2_first',  "10001011111110100000000"),
   ('doorbell_button_2',       "110001011111110101010000"),
]

def analyze_ook(data, baud, bin_input=False):
   bin_str = ""

   # if input is already a "bin string" e.g. "11110001001010101", do nothing
   # else convert raw data to above string
   if (not bin_input): 
      bin_str = ''.join(format(ord(byte), '08b') for byte in data)
   else:
      bin_str = data

   # determine speed of data by checking every speed regexp
   # then sort tupple and return one with biggest match
   speed = sorted(zero, key=lambda obj:zero[obj].subn('', bin_str))[0]

   # use matched regex to normalize data into _ZERO_ and _ONE_, remove every
   # tramsnission glitch at the end by removing 1 and 0 orphans
   normalized_bin_str = sub('[1,0]', '', zero[speed].sub('_ZERO_', one[speed].sub('_ONE_', bin_str)))

   # change back normalized string with '_ZERO_', '_ONE_' to '1' and '0'
   decoded = sub('_ZERO_', '1', sub('_ONE_', '0', normalized_bin_str))
  
   # only show logner ones
   if (len(decoded) < 4):
      return
   # search key in know keys
   found_key = [key[0] for key in know_keys if decoded == key[1]]

   if found_key == []:
      found_key = "unknown"
   else:
      found_key = found_key[0]

   # present data
   print "Packet len: {} speed: {} OOK decoded: {} key: {}".format(len(bin_str) / 8, baud / speed, decoded, found_key)
   print "Original: {}".format(bin_str)
   print
Additional function that feeds analyze_ook with data is pasted below. You can use longer packet size e.g. 128 bytes with 9600 baud and it should decode 9600/4800/2400 baud packets. Data is split by zeroes, its amount determines end of packet and start of another. For higher speeds 20 zeros could mean normal data not pause. Using higher speeds, you need to change it to something bigger.
baud = 9600

def split_by_zero(data):
   # make binnary string e.g "1110001010101011011" out of data
   bin_str = ''.join(format(ord(byte), '08b') for byte in data)
   for splitted in split('0{20,}', bin_str):
      if (len(splitted) > 0):
         # make sure that we've got some zeroes at the end for regexp
         splitted+="0" * 20
         # analyze and print ook data
         rf_analyzer.analyze_ook(splitted, baud, bin_input=True)
Example output of 2400 data listening with 9600 speed looks like:
$ ./rfkitten.py /dev/cc1101_green 
Corrected frequency 433919830 Hz
Corrected bandwidth 101562.5 Hz
Corrected baudrate 9595.87097168 baud
Packet len: 54 speed: 2400 OOK decoded: 110001011111110101010010 key: doorbell_button_1
Original: 11111111111110000111111111111110000111110000000000000111110000000000000111110000000000000111111111111110000111110000000000000111111111111110000111111111111110000111111111111110000111111111111110000111111111111110000111111111111110000111111111111100000111110000000000000111111111111100000111100000000000000111111111111100001111100000000000001111111111111100001111100000000000001111100000000000001111111111111100001111100000000000000000000
You can store OOK normal or in reverse polarity, it's your call. I store it exact the way that they came into receiver, and flip during TX.
   if(0):
      doorbell = "110001011111110101010010"

      code = ""
      # expand 1:1 for 2.4kbps
      for c in doorbell:
         if c == '1':
            code = code + "1110"
         else:
            code = code + "1000"

      # split into array
      arr = findall('.{1,8}', code)

      # determine packet length + additional prefix and suffix bytes
      push_config(s, packet_len(len(arr) + 2))

      # send key two times
      for e in xrange(0, 2):
         # prefix
         s.write(chr(0xFF))
         for z in arr: 
            byte = int(z,2)
            # invert data
            byte_inv = ~byte & 0xFF
            s.write(chr(byte_inv))
         # suffix
         s.write(chr(0xFF))
Using python for this task and moving analyze_ook to separate file has aditional benefit, you can Decode OOK with rtl_fm and python script only.

No comments:

Post a Comment