Ajouter un décodeur ASK à rtl_433

L'objectif de ce post est de modifier étape par étape le logiciel rtl_433 pour lui faire décoder (avec une clé rtl-sdr) un signal radio émit par un transmetteur ASK 433mhz lowcost (ou encore) connecté a un Arduino et contrôlé par la bibliothèque radiohead.

Note: le décodeur issu de ce post a été intégré a rtl_433, voir le PR associé. L'idée est donc de voir la méthode : comment ajouter un décodeur à rtl_433 ?

Montage de test

La première étape est de brancher un Arduino et un transmetteur radio ASK. Le montage n'est pas vraiment complexe :

Cablage ultra simple de l'arduino et du module ASK

À noter, pour fabriquer une antenne 433Mhz peu encombrante : 27cm de fil rigide enroulé sur une mèche de 9 (merci @guerby !).

Ensuite il suffit de brancher l'Arduino à un PC et de le programmer pour envoyer un signal de test régulièrement :

#include <RH_ASK.h>

RH_ASK driver(2000, 11, A0, 5);

void setup(){
    Serial.begin(115200);
    if (!driver.init()){
         Serial.println("init failed");
    }
}

const uint8_t len = 1;
uint8_t msg[len] = {0};

void loop(){
    driver.send(msg, len);
    driver.waitPacketSent();
    Serial.println("sent !");

    msg[len-1]++;
    delay(1000);
}

Enfin on branche une clé RTL-SDR à côté. Voilà ce que ça donne avec un adaptateur SMA et une antenne 433Mhz du commerce (ça marche aussi avec l'antenne livrée avec la clé) :

Montage de test IRL, avec la clé RTL-SDR et le PC

Enregistrement du signal

Une fois le setup hardware en place, on peut enregistrer avec rtl_433 les trames reçues par la clé RTLSDR :

$ rtl_433 -a -t

Ça va créer tout un tas de fichiers en gfileXXX.data.

On peut tester le décodage de chaque fichier pour être sûr que l'on a bien une trame non reconnue qui vient, à priori, de notre montage :

$ rtl_433 -r gfile001.data

Une fois que l'on a quelques trames d'enregistrées, on peut démonter et ranger toute l'installation hardware ! La suite va se faire à partir des fichiers .data (sauf à la fin, si on veut tester le décodeur en "live").

Ces fichiers de données brutes .data peuvent s'ouvrir dans Audacity. Pour ça : Fichier > Importer > Données brutes (raw). Et ensuite, indiquer les réglages suivants :
- 8 bit PCM
- pas de "boudisme"
- 2 canaux Stéréo
- fréquence d'échantillonnage 250000Hz (juste pour avoir les bonnes valeurs temporelles)

On doit avoir quelque chose comme ça :

Import dans audacity

Ajout d'un décodeur, 1re étape : démodulation

Maintenant que le setup hardware est en place, et que nous avons pu capturer quelques données brutes, nous allons voir comment décoder tout ça avec rtl_433. La première étape est d'ajouter une "device" spécifique, en indiquant le bon démodulateur/décodeur.

Pour ajouter un appareil à rtl_433 la procédure est simple :

  • créer un fichier radiohead_ask.c dans src/devices,
  • déclarer le nouveau décodeur dans include/rtl_433_devices.h,
  • ajouter ce fichier dans src/CMakeLists.txt et Makefile.am.

Le fichier src/devices/radiohead_ask.c va contenir principalement deux choses : la fonction de décodage elle même (nous y reviendrons), et une structure de type r_device configurant l'appareil et indiquant, entre autres, la modulation (et donc le démodulateur à utiliser) et un 1er niveau d'encodage.

On sait déjà que notre petit transmetteur fait du ASK/OOK, c'est-à-dire que le signal numérique est encodé par l'absence ou la présence de la porteuse. Dans le cas d'un reverse engineering, il est assez facile de reconnaitre ce type de modulation avec Audacity.

Il reste ensuite à savoir comment sont encodés les 0 et les 1. Nous allons voir que, comme souvent, plusieurs "couches" d'encodages sont empilées. Ici le premier encodage est très simple: chaque bit a une longueur fixe, un 1 est codé par la présence de la porteuse, un 0 son absence. La description de l'encodage est disponible dans la documentation de Radiohead. On peut vérifier en regardant la gueule du signal que l'on a bien les 36 bits de préambule en 0,1,... (la seule difficulté ici est que l'on commence par un 0) :

Les 36 bits de préambule

On peut aussi utiliser rtl_433 pour voir cette succession de pulse et de gap au début, cela permet aussi de mesurer leurs longueurs (en nombre d'échantillons):

$ rtl_433 -D -D -r gfile001.data
... ... ...
Test mode active. Reading samples from file: gfile001.data
Input format: uint8
Pulse data: 1 pulses
[  0] Pulse: 1087, Gap: 10871, Period: 11958
Pulse data: 68 pulses
[  0] Pulse:  133, Gap:  119, Period:  252
[  1] Pulse:  134, Gap:  117, Period:  251
[  2] Pulse:  134, Gap:  117, Period:  251
[  3] Pulse:  136, Gap:  117, Period:  253
[  4] Pulse:  136, Gap:  116, Period:  252
[  5] Pulse:  135, Gap:  117, Period:  252
[  6] Pulse:  136, Gap:  117, Period:  253
[  7] Pulse:  133, Gap:  119, Period:  252
[  8] Pulse:  134, Gap:  117, Period:  251
[  9] Pulse:  137, Gap:  117, Period:  254
[ 10] Pulse:  133, Gap:  119, Period:  252
[ 11] Pulse:  133, Gap:  119, Period:  252
[ 12] Pulse:  134, Gap:  116, Period:  250
[ 13] Pulse:  137, Gap:  116, Period:  253
[ 14] Pulse:  136, Gap:  116, Period:  252
[ 15] Pulse:  135, Gap:  117, Period:  252
[ 16] Pulse:  136, Gap:  118, Period:  254
[ 17] Pulse:  133, Gap:  369, Period:  502
... ... ...

Il nous faut donc un démodulateur OOK avec un 1er décodeur qui sache reconnaitre cet encodage. La liste des ceux disponibles dans rtl_433 est visible dans include/rtl_433.h :

/* Supported modulation types */
#define OOK_PULSE_MANCHESTER_ZEROBIT    3   // Manchester encoding. Hardcoded zerobit. Rising Edge = 0, Falling edge = 1
#define OOK_PULSE_PCM_RZ        4           // Pulse Code Modulation with Return-to-Zero encoding, Pulse = 0, No pulse = 1
#define OOK_PULSE_PPM_RAW       5           // Pulse Position Modulation. No startbit removal. Short gap = 0, Long = 1
#define OOK_PULSE_PWM_PRECISE   6           // Pulse Width Modulation with precise timing parameters
#define OOK_PULSE_PWM_RAW       7           // Pulse Width Modulation. Short pulses = 1, Long = 0
#define OOK_PULSE_PWM_TERNARY   8           // Pulse Width Modulation with three widths: Sync, 0, 1. Sync determined by argument
#define OOK_PULSE_CLOCK_BITS    9           // Level shift within the clock cycle.
#define OOK_PULSE_PWM_OSV1      10          // Pulse Width Modulation. Oregon Scientific v1

#define FSK_DEMOD_MIN_VAL       16          // Dummy. FSK demodulation must start at this value
#define FSK_PULSE_PCM           16          // FSK, Pulse Code Modulation
#define FSK_PULSE_PWM_RAW       17          // FSK, Pulse Width Modulation. Short pulses = 1, Long = 0
#define FSK_PULSE_MANCHESTER_ZEROBIT 18     // FSK, Manchester encoding

(Et, pour info, les fonctions de démodulation elles-mêmes sont définies dans src/pulse_demod.c.)

Le démodulateur qui convient ici est OOK_PULSE_PCM_RZ (on est en effet dans un cas trivial de Pulse Code Modulation).

Pour démarrer, voici une "device" (contenu du fichier radiohead_ask.c) qui affiche juste le bitbuffer construit avec une démodulation OOK_PULSE_PCM_RZ :

#include "rtl_433.h"
#include "pulse_demod.h"

static int radiohead_ask_callback(bitbuffer_t *bitbuffer) {
    bitbuffer_print(bitbuffer);

    return 0;
}

r_device radiohead_ask = {
    .name           = "Radiohead ASK",
    .modulation     = OOK_PULSE_PCM_RZ,
    .short_limit    = 133*4,
    .long_limit     = 133*4,
    .reset_limit    = 8000*4,
    .json_callback  = &radiohead_ask_callback,
};

Les *_limit sont indiquées en microsecondes. Comme on utilise un échantillonnage a 250MHz, il faut multiplier par 4 le nombre d'échantillons pour avoir, en microsecondes, les valeurs lues plus haut.

Détection de la séquence de start

La documentation de Radiohead indique que après les 36 bits de 01 on doit trouver une séquence de start de 12 bits valant : 0xb38 (donc 1011 0011 1000 en binaire). Pour retrouver cela, il faut bien noter que les bits de poids faible sont envoyés en premier. La représentation est en quelque sorte inversée :

Les 12 bits d'init 0xb38, avec les bits de poids faible en tête

On remarquera que ce 0xb38 n'est pas complètement arbitraire. Il génère une suite de trois pulse / gap de plus en plus fins, cela rend cette séquence facilement repérable à l'oeil.

On a vu que rtl_433 gère bien la démodulation et nous retourne la suite de bits. Voyons maintenant comment détecter cette séquence de start. Pour cela il suffit d'utiliser la méthode bitbuffer_search qui cherche bit par bit si une séquence donnée est présente dans un bitbuffer et retourne la position de celle-ci (si trouvée) :

static int radiohead_ask_callback(bitbuffer_t *bitbuffer) {
    // Looking for preamble and init
    uint8_t init_pattern[] = {
      0x55, // 8
      0x55, // 16
      0x55, // 24
      0x51, // 32
      0xcd, // 40
    };
    // The first 0 is ignored by the decoder, so we look only for 28 bits of "01"
    // and not 32. Also "0x1CD" is 0xb38 (RH_ASK_START_SYMBOL) with LSBit first.
    uint8_t init_pattern_len = 40;

    pos = bitbuffer_search(bitbuffer, row, 0, init_pattern, init_pattern_len);
    if(pos == len){
        if(debug_output)
            printf("RH ASK preamble not found\n");
        return 0;
    }

    bitbuffer_print(bitbuffer);

    return 0;
}

À noter que notre init_pattern contient qu'une partie des 36 bits de préambule, les quatre premiers 01 sont ignorés. En effet le premier 0 (absence de porteuse) n'est pas détecté par le démodulateur, le bitbuffer va donc commencer par 101 et non 0101. Pour faire simple, on ignore simplement ces quatre premiers bits. Notez que l'on pourrait tout autant ignorer complètement la détection du préambule (c'est ce qui est fait dans la partie réception ASK de RadioHead).

Enfin l'init 0xb38 se transforme en 0x1cd si on inverse chaque bit (LSB first).

4to6, une seconde (et dernière) couche de décodage

Il faut ensuite décoder le reste du message. RadioHead utilise la technique dite "4 to 6 bits". C'est à dire chaque groupe de 4 bits est encodé sur 6 bits. Un octet est donc codé en 12 bits. Tout l'intérêt de cette technique repose sur le choix des 16 valeurs possible des 6 bits, cela afin de limiter le nombre maximal possible de 0 ou de 1 consécutifs. En effet à chaque changement d'état (0 vers 1 ou l'inverse) le décodeur peut "remettre à zéro" son horloge pour éviter d'accumuler des erreurs, mais à l'inverse un nombre important de 0 ou de 1 peut amener à une désynchronisation de l'horloge (par ajout de petites erreurs). Notons que c'est pour cette même raison que des encodages dit "Manchester" sont souvent utilisé.

On trouve la table suivante dans le code de RadioHead:

// 4 bit to 6 bit symbol converter table
// Used to convert the high and low nybbles of the transmitted data
// into 6 bit symbols for transmission. Each 6-bit symbol has 3 1s and 3 0s 
// with at most 3 consecutive identical bits
static uint8_t symbols[] =
{
    0xd,  0xe,  0x13, 0x15, 0x16, 0x19, 0x1a, 0x1c, 
    0x23, 0x25, 0x26, 0x29, 0x2a, 0x2c, 0x32, 0x34
};

Ce qui donne un peu remit en forme :

4 6 (hex) 6 (bin)
0x0 0x0D 001101
0x1 0x0E 001110
0x2 0x13 010011
0x3 0x15 010101
0x4 0x16 010110
0x5 0x19 011001
0x6 0x1A 011010
0x7 0x1C 011100
0x8 0x23 100011
0x9 0x25 100101
0xA 0x26 100110
0xB 0x29 101001
0xC 0x2A 101010
0xD 0x2C 101100
0xE 0x32 110010
0xF 0x34 110100

On voit bien qu'il n'y a jamais plus de trois 0 ou 1 consécutif. Dans le pire des cas l'on aura quatre 0 ou 1 à la suite, si par exemple on a 0x2E ou 0x71.

Pour faire la conversion des 6 bits transmis vers les 4 bits du message lui-même on utilise simplement la fonction symbol_6to4 codée dans RadioHead :

// Convert a 6 bit encoded symbol into its 4 bit decoded equivalent
uint8_t symbol_6to4(uint8_t symbol)
{
    uint8_t i;
    // Linear search :-( Could have a 64 byte reverse lookup table?
    // There is a little speedup here courtesy Ralph Doncaster:
    // The shortcut works because bit 5 of the symbol is 1 for the last 8
    // symbols, and it is 0 for the first 8.
    // So we only have to search half the table
    for (i = (symbol>>2) & 8; i < 16 ; i++){
        if (symbol == symbols[i]) return i;
    }
    return 0xFF; // Not found
}

Notez au passage la petite astuce pour limiter la recherche :).

Ensuite le décodage se fait simplement en lisant les bits 12 par 12 avec la fonction bitbuffer_extract_bytes implémentée dans rtl_433 :

    // read "bytes" of 12 bit
    nb_bytes=0;
    pos += init_pattern_len;
    for(; pos < len && nb_bytes < msg_len; pos += 12){
        bitbuffer_extract_bytes(bitbuffer, row, pos, rxBits, /*len=*/16);
        // ^ we should read 16 bits and not 12, elsewhere last 4bits are ignored
        rxBits[0] = reverse8(rxBits[0]);
        rxBits[1] = reverse8(rxBits[1]);
        rxBits[1] = ((rxBits[1] & 0x0F)<<2) + (rxBits[0]>>6);
        rxBits[0] &= 0x3F;
        uint8_t hi_nibble = symbol_6to4(rxBits[0]);
        if(hi_nibble > 0xF){
            if(debug_output){
                fprintf(stdout, "Error on 6to4 decoding high nibble: %X\n", rxBits[0]);
            }
            return 0;
        }
        uint8_t lo_nibble = symbol_6to4(rxBits[1]);
        if(lo_nibble > 0xF){
            if(debug_output){
                fprintf(stdout, "Error on 6to4 decoding low nibble: %X\n", rxBits[1]);
            }
            return 0;
        }
        uint8_t byte =  hi_nibble<<4 | lo_nibble;
        payload[nb_bytes] = byte;
        if(nb_bytes == 0){
            msg_len = byte;
        }
        nb_bytes++;
    }

    // Get header
    data_len = msg_len - RH_ASK_HEADER_LEN - 3;
    header_to = payload[1];
    header_from = payload[2];
    header_id = payload[3];
    header_flags = payload[4];

La seule difficulté est le jeu d'opérations pour "retourner" les 12 bits pour passer de la représentation "LSB first" à une représentation standard. On note ensuite que l'on récupère les octets d'entête ajouté au packet par RadioHead (en plus de la taille du packet).

Vérification du CRC

La dernière étape consiste a vérifier que le checksum (les deux derniers octets) reçu est bon. RadioHead utilise ici un CRC-16-CCITT (voir aussi l'article wikipedia anglais). rtl_433 implémente déjà la plupart des checksum classiques, c'est dans util.c. il suffit donc d'appeler la fonction crc16 avec les paramètres du CRC "CCITT" (poly: 0x8408, init: 0xFFFF) :

    // Check CRC
    crc = payload[5 + data_len] + (payload[5 + data_len + 1]<<8);
    crc_recompute = ~crc16(payload, msg_len-2, 0x8408, 0xFFFF);
    if(crc_recompute != crc){
        if(debug_output){
            fprintf(stdout, "CRC error: %04X != %04X\n", crc_recompute, crc);
        }
        return 0;
    }

Il faut juste remarquer que radiohead utilise le complément de ce CRC.

Intégration actuelle dans rtl_433

Comme annoncé au début, cette modification a été intégrée dans rtl_433. On peut voir l'implémentation complète de la fonction de décodage dans devices/radiohead_ask.c.

rtl_433 va donc détecter out of the box les trames envoyées par un setup transmetteur AFK/arduino/RadioHead, à condition que le bitrate (RH_ASK_SPEED) soit le même ! Si ce n'est pas le cas il fait le changer dans devices/radiohead_ask.c et recompiler rtl_433.

Pour tester :

$ rtl_433 -R 66
Registering protocol "Radiohead ASK"
Found 1 device(s):
  0:  Realtek, RTL2838UHIDIR, SN: 00000001

Using device 0: Generic RTL2832U OEM
Found Rafael Micro R820T tuner
Exact sample rate is: 250000.000414 Hz
Sample rate set to 250000.
Bit detection level set to 8000.
Tuner gain set to Auto.
Reading samples in async mode...
Tuned to 433920000 Hz.
2016-09-13 00:19:53 :   RadioHead ASK
    Data len:    1
    To:  43
    From:    255
    Id:  0
    Flags:   0
    Payload:     [1]
2016-09-13 00:19:54 :   RadioHead ASK
    Data len:    1
    To:  43
    From:    255
    Id:  0
    Flags:   0
    Payload:     [2]
2016-09-13 00:19:55 :   RadioHead ASK
    Data len:    1
    To:  43
    From:    255
    Id:  0
    Flags:   0
    Payload:     [3]