Travel Box inspiration #2

Hello, vous voici dans la partie 2 du projet “Travel Inspiration”, si vous n’avez pas vu la partie 1, c’est par ici. Pour les autres on continue!

Récupérer les data des vols

Avant de commencer le projet et surtout d’évaluer la faisabilité de ce dernier, il me faut de la data: récupérer l’activité aéronautique au dessus de ma tête!

Pour cela deux possibilités se sont offertes à moi: soit exploiter une API existante, soit fabriquer un récepteur pour récupérer en direct les signaux émis par les avions.

Utiliser des APIs

A ma grande surprise, j’ai pu voir qu’il existait plusieurs API existantes sur le marché, mais qu’il n’en existait surtout aucune de gratuite pour récupérer ce que je voulais! J’ai donc cherché une autre solution.

Fabriquer un récepteur ADS-B

Je me souviens avoir assisté à une conf que j’avais trouvée un peu magique (lien ici), où un mec était venu parler des signaux ADS-B qu’émettent les avions pour communiquer entre eux et avec la tour de contrôle.

J’avais été surpris de voir que ces communications n’étaient pas chiffrées et qu’elles étaient accessibles pour n’importe quel bricoleur.
Inconvénient de cela: il faut du matos pour récupérer ces signaux et le traiter (donc au moins un truc genre raspberry pi).

Cette solution bien que me permettant d’être autonome aurait fait flamber mon cout et surtout aurait fait perdre le coté autonome en énergie de mon boitier.

Bidouiller ma propre API

Dernière solution qui s’offre à moi, c’est de fabriquer moi même ma propre API tout en scrappant des sites type flightradar24.
Bien que la solution ne m’enchante guère, c’est là dessus que je décide de partir, ce sera la meilleure solution pour moi pour avoir le contrôle total sur le formatage des données.

Je commence à regarder du coté des requêtes XHR qui sont émises par le site et je m’aperçois vite qu’il y a deux endpoints qui semblent correspondre à ce que je veux: le premier pour récupérer les avions dans un périmètre donné.

https://data-live.flightradar24.com/zones/fcgi/feed.js?bounds=45.97,45.65,4.37,5.97&faa=1&satellite=1&mlat=1&flarm=1&adsb=1&gnd=1&air=1&vehicles=1&estimated=1&maxage=14400&gliders=1&stats=1

Je reçois en réponse tous les avions visibles sur la carte que je consulte. Pour chaque vol, j’ai accès à des informations de base telle que leur position, origine, destination, numéro de vol, altitude et vitesse.

La seconde requête qui m’intéresse est quand je clic sur un avion de la map en vue d’afficher les détails de vol:

https://data-live.flightradar24.com/clickhandler/?version=1.5&flight=206a0199

Là c’est plutôt intéréssant, je reçois pour le vol, toutes les infos de l’avion (modèle, marque, photo), la compagnie qui l’exploite. On trouve plein d’autres infos, je vous laisse explorer, c’est une vraie mine d’infos.

Je teste vite fait ces requêtes à la main dans mon navigateur et ça semble passer (pas de restrictions niveau IP, hostname, pas de headers à spécifier): bref du pain béni!

En regardant de plus près à la première requête postée plus haut, elle contient ça: bounds=45.97,45.65,4.37,5.97 . Pas besoin d’avoir fait science-po (et ça ne vous servirait de toute façon à rien pour ce tuto) pour comprendre qu’il s’agit de coordonnées GPS. Bounds voulant dire “borne”, il s’agit en réalité d’une zone rectangulaire définie par deux coordonnées GPS.

Ce dont j’ai besoin est de définir une zone de surveillance près de chez moi et donc de récupérer le trafic à l’intérieur de cette zone. Pour définir une zone, ça fonctionne simplement avec 2 points GPS pour définir un rectangle. Le point 1, supérieur gauche, et le point 2: inférieur droit.

Voici une une image qui vaudra mieux que 1000 mots!

Il existe plein d’applis sur le web qui permettent de récupérer les coordonnées GPS d’un point. J’en utilise donc une pour récupérer mon point 1 et mon point 2.

https://data-live.flightradar24.com/zones/fcgi/feed.js?bounds=LATITUDE_POINT_1,LATITUDE_POINT_2,LONGITUDE_POINT_1,LONGITUDE_POINT_2&faa=1&satellite=1&mlat=1&flarm=1&adsb=1&gnd=1&air=1&vehicles=1&estimated=1&maxage=14400&gliders=1&stats=1

Une fois cela fait je récupère uniquement les vols dans la zone concernée. Je peux déjà obtenir à cet instant les coordonnées GPS de l’avion, sa vitesse, son altitude, son numéro de vol, mais pas de détails sur l’avion et le plan de vol.

Je peux maintenant requêter chaque vol pour obtenir encore plus d’infos: plan de vol, compagnie aérienne, modèle de l’avion, trajet…

Maintenant que j’ai toutes ces données récoltées, je calcule la distance qui me sépare de chaque avion présent dans la zone et je trie tout ça par distance. Ainsi, le premier avion qui ressortira de ma liste sera le plus proche de chez moi.

Voici un example de la réponse finale qu’on va avoir:

 [
{
latitude: 45.8212,
longitude: 5.0813,
altitude: 761.9628162145686,
speed: 249.4477,
flight: "EJU9498",
aircraft: "Airbus A320-214",
airline: "easyJet",
origin: "Marrakech",
destination: "Lyon",
distance: 3803
}
]

Coté code

Le code complet est ici:
https://github.com/xavierdeneux/flights-api mais je vais l’expliquer du mieux que je peux bloc par bloc pour ceux qui veulent en savoir plus.

On démarre l’app node avec ce morceau de code. Concrètement quand on appelera (via une requête GET) le script sur “/” il executera le code à l’intérieur.

Ensuite on dit d’écouter tout ça sur le port 3000 (on verra plus tard comment faire un truc plus sympa).

var express = require('express');
var app = express();

app.get('/', function(req, res) {
	
});

app.listen(3000, function () {
  console.log('Example app listening on port 3000!')
})


Je commence par définir deux trois constantes:

// Api flightradar24
const baseUrl = 'https://data-live.flightradar24.com';

// Coordonnées GPS
const bigBounds = '46.04,45.53,4.43,5.76'; // zone très large, utilisée pour débugguer l'app quand il n'y a pas assez de vol dans le périmètre
const homeBounds = '46e.90,45.80,4.88,5.20';
const homeLatitude = '45.8551';
const homeLongitude = '5.0747';

La constante bigBounds me sert à élargir ma zone de surveillance pour être à peu près sûr qu’il y aura des avions en réponse.

Je définis aussi la longitude et latitude de ma maison pour calculer à combien de mètres de chez moi se situent les avions. On s’en servira pour déterminer donc le plus près.

On a maintenant assez d’infos pour appeler les apis de flightradar. On va utiliser pour ça le module “axios” qui permet simplement de faire des requêtes http/https.

On commence par charger le module (après avoir fait un petit npm install -g axios)

const axios = require('axios');

On peut maintenant faire l’appel à l’api flightradar

const url = baseUrl+'/zones/fcgi/feed.js?bounds='+bounds+'&faa=1&mlat=1&flarm=1&adsb=1&gnd=1&air=1&vehicles=1&estimated=1&maxage=14400&gliders=1&stats=1';

axios.get(url).then(flightsInBoundsResponse => {
    // flightsInBoundsResponse contiendra la liste des vols dans le secteur défini.
});

On parcours la réponse de la première requête qui nous donne tous les vols du secteur. Mais aussi quelques infos qui nous intéressent pas : “full_count”, “version”, “stats”, “visible”).

const excludedFields = ['full_count','version','stats','visible'];

Maintenant on parcours chaque vol trouvé dans la zone pour aller chercher plus d’infos encore sur ce vol. J’en profite pour mettre en forme un beau json de réponse qui facilitera le traitement et l’affichage sur mon arduino.

Pour ça, on va utiliser un tableau de promesses et on va ajouter chaque requête à ce tableau.

for(let flight in flightsInBounds){
	// Si la propriété ne fait pas partie des champs exclus, c'est que c'est un avion.
	if(excludedFields.indexOf(flight) == -1){
		flightsDetail[flight] = {
			latitude : flightsInBounds[flight][1],
			longitude : flightsInBounds[flight][2],
			altitude : flightsInBounds[flight][4] / 3.281, // On convertit à la volée pieds en mètres
			speed : flightsInBounds[flight][5] * 1.60934, // On convertit à la volée miles/h en km/h
		};

		// Pour chaque vol dans la zone, on va devoir aller chercher le détail des ses infos.
		flightsPromises.push(axios.get(baseUrl+'/clickhandler/?version=1.5&flight='+flight));
	}
}

Maintenant que le tableau de promesses “flightsPromises” est plein de vols pour lesquels on doit récupérer des infos, on exécute tout ça!

// Dès que toutes les infos de tous les avions sont récupérées, on passe à la suite
Promise.all(flightsPromises).then(function(flightsDetailResponse){
    // On parcours chaque vol pour l'alimenter avec les nouvelles données reçues.
    flightsDetailResponse.forEach(flightDetailResponse => {
let data = flightDetailResponse.data;
				let flightId = data['identification']['id'];
				flightsDetail[flightId] = flightsDetail[flightId] || {};
				flightsDetail[flightId]['flight'] = data['identification']['callsign'] ? data['identification']['callsign'] : 'No callsign';
				flightsDetail[flightId]['aircraft'] = data['aircraft'] && data['aircraft']['model'] && data['aircraft']['model']['text'] ? data['aircraft']['model']['text'] : '';
				flightsDetail[flightId]['airline'] = data['airline'] && data['airline']['name'] ? data['airline']['name'] : '';
				flightsDetail[flightId]['origin'] = data['airport'] && data['airport']['origin'] && data['airport']['origin'] && data['airport']['origin']['code'] && data['airport']['origin']['code']['iata'] ? data['airport']['origin']['code']['iata'] : '';
				flightsDetail[flightId]['destination'] = data['airport'] && data['airport']['destination'] && data['airport']['destination'] && data['airport']['destination']['code'] && data['airport']['destination']['code']['iata'] ? data['airport']['destination']['code']['iata'] : '';

				// On calcule avec geolib la distance qui sépare notre point gps (homeLatitude,homeLongitude) de notre avion. Pratique pour trouver l'avion au dessus de notre tête (= le plus près)
				flightsDetail[flightId]['distance'] = geolib.getDistance({latitude: homeLatitude, longitude: homeLongitude}, {latitude: flightsDetail[flightId]['latitude'], longitude: flightsDetail[flightId]['longitude']});

				// On ne connait pas toujours l'origine et/ou la destination des vols (pour quelques vols privés uniquement)
				// On en profite pour récupérer avec "airports" le nom de la ville correspondant au code aéroport. Ex: "CDG" = "Paris", "LYS" = "Lyon"
				if(flightsDetail[flightId]['origin']){
					flightsDetail[flightId]['origin'] = airports.findWhere({ iata: flightsDetail[flightId]['origin'] }).get('city');
				}

				if(flightsDetail[flightId]['destination']){
					flightsDetail[flightId]['destination'] = airports.findWhere({ iata: flightsDetail[flightId]['destination'] }).get('city');
				}
    })
});

Bon ça fait un gros morceau de code, mais j’ai essayé de le commenter au mieux pour qu’il soit compréhensible.

On passe dans Promise.all() dès que toutes les requêtes pour aller chercher les infos des avions dans la zone sont terminées.

On parcours alors chaque détail de chaque vol reçu et on fait des traitements et on récupère:

  • Son immatriculation (identification.id)
  • Le modèle de l’avion (aircraft.model.text)
  • La compagnie (airline.name)
  • Le code aéroport international de la ville d’origine et de destination (airport.origin.code.iata /
    airport.destination.code.iata)
  • La distance qui nous sépare de l’avion (pour ça on utilise une lib “geolib” qui permet de retourner la distance entre deux coordonnées GPS).
  • Et dernière chose, les villes de départ et de destination dans un format compréhensible (et pour ça on utilise la lib airports qui nous retourne les villes internationalisées.

Dernière petite chose avant de renvoyer la réponse, on tri maintenant notre tableau de vols par distance (qu’on vient juste de calculer avec geolib): ça nous permettra d’avoir donc l’avion le plus près de nous en premier et par conséquent celui qui vient de passer au dessus de nous ou qui va passer.

let response = [];
for(let flight in flightsDetail){
	response.push(flightsDetail[flight]);
}
response = response.sort((a,b) => (a.distance > b.distance) ? 1 : ((b.distance > a.distance) ? -1 : 0));

Et c’est tout! Mais c’est déjà pas mal au final. Après libre à vous d’explorer toute la panoplie de data offertes par l’api. On pourrait imagine afficher les photos de l’avion, des infos sur la ville de destination ou de départ, la météo (ce que j’ai oublié de terminer d’ailleurs!).

Mise en place coté serveur

Pour tester l’app en local, rien de plus simple, il vous faudra:

  • Avoir une version de nodejs
  • Récupérer les sources sur github:
    https://github.com/xavierdeneux/flights-api
  • Lancer npm install pour installer toutes les dépendances
  • Et lancer “node index.js” pour faire tourner l’app.
  • Il ne vous reste maintenant plus qu’à aller sur http://localhost:3000 pour voir les résultats en json.

Pour faire tourner ça sur un serveur maintenant…

Sur mon serveur web, je place l’app NodeJS dans le dossier /www/flights et j’installe l’utilitaire “pm2” qui permet de faire tourner des app NodeJS. Je rentre dans le dossier de mon app (“flights” pour ma part) et je lance la commande:

pm2 start index.js

Voilà l’appli tourne maintenant sur le port 3000 de mon serveur. En revanche, à chaque redemarrage de serveur (ce que je vous ne souhaite pas), il faut le relancer à la main. Heureusement il existe une commande “pm2 startup” qui permet de faire en sorte de le lancer automatiquement à chaque boot.

Comme j’ai déjà un serveur Nginx qui fait tourner mes autres sites web, je crée un sous domaine (qu’on va appeler ici “flight-api.monsite.com) que je fais pointer sur mon serveur et j’ajoute le virtualhost suivant sur mon nginx, ça fait donc un reverse proxy

server {
    listen 80;
    server_name flight-api.monsite.com
    location / {
        proxy_pass   http://127.0.0.1:3000;
    }
}

Ainsi grâce à cela, quand j’accéderais à flight-api.monsite.com (sur le port par défaut 80), ça appellera sur mon serveur le port 3000 et ça chargera donc mon app.

Et voilà en gros, on a maintenant une pseudo api qui réponds avec les vols détaillés autour de nous!

Axes d’amélioration

Dans mon cas, les coordonnées GPS sont en dur dans l’app, mais on pourrait imaginer les passer en paramètre par exemple. Ça permettrait à notre arduino (via un récepteur GPS) d’envoyer sa position et de rendre le système fonctionnel un peu partout. Dans mon cas, je n’en n’avais pas l’utilité, donc j’ai préféré la mettre en dur, m’épargnant l’achat d’un récepteur et une consommation de batterie accrue.

On peut maintenant passer à partie arduino avec notre petit wemos d1 mini et à la réalisation de la boîte. Rendez-vous au prochain article!

Leave a Reply

Your email address will not be published. Required fields are marked *