lundi 14 octobre 2013

ADS-B et RTL SDR

En cours de rédaction...
 Un 'squitter' de 56 bits fort et clair, bien isolé, au milieu du bruit de fond.

Petite exploration dans la réception des messages qu'émettent les avions sur 1090 MHz...  Grâce à certaines clés de réception de TV et radio numérique, il est possible de capter les messages ADS-B émis par les avions.  Ces messages de 56 ou 112 bits contiennent des informations sur leur identification, leur position, leur vitesse,...  La captation de ces messages et leur mise en réseau permettent à des sites comme FlightRadar24.com d'afficher la position de la plupart des avions en vol dans les régions couvertes.

Je ne vais m'intéresser ici qu'à la réception des 'squitters' et non au décodage des messages qui semble assez complexe, étant le fruit d'une longue évolution (on le trouve déjà, par exemple, dans ce document.pdf de 1974 (p.14)).  L'encodage contient des archaïsmes comme le code Gray pour l'encodage de l'altitude, des latitude/longitude,...

Intéressons-nous donc uniquement à la réception des messages...  On est à la limite du possible avec ce type de matériel.  Pas tellement pour une question de fréquence (certaines clés peuvent monter jusqu'à 2000 MHz) mais pour une question d'échantillonnage : l'ADS-B émis par les avions à 1090 MHz a un débit binaire de 1 Mbps avec une modulation en position d'impulsions.
Ces impulsions font 0.5 µS et ces clés USB ne nous autorisent pas plus de 2 millions d'échantillons par seconde.  Ce qui fait que, soit le message est bien positionné dans le temps, on l'échantillonne aux moments propices et on a un espoir de pouvoir le décoder; soit on échantillonne au mauvais moment et on le rate complètement avec aucun espoir de le récupérer.  Le théorème de Nyquist-Shanon nous apprends, en effet, que pour bien faire, il aurait fallu échantillonner à 4 millions d'échantillons par seconde pour bien voir les impulsions de 0.5 µS.  Mais, bon...  Comme les avions sont très bavards (du genre un message par avion et pas seconde), on en capte quand même beaucoup.

Les clés comportent principalement deux composants électroniques : un 'tuner' que l'on règle sur 1090 MHz et qui descend le signal à une fréquence intermédiaire que le fameux Realtek RTL2832U, démodulateur COFDM, qui échantillonne le signal et envoie les I/Q sur le port USB.  Le driver Osmocom (git://git.osmocom.org/rtl-sdr) permet de piloter l'ensemble.  Il existe un module pour GNU Radio (gr-air-modes) mais je préfère partir de dump1090 qui est plus simple et, en même temps, plus avancé.
Diagramme de constellation du 'squitter' *5D4CA27B919917; tel qu'il sort de la clé.  La construction de la table des magnitudes (maglut) par ModesInit() dans dump1090.c nous apprend qu'il s'agit d'octets non signés (0..255 représentant -1..1).  Pour la petite histoire, ce message de type DF-11 provient d'un B737-800 de Ryanair (code AOCI 4CA27B correspondant au 'callsign' RYR39TY) qui volait à 38 000 pieds dans la région (comme nous l'apprend un message de type DF-4 avec un CRC égal à 4ca27b (!) un peu plus tard...).  Il avait une 'bonne qualité de signal' (50 points entre les '*' et les '_' du préambule).

Le but du jeu est ici d'extraire le code minimum qui permet de capter les messages ADS-B.

Dans un premier temps, on s'assure que l'on peut initialiser la clé correctement.  Le court programme suivant initialise la clé et envoie tout ce qu'on y lit dans la 'sortie standard'.  En la redirigeant dans un fichier, on obtient un fichier binaire brut qui peut servir d'entrée à 'dump1090 --ifile monfichier.bin'.  Si celui-ci parvient à récupérer des messages, on est bon.  Les routines appelées par init() on été retrouvée dans le code de dump1090 avec l'aide de l'API décrit dans rtl-sdr.h.

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <math.h>
#include "rtl-sdr.h"
  #include <string.h>
/*
** fast magnitude calculation mag = maglut[IQ]
*/
int error(char *str)
 {
 perror(str);
 return(-1);
 }
rtlsdr_dev_t *devp;
int running = 1;
int onint(int signum)
 {
 running = 0;
 }
int init(int sps, int freq)
 {
 int numgains;
 int gains[100];

 signal(SIGINT, onint);
 if (rtlsdr_open(&devp, 0) < 0)
  return(error("Can't open device!"));

 rtlsdr_set_tuner_gain_mode(devp, 1);  /* manual */

 numgains = rtlsdr_get_tuner_gains(devp, gains);
 rtlsdr_set_tuner_gain(devp, gains[numgains-1]); /* set max gain */
 fprintf(stderr, "Tuner gain = %d\n", rtlsdr_get_tuner_gain(devp));
 rtlsdr_set_freq_correction(devp, 0);  /* why not */
 rtlsdr_set_center_freq(devp, freq);
 rtlsdr_set_sample_rate(devp, sps);
 rtlsdr_reset_buffer(devp);

 return(0);
 }
#define BUFSIZE (32*1024)
int main(int argc, char *argv[])
 {
 ushort buf[BUFSIZE];
 int n_read=0;
 int len;
 int sps, freq;

  if (argc < 3)
  {
  fprintf(stderr, "rtl_grab: usage is 'rtl_grab sps freq'\n");
  exit(-1);
  }
 sps = atoi(argv[1]);
 freq = atoi(argv[2]);
 if (sps > 2024000) /* 2Msps */
  {
  fprintf(stderr, "sps too high\nrtl_grab: usage is 'rtl_grab sps freq'\n");
  exit(-1);
  }
 if (freq < 27000000 || freq > 2000000000)
  {
  fprintf(stderr, "freq too low or too hig\nrtl_grab: usage is 'rtl_grab sps freq'\n");
  exit(-1);
  }

 fprintf(stderr, "rtl_grab : %d samples/sec -- freq = %d Hz (-C to stop)\n", sps, freq);
 signal(SIGINT, onint);
 if (isatty(1))
  {
  fprintf(stderr, "Output should be redirected.\n");
  exit(-1);
  }
 if (init(sps, freq) < 0)
  exit(-1);

 while (running  && (len = rtlsdr_read_sync(devp, buf, sizeof(buf), &n_read)) >= 0)
  {
  if (write(1, buf, n_read) != n_read)
   {
   perror("Can't write.\n");
   break;
   }
  }
 rtlsdr_close(devp);
 return(0);
 }


Si je le laisse tourner pendant environ 8 secondes, ça me donne un fichier de 32 MB (2 octets * 2 millions d'échantillons par seconde * 8 secondes) que je peux utiliser avec '$ ./dump1090 --stats --ifile iq-130926.120740.bin', ce qui me donne :


16240 valid Mode-S preambles

26 DF-?? fields corrected for length
10 DF-?? fields corrected for type
219 demodulated with 0 errors
2 demodulated with 1 error
0 demodulated with 2 errors
0 demodulated with > 2 errors
221 with good crc
0 with bad crc
0 errors corrected
   0 with 1 bit error
   0 with 2 bit errors
221 total usable messages


Donc, jusque là, c'est bon, la routine init() fonctionne et rtlsdr_read_sync() me donne bien des données binaires brutes sur lesquelles je peux 'travailler'.

Le pas suivant, c'est de détecter le 'préambule', un 'pattern' '*_*____*_*' suivi de '___' qui précède un message ('*' = grande amplitude; '_' faible amplitude).  L'amplitude, c'est la racine carrée de la somme de I² + Q² avec les I/Q lus par  rtlsdr_read_sync().  Dans le code de dump1090, cela donne quelque chose comme :



for (p = magvec, i = 0; i < cnt;)
 { /*    *_*____*_*     0_2____7_9  */
 if ( p[0] < p[1] || p[2] < p[1]
  || p[2] < p[3]
  || p[3] > p[0] || p[4] > p[0] || p[5] > p[0] || p[6] > p[0]
  || p[7] < p[8] || p[8] > p[9]
  || p[9] < p[6])
  { /* not a valid preamble */
  p++; i++;
  continue;
  }
 h=(p[0]+p[2]+p[7]+p[9])/4;
 if (p[4] >= h || p[5] >= h)
  { /* not a valid preamble */
  p++; i++;
  continue;
  }
 if (p[11] >= h || p[12] >= h || p[13] >= h || p[14] >= h)
  { /* not a valid preamble */
  p++; i++;
  continue;
  }
 sstrength = p[0]+p[2]+p[7]+p[9] - (p[1]+p[3]+p[6]+p[8]);
 good_preamble_t0 += 1;


Mais, on remarquera que toutes ces précautions sont loin d'être suffisantes pour s'assurer qu'il s'agit bien d'un message ADS-B valide : en 8 secondes, dump1090 trouve 16240 préambules alors qu'il ne décode correctement que 221 messages valides (1/73!).  En fait, même dans un signal complètement aléatoire, on détecte une masse de préambules.

On va maintenant s'assurer que l'on reçoit 56 (ou 112) impulsions codées PPM (haut-bas ou bas-haut mais pas bas-bas ni haut-haut).  Les échantillons qui se suivent doivent être différents; si le premier est plus grand que le second, c'est un '1'; si le second est plus grand que le premier, on a un '0'; s'ils sont égaux, on a un problème...  À noter que l'égalité dépend probablement surtout du gain que l'on applique au signal : trop faible, on va avoir des zéro; trop fort des 'maximums' (sqrt(FF²+FF²).  C'est aussi possible pour les valeurs intermédiaires mais c'est alors la faute à 'pas de chance' (qui, sur 8 bits, n'est pas vraiment négligeable).  Dump1090 se lance dans certains paris pour quand même récupérer certains messages mais cela me paraît hasardeux (il n'en reste pas moins que son truc fonctionne et le mien, pas encore...)

On pourrait croire qu'il suffit maintenant de prendre 56 paires de magnitudes et de s'assurer que le CRC est nul mais il n'en est rien.  Il faut tenir compte du type de message DFxx, non seulement pour voir s'il ne s'agit pas d'un message de 112 bits, mais aussi parce que la valeur du CRC varie avec le type de message (!) (comme on peut le voir dans la routine decodeModesMessage() de mode_s.c; ce serait évidemment mieux de trouver les spécifications des messages...).


Le 'squitter' *5D4CA27B919917; en magnitude

for (byte = 0, err = 0; byte < 7; byte++)
 {
 x = 0;
 for (bit = 0; bit < 8; bit++)
  {
  x <<= 1;
  a = *mp++; b = *mp++;
  if (a > b)
   x |= 1;
  }
 msg[byte] = x;
 }
...À suivre...