Este é o quarto artigo da série, e é o hands-on prometido: o procedimento completo, do zero até a captura real de aeronaves, no servidor de homelab rodando EndeavourOS (Arch-based) com a RTL-SDR Blog V4.
Recapitulando o caminho até aqui: no primeiro artigo apresentei o projeto, a ideia de gerar meus próprios dados do mundo físico em vez de só olhar dashboards alheios, aproveitando que moro embaixo de uma das rotas aéreas mais movimentadas da América Latina, a de Guarulhos. No segundo mergulhei no protocolo: como o ADS-B funciona, a mensagem de 112 bits em 1090 MHz, a modulação PPM, e o detalhe incômodo de que tudo isso trafega sem autenticação nenhuma, aberto pra quem quiser ouvir (ou falsificar). No terceiro o hardware chegou: expliquei o que diferencia a v4 original dos clones (tuner R828D, TCXO de 1 PPM, bias tee, front-end filtrado) e documentei a instalação do driver nas duas máquinas, com o AUR resolvendo tudo em minutos no mini PC enquanto o Fedora do desktop virava uma saga de lib perdida, porta USB fraca e módulo de kernel zumbi. Agora falta o que interessa: transformar sinal de rádio em avião na tela.
1. Base já instalada (do artigo anterior)
Estes passos já haviam sido feitos e documentados no post anterior. Ficam registrados aqui pra dar o contexto completo.
1.1 Driver da RTL-SDR Blog V4
A v4 usa um chip R828D com mudanças de hardware que a librtlsdr padrão (osmocom) não trata direito, então é obrigatório usar o fork da própria RTL-SDR Blog.
yay -S rtl-sdr-blog-git
Instala a librtlsdr (fork) e os utilitários rtl_test, rtl_sdr, rtl_tcp, rtl_eeprom e rtl_biast.
1.2 Bloquear o driver de TV/DVB do kernel
O kernel Linux tenta reconhecer a RTL-SDR como uma placa de TV digital (dvb_usb_rtl28xxu) e assume o dispositivo antes que qualquer ferramenta SDR consiga abri-lo.
echo 'blacklist dvb_usb_rtl28xxu' | sudo tee /etc/modprobe.d/blacklist-rtlsdr.conf
sudo modprobe -r dvb_usb_rtl28xxu
Sem isso, rtl_test e rtl_sdr não conseguem reivindicar o dispositivo USB.
1.3 Validação inicial do hardware
rtl_test -t
Saída esperada (e obtida):
Found 1 device(s):
0: RTLSDRBlog, Blog V4, SN: 00000001
Using device 0: Generic RTL2832U OEM
Found Rafael Micro R828D tuner
RTL-SDR Blog V4 Detected
O aviso No E4000 tuner found, aborting. que aparece depois é normal: a flag -t do rtl_test é um teste de ganho específico do tuner E4000 (que não existe nesse dongle) e não indica falha real.
2. Confirmando a captura de RF em 1090 MHz
Antes de instalar qualquer decodificador, vale validar que o hardware realmente sintoniza e amostra em 1090 MHz:
rtl_sdr -f 1090000000 -s 2000000 -n 4000000 test1090.bin
-f 1090000000: sintoniza exatamente 1090 MHz (frequência do transponder Mode-S/ADS-B)-s 2000000: taxa de amostragem de 2 MS/s (padrão usado por dump1090/readsb)-n 4000000: captura 4 milhões de amostras e para sozinho
Resultado: arquivo de 8.000.000 bytes (4M amostras I/Q vezes 2 bytes) gravado sem erros de overflow. Isso confirma que o pipeline RF, USB e disco está saudável antes de envolver software de decodificação.
3. Antena: qual usar para 1090 MHz
O kit veio com 4 antenas (2 grandes, 2 pequenas) e 2 suportes (um em “V” pra 2 antenas, outro de antena única com extensão).
Para 1090 MHz: uma antena pequena, no suporte único (vertical), não no suporte em V.
Motivos:
- Polarização: ADS-B é transmitido em polarização vertical. O suporte em V é pensado pra satélites em 137 MHz (recepção de topo, ângulo variável) e não serve bem pra sinal de avião, que fica sempre perto do horizonte.
- Comprimento de onda: 1090 MHz resulta em λ ≈ 27,5 cm, ou seja, um elemento de 1/4 de onda de aproximadamente 6,9 cm. Bem mais curto que o usado em 137 MHz.
- Altura e linha de visada: ADS-B depende de visada direta; o suporte com extensão ajuda a ganhar altura e se livrar de obstruções.
A estação montada, por enquanto em versão provisória de bancada: o tripé flexível do kit agarrado no mini PC do homelab (GK3 PRO N5105), com a antena na vertical:

4. Instalando o readsb (decodificador ADS-B)
O readsb é o decodificador, sucessor do dump1090: consome as amostras I/Q brutas em 1090 MHz, encontra os preâmbulos das mensagens do transponder, decodifica e valida CRC.
4.1 Primeira tentativa (via yay, no cache padrão do AUR)
yay -S readsb-git --answerclean All --answerdiff None --removemake --noconfirm
Falhou na compilação. O Makefile do projeto usa -Werror, e o GCC do sistema (mais novo) trata como erro dois avisos que o código (sem manutenção ativa desde ~2020) não previa:
interactive.c:102: error: initializer-string ... truncates NUL terminator [-Werror=unterminated-string-initialization]
interactive.c:195: error: '%5d' directive output may be truncated [-Werror=format-truncation=]
4.2 Tentativa de patch no PKGBUILD do cache do yay
Adicionei uma função prepare() ao PKGBUILD em ~/.cache/yay/readsb-git/ removendo o -Werror do Makefile via sed. Não persistiu: o yay reseta o PKGBUILD pro estado oficial do AUR a cada execução. É uma proteção contra adulteração local, comportamento correto e esperado do yay, mas incompatível com patches manuais.
4.3 Build manual fora do yay (solução definitiva)
Copiei o PKGBUILD e os arquivos de suporte pra um diretório próprio, fora do controle do yay:
mkdir -p ~/readsb-build
cp ~/.cache/yay/readsb-git/{PKGBUILD,readsb.default,readsb.sysusers,readsb.service,readsb.tmpfiles} ~/readsb-build/
E editei o PKGBUILD adicionando um prepare():
prepare() {
cd "${srcdir}/${_gitname}"
# Upstream Makefile hardcodes -Werror; GCC novo emite avisos novos
# (unterminated-string-initialization, format-truncation) que esse
# código sem manutenção não previa, virando erro fatal de build.
sed -i 's/-Werror //' Makefile
# GCC 14+ também tornou incompatible-pointer-types/implicit-function-declaration/
# int-conversion erros por padrão em C, independente de -Werror.
# readsbrrd.c passa char** onde rrd.h espera const char** - volta a ser aviso.
sed -i '/^CFLAGS += \$(DIALECT)/ s/$/ -Wno-error=incompatible-pointer-types -Wno-error=implicit-function-declaration -Wno-error=int-conversion -Wno-error=implicit-int/' Makefile
}
Build e empacotamento:
cd ~/readsb-build
makepkg -si --noconfirm --cleanbuild
-s: resolve e instala dependências automaticamente antes de compilar (bladerf,libiio,libad9361,protobuf-c,ncurses,rrdtool,rtl-sdr, todas já satisfeitas, a última via oprovidesdortl-sdr-blog-git)-i: instala o pacote gerado ao final viapacman -U--cleanbuild: força reextração das fontes do zero (garante que oprepare()rode)
Compilou com sucesso, gerando os binários readsb e viewadsb. Se o pacman -U final pedir senha num contexto sem tty, basta rodar manualmente:
sudo pacman -U ~/readsb-build/readsb-git-4.0.4.r60.g845b65eb-1-x86_64.pkg.tar.zst
5. Permissão de acesso ao dispositivo USB
O pacote cria um usuário de sistema dedicado (readsb) pra rodar o serviço. A regra udev padrão do rtl-sdr libera o dispositivo pro grupo plugdev:
grep 2838 /usr/lib/udev/rules.d/10-rtl-sdr.rules
# SUBSYSTEMS=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2838", ..., GROUP="plugdev"
Só que o dispositivo real estava com dono root:root. O GROUP="plugdev" da regra não estava sendo aplicado: o systemd-logind dá ACL de acesso ao usuário com sessão ativa, o que mascarava o problema pro usuário interativo, mas não ajuda um usuário de serviço sem sessão como o readsb. A solução foi uma regra dedicada:
sudo tee /etc/udev/rules.d/99-readsb-rtlsdr.rules > /dev/null <<'EOF'
SUBSYSTEM=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2838", GROUP="readsb", MODE="0660"
EOF
sudo udevadm control --reload-rules
sudo udevadm trigger
udevadm control --reload-rules: recarrega as regras sem precisar reiniciarudevadm trigger: reprocessa os eventos de dispositivos já conectados (aplica a nova regra sem precisar desconectar e reconectar fisicamente o dongle)
Resultado: /dev/bus/usb/001/007 passou a crw-rw----+ root readsb.
6. Configuração do readsb (/etc/default/readsb)
sudo tee /etc/default/readsb > /dev/null <<'EOF'
USER_OPTIONS="--device 0 --lat -23.58 --lon -46.55"
EOF
--device 0: usa o primeiro (único) dongle RTL-SDR encontrado--lat/--lon: posição do receptor, usada pro cálculo de alcance e distância das aeronaves (use as suas coordenadas)- Deliberadamente sem
--net-connector feed.adsbexchange.com,...: por escolha, o feed fica só local por enquanto, sem compartilhar dados e posição com a rede ADSBExchange.
7. Corrigindo o readsb.service (flags incompatíveis com essa versão)
O readsb.service do pacote (/usr/lib/systemd/system/readsb.service) usa flags de uma versão mais nova do readsb do que a que foi compilada aqui. Em vez de editar o arquivo do pacote (seria sobrescrito em atualizações futuras), criei um override do systemd:
sudo mkdir -p /etc/systemd/system/readsb.service.d
sudo tee /etc/systemd/system/readsb.service.d/override.conf > /dev/null <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/readsb --device-type rtlsdr --gain -10 \
--ppm 0 \
--max-range 360 \
--net \
--net-heartbeat 60 \
--net-ro-size 1200 \
--net-ro-interval 0.1 \
--net-ri-port 0 \
--net-ro-port 30002 \
--net-sbs-port 30003 \
--net-bi-port 30004,30104 \
--net-bo-port 30005 \
--rx-location-accuracy 1 \
--write-output /run/readsb \
$USER_OPTIONS --quiet
EOF
sudo systemctl daemon-reload
sudo systemctl restart readsb
Correções feitas (descobertas iterativamente lendo readsb --help a cada falha nova):
| Flag original (não reconhecida) | Flag correta nesta build | Motivo |
|---|---|---|
--json-location-accuracy | --rx-location-accuracy | nome da flag mudou entre versões |
--write-json /run/readsb | --write-output /run/readsb | idem |
| (ausente) | --device-type rtlsdr | binário compilado com suporte simultâneo a RTL-SDR, BladeRF e PlutoSDR; explicitar evita ambiguidade |
O ExecStart= vazio antes do novo ExecStart=... é necessário: em overrides do systemd, isso limpa o ExecStart original do pacote antes de definir o novo, senão os dois se acumulam.
8. Habilitando e validando o serviço
sudo systemctl enable --now readsb
sudo systemctl status readsb --no-pager
Log de inicialização confirmando o hardware correto:
rtlsdr: using device #0: Generic RTL2832U OEM (RTLSDRBlog, Blog V4, SN 00000001)
Found Rafael Micro R828D tuner
RTL-SDR Blog V4 Detected
rtlsdr: enabling tuner AGC
Portas de rede abertas (confirmado via ss -tlnp): 30002 (raw out), 30003 (SBS/BaseStation out), 30004/30104 (Beast in), 30005 (Beast out).
Captura real de aeronaves
nc localhost 30003
Trecho de saída real capturada (formato SBS/BaseStation, uma linha por mensagem decodificada):
MSG,3,1,1,E491F0,1,2026/07/24,18:19:31.069,2026/07/24,18:19:31.077,,10750,,,-23.53653,-46.35493,,,0,,0,0
MSG,3,1,1,3C6706,1,2026/07/24,18:19:31.191,2026/07/24,18:19:31.240,,22125,,,-23.21251,-45.49174,,,0,,0,0
MSG,4,1,1,E491F0,1,2026/07/24,18:19:31.688,2026/07/24,18:19:31.732,,,305,251,,,960,,,,,0
MSG,3,1,1,E493D1,1,2026/07/24,18:19:33.515,2026/07/24,18:19:33.534,,,,,-23.46501,-46.07806,,,0,,0,0
Leitura rápida do formato SBS: MSG,<tipo>,...,<ICAO hex>,...,<data>,<hora>,...,<altitude>,...,<velocidade>,<proa>,<lat>,<lon>,<taxa vertical>,...
Três aeronaves distintas decodificadas nos primeiros 20 segundos:
| ICAO | Altitude | Posição aproximada |
|---|---|---|
E491F0 | ~10.775 ft | -23.53, -46.35 |
3C6706 | ~22.100 ft | -23.21, -45.49 |
E493D1 | ~28.325 ft | -23.46, -46.07 |
O pacote também traz o viewadsb, que conecta na porta Beast local e mostra a tabela de aeronaves ao vivo no terminal, com callsign, altitude, velocidade, posição e RSSI. Abaixo, a estação em operação: o viewadsb no topo, uma visão resumida ao vivo no meio e o stream SBS bruto do nc embaixo, com voos reais da TAM, Aerolíneas Argentinas e Lufthansa passando sobre a região:

Recepção de ADS-B confirmada de ponta a ponta: antena, RTL-SDR v4, readsb, mensagens decodificadas com posição, altitude e velocidade válidas.