O dump1090 disponibiliza por meio de página web local um mapa contendo as posições capturadas de aeronaves, porém essa ferramenta é bastante simples se comparada à outros sistemas de rastreamento, como por exemplo o FlightRadar24.
Pensando nisso, com o intuito de construir um sistema personalizado de coordenadas aéreas com Python, podemos configurar o dump1090 de modo que além de fornecer dados ao FlightRadar, ele também forneça dados para nosso novo sistema.
Dessa maneira, esse artigo depende de uma infraestrutura de captura de pacotes ADS-B para alimentar nosso sistema customizado.
Em síntese, trata-se de um projeto que utiliza um Raspberry Pi (ou PC) com um dispositivo RTL-SDR (ou Dongle DVB-T) configurados de tal forma que consigam sintonizar a frequência de transmissão dos dados de voo dos aviões.
Este artigo “ADS-B e Raspberry para rastreamento de aeronaves” descreve o passo a passo completo para a construção deste projeto. Portanto, você terá todas as informações sobre a teoria, hardware e software necessários para realizar o rastreio de aviões.
No decorrer deste artigo construiremos um sistema com Flask que, além de capturar os dados do dump1090, criará um mapa interativo (com Leaflet.js) capaz de atualizar-se automaticamente a cada movimentação das aeronaves.
Compartilhando dados de captura do dump1090
Com o propósito de facilitar o fornecimento dos dados aeroespaciais coletados, o dump1090 provê algumas opções de acesso a esses dados.
A maneira tradicional é o modo interativo, onde ele apenas imprime em tela as informações atualizadas. No entanto, para efeito de aquisição de dados, esse método por si só é ineficiente. Sendo assim, para utilizá-lo, precisamos encaminhar sua saída para um arquivo ao invés da própria tela, dessa maneira:
dump1090-mutability --interactive > dados.txt
Assim, basta lermos o arquivo de texto, extrair e organizar os dados nele contidos.
Uma maneira mais elegante porém mais trabalhosa de obter os dados é através do protocolo de texto BaseStation, através da porta 30003.
Esse método requer um trecho de código responsável por conectar-se via socket com o servidor do dump1090 e extrair seus dados de modo que possam ser utilizados pelo sistema. Um exemplo em Python ficaria mais ou menos como se segue:
import socket
# Configurações do servidor Dump1090
HOST = "192.168.1.100" # IP do servidor onde o Dump1090 está rodando
PORT = 30003 # Porta padrão para saída BaseStation
def connect_to_dump1090():
try:
# Criar o socket TCP
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
print(f"Conectado ao Dump1090 em {HOST}:{PORT}")
while True:
data = sock.recv(4096).decode("utf-8", errors="ignore") # Recebe dados do servidor
if not data:
break # Se a conexão for fechada, sai do loop
# Exibir cada linha do fluxo de dados ADS-B
for line in data.strip().split("\n"):
print(line)
except KeyboardInterrupt:
print("\nConexão encerrada pelo usuário.")
except Exception as e:
print(f"Erro: {e}")
finally:
sock.close()
if __name__ == "__main__":
connect_to_dump1090()
Por fim, existe uma maneira muito simples, prática e bastante compatível com Python. O dump1090 possui uma opção destinada a gravação dos dados em formato JSON, bastando apenas carregarmos o arquivo e convertê-lo para estrutura de dicionário.
Nesse sentido, o comando abaixo cumpre com esse objetivo, bastando apenas alterar <dir> pelo diretório desejado:
dump1090-mutability --write-json <dir>
Em razão da praticidade e simplicidade, este será o método adotado para nosso sistema customizado de rastreio, conforme veremos à seguir.
Visão geral
Este projeto depende da infraestrutura construída através do artigo “ADS-B e Raspberry para rastreamento de aeronaves”, portanto o conteúdo a seguir foi elaborado pensando nesse ambiente.
No entanto, nada impede que você adapte tal infraestrutura às suas necessidades. O importante é o dump1090 rodando com um dispositivo de captura de pacotes ADS-B, não importa se em um Raspberry Pi ou PC comum.
Dito isso, nosso projeto consiste em uma aplicação Flask responsável por obter os dados gerados pelo dump1090, processá-los e disponibilizá-los de tal forma que a biblioteca de mapas Leaflet.js possa renderizar os aviões em tempo real.
Uma vez que o dump1090 instala o servidor web lighttpd para seu aplicativo de rastreio próprio, não há necessidade da instalação de um novo ou ainda usar o servidor embutido do Flask.
Assim sendo, aproveitaremos essa estrutura e apenas faremos as adaptações necessárias para o correto funcionamento.
Conforme anteriormente mencionado, nossa aplicação fará uso do arquivo JSON que o dump1090 alimenta em tempo real. Dessa maneira, utilizaremos essa abordagem para a leitura direta dos dados .
Com relação ao mapa, a biblioteca Leaflet.js se encarrega de todo o trabalho pesado, bastando criarmos uma simples página HTML contendo um pouco de código javascript para configuração, gerenciamento dos marcadores e atualização do mapa a cada cinco segundos.
Preparação do ambiente
Aqui iremos configurar a estrutura de diretórios da nossa aplicação, seu ambiente virtual e o servidor web lighttpd.
Primeiramente acesse seu servidor via ssh (ou diretamente se preferir)
sudo ssh usuario@ip_servidor
e instale as seguintes dependências:
sudo apt update
sudo apt install python3-pip python3-venv lighttpd uwsgi uwsgi-plugin-python3
Em seguida, criamos a estrutura de diretórios abaixo dentro de sua pasta de usuário
/home/usuário/flask_app/
└── templates
└── static
└── airplane.png
(Obs.: Com a finalidade de renderizar um avião como marcador no mapa, utilizaremos a imagem airplane.png. caso queira pode baixá-la aqui.)
e construímos nosso ambiente virtual com as dependências Python instaladas:
python -m venv venv
# ou python3 -m venv venv
pip install flask uwsgi
# ou pip3 install flask uwsgi
Por fim faremos um ajuste no lighttp de modo que ele possa rodar Python através do servidor de aplicativos uwsgi.
O uwsgi funciona como uma espécie de intermediário entre o servidor web e nossa aplicação, já que o lighttp por si só não tem a capacidade de interpretar a linguagem Python.
O arquivo de configuração do lighttpd geralmente fica em /etc/lighttpd/lighttpd.conf, portanto basta abrir em seu editor de texto favorito e adicionar o seguinte ao final do arquivo:
sudo nano /etc/lighttpd/lighttpd.conf
server.modules += ("mod_fastcgi")
fastcgi.server = (
"/flask" => ((
"socket" => "/tmp/flask.sock",
"check-local" => "disable"
))
)
# Configuração com intuito de servir arquivos estáticos
alias.url += ( "/static/" => "/home/usuario/flask_app/static"
Além disso, não se esqueça de alterar o usuário em “alias.url += ( “/static/” => “/home/usuario/flask_app/static”.
Por fim, reinicie o serviço do servidor web para concluir as modificações:
sudo systemctl restart lighttpd
Back-end da aplicação (Flask + dump1090)
Logo após prepararmos o ambiente, daremos início à construção do nosso back-end criando o arquivo main.py na raiz da pasta flask_app:
from flask import Flask, render_template, jsonify, url_for
import json
# Leitura do arquivo gerado pelo dump1090:
def obter_dados_avioes():
with open("/run/dump1090-mutability/aircraft.json", "r") as f:
return json.load(f)
# Extrai apenas os dados necessários
def filtrar_dados_avioes(dados: dict):
return [
{
"voo": aviao.get("flight", "Sem informação"),
"squawk": aviao.get("squawk", "Sem informação"),
"altitude": f'{aviao.get("altitude")} ft',
"velocidade": f'{aviao.get("speed")} kt',
"angulo": aviao.get("track"),
"lat": aviao["lat"],
"lng": aviao["lon"],
}
for aviao in dados["aircraft"]
if "lat" in aviao and "lon" in aviao
]
app = Flask(__name__)
@app.route("/")
def home():
# URL do ícone do avião
airplane_icon_url = url_for('static', filename='airplane.png')
return render_template('index.html', airplane_icon_url=airplane_icon_url)
# Endpoint para atualização do mapa
@app.route("/posicao")
def posicao():
dados = obter_dados_avioes()
return jsonify(filtrar_dados_avioes(dados))
# Apenas para testes via webserver interno do Flask
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)
Este código cria uma aplicação Flask com o propósito de fornecer ao frontend, em tempo real, a posição de aviões com base nos dados extraídos de aircraft.json.
Conforme podemos observar trata-se de uma aplicação bem simples, cabendo apenas esclarecer o porquê do método filtrar_dados_avioes().
Com o intuito de aumentar a eficiência e reduzir o congestionamento de mensagens, o protocolo ADS-B nem sempre envia as informações de posicionamento das aeronaves.
Portanto, haja vista a necessidade de demarcar o posicionamento aeroespacial no mapa, descartamos todas as mensagens que não possuem latitude e longitude.
Dessa maneira, quando o endpoint “/posicao” é solicitado, este fornece apenas as informações filtradas e formatadas ao front-end.
Front-end da aplicação (HTML + Leaflet.js)
O front-end, por sua vez, é composto por apenas uma página HTML que deve ser criada dentro da pasta “templates”:
<!DOCTYPE html>
<html>
<head>
<title>Mapa de Aviões</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<style>
#map { height: 100vh; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script src="https://rawgit.com/bbecquet/Leaflet.RotatedMarker/master/leaflet.rotatedMarker.js"></script>
<script>
// Inicializar o mapa
var map = L.map('map').setView([<SUA_LAT>, <SUA_LNG>], 11);
// Adicionar camada de mapa
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
// Inicializar um array para armazenar os marcadores
var mapMarkers = [];
// Definir ícone personalizado para aviões
var airplaneIcon = L.icon({
iconUrl: "{{ airplane_icon_url }}", // URL do ícone do avião
iconSize: [32, 32], // Tamanho do ícone
iconAnchor: [16, 16], // Ponto de ancoragem do ícone (metade do tamanho)
popupAnchor: [0, -16] // Ponto de ancoragem do popup
});
function atualizarPosicao() {
fetch('/posicao')
.then(response => response.json())
.then(data => {
// Remover marcadores existentes
mapMarkers.forEach(marker => marker.remove());
mapMarkers = [];
// Adicionar novos marcadores
data.forEach(aviao => {
var marker = L.marker([aviao.lat, aviao.lng], {
icon: airplaneIcon,
rotationAngle: aviao.angulo // Rotacionar o ícone com base no valor de "track"
}).addTo(map);
var popupContent = `<b>Voo:</b> ${aviao.voo}<br><b>Altitude:</b> ${aviao.altitude}<br><b>Velocidade:</b> ${aviao.velocidade}`;
marker.bindPopup(popupContent).openPopup();
mapMarkers.push(marker);
});
})
.catch(error => console.error('Erro ao obter posição:', error));
}
// Atualizar posição a cada 5 segundos
setInterval(atualizarPosicao, 5000);
</script>
</body>
</html>
Com o fim de centralizar o mapa sobre sua localização, substitua o início do script onde consta “SUA_LAT” e “SUA_LNG” pela sua latitude e longitude, respectivamente.
Considerações
Essa página limita-se a apenas solicitar dados atualizados ao nosso back-end e assim refleti-los em nosso mapa, apontando o posicionamento das aeronaves em tempo real.
Esse procedimento é executado a cada cinco segundos, no entanto você pode ajustar para o intervalo que melhor lhe agradar.
// Atualizar posição a cada 5 segundos
setInterval(atualizarPosicao, 5000);
Uma vez que a ideia é deixar a movimentação dos aviões fluida, sugiro que o valor não ultrapasse estes cinco segundos.
Já com relação a construção do mapa, boa parte do script (juntamente com seus comentários) é auto-explicativo. No entanto, a princípio, o conceito de rotação do ícone pode ser um pouco confuso e cabe explicação.
O ADS-B, além das informações de posicionamento, envia também o ângulo de direção da aeronave (track), cuja finalidade, portanto, é indicar para onde ela está se deslocando.
Sabendo que o ângulo zero aponta para o norte do mapa, podemos utilizar a imagem de um avião com seu nariz para cima e rotacioná-la de acordo com o ângulo de track.
var marker = L.marker([aviao.lat, aviao.lng], {
icon: airplaneIcon,
// Rotacionar o ícone com base no valor de "track"
rotationAngle: aviao.angulo
Dessa maneira sempre renderizamos o avião com o nariz apontado para a direção do voo e assim melhoramos a experiência do rastreamento em tempo real.
Configuração da aplicação para o uWSGI
Segundo anteriormente mencionado, o lighttpd não tem a capacidade de interpretar a linguagem python sozinho e, dessa forma, precisa de um servidor de aplicação WSGI.
É aqui que o uWSGI aparece com o intuito de interligar o servidor web com nossa aplicação Python. Nesse sentido, precisaremos adicionar na raiz do projeto um arquivo de configuração capaz de orientar o uWSGI nessa tarefa.
Assim sendo, crie um arquivo chamado uwsgi.ini com o seguinte conteúdo:
[uwsgi]
# permite que processos filhos sejam gerenciador pelo processo pai
master = true
# para indicar que deve usar python
plugin = python3
# nome do arquivo python, sem o .py
module = main
# nome da variavel que recebe o objeto flask Ex.: app = Flask(__name__)
callable = app
# define que o servidor web se comunicará com o uWSGI via socket
socket = /tmp/flask.sock
# permissões do socket
chmod-socket = 660
# número de processos para lidar com as requisição web
processes = 5
# número de threads para cada processo
threads = 2
# faz com que o uWSGI sirva a aplicação diretamente via HTTP na porta 8000.
http-socket = 0.0.0.0:8000
# Remove arquivos temporários, como o socket Unix (flask.sock), quando o uWSGI é encerrado.
vacuum = true
# Faz com que o uWSGI encerre completamente quando um sinal TERM (SIGTERM) for recebido.
die-on-term = true
Conforme podemos notar, o arquivo está bem documentado e portanto, não requer tanto esforço para sua compreensão. Além disso, em geral a configuração não foge muito dessas definições e você pode se basear nelas para outras aplicações que queira criar.
Por outro lado, caso você precise de ajustes mais finos, a documentação completa do uWSGI está disponível nesse link.
Testando a aplicação
Agora basta subir nosso servidor de aplicação e ver se tudo funciona conforme o esperado! Nesse sentido basta digitar o comando abaixo:
uwsgi --ini uwsgi.ini
Não se esqueça de ajustar o caminho do arquivo uwsgi.ini caso você não esteja na pasta raiz do projeto. Além disso, não se esqueça também que o ambiente virtual deve estar ativo antes de rodar o comando.
Se tudo ocorrer conforme o esperado, você poderá acessar a aplicação através da porta 8000 do seu Raspberry Pi.
Por fim, de maneira opcional, você pode adicionar o uWSGI como serviço no sistema operacional e poupar o trabalho de inicializá-lo manualmente a cada boot.
Dessa maneira, crie um arquivo de serviço em /etc/systemd/system/uwsgi.service contendo o seguinte:
[Unit]
Description=uWSGI instance to serve rastreio_aeronaves
After=network.target
[Service]
User=seu-usuario
Group=seu-grupo
WorkingDirectory=/caminho/para/sua/aplicacao
Environment="PATH=/caminho/para/sua/aplicacao/venv/bin"
ExecStart=/caminho/para/sua/aplicacao/venv/bin/uwsgi --ini uwsgi.ini
[Install]
WantedBy=multi-user.target
Altere apenas as definições de usuário, grupo e dos caminhos de modo que correspondam ao seu projeto. Em seguida, habilite e inicie o serviço:
sudo systemctl enable uwsgi
sudo systemctl start uwsgi
Conclusão
Apesar de simples o projeto apresentado aqui abre um leque de possibilidades tanto em relação de melhorias quanto a aprendizado de uma forma geral.
Nele fomos capazes de compreender um pouco do funcionamento do dump1090, noções de alguns recursos do Linux e também de como criar um sistema web em Python integrado com um servidor de aplicação.
Além disso vimos uma pequena parte de javascript utilizando Leaflet.js e como recarregar uma página dinamicamente com Ajax evitando dessa maneira sobrecarga desnecessária de rede.
São conceitos simples mas muito importantes que todo desenvolvedor precisa conhecer afim de realizar um trabalho mais profissional e valorizado.
Sinta-se à vontade para modificar esse projeto e até mesmo adicionar mais recursos e melhorias a ele!
Espero ter ajudado!
Até a próxima!