# Esli — Full Content for LLMs
> Blog sobre SRE, Linux, segurança, privacidade e tecnologia. Artigos, Anotações, rascunhos e qualquer outro tema ;)
> Author: Esli Silva
> URL: https://esli.blog/
> Total posts: 218
---
## RTL-SDR v4 no Linux
- URL: https://esli.blog/posts/rtl-sdr-v4/
- Date: 2026-07-14
- Series: tech
- Tags: sdr, rtl-sdr, hardware, rf
Buscar uma RTL-SDR v4 irá gerar uma chuva de dongles similares por um terço do preço ou menos. A foto é idêntica, descrição, e jura que é v4. Mas não. Só um real aviso para não buscar algo similar.
## O que faz um receptor RTL-SDR funcionar
Todo dongle dessa família tem dois chips que importam. Vale entender a função de cada um antes de comparar versões, porque é justamente neles que mora a diferença entre o caro e o barato. E pra situar onde esses chips moram, o desenho da cadeia inteira, do avião ao mapa no browser:
Linha tracejada é enlace sem fio, linha cheia é cabo. A faixa de cima é o mundo analógico do rádio, a de baixo é software rodando no Linux, e a USB é a fronteira entre os dois. Os blocos em azul mais forte são as duas coisas que ficam na sua mesa, e este artigo é inteiro sobre o primeiro deles.
O primeiro é o RTL2832U. Ele é o conversor analógico-digital e demodulador, o coração! É o componente que pega o sinal de rádio já tratado e o transforma no fluxo de bits que o computador vai processar. Esse chip é praticamente igual em todo mundo, do clone mais barato ao modelo oficial. Não é nele que está a briga.
O segundo é o tuner, o sintonizador. É ele que pega a faixa gigante do espectro e seleciona a porção que você quer ouvir, fazendo a primeira filtragem e amplificação antes de entregar pro RTL2832U. E é aqui que a história começa a divergir.
## v3, v4 e o tuner
A geração anterior, a v3, usava um tuner chamado R820T2. Bom chip, popular, presente na maioria dos clones até hoje. A v4 trocou pra um R828D, e essa troca vem acompanhada de um front-end redesenhado, com filtragem melhor logo na entrada do sinal.
Por que isso importa na prática, e não só na ficha técnica? Porque o R828D com filtragem decente lida muito melhor com o problema mais chato de receptor barato em ambiente urbano: sobrecarga. Eu moro na região metropolitana de São Paulo. O espectro aqui não é um campo tranquilo, é uma feira livre lotada. Estações FM comerciais transmitindo com potência absurda, torres de celular por todo lado, tudo isso despejando energia na antena ao mesmo tempo. Um receptor sem filtragem adequada simplesmente engasga: o sinal forte de uma rádio popular vaza pra dentro da faixa que você quer ouvir e aparece como fantasma, imagem, ruído onde não deveria ter nada. Você acha que está recebendo um avião e na verdade está recebendo o programa do meio-dia de uma FM três quilômetros adiante.
O front-end melhorado da v4 não elimina o problema, mas reduz ele a um patamar tolerável. Pra ADS-B puro, em 1090 MHz, até um clone aguenta. Pra qualquer coisa abaixo disso, num lugar com poluição de RF como aqui, a diferença aparece rápido.
## Os outros componentes
Tem mais coisa separando a v4 oficial de um clone genérico, e nenhuma aparece numa miniatura de anúncio:
O TCXO de 1 PPM. TCXO é um oscilador compensado em temperatura, e o 1 PPM indica a precisão. O oscilador é a referência de frequência do receptor, o relógio que diz "isto aqui é 1090 MHz". Clones usam um cristal comum e barato, que deriva conforme esquenta. Na prática, você sintoniza uma frequência, o dongle aquece depois de uns minutos ligado, e a frequência escorrega. Pra escuta casual, irritante. Pra decodificação automatizada rodando o dia inteiro, que é o meu plano, inaceitável.
O bias tee ativável por software. É um circuito que injeta tensão pela própria linha da antena, pra alimentar um amplificador de baixo ruído ou uma antena ativa montada lá fora, sem precisar puxar energia separada até o telhado. Some na maioria dos clones.
O upconverter de HF integrado. Permite que a v4 sintonize lá embaixo, até 500 kHz, alcançando as ondas curtas. A v3 fazia isso por um método mais limitado, e os clones em geral nem tentam. Não é o foco do meu projeto aeronáutico, mas abre a porta pra brincar com HF depois sem comprar outro hardware.
E a caixa de alumínio, que não é estética: ela dissipa calor, o que ajuda a estabilidade do tal oscilador, e blinda o circuito de interferência externa. Clone costuma vir em plástico, ou em alumínio que é só enfeite.

## Como saber se o que te ofereceram é clone
Regra prática, porque links de "v4" falsificada existem aos montes. Desconfie se o anúncio não menciona o tuner R828D, ou pior, se menciona R820T2, que é o tuner da geração antiga. Desconfie se não fala em TCXO. Desconfie se o preço é bom demais. E principalmente, compre da loja oficial, a Open Source SDR Lab, ou de revendedores reconhecidos. A RTL-SDR Blog não fabrica versão "econômica" da v4. Se está barato e diz v4, ou é clone, ou é v3 remarcada.

Um detalhe que na verdade é a melhor prova de autenticidade: a v4 de verdade exige um driver específico, o fork da própria RTL-SDR Blog, e não funciona corretamente com o driver padrão que a maioria das distros instala. Esse atrito de instalação, que enfrento e documento logo abaixo, é praticamente um selo de originalidade. Clone que finge ser v4 costuma rodar liso no driver comum, justamente porque por dentro ele é um R820T2 de sempre.
## O que veio na minha caixa
Pra registro, comprei o kit que inclui, além do dongle, um conjunto de antena dipolo. Base com cabo curto, pares de elementos telescópicos de comprimentos diferentes, cabo de extensão, tripé flexível e suporte de ventosa. É um kit pensado pra iniciante, e isso não é demérito: os elementos curtos servem pra ADS-B, os longos pra frequências mais baixas, e montado em formato de V ele recebe satélite meteorológico em 137 MHz. Não é antena definitiva pra nenhum desses usos, mas é o suficiente pra sair do zero, e trocar antena depois é parte da diversão.



## A instalação, ou: o atrito prometido
O plano original era deixar a instalação pro próximo artigo. O plano durou até a caixa chegar. Instalei nas duas máquinas na mesma madrugada, e o que aconteceu rende mais do que uma nota de rodapé, porque virou um estudo de caso involuntário de empacotamento de software em Linux. De um lado, o meu mini PC de homelab, um Intel GK3 Pro modesto rodando EndeavourOS, que hospeda meia dúzia de serviços e não impressiona ninguém em benchmark. Do outro, o desktop, um Ryzen 9 3900X de 24 threads com 64 GB de RAM rodando Fedora. Adivinhe em qual deles a instalação levou cinco minutos?
Antes dos comandos, o contexto do problema. A v4 usa o tuner R828D, e o driver `rtl-sdr` que toda distro empacota não conhece direito esse hardware. O dongle até é detectado, mas se apresenta como um R820T genérico, a sensibilidade fica péssima e as frequências saem deslocadas, porque o driver antigo ignora o upconverter interno de HF. A solução é o fork mantido pela própria RTL-SDR Blog. E tem um segundo inimigo: o kernel Linux carrega automaticamente o módulo `dvb_usb_rtl28xxu`, que reivindica o dongle para uso como receptor de TV digital e não larga mais. Todo tutorial manda fazer blacklist. Quase nenhum conta o resto da história, que eu descobri do jeito clássico.
## EndeavourOS: o mini PC fraco que resolveu tudo em três comandos
No AUR o trabalho já está feito. O pacote `rtl-sdr-blog-git` declara conflito com o `rtl-sdr` genérico, então o gerenciador remove um e instala o outro sem me perguntar nada, e ainda traz as regras udev que liberam o acesso ao dispositivo sem root.
```bash
yay -S rtl-sdr-blog-git
echo 'blacklist dvb_usb_rtl28xxu' | sudo tee /etc/modprobe.d/blacklist-rtlsdr.conf
sudo modprobe -r dvb_usb_rtl28xxu
rtl_test -t
```
Saída:
```
Found 1 device(s):
0: RTLSDRBlog, Blog V4, SN: 00000001
Found Rafael Micro R828D tuner
RTL-SDR Blog V4 Detected
```
Fim. O hardware mais fraco da casa, com o processador que a Intel vende pra caber em mini PC de menos de mil reais, teve a experiência de instalação de um produto Apple. Não porque o Arch é mágico, mas porque alguém na comunidade já tinha resolvido o conflito de pacotes e codificado a solução no PKGBUILD. É isso que o AUR é na prática: memória coletiva de atrito.
## Fedora: o desktop parrudo que virou uma saga
No Fedora não existe pacote do fork, nem nos repos, nem em COPR funcional. É compilar da fonte, o que em si não é problema. O problema é o que vem depois, em camadas.
```bash
sudo dnf remove rtl-sdr
sudo dnf install git cmake gcc libusb1-devel
git clone https://github.com/rtlsdrblog/rtl-sdr-blog
cd rtl-sdr-blog && mkdir build && cd build
cmake ../ -DINSTALL_UDEV_RULES=ON
make -j$(nproc)
sudo make install
sudo ldconfig
```
Compilou, instalou, e o `rtl_test` respondeu com:
```
rtl_test: error while loading shared libraries: librtlsdr.so.0:
cannot open shared object file: No such file or directory
```
Primeira camada: no Fedora x86_64, o cmake instala bibliotecas em `/usr/local/lib64`, e esse diretório não está no caminho do linker dinâmico por padrão. O binário existe, a lib existe, os dois simplesmente não se conhecem. A correção é uma linha, desde que você saiba qual linha:
```bash
echo '/usr/local/lib64' | sudo tee /etc/ld.so.conf.d/local-lib64.conf
sudo ldconfig
```
Lib resolvida, `rtl_test` de novo, e agora: `No supported devices found`. O dongle sumiu do barramento. `lsusb` vazio. Abri o `dmesg -w`, repluguei, e encontrei a segunda camada, a mais bonita da noite.
## A porta USB que aceita YubiKey mas rejeita rádio
O log era um festival de `error -71`, o EPROTO do subsistema USB, em loop: o dongle enumerava, falhava ao configurar, desconectava, o kernel tentava de novo, ciclava a energia da porta e por fim desistia com a mensagem mais passivo-agressiva do kernel Linux:
```
usb usb1-port11: Cannot enable. Maybe the USB cable is bad?
```
Não era o cabo. Era a porta. Aquela porta tinha passado meses hospedando uma YubiKey sem nenhum incidente, e por isso eu nem suspeitava dela. Só que YubiKey é um dispositivo full-speed, 12 Mbps, consumindo uns 30 mA. Praticamente qualquer porta com mau contato, trilha marginal ou alimentação capenga sustenta isso. O RTL-SDR v4 é outro animal: high-speed, 480 Mbps de barramento, na casa dos 300 mA de consumo. Ele expõe toda fraqueza elétrica que um dispositivo leve mascara. A porta não estava boa, estava boa o suficiente pra criptografia e insuficiente pra rádio.
A lição de troubleshooting que fica: "funciona com outro dispositivo" não valida uma porta USB. Valida a porta para aquela classe de dispositivo. Mudei o dongle pra uma porta traseira ligada diretamente ao controlador USB do próprio processador, em vez do controlador do chipset da placa, e o EPROTO evaporou. Se o seu dongle enumera e desconecta em loop, antes de culpar o hardware ou abrir issue no GitHub do driver, teste outra porta física, de preferência as traseiras coladas na placa-mãe, e esqueça hubs e as portas frontais do gabinete, que chegam ao chipset por um cabo interno de qualidade duvidosa.
## O módulo zumbi
Terceira camada. Com o dongle enumerando limpo, o `rtl_test` reclamou que o kernel driver estava ativo. Mas eu tinha feito a blacklist. Rodei `lsmod` e lá estava o `dvb_usb_rtl28xxu`, carregado, vivo, saudável.
Acontece que `blacklist` no modprobe.d impede o carregamento automático do módulo no boot, mas não impede que ele seja carregado por alias quando um evento de hotplug pede. Replugou o dongle, o udev viu um dispositivo DVB, pediu o módulo pelo alias, e o kernel entregou, blacklist ou não. A diretiva que fecha essa porta é outra:
```bash
sudo tee /etc/modprobe.d/blacklist-rtlsdr.conf << 'EOF'
blacklist dvb_usb_rtl28xxu
install dvb_usb_rtl28xxu /bin/false
EOF
```
A linha `install` substitui o comando de carga do módulo por `/bin/false`, ou seja, qualquer tentativa de carregá-lo, por qualquer via, falha silenciosamente. Depois disso, `dracut -f` pra regenerar o initramfs e blindar também o cenário de boot com o dongle já plugado. Guarde essa segunda linha: ela falta em nove de cada dez tutoriais, e é a diferença entre um sistema que funciona e um sistema que funciona até você replugar o dongle.
## E o typo que protegia contra DVDs
Justiça seja feita ao Fedora: quando voltei ao EndeavourOS pra validar tudo com o mesmo rigor, descobri que a minha blacklist de lá continha `blacklist dvd_usb_rtl28xxu`. Com D de DVD. O arquivo estava lá havia semanas, protegendo o sistema com firmeza contra um módulo que não existe, enquanto o módulo real tinha simplesmente dado a sorte de não reivindicar o dongle nas sessões em que testei. Funcionava por acaso. O tipo de bug que nenhum monitoramento pega, porque não há erro nenhum, só uma configuração fazendo nada com muita convicção.
## O placar
| | EndeavourOS (mini PC GK3 Pro) | Fedora (Ryzen 9 3900X) |
| ---------------------------- | ----------------------------- | ---------------------------------------- |
| Driver | 1 pacote do AUR | build da fonte |
| Conflito com driver genérico | resolvido pelo PKGBUILD | gestão manual, `excludepkgs` no dnf.conf |
| Path da lib | automático | armadilha do `/usr/local/lib64` |
| Regras udev | vieram no pacote | flag do cmake |
| Tempo até `rtl_test` limpo | minutos | horas, contando a arqueologia USB |
O hardware não teve voto em nada disso. O desktop tem doze núcleos ociosos esperando trabalho e apanhou de um mini PC porque a variável que importa é ecossistema de empacotamento, não silício. Pra software de nicho como SDR, o AUR é imbatível, e não é fanboyismo de Arch, é constatação: a chance de alguém já ter empacotado o fork obscuro que você precisa, com os conflitos declarados e as regras udev no lugar, é ordens de magnitude maior lá do que em qualquer COPR.
E por falar em COPR: a saga do Fedora não acabou no driver. O SDR++ não está no Flathub, apesar do que a internet sugere. O COPR do SatDump respondia 404 pra versão atual do Fedora. Resultado: os dois também foram compilados da fonte, num ciclo de `cmake`, erro de dependência, `dnf install alguma-coisa-devel`, repeat, que qualquer um que já compilou software de rádio em RPM conhece de cor. No EndeavourOS, os três estão no AUR.
## O stack completo, e pra que serve cada peça
Com o driver no lugar, o que roda em cima dele:
**rtl-sdr-blog** é a fundação, o fork do driver com a `librtlsdr` que entende o R828D, mais os utilitários de linha de comando. O `rtl_test` valida detecção e mede perda de amostras, o `rtl_tcp` expõe o dongle pela rede pra outro computador consumir o sinal, e o `rtl_biast` liga o bias tee por software quando houver um LNA lá fora pra alimentar.
**readsb** é o decodificador de ADS-B, sucessor espiritual do dump1090. Ele consome as amostras brutas em 1090 MHz, encontra os preâmbulos das mensagens dos transponders, decodifica, valida CRC e serve o resultado em várias interfaces de rede. É o coração do pipeline que alimenta o Prometheus e, mais adiante, o mapa e o bot. Roda 24/7 no mini PC, que é exatamente o tipo de carga pra qual aquele hardware existe.
**SDR++** é o receptor de propósito geral com interface gráfica, o equivalente moderno de girar o dial de um rádio, só que vendo o espectro inteiro na tela em cascata. É a ferramenta de exploração: procurar sinais, escutar repetidoras, investigar aquele pico estranho em uma frequência que você não conhece. Suporte nativo à v4 e ao protocolo do `rtl_tcp`, o que significa que posso rodar a interface no desktop consumindo o dongle plugado no servidor.
**SatDump** é o decodificador de satélites, e é o que transforma um passe do NOAA-19 em imagem meteorológica. Ele conhece os pipelines de dezenas de satélites, do APT analógico dos NOAA aos digitais mais modernos, cuida da demodulação e da composição das imagens. É a peça do plano que justifica os elementos longos da antena dipolo montados em V.
Quatro programas, uma biblioteca embaixo de todos. Se a `librtlsdr` errada estiver no caminho do linker, os quatro enxergam o dongle como um R820T antigo e degradam em silêncio, sem erro, sem aviso, só recepção pior. No Fedora isso exige atenção permanente, porque instalar qualquer coisa via dnf que dependa de `rtl-sdr` reintroduz a lib genérica em `/usr/lib64`, na frente da do fork na ordem de resolução. A vacina é uma linha no `/etc/dnf/dnf.conf`:
```
excludepkgs=rtl-sdr
```
Lá em cima eu disse que o atrito de instalação é praticamente uma prova de originalidade da v4, porque clone roda liso no driver comum. Depois desta madrugada, sustento com mais convicção ainda: o meu dongle exigiu o driver certo, expôs uma porta USB fraca que dois anos de YubiKey nunca revelaram, sobreviveu a um módulo de kernel zumbi e a um typo meu, e no fim entregou `0 samples lost` e a detecção correta do R828D nas duas máquinas.
---
## ADS-B, SDR e a comunicação
- URL: https://esli.blog/posts/adsb-sdr-radio-no-linux/
- Date: 2026-07-08
- Series: tech
- Tags: linux, sdr, radio, dongle
[No primeiro artigo](https://esli.blog/posts/sdr-radio-no-linux/) eu disse que avião é o alvo perfeito pra começar no mundo do rádio definido por software porque toda aeronave moderna grita a própria posição em texto aberto. Ficou faltando explicar como ela faz isso. E, mais interessante pra quem trabalha com segurança, faltou explicar outro detalhe: ninguém verifica se a aeronave está falando a verdade.
Vamos por partes.
## O que é ADS-B
ADS-B é a sigla de Automatic Dependent Surveillance Broadcast. Traduzindo:
Automatic, porque a aeronave transmite sozinha, sem ninguém perguntar nada.
Dependent, porque ela depende dos próprios sistemas de bordo pra saber onde está, tipicamente o GPS.
Surveillance, porque o propósito é vigilância de tráfego aéreo.
Broadcast, e essa é a palavra que importa pra gente, porque ela joga a informação pro mundo, sem destinatário específico, pra quem quiser ouvir.
O modelo mental é o de um servidor que ficasse berrando o próprio estado num canal aberto, várias vezes por segundo, sem autenticação, sem TLS, sem nada.
## O que vai pelo ar
A aeronave determina a própria posição pelo GNSS e transmite, na frequência de 1090 MHz, mensagens curtas que carregam coisas como:
O endereço ICAO de 24 bits, que é o identificador único e permanente daquela fuselagem, o equivalente a um MAC address voador. O callsign, o indicativo do voo, tipo TAM3304. A posição em latitude e longitude. A altitude, barométrica ou por GNSS. A velocidade no solo, a direção do nariz, a razão de subida ou descida.
Cada um desses campos chega em mensagens diferentes, num fluxo contínuo. As de posição saem em torno de duas por segundo. Em alguns segundos de escuta você já tem um retrato bem completo de qualquer aeronave dentro do alcance.
## O quadro no ar, pra quem veio de redes
Eu sou formado em redes de computadores, então quando li que a aeronave "transmite mensagens", a primeira pergunta foi automática: qual o tamanho do quadro, qual a taxa, o que esse protocolo tem de parecido com TCP/IP? A resposta curta é quase nada, e é justamente isso que torna ele divertido de dissecar.
O nome técnico do que a gente vai receber é Mode S Extended Squitter, ou 1090ES. Squitter é o transponder falando espontaneamente, sem ninguém interrogar. Guarde esse vocabulário, porque é ele que vai aparecer na saída do decodificador.
Cada quadro tem exatos 112 bits, 14 bytes.
Pra calibrar a régua: o menor quadro Ethernet válido tem 64 bytes, e um cabeçalho IPv6 sozinho tem 40. Desses 112 bits, 5 são o downlink format, que diz o tipo do quadro, fazendo o papel que o EtherType faz num quadro Ethernet. Depois vêm 3 bits de capability, 24 do endereço ICAO, 56 de payload e 24 de CRC. Ou seja, o payload útil tem 7 bytes. Um cabeçalho UDP vazio tem 8.
Todo o retrato de um avião a 900 km/h passa por um cano mais estreito que o overhead do protocolo mais enxuto que você usa no dia a dia.
A modulação é PPM, pulse position modulation, a 1 Mbps. Cada bit ocupa 1 microssegundo dividido em duas metades: pulso na primeira metade é 1, pulso na segunda é 0.
Antes dos dados vem um preâmbulo de 8 microssegundos pra sincronizar o receptor, mesma função do preâmbulo Ethernet. Quadro completo: 120 microssegundos de ar.
E 1 Mbps é herança de projeto. O Mode S foi desenhado no MIT Lincoln Laboratory nos anos 70, então você está literalmente recebendo tráfego numa taxa de link da era do cabo coaxial.
De transporte, o ADS-B tem a filosofia do UDP broadcast levada ao extremo: sem ACK, sem janela, sem retransmissão, sem controle de fluxo. Fire and forget. Se dois transponders transmitem ao mesmo tempo, os quadros colidem e ambos se perdem. A mitigação é estatística, no espírito do ALOHA puro: cada aeronave sorteia o intervalo entre mensagens de posição num valor aleatório entre 0,4 e 0,6 segundo, justamente pra dois transmissores não ficarem colidindo em fase. E o canal de 1090 MHz é compartilhado: além do ADS-B, passam ali as respostas dos transponders ao radar secundário e ao TCAS dos outros aviões. Em espaço aéreo denso, é um barramento coaxial dos anos 80 em horário de pico. Quem separa sinal de destroço é o CRC de 24 bits, e o decodificador que vamos usar consegue inclusive recuperar quadro com erro de 1 bit fazendo força bruta em cima do CRC.
Potência de transmissão: tipicamente 125 a 250 watts em aeronave comercial, com teto de 500. Milhares de vezes o que o seu roteador WiFi tem permissão de emitir. É isso, somado à linha de visada de quem voa a 11 mil metros, que faz um quadro de 14 bytes atravessar 300 ou 400 quilômetros e cair inteiro na antena de mesa.
Pra fechar, a cara do bicho. Um quadro real, do jeito que o dump1090 cospe no modo raw:
```
*8D4840D6202CC371C32CE0576098;
```
28 caracteres hexadecimais, 112 bits. Ali dentro tem o downlink format 17, o endereço ICAO 4840D6 e o callsign KLM1023. O mesmo quadro aberto, campo a campo:
Lendo da esquerda pra direita: o preâmbulo são quatro pulsos que funcionam como um toque de campainha, avisando o receptor de que vem mensagem. O DF diz o tipo da mensagem e o CA o que aquele transponder sabe fazer. O ICAO é a identidade única da fuselagem, aquele MAC address voador do começo do texto. O payload é a carga útil, onde viajam posição, altitude e velocidade. E o CRC é o lacre: se a conta não bate, o receptor joga a mensagem fora. Um envelope minúsculo com remetente, carga e lacre, e nada mais.
O dongle, aliás, não entrega esses bits prontos: ele entrega amostras IQ de 8 bits a 2,4 milhões por segundo, uns 4,8 MB/s escoando pela USB. Achar o preâmbulo nesse fluxo e transformar pulso em bit é trabalho de software, e é exatamente aí que a parte de Linux dessa série começa.
## O detalhe que faz a posição caber em poucos bits
Tem uma sacada de engenharia aqui: Transmitir latitude e longitude completas, com precisão decente, gastaria bits demais pra uma mensagem que precisa ser curta e frequente. A solução é um esquema chamado Compact Position Reporting.
Em vez de mandar a coordenada absoluta toda vez, a aeronave alterna entre dois tipos de quadro, chamados par e ímpar, e cada um carrega uma representação comprimida da posição. Combinando um quadro par com um ímpar, ou usando uma posição de referência que você já conhece, o receptor reconstrói a coordenada global. É compressão com estado, basicamente, e funciona porque um avião não teleporta entre uma mensagem e a próxima. A continuidade física do voo é a chave de decodificação.
## Por que morar perto do aeroporto resolve metade do problema
Sinal de 1090 MHz é linha de visada. Ele vai em linha reta e não contorna montanha, prédio nem a curvatura da Terra. Isso quer dizer que o seu alcance depende de altitude e de obstáculo. Uma aeronave a nível de cruzeiro, bem alta, pode ser captada a mais de 200 milhas náuticas com uma antena decente e horizonte limpo. Uma aeronave baixa, em aproximação, some atrás do primeiro morro.
E aqui mora a vantagem geográfica que eu mencionei. Morando embaixo do corredor de aproximação do GRU, eu recebo aeronaves baixas, próximas e com sinal forte, exatamente a faixa que seria difícil pra quem mora longe. O que normalmente é a parte chata do ADS-B, captar tráfego de baixa altitude, no meu caso cai pronto no colo. Quando eu mudar pra perto do Catarina, a brincadeira muda de figura, porque jato executivo voa em perfil diferente, mas isso é conversa pra quando a antena estiver montada.
## A parte que deveria te incomodar
Releia a descrição do protocolo e procure a palavra autenticação. Ela não está lá, porque não existe.
O ADS-B foi concebido entre os anos 90 e 2000, num mundo onde a preocupação era fazer o sistema funcionar e ser interoperável, não defendê-lo de um adversário. O resultado é que as mensagens não são assinadas nem cifradas. Qualquer um com o transmissor certo pode injetar no ar uma mensagem dizendo que existe uma aeronave numa posição onde não há nada, com um identificador inventado, e os receptores vão acreditar, porque acreditar é tudo o que eles sabem fazer.
O Spoofing de ADS-B é um problema reconhecido na segurança da aviação, estudado em papers e demonstrado em laboratório. A defesa prática hoje não vem do protocolo em si, e sim de cruzar fontes: radar primário convencional, que enxerga o metal independente do que ele diz de si mesmo, e multilateração, uma técnica que vou detalhar depois e que infere posição pelo tempo que o mesmo sinal leva pra chegar em receptores diferentes. Em outras palavras, a aviação resolveu o problema da falta de autenticação adicionando observabilidade externa, o que, convenhamos, é o que um SRE faz quando recebe um sistema legado.
Pra mim, que vou só receber e nunca transmitir, nada disso é risco. Receber broadcast aberto é perfeitamente legal e passivo. Mas conhecer o buraco de segurança do protocolo muda a forma como você lê os dados que vão aparecer no mapa. Nem todo avião no seu dashboard é necessariamente um avião. Quase sempre é. Nem sempre.
## Próximo
No artigo 3 eu desço pro hardware: por que escolhi a RTL-SDR v4 especificamente, o que diferencia ela de um clone barato, e por que essa diferença importa morando numa cidade infestada de estações FM potentes.
## Referências
- [The 1090 Megahertz Riddle](https://mode-s.org/1090mhz/), de Junzi Sun (TU Delft): livro aberto e gratuito, a melhor referência que existe sobre decodificação de Mode S e ADS-B, do preâmbulo ao CPR.
- [On the Security of the Automatic Dependent Surveillance-Broadcast Protocol](https://doi.org/10.1109/COMST.2014.2365951), de Strohmeier, Lenders e Martinovic (IEEE Communications Surveys & Tutorials): o survey acadêmico sobre a falta de autenticação do protocolo e as defesas possíveis.
- [Ghost in the Air(Traffic)](https://media.blackhat.com/bh-us-12/Briefings/Costin/BH_US_12_Costin_Ghosts_In_Air_WP.pdf), de Costin e Francillon (Black Hat USA 2012): a demonstração prática de spoofing de ADS-B que tirou o assunto do campo teórico.
- [dump1090, fork da FlightAware](https://github.com/flightaware/dump1090): o decodificador que vai aparecer no hands-on desta série.
- [pyModeS](https://github.com/junzis/pyModeS): biblioteca Python pra decodificar as mensagens na unha, do mesmo autor do livro acima.
- [FAA: ADS-B](https://www.faa.gov/air_traffic/technology/adsb): a página oficial do regulador americano sobre o programa.
- [RTL-SDR Blog: About RTL-SDR](https://www.rtl-sdr.com/about-rtl-sdr/): panorama do hardware que faz tudo isso caber num dongle USB.
---
## SDR - Radio no Linux
- URL: https://esli.blog/posts/sdr-radio-no-linux/
- Date: 2026-07-06
- Series: tech
- Tags: linux, sdr, radio, dongle
Existe uma ironia que persegue quem trabalha com infraestrutura: você passa o dia inteiro olhando dashboards. Latência de API, saturação de CPU, p99 de request, fila de mensagens. Métrica em cima de métrica, todas medindo o trabalho de outra pessoa ou de outra máquina. Em algum momento, depois do enésimo alerta de disco cheio às três da manhã, bate uma pergunta meio existencial: e se eu gerasse os meus próprios dados? Não os dados de um servidor que alguém pediu pra eu monitorar. Dados de verdade, do mundo físico, que ninguém me obrigou a coletar.
E que tal um dongle de rádio via software capturando o sinal de radiofrequência e decodificando o ADS-B no Linux?
## O contexto, ou: por que eu moro num lugar conveniente
Eu moro perto do Aeroporto Internacional de Guarulhos. "Perto" no sentido de que quando o vento muda e a aproximação inverte, eu sei antes da torre (ou de um angulo mais triste: sei a direção do vento pelo barulho dos motores). É o tipo de detalhe que normalmente entra na coluna de desvantagens de um imóvel, junto com o barulho e etc... Acontece que, pra esse projeto específico, morar embaixo de uma das rotas aéreas mais movimentadas da América Latina deixou de ser custo e virou recurso.
E tem um bônus no horizonte. Futuramente vou me mudar pra perto do Aeroporto Catarina, o terminal executivo da JHSF em São Roque. Onde o GRU é volume bruto de aviação comercial, o Catarina é outro bicho: jatos privados, Gulfstreams, Globals, gente que não enfrenta fila de raio-x.
Dois perfis de tráfego completamente diferentes, dois conjuntos de dados pra comparar. Mas isso é assunto pra lá na frente.
## O que diabos é SDR
SDR é Software Defined Radio, rádio definido por software. A ideia central é simples e meio subversiva: em vez de um circuito de hardware dedicado pra cada coisa que você quer receber (um chip pra FM, outro pra TV, outro pra aquele controle remoto), você captura o sinal bruto de radiofrequência e joga o trabalho de interpretar tudo pra cima do software. Quem decide o que aquilo significa é o código, não a placa.
Na prática isso quer dizer que um único dongle USB barato, do tamanho de um pendrive, consegue ser um receptor de FM, um scanner aeronáutico, um decodificador de sensores meteorológicos da vizinhança e um radar de aviões, dependendo só de qual programa você roda. O hardware é genérico. A mágica é software. Pra quem vive de transformar configuração em comportamento, é uma filosofia bem familiar.
A história de origem é ainda melhor. O chip que move tudo isso, o RTL2832U, foi projetado pra ser um sintonizador de TV digital USB, dessas que você espetava no notebook em 2012. Alguém descobriu que dava pra acessar o fluxo de dados cru do chip, antes dele virar imagem de TV, e a comunidade transformou um acessório descartável de TV num receptor de rádio de banda larga. O hardware nunca foi pensado pra isso. É reaproveitamento puro, e eu respeito demais qualquer coisa que funcione fora da finalidade pra qual foi vendida.
## Por que aviões e não, sei lá, qualquer outra coisa
Porque avião é o alvo perfeito pra começar.
Toda aeronave comercial moderna transmite continuamente, em 1090 MHz, um pacote de dados chamado ADS-B. Esse pacote diz quem ela é, onde está, a que altitude, em que velocidade e pra onde aponta o nariz.
Não é interceptação, não é nada clandestino: é um sistema feito justamente pra ser ouvido por todo mundo, controle de tráfego aéreo e qualquer pessoa com um receptor. O avião está, literalmente, gritando a própria posição em texto aberto, várias vezes por segundo, pra quem quiser escutar.
Junte isso ao fato de eu morar embaixo do corredor de aproximação do GRU e você tem o cenário ideal: sinal forte, abundante, constante, e legal de receber. É o "hello world" do mundo SDR, só que o hello world voa a 250 nós sobre a minha cabeça.
Recentemente, vi um projeto interessante: o usuário usou dados de voos com projetor para refletir a trajetória das aeronaves em tempo real usando o mesmo ADS-B: https://www.youtube.com/watch?v=DHxR3Knwe_I
## Onde isso vai dar
Eu não comprei um dongle de rádio pra ficar olhando aviãozinho num mapa. Quer dizer, vou olhar, mas esse não é o ponto. O plano é construir um pipeline de verdade em cima dos dados:
Primeiro, receber e decodificar o ADS-B, colocar os aviões num mapa local. Depois, tratar a estação como qualquer outro serviço que eu opero: exportar métricas pro Prometheus, montar dashboard no Grafana, medir alcance, contar aeronaves, vigiar o uptime da própria captação. Em seguida vem a parte divertida, automação: um bot que publica no Threads e no YouTube quando algo interessante aparece no céu, com cards gerados na hora. E uma transmissão ao vivo 24 horas do mapa, tratada como um serviço com SLA, porque aparentemente eu não consigo desligar o cérebro de SRE nem no hobby.
E ainda tem o gancho com um projeto que eu já mantenho, o https://climabr.app , onde dados de satélites meteorológicos NOAA recebidos pela mesma antena podem virar mais uma fonte ambiental. Tudo conectado, porque eu sou incapaz de fazer um projeto isolado.
Sei lá... só ideias mesmo... o fator de conseguir os dados já é o suficiente para me manter motivado.
## Estado atual: esperando o navio
A essa altura você provavelmente quer saber o que eu já recebi, qual avião apareceu primeiro, como ficou o mapa. Não recebi nada. O hardware é uma RTL-SDR Blog v4 que comprei direto da loja oficial no AliExpress, e o pacote está em algum lugar entre a China e Guarulhos, com previsão otimista de vinte dias. O céu vai ter que esperar.
O que isso me dá é tempo. Nos próximos artigos, enquanto o rastreamento do pedido não sai do "saiu do centro de triagem", eu vou cobrir a teoria: como o ADS-B funciona de verdade, por que escolhi essa v4 e não um clone de doze dólares, e o que separa um dongle que serve de um que vai te dar dor de cabeça. Quando a caixa chegar, a gente liga o troço e vê se algum avião aparece.
Spoiler de quem já leu a documentação: o driver vai dar trabalho. Sempre dá.
Update: Não é de navio, veio... de avião. Bem, estou no aguardo da entrega ;-)
---
## ClimaBR.app - Painel ambiental por cidade com qualidade do ar, água, clima e alertas
- URL: https://esli.blog/posts/climabr-app/
- Date: 2026-06-02
- Series: tech
- Tags: tech, dev, code
Estava buscando projetos sobre a qualidade do ar, encontrei 3 interessantes:
O painel do https://waqi.info/ que além de dados de orgãos públicos, também oferece dados de qualidade do ar de diversas fontes como o https://sensor.community e o https://www.habitatmap.org/
O https://aqicn.org/ (com sensores GAIA https://aqicn.org/gaia/list/pt/ partindo de R$271) e em https://www.aqi.in/ onde enviam um sensor com uma contribuição acima de $110.
E nisto há dois pontos diferentes: enquanto no habitatmap e aircasting.org usam dados do AirBeam (precisa adquirir o device, já pronto por $99),
Com o sensor.community é necessário comprar/instalar o hardware e configurar o firmware, DIY e opensource - via https://sensor.community/pt/sensors/airrohr/ - sinceramente, este será meu proximo projeto de fim de semana!
Adquiri 2 sensores de qualidade do ar para minha rede local (wifi, compativel com Tuya). Não possuem o mesmo objetivo que os acima.
Outro ponto: uso um app de clima, mesmo com adblock na rede (DNS com NextDNS) ele força a exibição de ads e o uso de trackers, além disto, os sites existentes estão em níveis insuportáveis.
Qual a dificuldade em ter um site de clima e outros dados ambientais sem gerar custo, ou qual o custo minimo para ter algo superior?
## O que é o ClimaBR.app
O [ClimaBR.app](https://climabr.app) é um painel ambiental por município brasileiro. Para qualquer uma das 5.571 cidades do país ele reúne, a partir de dados públicos, a previsão do tempo, a qualidade do ar, o índice UV, o vento, o nível de reservatórios, focos de queimada e o alerta de dengue, zika e chikungunya.
A premissa de projeto é simples e rígida: consolidar dado aberto, ser acessível por máquina (AI-first) e rodar com **custo zero**, inteiramente dentro do free tier dos serviços envolvidos. Tudo o que descrevo abaixo cabe nos limites gratuitos, e este texto mostra exatamente quanto de cada limite o projeto consome hoje.
Um detalhe que define a personalidade do projeto: ele responde no terminal.
https://climabr.app/curl/

Também é possível conectar via MCP https://climabr.app/.well-known/mcp.json
Além de uma pagina para paineis e outra mobile. O objetivo é seguir com extensão no navegador e tornar o ClimaBR.app uma ferramenta completa para monitoramento ambiental até onde for possível num nível gratuito e aberto.
## Arquitetura em uma imagem
```
GitHub (repositório + Actions)
├── Actions (cron) rodam scrapers em Python → JSON por município (commit no repo)
└── Actions fazem o build do Astro → deploy no Cloudflare Pages
Cloudflare (tudo no free tier)
├── Pages → site estático (HTML/CSS/JS gerado pelo Astro)
├── Workers → detecção de curl, formatos SVG/PNG/Prometheus, geolocalização por IP
└── DNS/SSL/WAF/Analytics → camada de borda
Navegador do usuário
└── hidrata o tempo atual e a previsão direto do Open-Meteo (sem passar por mim)
```
A ideia central: o conteúdo é gerado estaticamente e o que é volátil (tempo agora, previsão) é atualizado no cliente, direto na fonte.
## A stack de coding
### Frontend: Astro em modo estático
O site é um [Astro](https://astro.build) 6.4 em SSG (Static Site Generation). No build ele gera **11.172 páginas HTML** (uma por município, mais o "Modo Painel" de cada cidade, mais as páginas de estado, a home e o "Modo Mobile") e **5.598 endpoints JSON** em `/api/{uf}/{cidade}.json`. No total são cerca de 16.792 arquivos, ~218 MB. Esse número de arquivos importa: como vou contar adiante, ele esbarra num limite concreto do Cloudflare Pages.
A camada visual usa:
- **Tailwind CSS v4** (via plugin Vite), com componentes no estilo shadcn/ui e Base UI para React
- **React 19** para os trechos interativos
- **TypeScript** em modo estrito
- **lucide-react** para ícones e a fonte variável **Geist** self-hosted (sem Google Fonts, por privacidade)
O build inteiro das 11 mil páginas leva cerca de 30 segundos.
### Worker
Na frente do site roda um Cloudflare Worker em TypeScript, na rota `climabr.app/*`. Ele inspeciona cada requisição e decide o formato da resposta:
- **Navegador**: repassa para o Pages (página HTML completa)
- **`curl`/`wget`** (detectado pelo User-Agent): devolve um painel formatado em ANSI
- **`?format=1`**: uma linha, ideal para tmux, i3, polybar ou prompt do shell
- **`?format=prometheus`**: métricas para scraping
- **`.svg`**: um card compartilhável
- **`.png`**: o mesmo card em raster, para og:image (preview em redes sociais)
- **raiz `/`** sem cidade: geolocaliza pelo IP (dado de borda da Cloudflare) e redireciona
O PNG é gerado dentro do próprio Worker com **resvg** compilado para WebAssembly (`@resvg/resvg-wasm`), que rasteriza o SVG do card. Isso evita qualquer serviço externo de imagem.
### Coleta de dados: Python + GitHub Actions
Os dados vêm de scrapers em Python puro (sem dependências externas, só a biblioteca padrão), um por fonte: `scrape-openmeteo.py`, `scrape-sabesp.py`, `scrape-copasa.py`, `scrape-dengue.py`, `scrape-ana.py`, `scrape-inpe.py`, `scrape-inmet.py` e `scrape-ondas.py`. Cada um escreve em `data/cidades/{uf}/{slug}.json`.
Eles são disparados por cron no GitHub Actions:
- 1x/dia às 5h: previsão CPTEC (fallback)
- 1x/dia às 6h30: reservatórios da ANA e queimadas do INPE/NASA
- 1x/dia às 8h: Open-Meteo, SABESP, COPASA e dengue
- 9h e 21h, mais a cada push: build e deploy
Os dados ficam versionados no próprio repositório (cerca de 22 MB de JSON), e o deploy reconstrói o site com o snapshot mais recente.
## As fontes de dados (todas públicas e gratuitas)
- **Open-Meteo**: previsão, índice UV, qualidade do ar, vento, nascer e pôr do sol e fase da lua
- **SABESP** e **COPASA**: nível real de reservatórios de abastecimento (SP e MG)
- **InfoDengue** (Fiocruz/SVS): dengue, zika e chikungunya por semana epidemiológica
- **NASA FIRMS**: focos de queimada (satélites NOAA-20, Suomi, MODIS)
- **ONS**: reservatórios do sistema elétrico
- **IBGE**: lista de municípios, coordenadas e malhas
- **CPTEC/INPE**: previsão, usada como fallback
## Hidratação: estático no servidor, ao vivo no cliente
A página de cada cidade chega com um snapshot dos dados gerado no build. No navegador, um pequeno script busca a previsão e o tempo atual **direto do Open-Meteo**, usando o lat/lon embutido na página, e sobrescreve só os blocos voláteis. Em caso de falha, o snapshot do build permanece.
Esse desenho tem um efeito interessante de custo: a chamada ao Open-Meteo na navegação sai do navegador do usuário direto para a API, sem passar pela minha infraestrutura. Cada visitante usa a própria "cota" de rede. O Worker só consulta o Open-Meteo quando precisa montar os cards SVG/PNG, e ainda assim com cache de borda.
## Duas telas de relance: Modo Painel e Modo Mobile
Além da página completa, cada cidade tem duas visões pensadas para olhar de relance, sem rolagem, cabendo numa única tela:
- **Modo Painel** (`/uf/cidade/telao`): pensado para TV ou monitor de parede, com fundo dinâmico que muda conforme o tempo (sol, nuvem, chuva, tempestade), relógio e tiles grandes. É pré-renderizado, um arquivo por cidade.
- **Modo Mobile** (`/mobile?c=uf/slug`): a mesma ideia, mas compacto para celular, para salvar como página inicial ou virar uma extensão de navegador.
O Modo Mobile rendeu uma boa lição de arquitetura. A intenção inicial era pré-renderizar uma página por cidade, como o Modo Painel. Só que isso somaria mais 5.571 arquivos ao build e estouraria um limite do Cloudflare Pages (detalhado adiante). A solução foi inverter o desenho: o Modo Mobile é **uma única página** que lê a cidade da URL (`?c=uf/slug`), busca o JSON em `/api` e monta os tiles no próprio navegador, mesclando os dados ao vivo do Open-Meteo. Resultado: a feature inteira custou **um arquivo** em vez de 5.571.
## Free tier: o que cada serviço oferece e quanto eu uso
Para cada serviço, o limite gratuito e o consumo atual do projeto.
### Cloudflare Pages
- **Limite**: tráfego e requisições ilimitados; 500 builds por mês no CI da Cloudflare; **20.000 arquivos por deploy**; 25 MiB por arquivo.
- **Uso**: faço deploy via `wrangler` (upload direto), que **não conta** nos 500 builds (esse limite vale só para builds rodados na infra da Cloudflare; o meu build roda no GitHub Actions). São **~16.792 arquivos por deploy**, cerca de 84% do teto de 20.000. Tráfego: irrelevante para o limite (é ilimitado).
- **O limite que mais incomoda**: o de 20.000 arquivos por deploy. Com três variações por cidade (página, Modo Painel e JSON da API), já uso 5.571 arquivos de cada uma e fico perto do teto. Foi exatamente isso que me forçou a fazer o Modo Mobile como página única client-rendered: uma quarta variação por cidade teria estourado o limite. A lição é que, num site estático com muitas páginas, o gargalo do free tier não é banda nem build, é a **contagem de arquivos**.
### Cloudflare Workers
- **Limite**: 100.000 requisições por dia; 10 ms de CPU por requisição; bundle de **1 MiB comprimido** no plano free.
- **Uso**: o bundle está em **980 KiB gzip**, cerca de 96% do limite (o peso vem quase todo do WebAssembly do resvg). As requisições diárias estão bem abaixo de 100k no estágio atual. O ponto de atenção é o tamanho do bundle: estou perto do teto, então otimizações futuras precisam respeitar essa folga apertada.
### Cloudflare R2
- **Limite**: 10 GB de armazenamento; 1 milhão de operações de escrita e 10 milhões de leitura por mês; **egress gratuito**.
- **Uso**: zero. O R2 está habilitado na conta mas dormente - ainda não estou usando ele. Hoje os dados ficam embutidos no build; o R2 entra em cena só quando fizer sentido atualizar dado sem rebuild.
### Outros recursos Cloudflare (todos no free)
- **Web Analytics (RUM)**: métricas de visita sem cookies, alimentadas por um beacon JavaScript. Grátis e ilimitado.
- **DNS, SSL/TLS, DDoS, HTTP/3, 0-RTT, Tiered Cache, DNSSEC**: incluídos.
- **WAF**: o conjunto gerenciado gratuito, mais **uma** regra de rate limiting (por IP).
- **Page Shield**: monitoramento de scripts (o inventário client-side é grátis; a detecção de script malicioso é paga).
### GitHub
- **Limite**: Actions com 2.000 minutos por mês em repositório privado, e **ilimitado** em repositório público.
- **Uso**: com o repositório **público**, o Actions é gratuito e ilimitado, então esse limite deixou de existir. A história até chegar aqui é instrutiva: enquanto o repo era privado, a fatura mostrava **404 minutos em apenas 2 dias**, uma projeção de ~6.000/mês, três vezes o teto gratuito. O culpado eram crons frequentes demais (a coleta CPTEC rodava a cada 3 horas, a da ANA a cada 6). Fiz duas coisas: reduzi todos os crons para **1x/dia** (já que reservatório, queimada e previsão não mudam de hora em hora) e tornei o repositório público. Qualquer uma resolveria; juntas, sobra folga.
### APIs de dados
- **Open-Meteo**: gratuito para uso não comercial, sem chave, com limites de uso justo de cerca de **10.000 chamadas por dia** e **600 por minuto**. Esse foi o limite mais sutil do projeto. Uma passada completa pelas 5.571 cidades, em duas APIs (previsão e qualidade do ar), passa de 10.000 chamadas, ou seja, **não cabe numa coleta diária única**. A solução foi tornar a coleta no servidor **incremental**: cada rodada atualiza primeiro as cidades mais desatualizadas e pula as que foram coletadas nas últimas horas, ciclando por todas em um a dois dias. O detalhe que salva a experiência do usuário é a hidratação: como o navegador chama o Open-Meteo direto, **cada visitante usa a própria cota**, e a página fica sempre ao vivo independente do ritmo da coleta no servidor.
- **InfoDengue, NASA FIRMS, IBGE, ONS, SABESP, COPASA, CPTEC**: APIs e arquivos públicos, sem chave e sem cobrança.
## Armadilhas que aprendi no caminho
Alguns problemas reais que apareceram e que valem como aprendizado:
**1. Content Security Policy bloqueando silenciosamente.** A CSP estrita do site bloqueou, em dois momentos diferentes, o `fetch` da hidratação ao Open-Meteo e depois o beacon do Web Analytics. O sintoma é traiçoeiro: nenhum erro visível, a feature simplesmente não funciona. A lição: toda vez que se adiciona algo que carrega script externo ou faz fetch externo, é preciso atualizar a allowlist da CSP (`script-src`/`connect-src`), e validar em um navegador real.
**2. resvg sem fonte gera PNG em branco.** O resvg em WebAssembly não tem fontes de sistema. Sem carregar um buffer de fonte, ele renderiza só as formas vetoriais e nenhum texto, ou seja, o card PNG saía com o layout e sem dado nenhum. A correção foi embutir um subset mínimo de fonte (cerca de 28 KB gzip, respeitando o limite de 1 MiB do Worker) e, como o resvg também não renderiza emoji colorido, trocar os ícones por formas vetoriais no PNG.
**3. Injeção automática de analytics não funciona atrás de um Worker.** O beacon que a Cloudflare injeta sozinho no HTML é instável quando a resposta é servida por um Worker. A solução foi injetar o beacon manualmente no layout do Astro.
**4. Card com dado estático enquanto a página estava ao vivo.** Os cards SVG/PNG eram montados só a partir do JSON estático, então ficavam atrasados em relação à página, que hidrata ao vivo. Passei a buscar o Open-Meteo também no Worker ao montar o card, com cache curto.
**5. Um marcador de "já processado" que congelou os dados.** Para respeitar o rate limit, a coleta do Open-Meteo pulava cidades já processadas, checando a presença de um campo. Só que o campo era permanente: depois da primeira coleta, **todas** as cidades passavam a ser puladas para sempre, e a previsão congelou numa data. A correção foi trocar o critério por **data de coleta** (recolhe se passou de N horas), e não pela mera existência do campo. De quebra, ao tentar acelerar com lotes de 100 coordenadas, estourei o limite de 600 chamadas/minuto e tomei uma enxurrada de erros 429: o ritmo certo é tão importante quanto a lógica.
**6. CSS escopado do Astro versus HTML gerado por JavaScript.** O Astro, por padrão, **escopa o CSS** de cada componente (adiciona um atributo aos elementos e restringe os seletores a ele). O Modo Mobile monta os tiles via `innerHTML` no cliente, em runtime, e esses elementos não recebem o atributo de escopo, então o CSS simplesmente não se aplicava: a tela vinha com as informações sobrepostas, sem as caixas. A correção foi marcar o estilo como global (`