# 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: 234 --- ## A ilusão do buffet livre de IA e o mito do modelo definitivo - URL: https://esli.blog/posts/gateways-de-llm-e-o-fim-do-buffet-livre/ - Date: 2026-09-07 - Series: blogging - Tags: blogging, listed, ai, llm, llms, devops, api Feriado no homelab costuma ser um perigo... mas nada de concreto saiu, só ideias, alguns aprendizados e testes... Foi num teste desses, ajustando um script local, que o GLM 5.3 identificou e corrigiu um bug que o Claude Sonnet 5 tinha deixado passar. Para quem acompanha preço de API, o contraste salta. O Sonnet 5 cobra US$ 10 por milhão de tokens de saída. O GLM 5.3 sai por US$ 4,40, na mesma faixa do Kimi K2.7 Code. Nos modelos mais enxutos, como o Qwen3.8 Flash, a conta cai para US$ 0,47. No outro extremo, os modelos de topo das grandes casas batem em US$ 50 por milhão de tokens de saída. Fiz essas contas com mais calma no post sobre [benchmarks de IA e o custo real de 200 mil tokens](/posts/comparativo-benchmarks-llms-modelos-abertos-fechados/). A distância de qualidade entre os modelos de topo e os modelos abertos ou baratos encolhe a cada release. A diferença existe, mas a curva de retorno sobre o investimento está cada vez mais achatada. ![Ilustração em preto e branco de uma corrida em que quatro pessoas ajoelhadas, com as logos de ChatGPT, Claude, Grok e Gemini na cabeça, fazem o papel de cavalos, cada uma montada por um jockey com a logo de Kimi, Qwen, DeepSeek ou Z.AI, chicote em riste](/images/gateways-de-llm-e-o-fim-do-buffet-livre/llm-war.jpeg) Daí a tese que tomou conta do mercado: se nenhum modelo é o melhor em tudo, o futuro exigiria um roteador inteligente na frente. Um proxy que fatia o prompt, despacha tarefa simples para modelo de centavos, reserva o raciocínio pesado para os caros, mantém a aplicação desacoplada do provedor e sobrevive cobrando uma fatia da economia gerada. A teoria é elegante. 2026 mostrou que o tabuleiro é mais cínico. ## A conta do restaurante e o fim do open bar Duas mudanças recentes derrubaram a ideia de que a infraestrutura de IA rodaria com subsídio infinito. A primeira veio do lado chinês: até a DeepSeek começou a cobrar como quem precisa pagar a conta de luz do datacenter. Com o DeepSeek V4 Pro, a tarifa fixa deu lugar a preço dinâmico por horário. No pico, medido no fuso de Pequim, a entrada com cache chegou a subir 1.100%. Ou seja, além do custo por milhão de tokens, o roteamento agora exige que o seu proxy conheça fuso horário e saiba que horas são na China antes de despachar a carga. A segunda veio da Microsoft e do GitHub: o Copilot abandonou a assinatura flat e migrou para consumo baseado em AI Credits. O Copilot mostra que o modelo "coma à vontade" não sobrevive num mercado onde todo mundo tem fome insaciável de token. Um autocomplete de três linhas não pode custar a mesma mensalidade que um agente autônomo rodando loops, varrendo monorepos inteiros, injetando contexto massivo e disparando subagentes em cadeia. A matemática da infraestrutura cobrou a conta. ## O cemitério dos intermediários Se a precificação ficou mais complexa, o gateway de LLM faz mais sentido do que nunca, certo? Em parte. Intermediar chamada de API virou um negócio ingrato. | Segmento | Solução | Status em 2026 | | --- | --- | --- | | Self-hosted e open source | LiteLLM | Padrão de fato na infraestrutura local. Proxy compatível com OpenAI, suporte a centenas de provedores, virtual keys, failover e controle local. | | Hospedado e multi-provedor | OpenRouter | O baseline do mercado. Agrega centenas de modelos com billing unificado, mas os roteadores automáticos perdem a especificidade técnica dos prompts. | | Enterprise gerenciado | Prisma AIRS (ex-Portkey) | Foco em governança, PII e auditoria corporativa. O Portkey foi absorvido pela Palo Alto Networks e virou componente de segurança de rede. | | Pioneiros e descontinuados | TensorZero e Helicone | Projetos que não encontraram modelo de negócio sustentável foram descontinuados ou entraram em modo estrito de manutenção. | Para um gateway comercial lucrar, a diferença de preço entre os modelos precisa continuar grande o bastante para que a economia de roteamento cubra a margem dele. Só que os modelos de entrada ficaram baratos demais e os próprios provedores consolidaram suas APIs. A margem do intermediário é esmagada no meio. ## Reflexão de feriado: para onde vai a nossa stack? Olhando para o terminal neste feriado, a conclusão que tiro dessa dinâmica não é que precisamos terceirizar o discernimento para mais um SaaS proprietário. Roteamento e controle de custo deixaram de ser firula. Mandar qualquer prompt cegamente para o endpoint mais caro é preguiça de engenharia e uma forma eficiente de queimar orçamento transformando ciclo de GPU em calor residual. Antes, os devs eram cobrados para usar IA (antes digo, até meses atrás!), agora precisam justificar o uso do modelo específico, ou espremer o máximo do limão para tirar mais suco. Não digo que há uma bolha estourando, creio que isso nunca vai ocorrer: já é open source, cada um pode rodar o seu próprio, na sua própria infraestrutura, não há como algo estourar neste ponto. Mas o preço deixou de ser convidativo, e agora não dá para afirmar que se substitui um dev por IA. A IA está cara demais para ser usada por alguém sem conhecimento técnico suficiente e que não consiga fazer o "finops de token" corretamente. Confiar em "roteadores mágicos" de terceiros também cobra o seu preço: perda de controle sobre system prompts ajustados, latência de rede a mais e dependência de um intermediário que pode ser descontinuado ou adquirido no próximo trimestre. Para quem roda a própria infraestrutura e mantém o controle do pipeline, a abordagem mais pragmática continua sendo a boa e velha engenharia: - LiteLLM ou outro gateway open source sob seu controle, para garantir tolerância a falhas, abstração de SDK e métrica real de consumo. - Roteamento determinístico na aplicação, sabendo exatamente qual etapa do fluxo exige raciocínio profundo e qual resolve com um modelo de quarenta centavos. - Atenção aos detalhes operacionais: tamanho de contexto, cache de prompt e, agora, até a hora do dia no provedor. Claro, se o seu ambiente for grande o suficiente para isto. O buffet livre fechou as portas. O que resta é fazer engenharia de software. --- ## Benchmarks de IA, modelos abertos e o custo real de 200 mil tokens - URL: https://esli.blog/posts/comparativo-benchmarks-llms-modelos-abertos-fechados/ - Date: 2026-09-07 - Series: blogging - Tags: blogging, listed, ai, llm, llms, benchmarks, open-source **Atualização:** 7 de setembro de 2026. No post anterior, [Apps com AI (system prompts) e benchmarks de LLMs](https://esli.blog/posts/apps-com-ai-system-prompts-e-benchmarks-de-llms/), listei três referências: ARC Prize, SWE-bench e CodeClash. Desde então, a quantidade de leaderboards cresceu bastante, e ficou mais difícil responder a uma pergunta simples: qual modelo é melhor? A resposta depende do teste. Um modelo pode liderar em programação e perder em matemática, uso de ferramentas, escrita, visão ou custo. Por isso, o primeiro erro é tratar um ranking isolado como se fosse uma tabela definitiva da inteligência de uma LLM. ## Onde comparar modelos de IA A lista abaixo está ordenada por reconhecimento, cobertura e frequência de atualização. A ordem não significa que o primeiro site seja melhor para todos os casos. - [Artificial Analysis](https://artificialanalysis.ai/) mantém comparativos de inteligência, coding agents, velocidade, custo por tarefa, tokens gerados, provedores e abertura dos pesos. O Intelligence Index v4.2 combina dez avaliações, entre elas AA-Briefcase, GDPval-AA, Terminal-Bench, SciCode, Humanity's Last Exam e AA-LCR. - [LMArena](https://lmarena.ai/leaderboard) é a evolução do Chatbot Arena. Pessoas comparam respostas sem saber qual modelo produziu cada uma, e os resultados formam um ranking Elo. É uma boa medida de preferência humana, mas não substitui tarefas verificáveis. - [LiveBench](https://livebench.ai/) tenta reduzir contaminação usando perguntas novas e atualizações frequentes. Divide os resultados por raciocínio, matemática, programação, linguagem e conhecimento. - [Scale Labs Leaderboards](https://scale.com/leaderboard) reúne avaliações de fronteira, segurança e agentes. A página inclui SWE Atlas, MCP Atlas, SWE-bench Pro, Humanity's Last Exam, MultiChallenge, VisualToolBench, TutorBench e outros. - [SWE-bench](https://www.swebench.com/) executa agentes em issues reais de repositórios públicos. Há versões Full, Verified, Lite, Multilingual e Multimodal. É uma das referências mais úteis para coding agents. - [Terminal-Bench](https://www.tbench.ai/) mede se um agente consegue concluir tarefas dentro de um terminal, incluindo configuração, uso de ferramentas, programação e depuração. O resultado combina taxa de resolução, custo e tokens. - [Aider Polyglot Leaderboard](https://aider.chat/docs/leaderboards/) testa edição de código em 225 exercícios de C++, Go, Java, JavaScript, Python e Rust. É particularmente útil para medir se o modelo consegue alterar um repositório sem intervenção humana. - [EvalPlus](https://evalplus.github.io/leaderboard.html) amplia HumanEval e MBPP com testes mais rigorosos. Também aponta para BigCodeBench, LiveCodeBench, RepoBench, CrossCodeEval, ClassEval, Code Lingua e outras avaliações de programação. - [ARC Prize Leaderboard](https://arcprize.org/leaderboard) mede raciocínio abstrato em problemas que exigem descobrir regras a partir de poucos exemplos. Também apresenta a relação entre desempenho e custo. - [CodeClash](https://codeclash.ai/) avalia agentes como desenvolvedores orientados a objetivos. Em vez de corrigir uma issue curta, eles precisam criar artefatos de software, como jogos, seguindo várias etapas. - [ProgramBench](https://programbench.com/) mede se modelos conseguem construir artefatos de software significativos do zero, uma tarefa diferente de completar uma função ou corrigir um bug. - [LiveCodeBench](https://livecodebench.github.io/leaderboard.html) usa problemas recentes de programação competitiva, com coleta contínua para dificultar o vazamento dos enunciados para o treinamento. - [BigCodeBench](https://bigcode-bench.github.io/) avalia geração de código em tarefas mais realistas, com bibliotecas, instruções compostas e chamadas de funções. - [Hugging Face Open LLM Leaderboard](https://huggingface.co/open-llm-leaderboard) comparou modelos de pesos abertos em várias avaliações padronizadas. Está arquivado desde 2025, como snapshot estático, e não recebe modelos novos. Serve como registro histórico, não como ranking atual. - [MMLU-Pro](https://github.com/TIGER-AI-Lab/MMLU-Pro) aumenta a dificuldade do MMLU e reduz o efeito de perguntas fáceis ou de respostas por eliminação. - [GPQA](https://github.com/idavidrein/gpqa) testa perguntas científicas feitas para serem difíceis até para especialistas com acesso a ferramentas. - [Humanity's Last Exam](https://lastexam.ai/) reúne questões de várias áreas acadêmicas e profissionais, com foco em conhecimento e raciocínio de fronteira. - [MMMU](https://mmmu-benchmark.github.io/) avalia raciocínio multimodal em imagens, diagramas e perguntas de nível universitário. - [BFCL](https://gorilla.cs.berkeley.edu/leaderboard.html) mede chamadas de função e uso de ferramentas, incluindo seleção da ferramenta e argumentos corretos. - [τ-bench](https://github.com/sierra-research/tau-bench) coloca agentes em diálogos com usuários e APIs de ambientes reais, avaliando se seguem políticas e concluem a tarefa. - [GAIA](https://huggingface.co/gaia-benchmark) mede assistentes gerais que precisam pesquisar, calcular, ler arquivos e usar ferramentas. - [BrowserGym](https://github.com/ServiceNow/BrowserGym) e [WebArena](https://webarena.dev/) avaliam agentes navegando em sites e executando tarefas de ponta a ponta. - [AgentBench](https://github.com/THUDM/AgentBench) compara agentes em ambientes como navegação, bancos de dados, sistemas operacionais e jogos. - [ToolBench](https://github.com/OpenBMB/ToolBench) concentra-se em planejamento e uso de APIs. - [HELM](https://crfm.stanford.edu/helm/latest/) organiza avaliações com foco em transparência, reprodutibilidade, segurança, robustez e eficiência. - [SafetyBench](https://github.com/thu-coai/SafetyBench) avalia segurança e comportamento sob prompts problemáticos, com questões de múltipla escolha em várias categorias de risco. - [METR](https://metr.org/) pesquisa a capacidade de agentes realizarem tarefas longas e relevantes para trabalho, além de medir a confiabilidade dessa autonomia. - [SWE-rebench](https://swe-rebench.com/) e [SWE-fficiency](https://swefficiency.com/) exploram, respectivamente, avaliação contínua de engenharia de software e eficiência do trabalho realizado. Também vale acompanhar os repositórios oficiais dos benchmarks. Leaderboards mudam, modelos saem do ar, mantenedores atualizam prompts e os próprios fornecedores submetem parte dos resultados. Um número sem versão, data, agente, nível de raciocínio e custo não é uma comparação completa. ## Modelos fechados versus modelos de pesos abertos Modelo fechado é aquele cujo peso, dados de treinamento ou método completo não estão disponíveis. O usuário normalmente acessa uma API ou aplicativo hospedado pela empresa. Claude, GPT e Gemini são exemplos dessa categoria, embora cada empresa divulgue quantidades diferentes de informação. Modelo de pesos abertos disponibiliza os pesos para download, execução local ou hospedagem por terceiros. Isso não significa automaticamente que os dados de treinamento, o código de treinamento e toda a documentação estejam abertos. Por isso, “open source” costuma ser usado de forma imprecisa. “Pesos abertos” é uma descrição mais segura para modelos como DeepSeek, Qwen, Llama e gpt-oss. A diferença prática fica assim: - Modelos fechados tendem a oferecer infraestrutura gerenciada, versões estáveis, suporte, filtros, ferramentas integradas e acesso aos modelos maiores sem hardware próprio. - Modelos de pesos abertos permitem auditoria parcial, ajuste fino, execução privada, escolha do provedor, menor dependência de uma empresa e custo potencialmente menor em escala. - Um modelo aberto exige mais decisões sobre GPU, quantização, latência, atualização, segurança e observabilidade quando é hospedado pelo próprio usuário. - “Aberto” não quer dizer gratuito. GPU, energia, armazenamento, engenharia e operação também entram na conta. - “Fechado” não quer dizer melhor. A qualidade depende da tarefa, do agente, do prompt, do contexto, do nível de raciocínio e do provedor que serve o modelo. Os resultados recentes mostram uma distância menor do que o marketing costuma sugerir. No Aider Polyglot, por exemplo, um teste de 225 problemas registrou Claude Opus 4 com 32k de thinking em 72,0%, DeepSeek R1-0528 em 71,4%, Qwen3 235B A22B sem thinking em 59,6% e gpt-oss-120b com esforço de raciocínio alto em 41,8%. As configurações fazem parte do resultado: o mesmo Opus 4 sem thinking cai para 70,7%, e o R1 original, anterior ao 0528, fica em 56,9%. As diferenças são de 0,6 ponto entre Opus e DeepSeek, 12,4 pontos entre Opus e Qwen e 30,2 pontos entre Opus e gpt-oss-120b. Os resultados vieram de datas, versões e configurações diferentes, então o que dá para tirar dali é limitado: em algumas tarefas um modelo aberto empata na prática com um fechado, em outras fica bastante atrás. A distância real precisa ser medida benchmark por benchmark. O próprio Artificial Analysis separa modelos Proprietary, Open Weights e Open Weights com restrições comerciais, além de oferecer custo por tarefa. Essa separação é melhor do que colocar todos os modelos abertos em um único grupo, porque Qwen, DeepSeek, Llama, gpt-oss e modelos locais menores têm capacidades e licenças muito diferentes. ## Minha assinatura e o mesmo harness Minha referência pessoal hoje é uma assinatura anual do Claude, mantida desde 2024. Também uso o OpenCode Go e o Abacus.AI, ambos a US$ 10 por mês. No OpenCode Go, a página oficial lista 27 modelos, misturando pesos abertos e proprietários. Alguns deles: - Kimi K3 - Grok 4.6 - Hy4 Preview - GPT 5.6 Luna - GLM-5.3-Flash - MiniMax M3 - Qwen3.7 Plus - Hy3 - Qwen3.8 Flash - DeepSeek V4 Flash - LongCat-2.0 - Omen Alpha - MiMo-V2.5 - Muse Spark 1.3 Contributor, com disponibilidade limitada por região O plano não conta requisições: mede o uso em dólares, ao preço por token de cada modelo, com teto de US$ 12 por janela de cinco horas, US$ 30 por semana e US$ 60 por mês. A página promete cerca de seis vezes o valor da assinatura em uso para a maioria dos modelos, com multiplicadores menores para os mais recentes ou já descontados. Isso não é uma unidade universal de tokens, então não dá para converter direto em cota de API sem consultar a conta. Na página pública do ChatLLM da Abacus.AI, os modelos e produtos destacados são: - Opus 5 - Fable 5.1 - Sonnet 5 - GPT-6 Astra - GPT-5.6 Sol - GPT-5.6 Terra - GPT-5.6 Luna - Gemini 3.1 Pro - Gemini 3.8 Flash - Kimi K3 - GLM 5.3 - DeepSeek v4 - Grok 4.6 - Abacus Smaug - Nano Banana Pro - GPT Image 2 - Seedance 2.5 - Kling 3.0 - Veo 3.1, pelo Abacus AI Studio A lista da Abacus é dinâmica e a própria página aponta para um FAQ com mais de cem modelos e modalidades. Portanto, esta é a lista visível publicamente no momento da captura, não uma promessa de que todos estarão disponíveis com a mesma cota em qualquer conta ou região. A marca do provedor importa menos do que o que fica constante entre elas. Com a minha [ai-md-stack](https://github.com/Esl1h/ai-md-stack), o harness, as premissas de execução, as skills e o agente global são os mesmos. Trocar Claude por um modelo do OpenCode Go ou da Abacus muda o motor, mas preserva o ambiente de teste. Sem isso, mudar de uma vez o modelo, o prompt, as ferramentas, as regras do agente e a forma de avaliar o resultado significa comparar produtos inteiros com configurações diferentes, não modelos. Ainda assim, o harness não elimina todas as diferenças. Cada provedor pode aplicar system prompts próprios, truncar contexto, alterar parâmetros, limitar raciocínio, usar roteamento interno ou filtrar ferramentas. O resultado é uma comparação controlada, não um experimento perfeitamente idêntico. ## Quanto custariam 200 mil tokens? A conta abaixo assume 100 mil tokens de entrada e 100 mil tokens de saída. É uma simulação simples para tornar os preços comparáveis. Em uma tarefa real, o agente pode consumir muito mais entrada do que saída, repetir chamadas, usar cache, gerar tokens de raciocínio e fazer várias chamadas de ferramenta. Para cada modelo, a fórmula é: `custo = (100.000 / 1.000.000 × preço de entrada) + (100.000 / 1.000.000 × preço de saída)` Usando preços públicos de API em setembro de 2026, na faixa de contexto padrão de cada modelo e sem descontos de cache: - Claude Fable 5.1, US$ 10 por milhão de tokens de entrada e US$ 50 por milhão de saída: **US$ 6,00**. - GPT-6 Astra, US$ 10 por milhão de entrada e US$ 50 por milhão de saída até 272 mil tokens de contexto: **US$ 6,00**. - Claude Opus 5, US$ 5 por milhão de entrada e US$ 25 por milhão de saída: **US$ 3,00**. - Gemini 3.1 Pro, US$ 2 por milhão de entrada e US$ 12 por milhão de saída até 200 mil tokens de contexto: **US$ 1,40**. - Claude Sonnet 5, US$ 2 por milhão de entrada e US$ 10 por milhão de saída: **US$ 1,20**. - Claude Haiku 4.5, US$ 1 por milhão de entrada e US$ 5 por milhão de saída: **US$ 0,60**. - GLM 5.3, US$ 1,40 por milhão de entrada e US$ 4,40 por milhão de saída: **US$ 0,580**. - Kimi K2.7 Code, US$ 0,95 por milhão de entrada e US$ 4,00 por milhão de saída: **US$ 0,495**. - DeepSeek V4 Pro, US$ 0,66 por milhão de entrada e US$ 1,98 por milhão de saída no preço fora de pico: **US$ 0,264**. - DeepSeek V4 Flash, US$ 0,22 por milhão de entrada e US$ 0,66 por milhão de saída no preço fora de pico: **US$ 0,088**. - GLM-5.3-Flash, US$ 0,15 por milhão de entrada e US$ 0,50 por milhão de saída: **US$ 0,065**. - Qwen3.8 Flash, US$ 0,15 por milhão de entrada e US$ 0,47 por milhão de saída: **US$ 0,062**. Do topo ao fim da lista são quase cem vezes de diferença na mesma tarefa. A DeepSeek cobra por horário desde agosto de 2026, e os modelos Pro do Google e da OpenAI mudam de faixa de preço acima de um certo contexto, então o mesmo trabalho pode custar o dobro dependendo de quando e de quão cheio o contexto vai. Os valores acima são o preço de API, não o valor da assinatura. Dentro de Claude, OpenCode Go e Abacus.AI, a tarefa não gera uma cobrança adicional de US$ 6,00 ou US$ 0,062. Ela consome a cota, o teto de uso da janela, os créditos ou os multiplicadores definidos pelo plano. O custo de US$ 10 mensais de um plano precisa ser comparado com a quantidade de trabalho permitida, não com um único preço de API. Se uma assinatura de US$ 10 permitir uma tarefa que custaria US$ 6 em API, ela já entregou mais da metade do preço mensal em uma única execução. Só que essa mesma tarefa comeria metade do teto de US$ 12 da janela de cinco horas do OpenCode Go. O preço marginal continua existindo, só que aparece como limite de uso, não como boleto separado. Tokens visíveis e tokens faturados também não são a mesma coisa. Um agente pode enviar o mesmo arquivo várias vezes, receber tokens de raciocínio que não aparecem na resposta final e repetir uma etapa após uma falha. Por isso, 200 mil tokens na interface não necessariamente significam 200 mil tokens cobrados pelo provedor. ## O que os benchmarks mostram A evidência atual não sustenta nem “abertos venceram fechados” nem “fechados são sempre superiores”. - Os melhores modelos de pesos abertos já chegam muito perto dos melhores fechados em várias tarefas de raciocínio e programação. - Em tarefas de agente longo, uso de ferramentas, segurança, multimodalidade e confiabilidade, a diferença pode aumentar. - Em coding, o agente e o ambiente importam tanto quanto o modelo. SWE-bench, Terminal-Bench e Aider medem sistemas completos, não apenas uma resposta isolada. - O preço por token favorece muito os modelos abertos quando existe um endpoint competitivo ou quando a execução local é viável. - Para uma pessoa que paga US$ 10 por mês e usa vários modelos com o mesmo harness, o ganho mais concreto é poder escolher o motor adequado para cada tarefa sem reescrever todo o fluxo. Em vez de perguntar qual LLM ganhou o ranking, hoje eu pergunto qual modelo resolve a minha classe de tarefas, com o meu agente, dentro do meu orçamento e com uma diferença de qualidade que justifique o custo. --- ## My AI setup: Claude, Abacus, and OpenCode - URL: https://esli.blog/posts/ai-setup-claude-opencode/ - Date: 2026-09-02 - Series: tech - Tags: ai, llm, sre, devops, claude-code, opencode, agents-md, linux I've already posted here about [prompts for SRE/SysAdmin/DevOps](/posts/unlocking-the-power-of-ai-llms-and-prompts-for-sres-sysadmins-and-devops/), about [local and open source agents](/posts/ai-coding-com-agents-locais-e-open-source/), and about [development with AI](/posts/desenvolvimento-com-ai/) more broadly. Those three pieces document different phases of the same obsession. This one documents the current phase: fewer new tools per week, more real infrastructure on top of what already works. ## From the "tried everything" phase to the "this one sticks" phase I ran OpenRouter for a good while just to compare model against model without marrying any provider. Through these aggregators I went through T3 Chat, Mammouth, NanoGPT, AymoAI, Perplexity... It even spawned a post about LLMs that run anonymously and via Tor: https://esli.blog/posts/best-free-ai-chatbots-without-login-accessible-via-tor-and-anonymous-use/ I tested a handful of others at work too (self-hosted, including LocalAI, Koboldcpp, and Llama...), pointing at models that each promised to be the next big leap in reasoning. In the end, what stuck in my daily routine, across 2 Arch laptops with Hyprland (Omarchy), EndeavourOS, and a Fedora 44 KDE workstation, was a much more boring trio: - **Claude** for heavy coding work and as a terminal agent - **AbacusAI** (ChatLLM, with RouteLLM routing between models behind the scenes) - **OpenCode** as the open source CLI/TUI that talks to both of the above and to the rest of the market That doesn't mean I stopped testing new things. It means I stopped rebuilding my workflow every time a new tool shows up. And that's the real motivation for this article: it's not just about standardizing an instruction file, it's about having a way to port to AbacusAI and OpenCode (and to whatever LLM I test next) what already works well in Claude Code, without rebuilding from scratch for every new tool. The SRE prompt I used to paste manually is the first example of that. The prompt is published [in the previous post](/posts/unlocking-the-power-of-ai-llms-and-prompts-for-sres-sysadmins-and-devops/) and in the [original Gist](https://gist.github.com/Esl1h/5188c37cf6136bf6cb009b94bec11912). In Claude Code it became a skill, only loads when invoked instead of weighing down every session, placeholders swapped for `$ARGUMENTS`... It's exactly the kind of pattern I want to carry to the other CLIs as each one gains equivalent support, instead of recreating a similar workflow from scratch in each one. ## RTK: token savings [RTK](https://www.rtk-ai.app/) (Rust Token Killer) is a CLI proxy that rewrites shell commands before execution to save tokens, handling things like paginating `git`/`kubectl`/`terraform` output before it blows up the context. Single Rust binary, no external dependency. Global install on Linux: ```bash curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh ``` Alternatives: `brew install rtk-ai/tap/rtk`, or via `cargo install --git https://github.com/rtk-ai/rtk --branch master rtk` (use the Git URL, not plain `cargo install rtk`: there's a name collision with another crate called "Rust Type Kit"). Once installed, the actual setup: ```bash rtk init -g # installs the native hook + generates RTK.md in the project rtk init -g --opencode # variant for OpenCode instead of Claude Code rtk init --show # confirms it installed correctly ``` `rtk init -g` does two things: it registers a native `PreToolUse` hook in Claude Code (no extra `jq`/bash needed since 0.37.2, works even on PowerShell) and creates a `RTK.md` in the project, documenting the commands and savings available for the agent itself to consult. That's why my `AGENTS.md` ends with `@RTK.md`: RTK generates the technical reference, AGENTS.md/CLAUDE.md carries the behavior rules. Once running, `rtk gain` shows the savings dashboard and `rtk gain --graph` draws the ASCII graph for the last 30 days. ## One AGENTS.md, symlinked everywhere This is my current `AGENTS.md`/`CLAUDE.md`, in constant evolution and evolved from the ones I already used in previous articles: ```markdown # Global Rules - Write in English by default; use Brazilian Portuguese only when explicitly requested. - When writing Portuguese, always use correct diacritics. - Do not use ASCII tables. - Do not use horizontal rules (`---`) or ASCII lines as section separators. - Do not use the em dash (U+2014, `—`, `—`) as punctuation; use a comma, colon, or rewrite. - Do not use the en dash (U+2013); use a hyphen or rewrite the sentence. - Files are UTF-8, LF line endings, with a trailing newline at EOF. ## Precedence 1. Direct user instructions in the current task. 2. Project-specific instructions (e.g. project AGENTS.md, RTK.md). 3. This global file. ## Git Commits - Commit messages MUST describe only the *what* and *why* of the change. - Do NOT include tooling metadata, trailers, attribution, or co-author lines (e.g. `Co-Authored-By`, `Signed-off-by` unless required by the project, `Generated-by`, `Made-with`, or any IDE/editor/assistant reference). - Keep messages tool-agnostic: nothing about the environment used to author the code belongs in the history. - One commit per logical change (ideally one file/concern at a time). - Follow Conventional Commits v1.0.0: https://www.conventionalcommits.org/en/v1.0.0/ - Subject line: imperative mood, 50 chars max, no trailing period. - Body (when needed): wrap at 72 chars, separated from subject by a blank line. - Reference issues/tickets in the footer (e.g. `Refs: #123`), not the subject. - Never bundle unrelated changes; split refactors from behavior changes. - Do NOT auto-generate or template commit messages; write them from the diff. ## Approach - Think before acting. Read existing files before writing code. - Be concise in output but thorough in reasoning. - Prefer editing over rewriting whole files. - Do not re-read files you have already read unless the file may have changed. - Test your code before declaring done. - No sycophantic openers or closing fluff. - Keep solutions simple and direct. - Do not create more than a few new files without confirming scope first. ## Think Before Coding **Don't assume. Don't hide confusion. Surface tradeoffs.** Before implementing: - State your assumptions explicitly. If uncertain, ask. - If multiple interpretations exist, present them; don't pick silently. - If a simpler approach exists, say so. Push back when warranted. - If something is unclear, stop. Name what's confusing. Ask. ## Simplicity First **Minimum code that solves the problem. Nothing speculative.** - No features beyond what was asked. - No abstractions for single-use code. - No "flexibility" or "configurability" that wasn't requested. - No error handling for impossible scenarios. - If you write 200 lines and it could be 50, rewrite it. Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. ## Surgical Changes **Touch only what you must. Clean up only your own mess.** When editing existing code: - Don't "improve" adjacent code, comments, or formatting. - Don't refactor things that aren't broken. - Match existing style, even if you'd do it differently. - If you notice unrelated dead code, mention it; don't delete it. When your changes create orphans: - Remove imports/variables/functions that YOUR changes made unused. - Don't remove pre-existing dead code unless asked. The test: every changed line should trace directly to the user's request. ## Goal-Driven Execution **Define success criteria. Loop until verified.** Transform tasks into verifiable goals: - "Add validation" becomes "Write tests for invalid inputs, then make them pass". - "Fix the bug" becomes "Write a test that reproduces it, then make it pass". - "Refactor X" becomes "Ensure tests pass before and after". For multi-step tasks, state a brief plan: ~~~ 1. [Step] → verify: [check] 2. [Step] → verify: [check] 3. [Step] → verify: [check] ~~~ Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. ## Shell / Scripts - zsh is the interactive shell; validate interactive customizations in `~/.zshrc`. - For scripts, target POSIX sh unless zsh/bash features are required; always state the shebang used. - Run shellcheck on shell scripts before declaring done. - Quote variable expansions; prefer `set -euo pipefail` in bash scripts. ## Secrets - Never hardcode credentials, tokens, or keys; read from env or a secrets store. - Never print secrets in logs or commit them. ## Comments - Comment the why, not the what. No redundant or narrating comments. ## Imports @RTK.md ``` Claude Code only loads `CLAUDE.md`, it doesn't recognize `AGENTS.md` natively. OpenCode and AbacusAI read `AGENTS.md` directly. Solution: `AGENTS.md` as the single source, and Claude Code points to it. **Global scope**, applying to any project. Canonical inside `~/.claude/`, sitting next to `CLAUDE.md` instead of living inside another tool's config directory: ```bash mkdir -p ~/.claude ~/.config/opencode $EDITOR ~/.claude/AGENTS.md # canonical file cd ~/.claude && ln -s AGENTS.md CLAUDE.md # relative symlink, same folder ln -s ~/.claude/AGENTS.md ~/.config/opencode/AGENTS.md ``` OpenCode still has a native fallback to `~/.claude/CLAUDE.md` if it can't find `AGENTS.md` (disable it with `OPENCODE_DISABLE_CLAUDE_CODE=1`), so even without any symlink neither tool is left without instructions. But symlinking makes explicit which file is the source of truth, instead of relying on an implicit fallback. ### Project scope: each `/init` with its own, or a symlink for both Claude Code and OpenCode each have their own `/init`, and each scaffolds the file its own tool's way: `CLAUDE.md` for one, `AGENTS.md` for the other. AbacusAI CLI has no native `/init`, probably because the number of models behind RouteLLM makes an automatic scaffold inconsistent depending on which model picks up the task; asking it directly to create the file works fine, it just goes through whichever model the routing picks. OpenCode's `/init` does file discovery before writing anything. Running it with GLM 5.3, the first call was a Glob: ``` {AGENTS.md,CLAUDE.md,.cursorrules,.cursor/rules/**,.github/copilot-instructions.md,opencode.json,opencode.jsonc,README*,package.json,Makefile,Taskfile*,*.toml,*.cfg,*.ini} ``` Same idea as Claude Code's `/init`, which reads Cursor and Copilot rules by default and, with `CLAUDE_CODE_NEW_INIT=1`, also `AGENTS.md`, `.devin/rules/`, `.windsurf/rules/`, and `.clinerules`: before generating new instructions, look for a convention that already exists from another tool. I actually tested all four: cloned an empty repository (just `.git/`, zero commits) and ran `/init` with Claude Code, with OpenCode using GLM 5.3, with OpenCode using Kimi K2, and asked AbacusAI directly (which routed to DeepSeek). Result, summarized: - **Claude Code**: detected the empty repo, left placeholders in the commands and architecture sections, and the interesting part is what it did NOT repeat: the conventions section opens with "Beyond the global rules in `~/.claude/CLAUDE.md`" and only adds the requirement for `shellcheck` on shell scripts, without duplicating the rest of my global CLAUDE.md that was already loaded in the session. - **OpenCode + GLM 5.3**: went beyond describing an empty repo. Asked for the language to bootstrap trivial configs. - **OpenCode + Kimi K2**: the opposite stance. Refused to assume language or toolchain, explicitly said to wait for code to exist before inferring build/test/lint. Safer, shallower. - **AbacusAI + DeepSeek**: structure similar to Claude's, and documented in passing a detail I hadn't noticed: there's a local `.abacusai/` for agent permission config, equivalent to a project's `.claude/settings.json`. The practical conclusion changed my own recommendation: since `/init` genuinely diverges between tools, letting each `/init` run separately produces a better result than forcing a single project `AGENTS.md` symlinked to both. I reserve the project symlink for when the file is hand-written by me, without any tool's `/init`, and I want the same text everywhere: ```bash ln -s AGENTS.md CLAUDE.md ``` If you want Claude Code-specific instructions on top of the shared `AGENTS.md`, you can swap the symlink for an import, since here the file lives inside the project itself: ```markdown @AGENTS.md ## Claude Code - Use plan mode for changes under src/billing/. ``` The `@path` syntax expands the referenced file at session load (up to 4 levels of recursion), and still leaves room for Claude Code-specific instructions below, without duplicating the whole AGENTS.md. ### What breaks in Cowork In a **Cowork** session running on desktop, the restriction is more specific than it looks at first: it only applies to the user-scoped `~/.claude/CLAUDE.md`, not to a project's `CLAUDE.md`. The symlink and the project import from the section above work normally in Cowork, no adjustment needed, because the referenced `AGENTS.md` lives inside the session's own working directory. **Global scope is where it actually breaks.** Here Cowork blocks two things in parallel, and one isn't a workaround for the other. `~/.claude/CLAUDE.md` being itself a symlink gets ignored entirely. And an `@import` inside a user-scoped file that resolves outside the session's working directory also gets ignored, line by line. Since `~/.claude/AGENTS.md` sits outside any specific project, a real `~/.claude/CLAUDE.md` containing only `@AGENTS.md` falls right into that second restriction: the import line is dropped, leaving an empty file. Swapping symlink for import doesn't get around anything at this scope. There's no clean workaround for this today. The options are accepting the gap (personal global rules don't reach a Cowork session, only rules versioned in the project do) or pasting the literal content, without `@import`, straight into `~/.claude/CLAUDE.md`. The second option works because neither the symlink restriction nor the import restriction applies to literal text, but it costs the single source of truth: any change to the canonical `AGENTS.md` has to be manually copied to that file too. On an import outside the project directory, outside a Cowork session (for example, `@~/.claude/my-project-instructions.md` referenced from a project's `CLAUDE.md`), Claude Code shows an external import approval dialog the first time it encounters the reference. ### AbacusAI CLI has no global, only project Abacus's documentation says AGENTS.md works "at the user and project level," but that didn't match actual behavior. I tested it by hand: I put an actionable rule (not a "repeat my instruction," which any model tends to refuse because it smells like system prompt extraction; a real behavior instruction, like "always end the response with tag X") in `~/AGENTS.md` and ran `abacusai -p "What's 2+2?"` in a folder with no project AGENTS.md. The tag never showed up. I swapped the same rule to the project folder's `AGENTS.md` and it appeared in the response. Ran it again with `--no-agents-md` and it disappeared. Practical conclusion: AbacusAI CLI's `AGENTS.md` is workspace-only (the folder where you run `abacusai`), and the `--no-agents-md` flag turns off exactly that loading. There's no working user-level global `AGENTS.md` in the CLI today, neither at `~/AGENTS.md` nor at `~/.abacusai/AGENTS.md`; what Abacus calls "user level" in the documentation text probably refers only to Skills (`~/.abacusai/builtin-skills`, `~/.abacusai/desktop/skills`), not to AGENTS.md. In practice, this means: for AbacusAI, keep `AGENTS.md` at the root of each project (the same canonical file that OpenCode and symlinked Claude Code use) and drop the idea of a single global covering all three CLIs at once. If Abacus changes this in a future version, it's worth running this same test again before trusting any "global support" announcement. ### Does the global file's `@` work in the other tools? And who wins: project, global, or hook? Three questions that are still missing after all this digging, and all three have a direct answer. `@path` is real syntax only in Claude Code. It works to import the global `AGENTS.md` or any other file, with automatic expansion at session load. In OpenCode, `@path` inside an `AGENTS.md` does nothing on its own, their documentation is explicit that OpenCode doesn't automatically expand file references. The equivalent mechanism there is the `instructions` field in `opencode.json` or `opencode.jsonc`, which accepts a list of paths (globs included) and even a remote URL: ```json { "instructions": ["AGENTS.md", "docs/guidelines.md", ".cursor/rules/*.md"] } ``` In AbacusAI I found no documented support nor any sign of import-equivalent behavior; `AGENTS.md` there is a single file, read whole, with no reference to others. The order of precedence changes from tool to tool, and none of them guarantee the more specific rule wins: - **Claude Code** concatenates everything: managed policy, then `~/.claude/CLAUDE.md` (user), then the project's `CLAUDE.md`/`.claude/CLAUDE.md`, then `CLAUDE.local.md`. The project one enters last in the context, so it sits textually closer to the prompt, but that's not a guarantee of override: if the global says one thing and the project says the opposite, it's the model reading both that decides which rule applies, not a deterministic precedence mechanism. - **OpenCode** is mutually exclusive, it doesn't concatenate: if a local `AGENTS.md` (or `CLAUDE.md`) exists, it loads only that one and doesn't even look at the global `~/.config/opencode/AGENTS.md`. It only falls back to global when it finds nothing local walking up the directory tree from the cwd. - **AbacusAI** doesn't even have this discussion: only project loading exists, so there's no scope conflict to resolve. Hooks don't enter this dispute in any of the three, because a hook isn't content the model interprets, it's code the client executes before or after the tool runs, with the power to block or rewrite the command. `.md` is instruction the model tries to follow; a hook applies regardless of what the model "decided." Hence the practical rule: anything that needs to hold always, without exception, doesn't go into `AGENTS.md`, it goes into a hook. And Claude Code's hooks don't port to the other two. `settings.json` with `PreToolUse`/`PostToolUse` is Claude Code's proprietary format. OpenCode has its own plugin system, conceptually similar but technically incompatible: JS/TS modules in `.opencode/plugins/` (project) or `~/.config/opencode/plugins/` (global), with lifecycle hooks like `tool.execute.before` and `tool.execute.after` instead of `PreToolUse`/`PostToolUse`, plus `command.executed`, `file.edited`, `session.created`. Porting one of my Claude Code hooks to OpenCode means rewriting the logic in JS/TS against that plugin API, not copying the `.sh`. Nothing similar is documented for AbacusAI. ### The gap in the first project After setting up the global config (`~/.claude/AGENTS.md` symlinked in both places) I moved to a project that already had an `AGENTS.md` at the root and a symlinked `CLAUDE.md` inside the repository itself. For Claude Code, nothing is missing. It concatenates `~/.claude/CLAUDE.md` with the project's `CLAUDE.md` through its own loading hierarchy, no import needed. An `@` pointing at the global there would just duplicate what already gets in on its own. The blind spot is OpenCode. With a project `AGENTS.md` present, it switches to using only that file and ignores the global `~/.config/opencode/AGENTS.md` entirely, without concatenating the way Claude Code does. And `@path` inside `AGENTS.md` doesn't fix this, because, as already explained, it's not syntax OpenCode expands. The real way to recover the global in that project is the `instructions` field of the root `opencode.json` (or `.jsonc`), listing both: ```json { "instructions": ["AGENTS.md", "~/.claude/AGENTS.md"] } ``` If `~` doesn't expand in your OpenCode, use an absolute path. And any rule that only existed in the global and wasn't repeated in the project's `AGENTS.md` (commit convention, requiring shellcheck on shell scripts, etc.) stays invisible to OpenCode in that repository until this `instructions` field is in place. For AbacusAI nothing changes, with or without `@`, with or without `instructions`: it only sees the project `AGENTS.md` already at the root and ignores anything at global scope either way. ## Hooks aren't optional Here's the point that usually slips by: CLAUDE.md/AGENTS.md is context, not applied configuration. Nothing guarantees the model remembers a rule in a long session. A rule that needs to hold always, without depending on the model remembering, becomes a hook. I published the package at [ai-md-stack](https://github.com/Esl1h/ai-md-stack): six files, `settings.example.json` plus four hooks and a shared library, shellcheck-clean and `bash -n` validated across all of them. `_common.sh` holds the position check the other hooks reuse, so `terraform plan` triggers the guard and `jq --arg c 'terraform plan'` doesn't: ```bash is_command_invoked() { local line="$1" name="$2" line=$(printf '%s' "$line" | tr '\n' ';') line=$(printf '%s' "$line" \ | sed -E "s/'[^']*'/''/g" \ | sed -E 's/"[^"]*"/""/g') printf '%s' "$line" | grep -qE "(^|[;&|(]|&&|\|\|)[[:space:]]*(sudo[[:space:]]+)?${name}([[:space:]]|$)" } ``` Without this check, any guard fires a false positive every time the command name shows up as data instead of an invocation, and the first instinct after getting hit by a false positive is to ignore the warning, worse than having no guard at all. On top of that, four small hooks: - **`mutation-guard.sh`** (`PreToolUse`): blocks write verbs on `aws`/`gh`/`kubectl`/`terraform`. With it on, you can open up `aws:*` and `gh:*` entirely in the `settings.json` allowlist without giving up control: the hook is what blocks mutation, not a granular permission list that's impossible to keep up to date. Explicit escape hatch per command: `ALLOW_MUTATION=1 `. - **`commit-guard.sh`** (`PreToolUse`): validates `git commit` against AGENTS.md's own commit rules, valid type, 50 characters, no trailing period, no tooling trailer. - **`session-context.sh`** (`SessionStart`): injects current branch (warning if `main`/`master`), dirty file count, and last commit, replacing what I'd run manually as the first task of the session. - **`redact-secrets.sh`** (`PostToolUse`): redacts any secret that leaked into stdout/stderr (GitHub token, OpenAI or Anthropic key, AWS access key, PEM block), trims fixed noise (Terraform's "Refreshing state..."), and truncates output above 30 KB while keeping the start and the end. Uses `updatedToolOutput`, the field that actually replaces what the model sees: PostToolUse isn't just a log after the fact, it rewrites the result before it becomes context. Beyond those four, one event deserves to be on the radar even without a custom hook: **InstructionsLoaded**, which fires when CLAUDE.md/rules are loaded, and is useful for debugging exactly the problem that motivates this whole article: confirming that the imported or symlinked file actually made it into that session's context, instead of finding out only when the model ignores a rule. ## Skills: one SKILL.md, symlinked to every CLI The same portability question applies to skills, and the answer is simpler, because the format already converged. All three tools accept the same shape: a directory named after the skill containing a `SKILL.md` with `name`/`description` frontmatter. What differs is where each one keeps its user-scoped skills: - **Claude Code**: `~/.claude/skills//SKILL.md` - **OpenCode**: `~/.config/opencode/skills//SKILL.md` - **AbacusAI**: `~/.abacusai/skills//SKILL.md` (on this machine `~/.abacusai/skills` is itself a symlink to `~/.abacusai/desktop/skills`, so the CLI and the desktop app share the same folder) AbacusAI adds more locations on top: project scope at `/.abacusai/skills/`, the cross-tool conventions `~/.agents/skills/` and `/.agents/skills/`, and the auto-managed built-ins under `~/.abacusai/builtin-skills//` (the `cowork` doc skills live there, extracted automatically, never edited by hand). This is also what "user level" in Abacus's AGENTS.md documentation actually maps to, as suspected in the previous section: skills, not instruction files. Since the format is identical, a skill needs only one canonical copy. Mine live in OpenCode's directory, and the other two tools point at it with the same symlink trick used for `AGENTS.md`: ```bash ln -s ~/.config/opencode/skills/v-repo-audit ~/.claude/skills/v-repo-audit ln -s ~/.config/opencode/skills/v-repo-audit ~/.abacusai/skills/v-repo-audit ``` Checking the result on this machine, both links resolve to the same file: ```bash $ ls -l ~/.claude/skills/ ~/.abacusai/skills/ /home/esli/.claude/skills/: v-repo-audit -> /home/esli/.config/opencode/skills/v-repo-audit /home/esli/.abacusai/skills/: v-repo-audit -> /home/esli/.config/opencode/skills/v-repo-audit ``` From then on, editing `SKILL.md` in the canonical directory updates the skill for every tool at once: no per-tool install step, no copies drifting apart. Skills are arguably a better fit for symlinks than `AGENTS.md` was, because they load on demand, so there is no global-vs-project loading conflict to resolve and nothing equivalent to OpenCode's mutually exclusive `AGENTS.md` discovery. Caveats, in the same spirit as the rest of this article: the canonical copy should live inside the directory of the tool you edit most (OpenCode here), the frontmatter `name` must match the directory name for discovery to work, and the symlink is only half of the story. Loaders differ between tools, and Claude Code's user scope is already known to ignore symlinked files in some sessions (the Cowork gap from earlier), so confirm each tool actually offers the skill by invoking it instead of assuming the directory listing means it loaded. ## Conclusion The central point isn't `AGENTS.md` itself, it's stopping the rebuild of the same base every time a new tool shows up. `AGENTS.md` covers what the model should try to follow; a hook covers what needs to hold always, without depending on the model remembering. One solves context, the other solves guarantee, and the three CLIs (Claude Code, OpenCode, AbacusAI) handle that pair differently enough to be worth documenting before assuming it "works the same everywhere." Full package, sanitized and tested: [github.com/Esl1h/ai-md-stack](https://github.com/Esl1h/ai-md-stack). --- ## Rádio online no Linux: curadoria humana, FLAC e nenhum algoritmo - URL: https://esli.blog/posts/radio-online-no-linux/ - Date: 2026-08-17 - Series: tech - Tags: linux, audio, radio, streaming, flac, pipewire, flatpak, terminal Existe um ponto na vida de qualquer assinante de streaming em que a playlist "feita para você" entrega o mesmo roteiro robotizado de sugestões ou pseudo match sem lógica feito por IA, o YouTube resolve que você quer um mix de lo-fi com um vídeo de dez minutos de música fake ou áudio sem qualidade mesclado com música pasteurizada feita por IA... e você percebe que falta algo além das playlists e artistas de sempre (ou simplesmente a necessidade de surpresa num random real). A solução tem 25 anos de idade e continua funcionando: rádio na internet. Um humano escolhendo a música, uma URL de Icecast, um player e nada mais. Sem conta, sem cookie de consentimento, sem "descubra semanalmente" feito por IA, tags ou regex de um database por semelhança. Este artigo é o complemento natural do [Qobuz no Linux](https://esli.blog/posts/qobuz-no-linux/): lá o assunto era extrair qualidade máxima de um serviço pago sem cliente oficial, aqui é conseguir a mesma disciplina técnica de graça. O parque de equipamentos está em [Meu Setup Musical](https://esli.blog/posts/meu-setup-musical/), e a parte móvel dessa história em [Transformando antigo smartphone em DAP](https://esli.blog/posts/transformando-antigo-smartphone-em-dap/). ## TL;DR - Base de dados de todo mundo: [radio-browser.info](https://www.radio-browser.info/), comunitário, API pública, sem chave. - Descoberta divertida: [Radio Garden](https://radio.garden/), o globo 3D com dezenas de milhares de estações. - Qualidade real: [Radio Paradise](https://radioparadise.com/) (FLAC lossless, mantido por doação) e [SomaFM](https://somafm.com/) (AAC-LC 128k, também por doação). - Cliente desktop: Shortwave (GNOME), Elisa (KDE), [Strawberry](https://www.strawberrymusicplayer.org/) para quem quer ver `sample rate` e `bit depth` na tela. - Cliente de terminal: [cliamp](https://www.cliamp.stream/) (padrão no Omarchy, também toca Spotify, Qobuz e coleção local), PyRadio ou `mpv` puro com um script de 20 linhas usando a API do radio-browser. ## Como rádio online funciona, tecnicamente Praticamente tudo que você vai ouvir é um servidor [Icecast](https://icecast.org/) (ou Shoutcast, o veterano proprietário) servindo um fluxo HTTP infinito com um codec de áudio dentro. O player abre a URL, não fecha, e vai decodificando. Metadados de faixa chegam por ICY (`Icy-MetaData: 1` no request, blocos de texto intercalados no fluxo) ou, nos casos mais modernos, por HLS com manifesto `.m3u8`. Isso importa por um motivo prático: rádio é um fluxo contínuo, não um arquivo. Não existe seek, o buffer é o que separa você de um `underrun`, e a taxa de bits é fixa durante toda a transmissão. | Codec | Bitrates típicos | Comentário | | --- | --- | --- | | MP3 | 128, 192, 320 kbps | Compatível com absolutamente tudo, eficiência ruim | | AAC-LC | 96 a 320 kbps | Melhor que MP3 no mesmo bitrate, padrão das estações que se importam | | HE-AAC (aacPlus) | 32 a 64 kbps | SBR para segurar agudos em banda baixa, teto de qualidade limitado | | Ogg Vorbis | 96 a 192 kbps | Bom, mas metadados em Ogg quebram em vários players e receptores | | Opus | 96 a 128 kbps | Melhor eficiência do lote, suporte irregular em hardware antigo | | FLAC sobre Icecast | ~700 a 1000 kbps | Lossless de verdade, 16 bits / 44,1 kHz na prática | Regra de bolso: 128 kbps AAC-LC soa melhor que 128 kbps MP3, e 320 kbps MP3 é bom o suficiente para 99% dos cenários com fone decente. FLAC em rádio existe, funciona, e custa cerca de dez vezes mais banda que o AAC de 128k. Se você paga internet por franquia, faça a conta antes de deixar tocando o dia inteiro. ## Indexadores e buscadores de rádio ### radio-browser.info É a infraestrutura invisível de quase todo app FOSS de rádio (Shortwave, Tuner, MusicPod, PyRadio, Strawberry, RadioDroid). Base comunitária, dezenas de milhares de estações, API REST pública, sem autenticação, com mirrors resolvidos por DNS round-robin em `all.api.radio-browser.info`. ```bash # descobrir os mirrors disponíveis dig +short all.api.radio-browser.info # top 20 estações brasileiras, com codec, bitrate e URL resolvida curl -sG 'https://de1.api.radio-browser.info/json/stations/search' \ -H 'User-Agent: esli-radio/1.0' \ --data-urlencode 'countrycodeexact=BR' \ --data-urlencode 'hidebroken=true' \ --data-urlencode 'order=clickcount' \ --data-urlencode 'reverse=true' \ --data-urlencode 'limit=20' | jq -r '.[] | "\(.name)\t\(.codec)/\(.bitrate)\t\(.url_resolved)"' # só o que transmite em FLAC, no mundo inteiro curl -sG 'https://de1.api.radio-browser.info/json/stations/search' \ -H 'User-Agent: esli-radio/1.0' \ --data-urlencode 'codec=FLAC' \ --data-urlencode 'hidebroken=true' \ --data-urlencode 'order=votes' --data-urlencode 'reverse=true' | jq -r '.[] | "\(.name)\t\(.url_resolved)"' # a API cospe playlist pronta, não precisa gerar no braço curl -s 'https://de1.api.radio-browser.info/m3u/stations/bytag/jazz?limit=30' > jazz.m3u ``` O mantenedor pede duas coisas: `User-Agent` descritivo (`app/versão`) e um `GET /json/url/` a cada reprodução, que é como o ranking de popularidade se alimenta. Custa uma linha de `curl` e mantém a base útil para todo mundo. ### Radio Garden O [Radio Garden](https://radio.garden/) é o globo giratório com pontos verdes: cada ponto é uma cidade com estações ao vivo, você arrasta o planeta e cai em uma rádio de bairro em Ulan Bator. Nasceu em 2016 em Amsterdã como projeto de pesquisa do Netherlands Institute for Sound and Vision com o Studio Moniker, virou empresa, e hoje passa de 40 mil estações. ![Radio Garden aberto no navegador Brave mostrando o globo terrestre em imagem de satélite, coberto por milhares de pontos verdes concentrados na Europa e na costa africana. Um círculo destaca o arquipélago da Madeira e o player inferior toca a Rádio Clube 106.8 FM de Funchal](/images/radio-online-no-linux/radio-garden-globo-funchal.jpg) Dando zoom, o globo vira mapa e cada ponto verde vira uma emissora com nome e cidade. O player fica no canto, o indicador diz LIVE, e não existe nada além disso: sem cadastro, sem recomendação, sem histórico. ![Radio Garden com zoom no Mediterrâneo oriental, exibindo Chipre, Israel e o delta do Nilo cobertos de pontos verdes. O card superior direito mostra a Kol Galim FM 106, de Kfar Galim, Israel, tocando ao vivo, e o menu inferior traz as abas Explore, Favorites, Browse, Search e Settings](/images/radio-online-no-linux/radio-garden-kfar-galim.jpg) É uma ferramenta de curiosidade e sem concorrentes. Serve muito bem para achar a estação e depois extrair a URL real no radio-browser ou no fmstream. ### Os outros que valem a barra de endereços | Serviço | Para quê | | --- | --- | | [fmstream.org](https://fmstream.org/) | Cerca de 100 mil estações e 200 mil streams, filtra por bitrate e mostra URLs diretas. O melhor para caçar o link cru | | [Icecast Directory](https://dir.xiph.org/) | O diretório oficial do Xiph, servidores Icecast que optaram por se anunciar | | [Shoutcast Directory](https://directory.shoutcast.com/) | O acervo legado, ainda enorme, ainda funcionando | | [Online Radio Box](https://onlineradiobox.com/) / [Streema](https://streema.com/) / [myTuner](https://mytuner-radio.com/) | Comerciais, ótimos de SEO, cheios de anúncio e telemetria. Úteis para achar a rádio AM/FM local | | [TuneIn](https://tunein.com/) | O maior de todos, com plano pago e o hábito de esconder a URL do stream | | [hiresaudio.online](https://www.hiresaudio.online/cd-quality-internet-radio/) | Lista curada de estações CD quality e hi-res em FLAC, atualizada há anos | | [Pulham/Internet-Radio-HQ-URL-playlists](https://github.com/Pulham/Internet-Radio-HQ-URL-playlists) | Playlists `.m3u` prontas com streams de alta qualidade | | [deroverda/recommended-radio-streams](https://github.com/deroverda/recommended-radio-streams) | Lista curada de eletrônica, freeform, jazz e ambient, com URLs diretas | ## As estações que justificam o artigo ### Radio Paradise Fundada em 2000, sem anúncio, sustentada por doação e venda de camiseta. A programação é montada por gente, não por modelo, e o acervo é armazenado em FLAC para não perder qualidade antes mesmo de codificar. Os canais atuais incluem Main, Mellow, Rock, Global (ex World/Etc), Beyond, Serenity e o Radio 2050. As URLs diretas: ```bash # FLAC lossless com metadados ICY (http de propósito, https quebra o title em vários players) mpv --no-video http://stream.radioparadise.com/flacm # Main mpv --no-video http://stream.radioparadise.com/mellow-flacm # Mellow mpv --no-video http://stream.radioparadise.com/rock-flacm # Rock mpv --no-video http://stream.radioparadise.com/global-flacm # Global mpv --no-video http://stream.radioparadise.com/beyond-flacm # Beyond # versões com menos banda mpv --no-video http://stream.radioparadise.com/aac-320 mpv --no-video http://stream.radioparadise.com/mp3-192 ``` Tem ainda uma API pública de "tocando agora", útil para widget de barra ou script de notificação: ```bash curl -s 'https://api.radioparadise.com/api/now_playing' | jq '{artist, title, album, time}' curl -s 'https://api.radioparadise.com/api/now_playing?chan=1' | jq . # Mellow ``` A lista completa e sempre atualizada fica em [radioparadise.com/listen/stream-links](https://radioparadise.com/listen/stream-links). Nos meus [dotfiles](https://github.com/Esl1h/dotfiles) tenho tanto um `.xspf` com as estações do Radio Paradise (FLAC e com metadados) para o Strawberry quanto um `.opml` com as mesmas estações para o MusicPod. Um grande problema é que salvar sua playlist de rádios não é fácil: não existe padronização entre os apps para importar e exportar uma lista de estações. ### SomaFM San Francisco, desde 2000, mais de 30 canais, zero anúncio, sustentada por doação mensal dos ouvintes. Groove Salad, Drone Zone, Secret Agent, Deep Space One, Underground 80s e a DEF CON Radio, que praticamente se explica sozinha para este público. A própria estação documenta a hierarquia de qualidade: os streams de 128k AAC-LC são os melhores, os de 64k HE-AAC vêm logo atrás, e o MP3 de 128k fica em terceiro. Alguns canais têm MP3 experimental em 256k ou 320k. A infraestrutura roda Icecast KH em servidores Ubuntu, o que é uma nota de rodapé simpática para um blog de Linux. ```bash # stream direto mpv --no-video https://ice5.somafm.com/groovesalad-128-aac mpv --no-video https://ice5.somafm.com/defcon-128-aac # gerar uma playlist com o melhor stream de cada canal curl -s https://somafm.com/channels.json | jq -r '.channels[] | "#EXTINF:0,\(.title)\n\(.playlists | max_by(.quality) | .url)"' | sed '1i #EXTM3U' > somafm.m3u ``` ### As demais que merecem estar no seu `.m3u` - [Nightride FM](https://nightride.fm/): synthwave, darksynth, EBSM e afins, 320 kbps, sem anúncio, comunidade no Discord. Stream direto em `https://stream.nightride.fm/nightride.mp3`. - [NTS Radio](https://www.nts.live/): Londres, curadoria absurda, canais temáticos ao vivo 24 horas. - [FIP](https://www.radiofrance.fr/fip): a Radio France sem locutor pisando na música, saltando de jazz para afrobeat sem pedir licença. - [KEXP](https://kexp.org/) e [WFMU](https://wfmu.org/): rádio pública e freeform americanas, ambas com acervo de sessões ao vivo. - [Nightwave Plaza](https://plaza.one/): vaporwave com interface de desktop dos anos 90, porque sim. - [Linn Radio](https://www.linn.co.uk/radio): Classical, Jazz e Radio, todos em FLAC, mantidos por um fabricante escocês de hi-fi. Um aviso sobre esse último grupo: rádio hi-res gratuita é frágil. A Mother Earth Radio, que transmitia quatro canais em FLAC 24 bits até 192 kHz digitalizando vinil com interface RME, encerrou as operações em 2026 depois de seis anos no ar. Salve suas playlists e não se apegue a uma URL específica. Para rádio brasileira, o caminho honesto é o radio-browser filtrando `countrycodeexact=BR`, porque as emissoras nacionais mudam de CDN com uma frequência que não sobrevive a um artigo de blog. ## Qualidade: o que chega no seu DAC Aqui vale exatamente o mesmo raciocínio do artigo do Qobuz. A estação manda 44,1 kHz, o PipeWire roda o grafo em 48 kHz por padrão, e alguém vai reamostrar. Como toda rádio FLAC transmite em 16/44,1, a checagem é rápida: ```bash # o que o servidor está mandando de fato ffprobe -hide_banner -i http://stream.radioparadise.com/flacm 2>&1 | grep -E 'Audio|Stream' # cabeçalhos ICY: nome, gênero, bitrate declarado curl -sI -H 'Icy-MetaData: 1' https://ice5.somafm.com/groovesalad-128-aac | grep -i '^icy' # o que o kernel entregou ao hardware cat /proc/asound/card*/pcm0p/sub0/hw_params # permitir que o grafo mude de taxa em vez de converter pw-metadata -n settings | grep clock ``` Se `default.clock.allowed-rates` incluir 44100, o grafo troca de taxa e o FLAC chega intacto. Se não incluir, você ouve uma reamostragem de 44,1 para 48 kHz o dia inteiro, o que é audivelmente irrelevante para a maioria e insuportável para quem leu até aqui. Para saída direta, sem passar pelo servidor de som: ```bash mpv --no-video --audio-device=alsa/hw:2,0 http://stream.radioparadise.com/flacm mpv --no-video --audio-display=no --display-tags=icy-title,icy-name https://ice5.somafm.com/dronezone-128-aac ``` Gravar o que está tocando também é uma linha, e a responsabilidade sobre o que você faz com o arquivo é inteiramente sua: ```bash mpv --stream-record=sessao.mka --no-video https://ice5.somafm.com/groovesalad-128-aac streamripper http://stream.radioparadise.com/aac-320 -d ~/Music/rips ``` ## Clientes de rádio para Linux ### GNOME e GTK **[Shortwave](https://apps.gnome.org/Shortwave/)** é a referência. Sucessor do Gradio, escrito em Rust com libadwaita, consome o radio-browser, tem MPRIS, reprodução em segundo plano, envio para dispositivos Google Cast e gravação automática das faixas em `~/Music`. Porém ele não suporta metadados nativos (como capa dos álbuns), apenas tags ICY. ```bash flatpak install flathub de.haeckerfelix.Shortwave # Arch paru -S shortwave ``` **[Goodvibes](https://gitlab.com/goodvibes/goodvibes)** é o oposto filosófico: C, GLib, LibSoup, GStreamer e GTK, consumo de memória ridículo, ideal para Raspberry Pi ou máquina velha. Não tem busca, você cola a URL e pronto. ```bash flatpak install flathub io.gitlab.Goodvibes ``` **[MusicPod](https://github.com/ubuntu-flutter-community/musicpod)** é o mais completo em escopo: rádio, podcast, TV e coleção local no mesmo app. É Flutter com `yaru.dart` por cima de um front-end para o mpv, mostra icytags e busca capas sob demanda. ```bash flatpak install flathub org.feichtmeier.Musicpod snap install musicpod ``` ![MusicPod com a barra lateral listando Search, Local, Radio e Podcasts, além dos canais salvos do Radio Paradise. No centro, a estação Radio Paradise Rock Mix FLAC+meta identificada como Station, 1442 kbps, OGG, com as tags california, alternative, eclectic, rock e free. O rodapé mostra a faixa exmagician - Keep Your Nose Clean com a capa do álbum](/images/radio-online-no-linux/musicpod-radio-paradise-rock.png) Repare no detalhe que separa o MusicPod da maioria: a capa da faixa vem de uma busca sob demanda a partir da icytag, então mesmo um stream que só manda `artista - título` em texto acaba com arte na tela. No modo tela cheia isso vira a interface inteira. ![Modo tela cheia do MusicPod, com fundo escuro em tom vinho extraído da capa, a arte da faixa exmagician - Keep Your Nose Clean ampliada no centro, o nome da estação Radio Paradise Rock Mix FLAC+meta abaixo e os controles de reprodução centralizados. A barra de tempo marca 64:52:11 de transmissão contínua](/images/radio-online-no-linux/musicpod-tela-cheia.png) O contador de tempo dessa tela é uma boa ilustração do que foi dito lá em cima: 64 horas e 52 minutos não são a duração da faixa, são o tempo que aquele fluxo está aberto. Rádio não tem fim de arquivo. Ainda no ecossistema GTK, o **Rhythmbox** tem a aba Rádio desde sempre e foi o player clássico/padrão de muitas distros por um bom tempo. Não busca no radio-browser, mas aceita URL colada, mostra o título ICY na barra superior e toca FLAC sobre Icecast sem reclamar. ![Rhythmbox com a aba Radio selecionada na barra lateral, listando 15 estações com título e gênero, entre elas HBR1.com, Kink, Studio Brussel e várias rádios universitárias americanas. A linha selecionada é a URL stream.radioparadise.com/rock-flacm e o cabeçalho mostra Radiohead - 15 Step em reprodução](/images/radio-online-no-linux/rhythmbox-radio.png) ### KDE e Qt **[Elisa](https://apps.kde.org/elisa/)** tem uma visão Rádios nativa, aceita adicionar estação por URL e respeita o esquema de cores do Plasma. A lista embutida é curta, o que na prática significa que você vai colar URLs vindas do radio-browser. Para uso casual no Plasma, resolve. ```bash sudo dnf install elisa flatpak install flathub org.kde.elisa ``` **[Strawberry](https://www.strawberrymusicplayer.org/)** é a escolha para quem trata rádio com o mesmo rigor do resto da coleção. A barra lateral Radios já traz provedores nativos de **Radio Paradise**, **SomaFM** e **Radio Browser**, além de streams customizados via `Playlist > Add Stream`. E, como comentei no artigo do Qobuz, as colunas `Sample Rate`, `Bit Depth` e `Source` na mesma linha economizam uma viagem ao `/proc`. Permite exportar/importar playlists de rádios. ![Strawberry Music Player com a barra lateral em Radios, a aba Channels mostrando os provedores Radio Paradise e SomaFM, e a playlist com os cinco streams FLAC do Radio Paradise. A faixa em reprodução, Unbelievable do EMF, exibe 44100 Hz na coluna Sample Rate, 16 Bit em Bit Depth, 933 kbps em Bitrate e FLAC em File Type, com a capa do single ocupando o painel central](/images/radio-online-no-linux/strawberry-radio-paradise-flac.png) São essas quatro colunas que resolvem a discussão sem `ffprobe`: 44100 Hz, 16 Bit, FLAC, 933 kbps de bitrate instantâneo. Nenhuma estimativa, nenhuma promessa da página da estação, é o que o decodificador está entregando naquele segundo. ```bash sudo dnf install strawberry # AppImage: https://appimage.github.io/Strawberry/ ``` Em `Configurações > Radios` você define a qualidade que o provedor do SomaFM vai pedir e como a busca no Radio Browser se comporta. Deixe o SomaFM em `Highest`, que é o AAC-LC de 128k, e marque `Hide broken stations` para não encher a lista de URLs mortas. ![Tela de configurações do Strawberry na seção Radios, com o bloco SomaFM definindo Stream quality como Highest e o bloco Radio Browser com limite de 100 resultados, a opção Hide broken stations marcada, ordenação padrão By votes e país padrão All countries. A barra lateral mostra as fontes de streaming Subsonic, Tidal, Spotify, Qobuz e Radios](/images/radio-online-no-linux/strawberry-configuracoes-radios.png) Configuração relevante em `Configurações > Backend`: `Output` em `alsasink`, `Device` no `hw:` do seu DAC (nunca `plughw:`), fading e replaygain desligados. **[Amarok](https://amarok.kde.org/)** voltou dos mortos e merece o crédito. Depois de seis anos parado, saiu o 3.0 em 2024, e o 3.3 "Far Above the Clouds", de julho de 2025, foi o primeiro totalmente em Qt6 e KDE Frameworks 6, com engine de áudio nova baseada em GStreamer. O 3.3.2, de janeiro de 2026, corrigiu justamente a gravação de URLs de stream em playlists, o que diz bastante sobre quem ainda usa Amarok para rádio. A seção Internet lista streams e podcasts, o gerenciador de scripts injeta listas prontas de estações e o scrobbling para Last.fm continua lá. ```bash sudo dnf install amarok flatpak install flathub org.kde.amarok ``` **Clementine**, o pai do Strawberry e inspirado no antigo amarok. **Cantata** e outros clientes de MPD entram na categoria seguinte: se o daemon toca, o cliente só controla. E vale uma passada na [KDE Store](https://store.kde.org/), que tem plasmoides de rádio e listas geradas direto da API do radio-browser (sinceramente, nunca testei e nem tentei achar algo aqui). ### Players universais Fora dos dois campos de desktop, qualquer player decente abre uma URL de Icecast. Os que valem menção: | Player | Por que está aqui | | --- | --- | | **VLC** | Toca literalmente qualquer coisa, inclusive FLAC sobre Icecast e HLS | | **Audacious** | Leve, cara de Winamp, aba de streams. O fork **Fauxdacious** trata metadados ICY melhor que o original | | **DeaDBeeF** | Minimalista, plugins, saída ALSA direta | | **Quod Libet**, **Sayonara**, **Tauon Music Box** | Gerenciadores de coleção que aceitam stream na fila | | **nuclear**, **odio** | Electron, ambos consomem o radio-browser, para quem não se importa com 300 MB de RAM | | **Kodi** | Addons de rádio e do Radio Paradise, o caminho para HTPC e TV | ### Terminal **[PyRadio](https://github.com/coderholic/pyradio)** é o mais completo entre os dedicados só a rádio: TUI em curses, integração com o radio-browser, backends intercambiáveis (mpv, MPlayer, VLC), temas e teclado no estilo vi. Instale pelo repositório da distro ou pelo script oficial, nunca pelo PyPI, que está abandonado. ```bash paru -S pyradio # Arch pipx install radio-active # alternativa em Python, mais simples cargo install termusic # TUI em Rust, também toca coleção local ``` **[cliamp](https://www.cliamp.stream/)** vem por padrão no [Omarchy](https://omarchy.org/) e é um Winamp de terminal escrito em Go, com Bubbletea na interface, Beep no áudio e `go-librespot` para o Spotify. `R` abre o navegador do radio-browser, com filtro por país e gênero e espaço para cadastrar URL própria, e a estação toca com metadados ICY ao vivo, equalizador paramétrico de 10 bandas, MPRIS e HLS. Fora da rádio ele toca arquivo local em MP3, FLAC, OGG, Opus, WAV, AAC, ALAC e WMA, e puxa de serviços que vão do Spotify e Qobuz ao Navidrome e Jellyfin. O `cliamp setup` abre um assistente para configurar os provedores remotos. ```bash yay -S cliamp # Arch (AUR) go install github.com/bjarneo/cliamp@latest # Go curl -fsSL https://raw.githubusercontent.com/bjarneo/cliamp/HEAD/install.sh | sh ``` ![cliamp rodando no terminal com o logotipo CLIAMP no topo, a faixa Santana Brothers - Luz Amor Y Vida, o contador 03:14 / LIVE e o indicador Streaming em verde. No centro, o visualizador de espectro em barras verdes sobre a linha STREAMING, o equalizador em modo Custom com as bandas de 70 Hz a 16 kHz, o volume em +0dB e a fonte SRC em Radio. A playlist mostra a estação Radio Paradise Main Mix (EU) 320k AAC e o rodapé traz os atalhos Esc, Space, Ctrl+K e N, com o contador de download em 7,7 MB a 62 KB/s](/images/radio-online-no-linux/cliamp-radio-paradise.png) O `↓ 7.7 MB 62 KB/s` no rodapé é o AAC de 320k do Radio Paradise chegando em tempo real. Consumo de banda na tela é o tipo de número que os players gráficos daqui não mostram. Se você quer o mínimo absoluto, `mpv` mais `fzf` mais a API do radio-browser resolvem o problema inteiro em um script: ```bash #!/usr/bin/env bash # ~/.local/bin/radio - busca no radio-browser e toca no mpv set -euo pipefail API='https://de1.api.radio-browser.info' UA='esli-radio/1.0' query="${*:-}" sel=$(curl -sG "$API/json/stations/search" -H "User-Agent: $UA" \ --data-urlencode "name=${query}" \ --data-urlencode 'hidebroken=true' \ --data-urlencode 'order=clickcount' \ --data-urlencode 'reverse=true' \ --data-urlencode 'limit=300' | jq -r '.[] | [.name, .countrycode, "\(.codec)/\(.bitrate)k", .stationuuid, .url_resolved] | @tsv' | fzf --with-nth=1,2,3 --delimiter='\t' --prompt='radio> ') || exit 0 uuid=$(cut -f4 <<<"$sel") url=$(cut -f5 <<<"$sel") # contabiliza o clique para a base comunitária curl -s -o /dev/null -H "User-Agent: $UA" "$API/json/url/$uuid" & exec mpv --no-video --display-tags=icy-title,icy-name --term-osd-bar "$url" ``` Duas dependências, nenhuma janela, integra com Waybar via `playerctl` porque o mpv expõe MPRIS. É o tipo de coisa que sobrevive a três trocas de desktop environment. ### MPD como rádio da casa Se você já roda MPD, rádio é só mais uma entrada na fila, com a vantagem de o daemon continuar tocando quando a sessão gráfica cair: ```bash mpc add http://stream.radioparadise.com/flacm mpc play mpc current -f '%name% | %artist% - %title%' ``` Combinado com [Snapcast](https://github.com/badaix/snapcast) você tem multiroom sincronizado sem depender de nuvem alguma. E, para quem já montou o cenário do artigo do Qobuz, [Lyrion](https://lyrion.org/) e [Music Assistant](https://www.music-assistant.io/) tratam rádio como mais uma fonte ao lado do serviço pago. ### Comparativo | Cliente | Stack | Base de estações | Destaque | Instalação | | --- | --- | --- | --- | --- | | Shortwave | Rust, GTK4 | radio-browser | Grava faixas, Chromecast, MPRIS | Flatpak, AUR, Nix | | Tuner | Vala, GTK | radio-browser | Interface, favoritos | Flatpak | | Goodvibes | C, GStreamer | Manual (URL) | Consumo mínimo | Flatpak, deb, AUR | | MusicPod | Flutter, mpv | radio-browser | Rádio, podcast e local juntos | Flatpak, Snap | | Elisa | Qt, KDE | Manual (URL) | Integração com Plasma | Distro, Flatpak | | Amarok | Qt6, GStreamer | Manual e scripts | Streams, podcast e scrobbling no mesmo lugar | Distro, Flatpak | | Strawberry | Qt, GStreamer | RP, SomaFM, radio-browser | Saída ALSA direta, metadados técnicos | Distro, AppImage | | PyRadio | Python, curses | radio-browser | TUI completa, backend trocável | Distro, script | | mpv + script | C, shell | radio-browser (API) | Zero interface, total controle | Já está instalado | | MPD + mpc | C | Manual (URL) | Headless, multiroom | Distro | ## Rádio online paga existe? Existe, e o modelo é quase sempre o mesmo: o tier gratuito tem anúncio e bitrate capado, o pago remove os dois. | Serviço | O que a assinatura entrega | | --- | --- | | [DI.FM](https://www.di.fm/) e a família AudioAddict (RadioTunes, JazzRadio, ClassicalRadio, RockRadio) | Sem anúncio e escolha entre 320k MP3 ou 128k AAC. O diferencial para Linux é a *listen key*, que gera `.pls` autenticado e toca em mpv, MPD ou Strawberry sem app proprietário | | SuperStereo Network | Virou serviço por assinatura, com canais em FLAC lossless a partir de vinil e SACD | | [Calm Radio](https://calmradio.com/) | Freemium, centenas de canais de wellness, premium sem anúncio e em qualidade HD | | [Audiophile.fm](https://audiophile.fm/) | Agregador de estações lossless com camada paga e EQ próprio | | [TuneIn](https://tunein.com/) Premium | Paga por esporte, audiolivro e ausência de anúncio, não por bitrate da estação | | SiriusXM | Assinatura clássica, sem cliente Linux, sobra o web player | O critério prático para quem usa Linux é um só: se o serviço entrega uma URL ou um arquivo `.pls` autenticado, ele toca em qualquer player desta lista. Se depende de app próprio ou DRM, você volta para a aba do navegador, e aí já sabe o desfecho. Radio Paradise e SomaFM ficam de fora dessa tabela de propósito pois funcionam e existem devido a doação e não há restrições, ads ou paywall. ## No navegador: extensões e PWA Nem todo mundo quer instalar aplicação para ouvir rádio, e o navegador dá conta. **Extensões** existem, a mais completa sendo a *Radio Player*, disponível para Firefox, Chrome, Edge e Opera, com mais de 20 mil estações, e também na forma de webapp. Tem ainda a *TuneYou Radio* no Firefox. A ressalva é a de sempre: a maioria dessas extensões é reembalagem da base do radio-browser pedindo permissão para ler todos os sites que você visita. Para tocar música. Pense duas vezes. **PWA** é o caminho mais limpo, usando o mesmo truque do artigo do Qobuz: `Instalar página como aplicativo` no Chromium, e o site ganha janela própria, ícone e, em alguns casos, MPRIS. Os que funcionam bem assim: - [radio.garden](https://radio.garden/), o globo instalado como app - [somafm.com](https://somafm.com/) e [radioparadise.com](https://radioparadise.com/), cujos players web tocam FLAC em navegadores Chromium - [nightride.fm](https://nightride.fm/) - [plaza.one](https://plaza.one/) - [poolsuite.net](https://poolsuite.net/) - [radio-browser.info](https://www.radio-browser.info/) como buscador instalado, para caçar URL sem abrir aba Dois merecem parágrafo próprio: O [Radiolise](https://radiolise.com/), também acessível em [radiolise.gitlab.io](https://radiolise.gitlab.io/), é a interface web mais completa que existe em cima do radio-browser: busca na base comunitária, listas próprias de estações, metadados ICY em tempo real vindos por WebSocket do backend, HLS para canais de TV e um analisador de espectro. É Vue 2 sob AGPLv3, com o código em [gitlab.com/radiolise](https://gitlab.com/radiolise/radiolise.gitlab.io), e dá para subir instância própria com um `npx radiolise` ou o Docker Compose do repositório, sem depender do servidor de ninguém. Ele não publica manifest nem service worker, então a instalação é exatamente pelo `Instalar página como aplicativo` do Chromium. O [scrobblerad.io](https://scrobblerad.io/) resolve outro problema, o de scrobbling de rádio, que é notoriamente mal resolvido. São mais de 170 estações escolhidas justamente por terem API pública de metadados, incluindo KEXP, WFMU, BBC Radio 6 Music, FIP, Radio Paradise e vários canais da SomaFM. Título e artista passam pelo metadata-filter do Web Scrobbler antes de subir, e o envio ao Last.fm é nativo depois de você autorizar a conta, sem precisar de extensão. Também expõe MediaSession, o que no Linux vira MPRIS, então as teclas de mídia trocam de estação. O [código é MIT](https://github.com/jbwharris/scrobblerad.io) em HTML, CSS e JavaScript puros, estático o bastante para você hospedar sua cópia copiando arquivos para qualquer servidor web. Qualquer PWA continua sujeito ao WebAudio na taxa fixa do grafo, normalmente 48 kHz, então o FLAC de 44,1 kHz vai ser reamostrado antes de chegar ao DAC. Exatamente a mesma conclusão a que cheguei medindo o web player do Qobuz, pelo mesmo motivo. ## Perguntas rápidas **Rádio online tem qualidade de streaming pago?** Em FLAC, sim: Radio Paradise transmite lossless 16 bits / 44,1 kHz, o mesmo do CD e do tier básico de qualquer serviço pago. O que você não tem é escolha de faixa nem catálogo sob demanda. **Qual o melhor bitrate para ouvir no trabalho?** 128 kbps AAC-LC. Consome cerca de 55 MB por hora e é transparente o suficiente para fone de escritório. **Dá para usar rádio online sem app nenhum?** Sim. `mpv URL` resolve, e o mpv já está instalado em praticamente toda distro moderna. **Como descobrir a URL real de uma rádio que só tem player web?** Procure no radio-browser ou no fmstream primeiro. Se não estiver lá, o DevTools do navegador na aba Network filtrando por `media` ou `m3u8` entrega o endereço em segundos. **Vale pagar por rádio online?** Só se o serviço entregar URL ou `.pls` autenticado, que é o caso do DI.FM. Assinatura que exige app próprio não tem cliente Linux e te devolve para o navegador. **Rádio online rastreia?** Os agregadores comerciais sim, com fartura. Um stream direto de Icecast vê seu IP e seu User-Agent, e nada mais. Vale a leitura cruzada com o que já escrevi sobre privacidade aqui no blog. ## O que eu uso No desktop Fedora e EndeavourOS (ambos com KDE), Strawberry, pelo mesmo motivo de sempre: saída ALSA direta e as colunas técnicas visíveis. Quase sempre Radio Paradise em FLAC e o Radio Garden quando a curiosidade fala mais alto que a qualidade. ## Referências - [radio-browser.info](https://www.radio-browser.info/) e a [documentação da API](https://api.radio-browser.info/) - [Radio Garden](https://radio.garden/) - [Radio Paradise, stream links](https://radioparadise.com/listen/stream-links) - [SomaFM, FAQ técnico](https://somafm.com/about/faq.html) - [Shortwave](https://apps.gnome.org/Shortwave/), [Goodvibes](https://gitlab.com/goodvibes/goodvibes), [Tuner](https://github.com/louis77/tuner), [MusicPod](https://github.com/ubuntu-flutter-community/musicpod) - [Strawberry Music Player](https://www.strawberrymusicplayer.org/), [Elisa](https://apps.kde.org/elisa/) e [Amarok](https://amarok.kde.org/) - [PWAsForFirefox](https://github.com/filips123/PWAsForFirefox), para instalar PWA no Firefox - [Radiolise](https://radiolise.com/) e o [código no GitLab](https://gitlab.com/radiolise/radiolise.gitlab.io) - [scrobblerad.io](https://scrobblerad.io/) e o [código no GitHub](https://github.com/jbwharris/scrobblerad.io) - [PyRadio](https://github.com/coderholic/pyradio) e [cliamp](https://github.com/bjarneo/cliamp), [site](https://www.cliamp.stream/) - [Lista de estações CD quality em FLAC](https://www.hiresaudio.online/cd-quality-internet-radio/) - [Qobuz no Linux](https://esli.blog/posts/qobuz-no-linux/), aqui mesmo --- ## ADS-B on Arch Linux: from an RTL-SDR dongle to a live map, and the installer it took - URL: https://esli.blog/posts/adsb-on-arch-linux/ - Date: 2026-08-13 - Series: tech - Tags: adsb, sdr, rtl-sdr, readsb, tar1090, arch-linux, easy1090, linux *This is the English version of a series written in Portuguese, condensed into one article. The original starts [here](https://esli.blog/posts/sdr-radio-no-linux/) and runs to eight parts, all listed under the [adsb tag](https://esli.blog/tags/adsb/).* I live under one of the busiest approach paths in Latin America. When the wind shifts and Guarulhos reverses its approach, I know before the tower does, by the sound of the engines. For years that was filed under the disadvantages of the property. Then I realised something. Every one of those aircraft is continuously broadcasting its identity, position, altitude and speed, in the clear, on 1090 MHz, to anyone with an antenna. I work in infrastructure, which means I spend my days looking at dashboards measuring somebody else's machines. The idea of generating my own data, from the physical world, was too good to pass up. This is the short version of a series I wrote in Portuguese over eight articles. It ends with a tool, because getting there on Arch Linux turned out to require one. The tool is at [github.com/Esl1h/easy1090](https://github.com/Esl1h/easy1090), MIT licensed. ## Why aircraft are the perfect first target ADS-B stands for Automatic Dependent Surveillance Broadcast. Every modern aircraft transmits a 112 bit frame roughly twice a second, containing a unique 24 bit ICAO address, a callsign, position, altitude, ground speed and vertical rate. The frame has no authentication of any kind. There is no signature, no encryption, no challenge. The 24 bit CRC at the end tells you whether the message survived the air, not whether the aircraft told the truth. A protocol designed at MIT Lincoln Laboratory in the 1970s, running at 1 Mbps, on a shared channel, with the transport philosophy of UDP broadcast taken to its logical extreme: no ACK, no retransmission, no flow control. That is uncomfortable if you think about security, and it is exactly what makes the hobby possible. Nothing is being broken into. The signal arrives at your window whether you listen or not. The frame itself is worth a close look, because it is beautifully small. Here is a real one, as your decoder hands it to you: ``` 8D4840D6202CC371C32CE0576098 ``` Twenty-eight hexadecimal characters, which is 112 bits, which is 14 bytes. They break down like this: | Field | Size | What it is | | --- | --- | --- | | DF | 5 bits | Downlink format, the message type. 17 means ADS-B Extended Squitter | | CA | 3 bits | Capability, what this transponder can do | | ICAO | 24 bits | The aircraft's unique address, a MAC address that flies | | ME | 56 bits | The payload: position, altitude, velocity or identification | | CRC | 24 bits | The seal. If it does not add up, the receiver drops the frame | Seven bytes of actual payload. An empty UDP header is eight. In that example, `8D` gives downlink format 17, `4840D6` is the ICAO address, and the payload decodes to the callsign `KLM1023`. The ICAO address is the interesting field for anyone doing infrastructure. It is permanent, assigned to the airframe rather than to the flight, and it is the primary key of the entire hobby: it is what lets you look up registration, type and operator, and what lets you follow the same physical aircraft across days and callsigns. Add an 8 microsecond preamble of four pulses to synchronise the receiver and the whole thing occupies 120 microseconds of air, at 1 Mbps, using pulse position modulation. Fitting a global position into 56 bits alongside altitude and status required a genuinely clever trick, called Compact Position Reporting. Instead of transmitting full latitude and longitude, the aircraft alternates between two encodings, called even and odd frames, each carrying a position relative to a grid. A receiver that has both can resolve the absolute position unambiguously. A receiver that already knows roughly where the aircraft is can resolve it from a single frame. That is why your decoder sometimes shows an aircraft with altitude and speed but no position yet: it is holding one half of a pair, waiting for the other. The collision strategy is equally frugal. The 1090 MHz channel is shared, and it carries not only ADS-B but also transponder replies to secondary radar and to other aircraft's collision avoidance systems. There is no coordination whatsoever. Each aircraft randomises the interval between position messages, somewhere between 0.4 and 0.6 seconds, purely so that two transmitters do not settle into lockstep. It is ALOHA, the 1970s solution, still flying. If you want the protocol in depth, I wrote it up [here, in Portuguese](https://esli.blog/posts/adsb-sdr-radio-no-linux/). And if software defined radio itself is new to you, the [About RTL-SDR](https://www.rtl-sdr.com/about-rtl-sdr/) page is the canonical starting point: the whole hobby exists because someone noticed that a cheap DVB-T television tuner could be repurposed into a wideband receiver. ## The hardware, and the one thing worth paying for A 1090 MHz receiver costs about the same as a pizza. Search for the RTL-SDR Blog V4 and you will find a flood of near-identical listings at a third of the price. The photos are the same. The descriptions swear they are V4s. They are not. Two chips matter. The RTL2832U does analogue to digital conversion and demodulation, and it is essentially identical across the whole family, from the cheapest clone to the official unit. The tuner is where they diverge. The V4 uses an R828D with a redesigned front end and proper filtering, which matters enormously in a city. The RF spectrum in a metropolitan area is not a quiet field, it is a crowded market: commercial FM stations transmitting with absurd power, mobile towers everywhere, all of it dumping energy into your antenna at once. A receiver without decent filtering simply chokes, and a strong signal from a local station leaks into the band you actually want as a ghost. The other differences that never show up in a listing thumbnail: a 1 PPM TCXO, a temperature compensated oscillator, so your frequency does not drift as the dongle warms up over hours of unattended decoding; a software switchable bias tee to power a low noise amplifier at the antenna; and an integrated HF upconverter. ![RTL-SDR Blog V4 dongle resting on its printed manual, with RTL2832U, R828D, TCXO, bias-T and HF printed on the aluminium case](/images/rtl-sdr-v4/dongle-e-manual.jpeg) The aluminium case is not decoration either: it dissipates heat, which helps that oscillator stay stable, and it shields the circuit from external interference. Clones usually ship in plastic, or in aluminium that is purely cosmetic. There is also an unexpected proof of authenticity. A real V4 requires the RTL-SDR Blog fork of `librtlsdr` and misbehaves with the standard driver most distributions ship. That installation friction is close to a certificate of originality: a clone pretending to be a V4 usually runs perfectly on the stock driver, because inside it is the same old R820T2. ![Contents of the kit: flexible tripod, suction cup mount, extension cable and the telescopic dipole elements](/images/rtl-sdr-v4/kit-antenas.jpeg) For 1090 MHz specifically, use the shortest antenna element from the kit, mounted vertically on the single stand. ADS-B is vertically polarised, the wavelength is about 27.5 cm, so a quarter wave element is roughly 6.9 cm, and reception depends on line of sight. The V shaped mount that comes in the kit is for 137 MHz weather satellites, not for aircraft near the horizon. My own station, in its provisional bench version: the kit's flexible tripod clamped onto the homelab mini PC, antenna vertical. It is not a permanent installation and it still decodes aircraft over 200 km away: ![Homelab mini PC with the kit's flexible tripod mounted on top of the case and the telescopic antenna extended vertically](/images/rtl-sdr-v4-adsb-1090/server-antena.png) ### The USB port that accepted a YubiKey and rejected a radio One debugging story from that install is worth repeating, because the lesson generalises. On my Fedora desktop the dongle enumerated, failed to configure, disconnected, and looped, filling `dmesg` with `error -71`, the USB subsystem's EPROTO, until the kernel gave up with its most passive-aggressive message: ``` usb usb1-port11: Cannot enable. Maybe the USB cable is bad? ``` It was not the cable. It was the port, and I had not suspected it because that same port had been hosting a YubiKey for months without a single incident. A YubiKey is a full-speed device: 12 Mbps, drawing about 30 mA. Almost any port with a marginal trace or weak power delivery will carry that happily. An RTL-SDR V4 is high-speed, 480 Mbps, drawing around 300 mA, and it exposes every electrical weakness a light device hides. The port was not good, it was good enough for cryptography and insufficient for radio. The lesson worth taking: "it works with another device" does not validate a USB port. It validates the port for that class of device. Move to a rear port wired directly to the CPU's own controller before you suspect the hardware or open an issue against the driver. Details, and the two very different installation experiences I had on Arch and on Fedora, are [here](https://esli.blog/posts/rtl-sdr-v4/). ## The stack Three pieces, and each one earns its place. **readsb** is the decoder, the spiritual successor to dump1090. It consumes raw I/Q samples at 1090 MHz, finds the preambles of transponder messages, decodes them, validates the CRC, and serves the result over several network interfaces. Use the wiedehopf fork, not the Mictronics one: the latter writes `aircraft.pb`, binary protobuf, with no flag to produce JSON, and every web frontend expects JSON. I learned that the hard way, mid project, and had to swap forks. **tar1090** is the live map, the thing that turns text streams into something a human wants to look at: ![tar1090 web interface showing aircraft over the Guarulhos area with altitude coloured trails, a table of flights and a detail panel for a LATAM Boeing 787-9](/images/rtl-sdr-v4-tar1090/tar1090.png) Aircraft icons sit on real positions, trails are coloured by altitude, and clicking one opens everything the transponder is sending, enriched by tar1090's own aircraft database: registration, airline, type, route, altitude with trend, speed, track and signal strength. Here is a 787-9 leaving Guarulhos for Amsterdam: ![tar1090 detail panel for flight TAM8078, a Boeing 787-9 on the GRU to AMS route, over a map with altitude coloured trails](/images/rtl-sdr-v4-tar1090/tar1090-detalhado.png) The hover tooltip answers the question that started all of this, the one you ask looking up at the sky. That is an A330 of South African Airways climbing through 9,250 feet: ![tar1090 map with a tooltip for flight SAA227, an Airbus A330 of South African Airways, showing registration, altitude, speed and RSSI](/images/rtl-sdr-v4-tar1090/tar1090-voo.png) **lighttpd** serves it. That is the whole thing. The dongle emits I/Q samples over USB at about 4.8 MB/s, readsb turns pulses into aircraft, tar1090 draws them. What makes readsb pleasant to build on is that it does not hide the data behind the map. It exposes the same decoded traffic through several interfaces at once, and you pick the one that fits what you are doing: | Interface | What it gives you | | --- | --- | | `/run/readsb/aircraft.json` | current state of every tracked aircraft, refreshed once a second | | port 30003 | SBS/BaseStation, one CSV line per decoded message, ideal for `grep` and logging | | port 30002 | raw Mode S in hex, no position decoding, for debugging the decoder itself | | port 30005 | Beast binary, what other tools and feeders consume | The JSON is the one that matters most, because it is what the map reads and what any script of yours should read too. Everything else is a stream you can pipe. ## Where the friction lives Here is the part nobody writes down, and the reason this article ends with a tool. Practically every ADS-B guide in existence assumes Raspberry Pi OS or Debian. That is a reasonable assumption in market terms, since the overwhelming majority of receivers in the world are a Pi on a windowsill. It produces fragile code, and on Arch you meet all of it at once. A sample of what I hit, each of which cost real debugging: `pacman --noconfirm` answers **N** to the prompt that replaces a conflicting package, silently aborting the install rather than proceeding. `blacklist` in modprobe.d only stops a module from autoloading at boot. On hotplug, udev asks for it by alias and the kernel hands it over anyway, so the kernel's DVB driver claims your dongle the moment you replug it. The `install /bin/false` line is what actually closes that door, and it is missing from almost every tutorial. The stock `udev` rule grants the device to the `plugdev` group. That appears to work, because systemd-logind adds a session ACL for your interactive user, which masks the problem completely. The readsb service user has no session and no ACL, so the daemon gets EACCES while your manual test succeeds. Arch's `lighttpd.conf` is minimal and never includes `conf-enabled`, so the entire configuration the tar1090 installer writes is dead on arrival, with no error whatsoever. Arch does not load `mod_redirect`, so tar1090's `url.redirect` is ignored and the slash-less URL returns 404. That is precisely the URL their installer prints when it finishes. And then there is the compiler. The Mictronics fork of readsb hardcodes `-Werror` in its Makefile and has had no maintenance since around 2020, which breaks against a current GCC in two distinct ways. First, new warnings that become errors because of the project's own flag, which you can work around by removing it. Second, and far more treacherous, diagnostics that GCC 14 started treating as errors **by default**, regardless of `-Werror`: `incompatible-pointer-types`, `implicit-function-declaration`, `int-conversion`. Those need explicit `-Wno-error=` entries, and no amount of removing `-Werror` will help. Patching that is its own trap. Editing the `PKGBUILD` inside `~/.cache/yay/` does nothing, because yay resets it to the upstream state on every run. That is correct anti-tampering behaviour and completely incompatible with manual patching, so the build has to happen from a copied directory, outside the helper's control. None of this teaches you anything about radio. It is a tax you pay for not running Debian. ## So I wrote an installer `easy1090` is about 2,700 lines of bash that encode every one of those workarounds, plus the ones that came later. ```bash git clone https://github.com/Esl1h/easy1090.git cd easy1090 ./easy1090 install ``` ![Output of easy1090 with no command, showing an error and the full usage with the nine available subcommands and the global options](/images/adsb-on-arch-linux/help.png) Before it touches anything as root, you can see exactly what it would do. `--dry-run` runs the entire read-only preflight and prints the literal commands, not descriptions of them: ![Output of easy1090 install --dry-run, with the preflight passing, SKIP lines for everything already in place and DRY lines showing the exact commands that would run](/images/adsb-on-arch-linux/dry-run.png) That screenshot shows two design decisions at once. The `[DRY]` lines are commands you could paste yourself, which is the minimum I think you owe someone before asking them to run a script as root. And the volume of `[SKIP]` is idempotency working: on a machine that is already configured, almost everything is recognised and skipped. Re-running is how you update, there is no separate mode. A complete run, with `--full`, on a machine that already had everything: ![Full output of easy1090 install --full, from preflight to final validation, ending with all three services active and enabled and 10 aircraft being decoded](/images/adsb-on-arch-linux/install-full.png) The final validation does not trust its own installation. It queries systemd, measures the age of the JSON file, and makes a real HTTP request against the map before declaring anything is up. Two commands never ask for `sudo`, deliberately, because checking the state of your system should not cost a password: ![Output of easy1090 status listing hardware and driver, decoding with 12 aircraft, the web services and the optional components, each with state and version](/images/adsb-on-arch-linux/status.png) And `open` runs things in your current terminal rather than spawning windows, because this stack usually lives on a headless box you reach over SSH: ![Output of easy1090 open listing the available targets: viewadsb, sbs, map, sdrpp and satdump](/images/adsb-on-arch-linux/open-targets.png) ## Watching the traffic The map is the destination, but it is not the only way in, and the others are often more useful. Three of them running side by side, which is how I usually leave a session while working on this: ![Three terminals on the server: viewadsb with its live table at the top, a custom summary script in the middle, and the raw SBS stream from nc at the bottom](/images/rtl-sdr-v4-adsb-1090/terminal-viewadsb-nc.png) `viewadsb` ships with readsb and gives you an ncurses table that updates live. It needs a real terminal, since it takes over the screen. The header looks like this: ``` Hex Mode Sqwk Flight Alt Spd Hdg Lat Long RSSI Msgs Seen ``` Those abbreviations are not obvious if you did not grow up around aviation, so here is what each one means: | Column | Meaning | | --- | --- | | `Hex` | The aircraft's unique ICAO code, a sort of licence plate for the transponder | | `Mode` | Transponder mode. S means Mode S, the modern standard | | `Sqwk` | Squawk, the four digit code the pilot sets. 7700 is a general emergency | | `Flight` | Flight number or callsign, for example `TAM3994` | | `Alt` | Altitude in feet | | `Spd` | Ground speed in knots | | `Hdg` | Track, in degrees from 0 to 360 | | `Lat`/`Long` | Position | | `RSSI` | Received signal strength. Closer to zero means stronger | | `Msgs` | How many messages that aircraft has sent you so far | | `Seen` | Seconds since the last message from it | `RSSI` is the one to watch while you are positioning the antenna: move it, and the numbers tell you immediately whether you improved anything. `Seen` climbing means an aircraft is leaving your coverage. The SBS stream on port 30003 is the one I reach for most, because it is a line per message in CSV and therefore composable with everything you already know: ![Raw SBS message stream, one decoded ADS-B message per line in CSV format with ICAO addresses, timestamps, altitudes and coordinates](/images/adsb-on-arch-linux/open-sbs.png) That is the raw material of the whole hobby: one decoded transponder message per line, straight off the antenna, seconds old. From there it is ordinary Unix: ```bash nc localhost 30003 | tee -a ~/adsb-$(date +%F).csv # log everything nc localhost 30003 | grep E491F0 # follow one aircraft jq '.aircraft | length' /run/readsb/aircraft.json # how many right now ``` Reading those lines is simpler than it looks. Take one: ``` 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 ``` After `MSG` comes the transmission type, then some session fields, then the ICAO hex address, then the date and time the message was generated and logged, and after that the payload: callsign, altitude, ground speed, track, latitude, longitude, vertical rate and status flags. That one is a type 3, an airborne position message from aircraft `E491F0` at 10,750 feet. The columns look ragged because each message type carries a different subset. Type 3 brings position, type 4 brings velocity and heading, type 1 brings the callsign. No single message gives you the full picture, which is exactly why a decoder exists: it accumulates messages per aircraft over time and maintains the state that the map draws. I wrote a small script that reads the JSON with `jq` and prints a table with full column names instead of abbreviations, for the times when I want to glance rather than parse. It is in the [Portuguese guide](https://esli.blog/posts/guia-visualizacao-adsb/), along with every other interface in detail. ## What real testing taught me The installer passed shellcheck. The dry-run looked perfect. Then I ran it on a clean machine, and the first install worked. **The second one is where everything appeared.** I re-ran it with new coordinates. It wrote the config correctly and called `systemctl enable --now readsb`, which does nothing to a unit that is already active. The daemon kept running on the previous configuration, with no error anywhere. The proof was in the timestamps: config written at 16:16, process up since 15:39, and `journalctl` reporting the old latitude while the file on disk had the new one. Fixing that was not enough. On the third run, a config file existed, its symlink existed, and the URL still 404ed, because the file had been written by an earlier run that never restarted the daemon. Presence of a file does not prove the process read it. The fix compares the unit's `ActiveEnterTimestamp` with the file's mtime: if the service started before the file was written, it cannot possibly have loaded it. Then, testing the full install, I watched yay remove 24 packages at the end, including `airspy`, `hackrf`, `bladerf`, `rtaudio` and `soapysdr`. The line immediately above listed those same packages as optional dependencies of SDR++, all marked installed. It installed and uninstalled them in the same run. That one was my fault, from a flag I added thinking it was housekeeping: `--removemake`. AUR packages routinely list the same library in both `makedepends` and `optdepends`, needed to build a plugin and needed again at runtime to load it. yay only sees the build side. The damage, measured with `ldd` across the plugin directory: ten plugins without their libraries, including the audio sink. SDR++ opened fine, showed as installed, and had no audio output. A fourth case is more subtle, and it is the one that taught me the most. On a re-run, the installer warned that it could not identify the tuner. Investigating, `rtl_test` was ending with `usb_claim_interface error -6`. Nothing was broken: readsb was already running and holding the device, so the test enumerated the card and could not claim it. A healthy system producing an alarming message, which is the mirror image of the other three. And one of them was entirely mine. When I moved the separate scripts into a single entrypoint with subcommands, the module files started being sourced from inside a function, and `declare` inside a function creates a local variable: ```bash f(){ source /dev/stdin <<< "declare A=1 readonly B=2"; } f echo "A=${A:-GONE} B=${B:-GONE}" # A=GONE B=2 ``` `readonly` survives, `declare` does not. Every module constant used `readonly` and kept working; the two state variables that used `declare` became ghosts. The `uninstall` command started failing with an unbound variable on its first real use, and I did not notice, because I never re-tested it after the refactor. It surfaced weeks later when I wrote a new command that broke the same way. The lesson there is not about bash. It is that a refactor which changes the **execution context** of your code invalidates every test you ran before it, and I had only revalidated what I happened to exercise that day. Look at what those failures have in common: **none of them produces an error**. The service is green in systemctl while running the wrong config. The file is there and was never read. The program is installed and half its plugins do not load. The hardware test fails precisely because the hardware is in use. Noisy failures are easy, they tell you where to look. These require you to distrust a system that is actively telling you it is fine. The only way I found to catch them was running again, and again, on real hardware. That changed my definition of done: an installer that works on the first run is not finished, it is untested. ## Feeding the networks, if you want to Your receiver sees a few hundred kilometres. Thousands of receivers together see the world. Contributing is optional, off by default in easy1090, and asked explicitly, because sending your data and your IP to a third party should be a deliberate act. Two networks are worth knowing about, and the difference between them is political rather than technical. **ADSBExchange** built its reputation on not filtering. In the US, the FAA runs a programme called LADD that lets aircraft owners request their flights be hidden, and the large networks honour those requests. ADSBExchange refused, which is why it became the source of choice for journalists tracking private jets belonging to oligarchs and billionaires. In January 2023 it was sold to JETNET, an aviation data intelligence company, for a reported figure around 20 million dollars. The community, made of unpaid volunteers who had donated the hardware and the bandwidth, reacted badly, and the fear was obvious: a company selling aviation intelligence to corporate clients has commercial incentives a community does not. **airplanes.live** is one of the community run alternatives that grew after that sale. You can feed both at once. They are independent TCP connectors on the same readsb, and feeding one does not disturb the other: ```bash ./easy1090 feed # list the networks and their state ./easy1090 feed adsbexchange ./easy1090 feed airplaneslive ``` Once you are feeding, each network gives you a page confirming it. ADSBExchange checks by IP and tells you whether the ADS-B connection and the statistics package are both alive: ![ADSBExchange myip page showing Feeder Status with ADS-B connected and stats ok, with the IP and Feed UID blurred](/images/adsbexchange-feed/myip-status-do-feeder.png) Their statistics page plots what your receiver has been delivering, one sample per minute. The dip in the middle is the small hours: it is the traffic over Guarulhos drawn by hour of day: ![ADSBx Anywhere Stats page with a bar chart of aircraft count over 218 samples and a table of captured flights with position, altitude and RSSI](/images/adsbexchange-feed/stats-anywhere-aeronaves.png) airplanes.live does the same, more compactly: ![airplanes.live myfeed page showing Beast connection detected with a success badge and the feed metrics, with the IP blurred](/images/adsbexchange-feed/airplaneslive-myfeed.png) And then the payoff, which is seeing your own data inside a global map. This is ADSBExchange over the São Paulo metropolitan area, 31 aircraft on screen out of the 12,000 the whole network was tracking at that moment: ![ADSBExchange global map over the São Paulo metropolitan area with dozens of aircraft and a detail panel for an Embraer E195](/images/adsbexchange-feed/globe-mapa-sao-paulo.png) And the same region on airplanes.live: ![airplanes.live map over São Paulo with dozens of aircraft, a detail panel for a LATAM Airbus A320neo and a side table with callsign, altitude and RSSI](/images/adsbexchange-feed/airplaneslive-mapa-sao-paulo.png) A detail worth knowing if you care about privacy: feeding ADS-B does not send your coordinates. The Beast protocol carries aircraft messages, not the position of whoever received them, and I verified that the ADSBExchange stats package contains no reference to receiver latitude or longitude either. What they know is your IP and which aircraft you hear. Your exact position is required only by MLAT, multilateration, which needs it to compute positions by time difference of arrival and runs as a separate client. The uncomfortable inversion is that the exact coordinate does sit in your **local** `receiver.json`, which is what your own map reads. If your tar1090 is reachable from outside your network, that is where your address is exposed, not at the aggregator. I wrote the whole decision up, including why I changed my mind about contributing, [here](https://esli.blog/posts/adsbexchange-feed/). ## One more pattern While validating the feeding scripts, I read them before running them, which is a habit worth having when something wants root. The ADSBExchange stats installer calls `adduser` with no fallback, under `set -e`. Arch has `useradd`, not `adduser`, so the script dies on that line having created an empty directory and nothing else. Their website keeps telling you the stats package is not configured, no matter how many times you run their command. The airplanes.live installer, written independently by a different team, has exactly the same bug, with a comment above it claiming the second form is "for fedora / centos" when both forms are `adduser`. Neither project is wrong to assume Debian. It is just that the assumption is invisible until you are the exception, and then it is invisible in the worst way: silently. easy1090 works around both without patching either. It creates the system user first, so their own `id -u` check passes and the adduser branch is skipped, installs the dependencies their script only knows how to fetch with apt or yum, and hands over control. Their script runs unmodified, which means an upstream update never conflicts with a patch of mine. ## Where to start If you want to build one: an RTL-SDR Blog V4 from the official store, the shortest antenna element mounted vertically with a view of the sky, and any machine that stays on. A Raspberry Pi is the obvious choice, and if that is what you have, the upstream scripts will treat you well. If you run Arch, or EndeavourOS, or Omarchy, [easy1090](https://github.com/Esl1h/easy1090) exists so that you spend your time on the radio rather than on the packaging. Read the `--dry-run` output first. The `KNOWN_ISSUES.md` in the repository documents every workaround with the reason it exists, which is the file I would want to read before trusting somebody else's installer. The full series, in Portuguese, starts [here](https://esli.blog/posts/sdr-radio-no-linux/). ## References Everything above stands on other people's work. These are the sources worth your time, in the order I would read them. [**The 1090 Megahertz Riddle**](https://mode-s.org/1090mhz/), by Junzi Sun (TU Delft). A free, open book and the best reference that exists on decoding Mode S and ADS-B, from the preamble to CPR. If you read one thing from this list, read this one. [**pyModeS**](https://github.com/junzis/pyModeS), by the same author. A Python library for decoding the messages by hand, which is the fastest way to turn the book into intuition. [**On the Security of the Automatic Dependent Surveillance-Broadcast Protocol**](https://ieeexplore.ieee.org/document/6873962), by Strohmeier, Lenders and Martinovic, in IEEE Communications Surveys & Tutorials. The academic survey on the protocol's lack of authentication and the defences that have been proposed. [**Ghost in the Air(Traffic)**](https://www.blackhat.com/html/bh-us-12/bh-us-12-briefings.html), by Costin and Francillon, Black Hat USA 2012. The practical ADS-B spoofing demonstration that moved the subject out of the theoretical. [**dump1090**](https://github.com/flightaware/dump1090), the FlightAware fork. The decoder most guides start from, and the ancestor of readsb. [**FAA: ADS-B**](https://www.faa.gov/air_traffic/technology/adsb). The American regulator's official page on the programme, useful for the mandate dates and the equipage rules. [**RTL-SDR Blog: About RTL-SDR**](https://www.rtl-sdr.com/about-rtl-sdr/). An overview of the hardware that makes all of this fit inside a USB dongle. --- ## easy1090: instalador completo - URL: https://esli.blog/posts/easy1090/ - Date: 2026-08-12 - Series: tech - Tags: easy1090, adsb, sdr, rtl-sdr, bash, arch-linux, linux Oitavo artigo da série. Os sete anteriores descrevem, passo a passo, como sair de um dongle na caixa até um mapa de tráfego aéreo rodando em casa. São muitos passos, e vários deles existem apenas porque alguma coisa não funcionou como devia. Este artigo é sobre transformar isso em código, e sobre o que aprendi ao testar esse código em máquinas de verdade - Validado e em uso num mini servidor com EndevourOS e em um laptop (thinkpad) com Omarchy. O projeto está no [github.com/Esl1h/easy1090](https://github.com/Esl1h/easy1090), sob licença MIT. ## Por que um instalador Releia a [parte 4](https://esli.blog/posts/rtl-sdr-v4-adsb-1090/) e conte quantas coisas deram errado: o pacote do AUR que não compila com o GCC atual, o `yay` que descarta o patch, a regra `udev` que funciona para você e não para o serviço, o override do systemd com flags de outra versão. Some a [parte 5](https://esli.blog/posts/rtl-sdr-v4-tar1090/), com o fork trocado no meio do caminho e o `lighttpd` do Arch que ignora a configuração inteira em silêncio. Nenhum desses atritos é interessante. Nenhum ensina algo sobre rádio, ADS-B ou aviação. São impostos que você paga por rodar um stack pensado para Raspberry Pi numa distro que não é Debian. Um instalador não deixa esses problemas mais fáceis. Ele os resolve **uma vez** e guarda a solução onde a próxima pessoa encontra. ## As decisões antes da primeira linha Escrevi um documento de planejamento antes de qualquer código, e ele evitou pelo menos duas reescritas. **Idempotente, sem modo update.** Rodar de novo é a forma de atualizar. Cada módulo detecta o que já está pronto e pula, define que o arquivo de configuração é a declaração do estado desejado da máquina, e que reexecutar converge para ele. **Bash primeiro, V depois.** A tentação era começar em Go ou em V, que eu gosto. Mas o trabalho aqui é 90% orquestrar `pacman`, `yay`, `makepkg` e `systemctl`, e o argumento decisivo não foi técnico: um instalador que roda `sudo` na máquina alheia sendo um script legível permite auditoria antes de rodar. Binário compilado quebra esse contrato - talvez num segundo momento e como alternativa, mais como satisfação pessoal do que como uma solução técnica ou com algum tipo de ganho. **O `--dry-run` imprime os comandos exatos**. ![Saída do comando easy1090 install --dry-run, com o preflight aprovado, várias linhas SKIP indicando o que já está instalado, e as linhas DRY mostrando os comandos exatos que seriam executados](/images/easy1090/dry-run.png) Esse print, aliás, mostra as duas ideias juntas. As linhas `[DRY]` são os comandos literais. E a quantidade de `[SKIP]` é a idempotência funcionando: numa máquina já configurada, quase tudo é reconhecido e pulado, sobrando só o que de fato precisa mudar. **Roda como usuário normal, nunca como root**, porque `makepkg` e `yay` se recusam a rodar assim. A escalação é pontual, com um `sudo -v` no início e um keepalive em background, para a senha não ser pedida no meio de uma compilação de 45 minutos. **A camada de gerenciador de pacotes fica isolada**, num `lib/pkg-arch.sh`. Suportar outra distro é escrever um arquivo irmão com a mesma interface, sem tocar nos módulos. ## O padrão que só aparece testando de verdade Aqui começa a parte que interessa. Escrevi o instalador, ele passou no `shellcheck`, o dry-run parecia perfeito. Aí rodei numa máquina limpa. A primeira instalação funcionou. **A segunda foi onde tudo apareceu.** ### O serviço que roda com a configuração antiga Reexecutei o instalador com coordenadas novas. Ele escreveu o `/etc/default/readsb` corretamente e chamou: ```bash systemctl enable --now readsb ``` E não aconteceu nada. `enable --now` não reinicia uma unidade que já está ativa. O daemon seguiu rodando com a configuração anterior, sem erro nenhum. A prova estava no carimbo de tempo: o arquivo escrito às 16:16, o processo no ar desde 15:39, e o `journalctl` mostrando a latitude antiga enquanto o disco tinha a nova. A correção foi fazer o `run::sudo_write` sinalizar se o conteúdo mudou de fato, e os módulos reiniciarem quando mudou. O detalhe constrangedor: essa é exatamente a armadilha que eu já tinha documentado no instalador do tar1090, dois artigos antes. Documentar um erro alheio não impede você de repeti-lo. ### Arquivo existir não significa que o serviço leu A correção acima não bastou. Na terceira execução, o `mod_redirect` aparecia como `[SKIP] já habilitado`, porque o arquivo e o symlink existiam. E a URL continuava devolvendo 404. O arquivo tinha sido criado às 15:45 por uma execução anterior que não reiniciou o `lighttpd`, e o daemon estava no ar desde 15:39. Presença de arquivo não prova leitura de arquivo. A solução foi uma função que compara o `ActiveEnterTimestamp` da unidade com o `mtime` do arquivo: ```bash svc::predates_file() { started="$(systemctl show "$unit" -p ActiveEnterTimestamp --value)" started="$(date -d "$started" +%s)" file_mtime="$(stat -c %Y "$file")" [[ "$started" -lt "$file_mtime" ]] } ``` Se o serviço subiu antes do arquivo ser escrito, ele não pode tê-lo carregado. Reinicia, mesmo no caminho de `[SKIP]`. ### A flag que apaga o que acabou de instalar Testando a instalação completa, com SDR++ e SatDump, reparei num bloco estranho ao final: o `yay` removendo 24 pacotes. Entre eles `airspy`, `hackrf`, `bladerf`, `rtaudio`, `soapysdr`. A linha imediatamente anterior listava esses mesmos pacotes como dependências opcionais do SDR++, todas marcadas como instaladas. Ele instalou e desinstalou na mesma execução. A culpa era minha, de uma flag que eu tinha colocado achando que era higiene: ```bash yay -S --removemake ... ``` O `--removemake` descarta os pacotes instalados como dependência de build. Acontece que pacotes do AUR frequentemente listam a mesma biblioteca em `makedepends` **e** em `optdepends`: ela é necessária para compilar um plugin e necessária de novo, em runtime, para carregá-lo. O yay só enxerga o lado de build. O estrago, medido: ```bash for p in /usr/lib/sdrpp/plugins/*.so; do ldd "$p" | grep -q "not found" && echo "quebrado: $p" done ``` Dez plugins sem suas bibliotecas, entre eles o `audio_sink.so`. O SDR++ abria normalmente, aparecia instalado no `pacman -Q`, e estava sem saída de áudio, sem suporte a Airspy, HackRF, BladeRF, LimeSDR e sem o decodificador M17. Nenhuma mensagem de erro em lugar nenhum. ### O `rtl_test` que falha porque está tudo certo Na reexecução, apareceu um aviso dizendo que o tuner não foi identificado. Investigando, o `rtl_test` terminava com `usb_claim_interface error -6`. O `readsb` já estava rodando e detinha o dispositivo, então o teste enumerava a placa e não conseguia reivindicá-la. Sistema saudável, mensagem alarmante. O módulo do driver agora reconhece essa saída e pula o teste, dizendo o porquê: ``` [SKIP ] Dongle em uso pelo readsb (RTL-SDR Blog V4); pulando o rtl_test. ``` ## A assinatura comum Releia os quatro casos acima e note o que eles têm em comum: **nenhum deles emite erro**. O serviço fica `active` e `enabled`, verde no `systemctl`, rodando com a configuração errada. O arquivo está lá, o symlink está lá, e o daemon nunca os leu. O programa está instalado, os plugins estão no disco, e metade não carrega. O teste de hardware falha justamente porque o hardware está sendo usado. Falha ruidosa é fácil: ela te diz onde olhar. Esse tipo de falha exige que você desconfie de um sistema que está afirmando estar bem. E a única forma que encontrei de pegá-las foi rodar de novo, e de novo, numa máquina de verdade. Foi isso que mudou o meu critério de "pronto". Um instalador que funciona na primeira execução não está pronto; está apenas não testado. ## O segundo padrão: todo mundo presume Raspberry Pi O outro tema recorrente apareceu nos scripts de terceiros. O instalador do tar1090 usa `adduser`, com fallback para `useradd`, e sobrevive no Arch. O do ADSBExchange usa `adduser` **sem** fallback, sob `set -e`, e morre na linha 10, deixando um diretório vazio, nenhum serviço e nenhuma mensagem útil no site deles. E o do airplanes.live, escrito por outra equipe, comete exatamente o mesmo erro, com um comentário no código dizendo que a segunda forma é "para fedora / centos", quando as duas formas são `adduser`. Some a isso o `lighttpd` do Arch que não inclui `conf-enabled`, o `mod_redirect` que não vem carregado, e o pacote de estatísticas que procura o JSON num diretório que só existe se você usar o cliente de feed deles. Nenhum desses projetos está errado em presumir Debian: a esmagadora maioria dos receptores ADS-B do mundo é um Raspberry Pi. É uma decisão de mercado razoável que produz código frágil, e o easy1090 existe justamente para absorver essa fragilidade num lugar só. A regra que segui foi nunca alterar script de terceiro. Trabalho **em volta** dele: crio o usuário de sistema antes, resolvo as dependências que ele não sabe buscar, e então entrego o controle. O script roda intacto, o que significa que uma atualização do upstream não conflita com um patch meu. ## O bug que eu mesmo escrevi Justiça seja feita, nem todo problema foi de terceiro. Quando unifiquei os scripts num entrypoint único com subcomandos, movi a lógica para arquivos carregados dentro de uma função. E variável declarada com `declare` dentro de função é local: ```bash f(){ source /dev/stdin <<< "declare A=1 readonly B=2"; } f echo "A=${A:-SUMIU} B=${B:-SUMIU}" # A=SUMIU B=2 ``` O `readonly` sobrevive, o `declare` não. Todas as constantes dos módulos usavam `readonly` e continuaram funcionando, e as duas variáveis de estado que usavam `declare` viraram fantasmas. O comando `uninstall` passou a falhar com `unbound variable` na primeira tentativa de uso, e eu não percebi porque não o retestei depois do refactor. Só apareceu quando escrevi o comando `update` e ele quebrou do mesmo jeito. A correção é `declare -g`. A lição não é sobre bash. É que refactor que muda o **contexto de execução** do código invalida todo teste anterior, e eu só revalidei o que exercitei na hora. ## O que a ferramenta é hoje Uma execução completa, com `--full`, numa máquina que já tinha tudo instalado: ![Saída completa do comando easy1090 install --full, do preflight à validação final, com linhas SKIP para tudo que já estava pronto e a confirmação de que os três serviços estão ativos e 12 aeronaves sendo decodificadas](/images/easy1090/install-full.png) É o retrato do que eu queria alcançar. Nada foi reinstalado, porque nada precisava. O `rtl_test` foi pulado com o motivo explicado. Os dois feeds aparecem com aviso, porque compartilhar dados merece ser dito em voz alta toda vez. E a validação final não confia na própria instalação: ela consulta o systemd, mede a idade do JSON e faz uma requisição HTTP real no mapa antes de dizer que está tudo no ar. São cerca de 2.700 linhas de bash, com um entrypoint único e nove subcomandos: ``` easy1090 install instala o stack (idempotente) easy1090 update atualiza versões de pacote easy1090 feed alimenta ADSBExchange e airplanes.live easy1090 status o que está rodando, o que caiu, o que falta easy1090 start | stop | restart easy1090 open abre um componente easy1090 uninstall desfaz a instalação ``` O `status` e o `open` nunca pedem `sudo`, o que é uma propriedade deliberada: consultar o estado do sistema não deveria custar uma senha. ![Saída do comando easy1090 status, listando hardware e driver, decodificação com 7 aeronaves, os serviços web e os componentes opcionais, cada um com estado e versão](/images/easy1090/status.png) O `status` responde à pergunta que eu mais me fazia durante o desenvolvimento: o que está de pé agora? Ele checa presença de pacote, estado de unidade do systemd e, no caso da decodificação, a idade do JSON, porque céu vazio não é defeito e contagem de aeronaves não serve como sinal de saúde. A interface é bilíngue, português e inglês, com 287 mensagens em catálogos separados. O CI verifica mecanicamente que os dois catálogos definem exatamente as mesmas chaves, porque tradução faltando não quebra nada, só imprime a chave crua na tela de alguém. O instalador do tar1090 é vendorizado no repositório e verificado por checksum antes de rodar, com a licença GPL dele preservada e documentada. Se o arquivo mudar sem o pin mudar junto, a execução aborta. E o `KNOWN_ISSUES.md` tem 20 entradas, cada uma com a causa e o motivo do contorno existir. É o arquivo de que mais me orgulho, porque é o que sobra quando o código for reescrito. ## O que ficou de fora O `--full` roda limpo de ponta a ponta, como no print acima, mas naquele servidor o SDR++ e o SatDump já estavam instalados desde a parte 5, então ele percorreu o caminho de `[SKIP]`. O build do SatDump, aquele de 45 minutos, continua sem ter sido exercitado pelo instalador numa máquina onde ele não exista. O caminho de remoção do fork antigo do readsb também não foi exercitado, porque nenhuma das minhas máquinas tem mais aquele pacote, e forçar o cenário exigiria instalá-lo de propósito. E não existe CI de verdade: nada do que importa aqui pode ser testado sem um dongle conectado. O que dá para automatizar é a parte estática, e é o que o CI faz. Tudo isso está escrito no repositório, em inglês e sem eufemismo. ## O próximo O próximo artigo reúne essa série inteira em inglês, para quem chega pelo repositório e não lê português. Depois disso, a série volta ao que ela era antes de virar engenharia de software: rádio. Tem um SatDump instalado no servidor esperando um satélite passar. --- ## ADSBExchange: enviando dados dos aviões para internet - URL: https://esli.blog/posts/adsbexchange-feed/ - Date: 2026-08-12 - Series: tech - Tags: adsbexchange, adsb, sdr, rtl-sdr, readsb, privacidade, linux Sétimo artigo da série. Até aqui o receptor era meu e os dados ficavam em casa: [montei o pipeline](https://esli.blog/posts/rtl-sdr-v4-adsb-1090/), [coloquei o mapa no ar](https://esli.blog/posts/rtl-sdr-v4-tar1090/) e [documentei todas as formas de olhar para os dados](https://esli.blog/posts/guia-visualizacao-adsb/). Na parte 4 eu escrevi, com todas as letras, que deixava o feed externo desligado de propósito, sem compartilhar dados nem posição com a rede ADSBExchange. Mudei de ideia. Este artigo é sobre o que eu aprendi no caminho, e por que a decisão é menos óbvia do que parece. ## O que é o ADSBExchange É um agregador. Milhares de pessoas com receptores caseiros, como o que montei nesta série, enviam o que captam para um servidor central, que junta tudo num mapa global. Sozinho, o meu receptor enxerga um raio de algumas centenas de quilômetros em volta de Guarulhos. Somado a milhares de outros, vira cobertura mundial. Existem vários agregadores assim. FlightRadar24, FlightAware, Plane Finder, ADSB.lol, airplanes.live. O que separava o ADSBExchange dos grandes não era tecnologia mas sim a política de dados. O mapa deles, o `globe.adsbexchange.com`, na região que o meu receptor cobre. Os 31 aviões na tela aqui são parte dos mais de 12 mil que a rede inteira estava vendo naquele momento: ![Mapa global do ADSBExchange sobre a região metropolitana de São Paulo, com dezenas de aeronaves e o painel de detalhes de um Embraer E195 da Azul](/images/adsbexchange-feed/globe-mapa-sao-paulo.png) ## O diferencial: não filtrar Aeronave transmite ADS-B em texto aberto, sem autenticação, como expliquei [no segundo artigo](https://esli.blog/posts/adsb-sdr-radio-no-linux/). Qualquer pessoa com uma antena capta. Só que o que uma **rede** decide fazer com esses dados é outra história. Nos Estados Unidos existe um programa da FAA chamado LADD, que permite a proprietários de aeronaves pedir que seus voos não sejam exibidos publicamente. FlightRadar24 e FlightAware honram esses pedidos, e também removem aeronaves mediante solicitação. O resultado é um mapa onde o jatinho executivo de quem tem advogado simplesmente não aparece. O ADSBExchange construiu a reputação inteira em cima de recusar isso. A tese era simples: o sinal é público, foi captado por voluntários com equipamento próprio, e filtrar a pedido de quem tem dinheiro transforma uma ferramenta de transparência em serviço de conveniência para poucos. Foi por isso que a rede virou a fonte preferida de jornalistas e pesquisadores que rastreiam jatos de oligarcas, políticos e bilionários, incluindo a conta que acompanhava o avião do Elon Musk depois que o Twitter a baniu. Esse é o argumento que sempre me pareceu forte. Ele é, inclusive, o mesmo argumento que uso quando escrevo sobre vigilância aqui no blog, só que apontado na direção contrária do usual: transparência para quem tem poder, privacidade para quem não tem. ## A venda para a JETNET, em 2023 Aqui a história complica: Em janeiro de 2023, o fundador Dan Streufert vendeu o ADSBExchange para a JETNET, uma empresa de inteligência de dados de aviação, por um valor estimado em torno de 20 milhões de dólares. A comunidade reagiu muito mal, e por três motivos distintos. O primeiro foi de princípio: a rede era construída por voluntários não remunerados, que doavam hardware, energia elétrica e banda, e o valor acumulado por esse trabalho coletivo foi capitalizado por uma pessoa. Colaboradores do próprio projeto, incluindo gente que escreveu código, reclamaram publicamente de não terem recebido nada. O segundo foi sobre os dados históricos, vendidos junto sem que quem os produziu fosse consultado. O terceiro, e o mais relevante para o futuro, foi o receio óbvio: uma empresa que vende inteligência de aviação para clientes corporativos tem incentivo comercial que uma comunidade não tem. E o principal ativo do ADSBExchange sempre foi a promessa de não filtrar. Se essa promessa cair, sobra um agregador igual aos outros, só que construído de graça. Reação houve, e migração também. Projetos como o ADSB.lol e o airplanes.live nasceram ou cresceram justamente como alternativas comunitárias depois disso. ## Então por que enviar Porque a alternativa que eu vinha praticando, que era não contribuir com ninguém, não é neutra. Ela só significa que eu uso a cobertura construída por outras pessoas, quando abro qualquer site de rastreamento, sem devolver nada. Meu receptor cobre uma região densa, embaixo de uma das rotas mais movimentadas da América Latina, e essa cobertura tem valor real para o mapa coletivo. A tese de não filtrar continua sendo a mais alinhada com o que eu defendo, e enquanto ela se sustentar na prática, contribuir faz sentido. Se um dia mudar, desligar é um comando só, e eu escrevo sobre isso aqui. Vale dizer que essa não é a única escolha possível, e nem estou dizendo que é a certa para você. Alimentar o ADSB.lol ou o airplanes.live, que são comunitários, é uma posição igualmente defensável. Não alimentar ninguém, também. ## O que o feed realmente é Tecnicamente, é decepcionantemente simples. Não existe cliente, agente ou daemon próprio para enviar os dados. O `readsb`, que já é o meu decodificador, ganha um destino a mais: ``` --net-connector feed.adsbexchange.com,30005,beast_reduce_out ``` É isso. Uma conexão TCP de saída para a porta 30005 deles, no formato Beast, com o sufixo `reduce` indicando taxa reduzida, que envia menos mensagens por segundo por aeronave, o suficiente para o agregador e mais leve para a rede. Dá para confirmar que está funcionando olhando o processo e a conexão: ```bash ss -tn state established | awk '$4 ~ /:30005$/' journalctl -u readsb | grep -i "connection established" ``` No meu caso apareceu `BeastReduce TCP output: Connection established: feed.adsbexchange.com`, e uma conexão viva com o IP deles. Dado enviado é dado enviado, sem cerimônia. ## O pacote de estatísticas é outra coisa Aqui mora uma confusão que a documentação deles não ajuda a desfazer: **alimentar e aparecer nas estatísticas são coisas separadas**. O feed acima já entrega os dados. Mas o receptor não aparece em lugar nenhum, não tem página, não tem número, não sabe quantas aeronaves contribuiu. Para isso existe um segundo pacote, o `adsbexchange-stats`, que gera um identificador único para o meu feeder, o UUID, e envia periodicamente estatísticas de recepção. Com ele instalado, você ganha três endereços: A página do feeder, em `adsbexchange.com/api/feeders/?feed=`, com as estatísticas. O `adsbexchange.com/myip/`, que confirma se o IP de onde estou acessando está mesmo alimentando a rede, útil para bater o olho. E o `account.adsbexchange.com`, onde vincula o feeder a uma conta, que é o que dá acesso aos benefícios de quem contribui. Os mapas, para quem só quer olhar, são o `globe.adsbexchange.com`, que é a interface principal, e o `adsbexchange.com/data/` para acesso aos dados. O `myip` é a forma mais rápida de saber se está tudo certo. No meu caso, `Feed Ok` e `Stats Ok`: ![Página myip do ADSBExchange mostrando Feeder Status com ADS-B conectado, estatísticas ok e MLAT sem feed, com IP e Feed UID borrados](/images/adsbexchange-feed/myip-status-do-feeder.png) Repare no terceiro campo: **MLAT, sem feed**. Isso não é um defeito: MLAT é multilateração, a técnica de cruzar o instante de chegada do mesmo sinal em receptores diferentes para calcular a posição de aeronaves que não transmitem a própria, como as que só respondem a radar secundário. Fazer parte disso exige um cliente separado, o `mlat-client`, que sincroniza relógio com os servidores deles. O `--net-connector` do `readsb` não faz isso: ele envia as mensagens que eu decodifico, e só. Ou seja, alimentar ADS-B e alimentar MLAT são coisas distintas e dá para fazer as duas. Vinculando o UUID a uma conta, o receptor aparece na área pessoal, e é ali que ficam o leaderboard, os alertas de queda do dispositivo e o acesso premium sem anúncios que a rede dá a quem contribui: ![Página de receptores da conta ADSBExchange, com o formulário de vincular receptor e um dispositivo ativo listado, com identificadores borrados](/images/adsbexchange-feed/conta-receptores-vinculados.png) E a página de estatísticas mostra o histórico do que o receptor entregou, uma amostra por minuto, mais a lista de aeronaves do instante, com posição, altitude e força de sinal. Estas 218 amostras são as primeiras horas do meu feeder no ar: ![Página ADSBx Anywhere Stats com gráfico de contagem de aeronaves ao longo de 218 amostras e a tabela de voos captados com posição, altitude e RSSI](/images/adsbexchange-feed/stats-anywhere-aeronaves.png) O vale no meio do gráfico é madrugada. Ele é, na prática, o tráfego de Guarulhos desenhado por hora do dia. Um aviso importante sobre esse UUID: ele **não é público**. O script envia ele como cabeçalho HTTP para autenticar os envios, então quem tiver o meu UUID consegue tanto ver as suas estatísticas quanto enviar dados falsos no meu nome. Trate como credencial, não como número de série. Se vazar, apague o `/usr/local/share/adsbexchange/adsbx-uuid` e reinstale o pacote, que ele gera outro. O identificador que aparece publicamente na página do feeder é outro, mais curto, e esse sim pode ser mostrado. ## Dois problemas corrigidos no meu instalador O site do ADSBExchange manda rodar isto para instalar as estatísticas: ```bash curl -L -o /tmp/axstats.sh https://adsbexchange.com/stats.sh sudo bash /tmp/axstats.sh ``` **Isso não funciona no Arch.** O `stats.sh` tem onze linhas e é só um bootstrap: instala o `git` com `apt-get` e clona o repositório de verdade. O instalador real está lá dentro, e na linha 10 ele chama `adduser`, que é um utilitário do Debian e não existe no Arch. Diferente de outros scripts do ecossistema, aqui **não há fallback para `useradd`**. Como o script roda sob `set -e`, ele morre exatamente ali. O resultado é um diretório vazio em `/usr/local/share/adsbexchange-stats`, nenhum arquivo copiado, nenhum serviço criado, nenhum UUID gerado. E a mensagem no site continua dizendo que você não tem o pacote configurado, por mais vezes que você repita o comando. O segundo problema aparece depois de resolver o primeiro, e é mais sutil. O serviço instala, sobe, o `systemctl` mostra `active` e `enabled`, tudo verde. E ele não faz absolutamente nada: ``` No valid data source directory found, do you have the adsbexchange feed scripts installed? Tried each of: [/run/adsbexchange-feed] ``` O script deles procura o JSON em `/run/adsbexchange-feed`, que é o diretório criado pelo pacote de **feed** do ADSBExchange. Quem alimenta direto pelo `readsb`, como nós, tem o JSON em `/run/readsb`. Ele nunca encontra, e repete o erro a cada 20 segundos para sempre. A saída existe, e é deles mesmos: colocar `USE_OLD_PATH=1` no `/etc/default/adsbexchange-stats` faz o script procurar em `/run/readsb` primeiro. Só que o instalador deles só escreve esse arquivo quando detecta uma imagem de Raspberry Pi, procurando por um `/boot/adsb-config.txt`. Numa máquina comum, nunca. E tem um terceiro, puramente cosmético, que só registro pela curiosidade: no `json-status` deles a palavra `printf` está partida ao meio por uma quebra de linha, terminando uma linha com `{ pr` e começando a seguinte com `intf("1")`. O perl reclama, o teste seguinte falha por variável vazia, e o efeito prático é o script não usar o cache de DNS próprio. Inofensivo, mas está lá, em produção, para todo mundo. ## Isso tudo virou código Como esta série já tinha rendido um instalador para o stack inteiro, achei natural resolver esses atritos ali também. O ADSBExchange agora tem um subcomando próprio: ```bash easy1090 feed # lista as redes e o estado de cada uma easy1090 feed adsbexchange # habilita, com o pacote de estatísticas easy1090 feed adsbexchange --disable ``` Ele cria o usuário de sistema com `useradd` antes de chamar o instalador deles, o que faz a verificação interna passar e o ramo do `adduser` ser pulado; resolve as dependências que o script só sabe buscar com `apt` ou `yum`; escreve o `USE_OLD_PATH=1`; reinicia o serviço; e imprime o UUID com as três URLs no final. O script de terceiro roda intacto, sem patch, exatamente como fiz com o tar1090 na parte 5. O compartilhamento continua sendo opt-in, com pergunta explícita e resposta padrão negativa. Escrever "não compartilhar" como padrão foi decisão de projeto, não descuido: enviar a sua posição para servidores de terceiros tem que ser um ato deliberado. O instalador é assunto do próximo artigo, que conta a construção dele por inteiro, incluindo a sequência de bugs que só apareciam na segunda execução. ## A parte incômoda do MLAT: a sua geolocalização O `readsb` tem uma opção: ``` --json-location-accuracy 2 ``` Zero não publica, 1 publica aproximada, 2 publica exata. Ela decide o que vai no `receiver.json` **local**, o arquivo que o tar1090 lê para saber onde centralizar o mapa. Conferindo no meu servidor: ```bash cat /run/readsb/receiver.json # { "refresh": 1000, "history": 1, "lat": -23.583944, "lon": -46.554710, ... } ``` Coordenada exata, com precisão de metros. Mas isso está no **MEU** servidor, não no deles. O que sai pelo `--net-connector` é o protocolo Beast, que carrega mensagens de aeronaves, não a posição de quem recebeu. E o pacote de estatísticas também não envia: procurei por latitude e longitude no `json-status` deles e não há nenhuma referência. Bate com o que a minha própria conta mostra, com as coordenadas do receptor em branco. Quem precisa da sua posição exata é o MLAT, e por um motivo legítimo: sem saber onde cada receptor está, não dá para calcular posição por diferença de tempo de chegada. É justamente o serviço que, no meu caso, aparece como sem feed. Então o desenho real da exposição é este. O ADSBExchange sabe o meu endereço IP, porque abre uma conexão TCP com eles, e sabe quais aeronaves recebi dados, o que já dá uma noção grosseira de onde você está. Mas não sabe a minha coordenada, a menos que adicione o MLAT. E quem realmente vê a posição com precisão de metros é qualquer pessoa capaz de abrir o meu mapa local, porque ela está no `receiver.json`. Isso inverte a recomendação óbvia. Se o tar1090 estiver acessível fora da rede local, altere para 1 protege você de quem abre o mapa, e não muda nada do lado do agregador. E se um dia resolver alimentar MLAT também, aí sim estará entregando a coordenada exata. ## E se eu quiser alimentar o airplanes.live também? Pode, inclusive para ambos. As duas redes são conexões TCP independentes saindo do mesmo `readsb`, então alimentar uma não atrapalha a outra. A documentação do airplanes.live diz isso com todas as letras, que os scripts deles não interferem em clientes de feed já presentes. O airplanes.live nasceu justamente no contexto do capítulo anterior deste artigo: é uma rede comunitária, sem filtro, criada depois da venda do ADSBExchange, por gente que não quis apostar no novo dono. Se o argumento de não filtrar é o que te move, alimentar as duas é a posição mais coerente possível, porque distribui o mesmo dado entre um agregador comercial e um comunitário. ### O jeito oficial O site deles manda rodar isto: ```bash curl -L -o /tmp/feed.sh https://raw.githubusercontent.com/airplanes-live/feed/main/install.sh sudo bash /tmp/feed.sh ``` Fui ler antes de rodar, como fiz com o do ADSBExchange, e encontrei **o mesmo bug**, escrito de forma independente por outra equipe. No `update.sh` deles, sob `set -e`, na linha 164: ```bash adduser --system --home "$IPATH" --no-create-home --quiet "$UNAME" || adduser --system --home-dir "$IPATH" --no-create-home "$UNAME" ``` O comentário logo acima diz que a segunda forma é "para fedora / centos". Só que as duas são `adduser`, que simplesmente não existe no Arch. Sem fallback para `useradd`, o script morre ali. E tem um segundo buraco no mesmo arquivo: a instalação de dependências é uma cadeia `if apt / elif yum / elif dnf / fi`, sem `else`. Numa distro fora dessas três, nada é instalado, em silêncio, e o script segue até tropeçar em algo que precisava de `socat` ou de um ambiente virtual de Python. Dois projetos diferentes, escritos por pessoas diferentes, com exatamente o mesmo ponto cego: presumir que todo receptor ADS-B é um Raspberry Pi rodando Debian. O que é uma presunção razoável em termos de mercado, e péssima em termos de código. ### O jeito curto Para ADS-B, você não precisa de pacote nenhum. Basta um destino a mais no `readsb`, exatamente como no ADSBExchange: ``` --net-connector feed.airplanes.live,30004,beast_reduce_plus_out,feed.airplanes.live,64004 ``` Não inventei esse formato: é a mesma string que o instalador **deles** escreve, incluindo o endpoint de contingência na 64004. A diferença é que aqui ela vai direto no `readsb` que você já tem, sem um segundo decodificador intermediário e sem script de root nenhum. Com as duas redes ligadas, o `NET_OPTIONS` fica assim: ``` --net-connector feed.adsbexchange.com,30005,beast_reduce_out --net-connector feed.airplanes.live,30004,beast_reduce_plus_out,feed.airplanes.live,64004 ``` E para conferir que as duas conexões subiram: ```bash ss -tn state established | grep -E ':30004|:30005' ``` O status do feed fica em `airplanes.live/myfeed/`, e o mapa da rede em `globe.airplanes.live`. Poucos minutos depois, o feed aparece reconhecido no `myfeed` deles: ![Página myfeed do airplanes.live mostrando Beast Connection Detected e MLAT Connection Detected com selo de sucesso, e as métricas do feed BEAST-0, com o IP borrado](/images/adsbexchange-feed/airplaneslive-myfeed.png) Vale a mesma ressalva do MLAT: o instalador oficial deles também configura multilateração, que exige um cliente separado e, aí sim, a sua coordenada exata. Quem quiser só ADS-B, como eu, resolve com a linha acima. E aqui vale registrar um detalhe curioso. A página marca **MLAT Connection Detected** com selo de sucesso, mas essa máquina não alimenta MLAT. Conferi de três formas: o instalador oficial nunca rodou nela, não existe serviço `airplanes-mlat`, e não há conexão estabelecida nas portas 31090 nem 64004. A prova definitiva estava logo abaixo, na mesma página. O `myfeed` também expõe um JSON com os dados brutos do feeder, e nele consta: ```json "mlat_clients": [] ``` Lista vazia. Ou seja, o próprio site sabe que não há cliente MLAT nenhum, e ainda assim o indicador acima aparece verde. É bug de exibição deles, e o dado estruturado embaixo desmente o selo colorido em cima. Fica a dica de sempre olhar o JSON quando o painel bonito disser algo que a sua máquina não confirma. O resto dos números confere com o que o meu receptor faz: 1,32 kbit/s de tráfego, 2,275 posições por segundo e latência de 266 ms até o servidor deles, na Alemanha. E o resultado final, o meu dado no mapa deles, com 50 aeronaves na tela das quase 15 mil que a rede inteira via naquele momento: ![Mapa do airplanes.live sobre São Paulo, com dezenas de aeronaves, o painel de detalhes de um Airbus A320neo da LATAM e a tabela lateral com callsign, altitude e RSSI](/images/adsbexchange-feed/airplaneslive-mapa-sao-paulo.png) O `easy1090` agora conhece as duas: ```bash easy1090 feed # lista as redes e o estado de cada uma easy1090 feed airplaneslive # habilita easy1090 feed airplaneslive --disable ``` ## Onde eu fico Contribuo com o ADSBExchange, sabendo que ele foi vendido em 2023 e que isso pode mudar o que a rede é. A tese de não filtrar aeronaves segue de pé, e enquanto seguir, contribuir com ela é coerente com o que eu defendo aqui. E fica o registro de que a alternativa comunitária existe e é legítima. Se a promessa mudar, o ADSB.lol e o airplanes.live estão a um `--net-connector` de distância (e provavelmente serão meus proximos testes), o que é a beleza de todo esse stack ser aberto: quem decide para onde os seus dados vão é você, não o fabricante do dongle nem o site que você abre para ver o avião passar. --- ## Seu navegador está seguro? Um guia dos testes de vazamento e fingerprint - URL: https://esli.blog/posts/teste-seu-navegador-contra-vazamentos/ - Date: 2026-07-30 - Series: psa - Tags: psa, privacy, security, browsers, webrtc, dns, leaks, fingerprint, sni, ech, tls, vpn, brave-browser, firefox, tor, mullvad, android Você configurou a VPN, desativou o GPS, ativou o kill switch e se sente o Edward Snowden da sua sala. Aí entra num site aleatório e ele te cumprimenta em português, sugere a loja da sua cidade e já assume seu fuso horário. Estranho, né? Não é. Seu navegador entregou tudo e nem precisou do IP pra isso. A verdade é que **IP é só um dos vetores**. Idioma, WebRTC, IPv6, fuso horário, canvas, fontes, resolução de tela: cada um desses é uma migalha que, somada, forma uma impressão digital única. VPN só esconde o IP, não esconde o resto. Se você ainda acha que a barra "conectado" resolve tudo, releia [VPN não é o suficiente](/posts/vpn-nao-e-o-suficiente/) e lembre que [seu provedor te vê mais do que você imagina](/posts/nao-confie-no-seu-provedor-de-internet/). Este post é um checklist. Primeiro, **onde testar** o quanto você está vazando. Depois, **como fechar as torneiras** no Brave e no Mullvad no desktop Linux, o papel do **Tor Browser**, e o que usar no Android: Brave como principal, e a briga **Fennec vs Cromite** pra secundário. ## Antes de tudo: vazamento e fingerprint não são a mesma coisa "Vazamento" virou palavra guarda-chuva e isso atrapalha na hora de ler os resultados. Vale separar em duas famílias, porque a defesa de cada uma é completamente diferente. **Vazamento de rede.** Algum dado sai por um caminho que contorna a camada que você configurou. A VPN está ativa mas o DNS ainda vai pro provedor. O túnel encapsula IPv4 e o IPv6 escapa pela interface nativa. O WebRTC abre UDP direto e entrega seu IP real. Aqui existe certo e errado objetivos: ou vazou, ou não vazou. **Fingerprinting.** Nada "vaza" no sentido literal. O site apenas combina dezenas de características legítimas do navegador (fontes instaladas, resolução, renderização de canvas, versão do WebGL, fuso) até montar um identificador estável que dispensa cookie. Aqui não existe aprovado ou reprovado, existe grau de raridade e consistência. Confundir as duas leva a conclusão errada. "Seu navegador é único entre 300 mil" não significa que a VPN falhou. E DNS vazando não se resolve instalando extensão anti-fingerprint. ## Parte 1: onde testar (o arsenal de dedo-duro) Regra antes de sair clicando: **teste com a VPN ligada e desligada**, compare, e use sempre o **teste estendido** quando disponível. Um resultado "verde" com VPN não significa nada se você não sabe qual era o "vermelho" sem ela. Mais alguns cuidados que mudam o resultado: - **Teste no perfil que você usa de verdade.** Janela anônima limpa mede um navegador que você não usa. Faça as duas medições se quiser comparar, mas a que importa é a do dia a dia. - **Um navegador por vez.** Resultado de Brave, Mullvad e Fennec não é intercambiável, nem entre perfis do mesmo navegador. - **Feche o resto.** Extensão sincronizando, cliente de e-mail, torrent: tudo isso polui o teste de DNS. - **Repita depois de cada atualização grande.** Navegador que atualiza reseta flag, cliente de VPN que atualiza muda regra de rota. Já vi as duas coisas acontecerem. ### IP, DNS, WebRTC e IPv6 (os vazamentos clássicos) | Ferramenta | O que cobre | Observação | |---|---|---| | **[browserleaks.com](https://browserleaks.com/)** | WebRTC, DNS, IPv6, canvas, WebGL, fontes, geolocalização, TCP/IP OS fingerprint | A referência. Um vetor por página, bem granular | | **[ipleak.net](https://ipleak.net/)** (AirVPN) | DNS estendido, WebRTC, IPv6, torrent leak, fuso | O que eu já linkava há anos. Torrent address detection é útil | | **[ipx.ac](https://ipx.ac/)** (AirVPN) | Bateria completa de leaks num lugar só | Feito pela mesma turma do ipleak | | **[doileak.com](https://doileak.com/)** | Relatório único: IP, DNS, WebRTC, IPv6, fuso, SSL, tipo de conexão | Bom pra quem quer um panorama de uma tacada | | **[dnsleaktest.com](https://www.dnsleaktest.com/)** | DNS (standard e extended) | Clássico, direto ao ponto | | **[dnscheck.tools](https://dnscheck.tools/)** | Resolvers, DNSSEC, ECS (EDNS Client Subnet) | Ótimo pra ver se seu resolver vaza subnet | | **[test-ipv6.com](https://test-ipv6.com/)** | Exclusivo IPv6 | **Rode este.** IPv6 fora do túnel é o vazamento nº 1 no Android | | **[1.1.1.1/help](https://1.1.1.1/help)** (Cloudflare) | Status de DoH/DoT/ECH | Mostra se você está de fato criptografando o DNS | | **[test.nextdns.io](https://test.nextdns.io)** | Perfil e protocolo do NextDNS, em JSON | Resposta legível por máquina, boa pra script | #### IPv6: o que mais quebra em silêncio Boa parte das VPNs encapsula IPv4 e, no melhor caso, bloqueia IPv6; no pior, deixa o tráfego v6 sair pela interface nativa. Como praticamente toda operadora residencial brasileira já entrega IPv6, o site pode simplesmente preferir o v6 e enxergar seu endereço real enquanto o painel da VPN mostra "conectado". Se o `ipleak.net` ou o `test-ipv6.com` mostrarem um IPv6 com o prefixo da sua operadora, é vazamento, ponto final. Ou a VPN suporta IPv6 de verdade, ou ela bloqueia v6 por completo. Meio termo aqui é o pior dos mundos. #### DNS: tudo funciona, e é por isso que ninguém percebe O resultado esperado depende do seu desenho de rede. Se você usa o resolver da VPN, deve aparecer só o ASN dela. Se usa NextDNS ou DNSCrypt local, deve aparecer o provedor escolhido e o perfil certo. O `test.nextdns.io` é o mais direto de todos, porque devolve JSON: ```plaintext { "status": "ok", "protocol": "DOH", "profile": "fp3c610bc5d9319252", "client": "2804:7f0:6941:756b::1002", "clientName": "nextdns-cli" } ``` `"status": "unconfigured"` significa que suas consultas não estão passando pelo perfil que você acha que configurou. Um detalhe que confunde muita gente: **o navegador pode ter DNS próprio**. Firefox e Chromium implementam DoH internamente e ignoram a configuração do sistema quando isso está ativo. Você pode ter DNSCrypt impecável no Linux e o navegador resolvendo tudo pela Cloudflare sem te avisar. Teste o navegador, não só o sistema. A parte de sistema eu cobri na [série de DNS criptografado](/posts/dnscrypt-dns-stamps-e-dns-criptografado-o-guia-que-faltava/). #### WebRTC: o vazamento que sobrevive à VPN WebRTC é a API de comunicação em tempo real do navegador (videochamada, áudio, transferência peer to peer). Pra funcionar atrás de NAT, ela precisa descobrir todos os endereços da sua máquina e, pra isso, consulta servidores STUN por UDP. Esse tráfego pode sair fora do túnel. Teste em [browserleaks.com/webrtc](https://browserleaks.com/webrtc) e leia assim: - **Public IP address.** Se aparecer seu IP residencial com a VPN ligada, é vazamento crítico. Só deveria aparecer o IP da VPN, ou nada. - **Local IP address.** Endereços privados (`192.168.x.x`, `10.x.x.x`) expõem a topologia da sua rede interna. Navegador moderno substitui isso por um identificador mDNS terminado em `.local`, que é o comportamento correto. - **IPv6 candidates.** Mesma armadilha da seção anterior, agora por outro caminho. A correção fica na Parte 2, por navegador. ### TLS, ECH e criptografia de transporte **ECH (Encrypted Client Hello)**. Sem ele, mesmo com HTTPS e DNS criptografado, seu provedor vê o SNI, ou seja, *qual* site você acessa. Já disseco isso em [SNI Leak: o calcanhar de Aquiles do DNS seguro](/posts/sni-leak-o-calcanhar-de-aquiles-do-dns-seguro/). | Ferramenta | O que verifica | |---|---| | **[tls-ech.dev](https://tls-ech.dev/)** | Se o ECH está ativo de fato | | **[defo.ie/ech-check.php](https://defo.ie/ech-check.php)** | Segunda opinião sobre ECH, com o motivo da falha | | **[crypto.cloudflare.com/cdn-cgi/trace](https://crypto.cloudflare.com/cdn-cgi/trace)** | Linha `sni=`, que deve dizer `encrypted` | | **[howsmyssl.com](https://www.howsmyssl.com/)** / **[clienttest (SSL Labs)](https://clienttest.ssllabs.com:8443/ssltest/viewMyClient.html)** | Qualidade do seu cliente TLS, versões e cifras | | **[tls.browserleaks.com/json](https://tls.browserleaks.com/json)** | Seu fingerprint JA3/JA4 | Lembre que o ECH depende de DNS criptografado pra funcionar: o navegador precisa buscar a chave pública no registro DNS do tipo HTTPS. Sem DoH ativo, o Firefox nem tenta. Dá pra conferir se um site publica a chave: ```plaintext dig +short HTTPS esli.blog ``` ```plaintext 1 . alpn="h3,h2" ipv4hint=104.21.15.52,172.67.161.182 ech=AEX+DQBB0AAg... ipv6hint=2606:4700:3036::ac43:a1b6 ``` O campo `ech=` presente significa que o servidor suporta. Ausente significa que, pra aquele domínio, seu SNI continua em texto claro por mais bem configurado que esteja o seu lado. #### O fingerprint que quase ninguém testa: JA3 e JA4 Antes de qualquer HTTP, seu cliente TLS anuncia a lista de cipher suites que suporta, as extensões, a ordem delas, os grupos de curvas. Essa combinação é estável o suficiente pra identificar o software, e às vezes a versão exata. É o JA3 (formato antigo) e o JA4 (o atual): ```plaintext { "user_agent": "curl/8.18.0", "ja4": "t13d3513h2_bfa337485184_8d7351bd871b" } ``` Por que importa na prática: **o JA4 desmente o User-Agent**. Se você usa uma extensão que finge ser Chrome no Windows enquanto o seu JA4 é o do Firefox no Linux, você não ficou anônimo, ficou raro. A inconsistência é mais identificável que a verdade. Não existe correção simples aqui, e é justamente esse o recado. Fingerprint de TLS é propriedade da implementação, não configuração. O caminho é usar um navegador comum, com a stack TLS padrão dele, sem mascarar User-Agent na mão. ### Fingerprint e rastreamento | Ferramenta | O que faz | |---|---| | **[coveryourtracks.eff.org](https://coveryourtracks.eff.org/)** (EFF) | O sucessor do Panopticlick. Mede unicidade e se seu bloqueio de trackers funciona | | **[privacytests.org](https://privacytests.org/)** | Grade pass/fail comparando navegadores. Benchmark, não teste individual (mais abaixo) | | **[amiunique.org](https://amiunique.org/)** | Quão único é seu fingerprint no universo de amostras | | **[creepjs](https://abrahamjuliot.github.io/creepjs/)** | Fingerprint agressivo: detecta inconsistências e tentativas de spoof. Humilhante e educativo | | **[pixelscan.net](https://pixelscan.net/)** | Consistência do fingerprint, pega quando seu spoof não bate com o resto | | **[webbrowsertools.com](https://webbrowsertools.com/)** | Despeja tudo que o browser expõe via JS, uma página por API | O BrowserLeaks continua sendo o mais útil da lista justamente porque não te dá um número: ele mostra o hash de canvas, o renderer do WebGL (que costuma entregar o modelo exato da sua GPU), a lista de fontes detectadas, o fingerprint de áudio e as APIs de mídia. Vale percorrer a barra lateral inteira uma vez na vida. O CreepJS merece destaque separado: em vez de medir raridade, ele procura contradição. Cruza o que o navegador declara com o que o comportamento revela e aponta onde você está mentindo. Se você usa `privacy.resistFingerprinting` ou o escudo do Brave, é o teste que mostra se o disfarce está consistente. (O `deviceinfo.me`, que já foi padrão nessa lista, está fora do ar. O domínio ainda resolve, mas o servidor não responde.) ### O vetor esquecido: relógio Fuso horário é fingerprint, e a hora do sistema é atacável na rede. Se o `Intl.DateTimeFormat` do seu browser entrega `America/Sao_Paulo`, adivinha de onde você é. Padronizar isso também é parte do jogo (juntamente com autenticar a própria sincronização), assunto que cobri em [NTS: por que você precisa autenticar a hora do seu Linux](/posts/nts-por-que-voc-precisa-autenticar-a-hora-do-seu-linux/). Pra medir fuso, locale e formatação de data isoladamente: [arkenfox.github.io/TZP](https://arkenfox.github.io/TZP/tzp.html). ### Android: camada de apps (aqui o bicho pega) No celular o vazamento raramente é só o navegador. São os **apps** despejando telemetria por baixo do pano. | Ferramenta | Função | |---|---| | **[Exodus Privacy](https://reports.exodus-privacy.eu.org/)** | Escaneia APKs e lista trackers e permissões embutidos | | **[TrackerControl](https://trackercontrol.org/)** | Mostra e bloqueia trackers por app, em tempo real (via VPN local) | | **[PCAPdroid](https://emanuele-f.github.io/PCAPdroid/)** | Captura o tráfego real do device sem root, o juiz final de "quem está falando com quem" | | **[NetGuard](https://netguard.me/)** | Firewall por app + log de conexões | O PCAPdroid é o que fecha a discussão: se você acha que algo vaza, capture e olhe os pacotes. Chutômetro não é diagnóstico. ### Checklist rápido | Camada | Onde testar | Resultado esperado | |---|---|---| | IPv4 | [ipleak.net](https://ipleak.net/) | IP da VPN | | IPv6 | [test-ipv6.com](https://test-ipv6.com/) | IP da VPN, ou nenhum IPv6 | | DNS | [dnsleaktest.com](https://www.dnsleaktest.com/) (extended) | só o resolver que você escolheu | | DNS do navegador | [test.nextdns.io](https://test.nextdns.io) | `status: ok` com o perfil certo | | WebRTC | [browserleaks.com/webrtc](https://browserleaks.com/webrtc) | sem IP público real, local em `.local` | | ECH / SNI | [crypto.cloudflare.com/cdn-cgi/trace](https://crypto.cloudflare.com/cdn-cgi/trace) | `sni=encrypted` | | TLS | [tls.browserleaks.com/json](https://tls.browserleaks.com/json) | JA4 coerente com o User-Agent | | Fingerprint | [creepjs](https://abrahamjuliot.github.io/creepjs/) | sem contradições apontadas | | Fuso | [arkenfox TZP](https://arkenfox.github.io/TZP/tzp.html) | coerente com a saída da VPN | | Apps (Android) | [PCAPdroid](https://emanuele-f.github.io/PCAPdroid/) | nenhum destino que você não reconheça | ### Como ler os resultados sem se enganar **"Único entre 250 mil" não é sentença de morte.** No Tor Browser e no Mullvad Browser a estratégia é uniformizar: todo mundo parece igual, então a métrica de raridade fica boa. No Brave a estratégia é randomizar: o fingerprint muda a cada sessão e por site, então o teste pode acusar unicidade justamente porque a defesa está funcionando. Rode duas vezes em abas diferentes; se o hash mudou, a randomização está ativa. **Consistência importa mais que raridade.** Fingerprint comum e coerente esconde melhor que fingerprint raro e contraditório. É por isso que empilhar extensão "anti-detect" costuma piorar o resultado. **Nenhum teste mede o que você faz logado.** Dá pra zerar todos os vazamentos técnicos e continuar identificado porque você entrou na sua conta. **O painel da VPN não é evidência.** "Conectado" quer dizer que o túnel subiu, não que todo o tráfego passa por ele. ## Parte 2: refinando o Brave e o Mullvad no desktop (Linux) Por que esses dois? Porque, segundo a grade do **PrivacyTests.org**, são os que passam mais testes de proteção prontos pra uso, como já argumentei em [Qual o melhor navegador](/posts/qual-melhor-navegador/). O site não publica score agregado, mas a contagem reportada pelo PCMag (run #95) coloca o **Brave em ~143/156** e o **Mullvad em ~141/156**, empatados no topo, à frente até do Tor Browser em bloqueio puro (o Tor ganha em anonimato de rede, que é outra briga). Ou seja: o melhor dos dois mundos é usar cada um pro que ele serve. ### Brave: o daily driver blindado `brave://settings/shields` - Trackers & ads: **Aggressive** - Fingerprinting: **Block (Strict/aggressive)** - Upgrade connections to HTTPS: **Strict** `brave://settings/privacy` - WebRTC IP Handling Policy: **Disable Non-Proxied UDP**, que fecha o vazamento de IP via STUN - Bloqueie cookies de terceiros - Desative P3A, "diagnostic reports" e o Web Discovery Project (telemetria, mesmo que "anônima") - "Forget me when I close this site": ligado nos domínios que importam `brave://settings/security` - Secure DNS: aponte pro seu resolver (NextDNS/Quad9 via DoH). No desktop faz sentido; no mobile, se a VPN já cuida do DNS, é escolha sua Tem muito mais escondido nas `brave://` internas, já mapeei o que vale em [Explorando funções avançadas no Brave Browser](/posts/funcoes-avancadas-no-brave-browser/). E antes que você perca tudo num reinstall: [Sync não é backup](/posts/brave-browser-como-fazer-backup/). Opcional mas recomendado: **[uBlock Origin](https://ublockorigin.com/)** por cima do escudo nativo, pra ter controle fino de filtros. E a janela Tor (`New private window with Tor`) pra quando precisar de `.onion` sem sair do Brave. Depois de mexer, volte no `browserleaks.com/webrtc` e confirme. A configuração só vale se aparecer na medição. ### Mullvad Browser: o anti-fingerprint de fábrica Aqui a regra é o oposto do Brave: **não mexa**. O Mullvad entrega um fingerprint padronizado de propósito pra todo mundo parecer igual. Cada "melhoria" ou alteração sua te torna único, que é exatamente o que você não quer. (Se quiser o contexto da empresa por trás, escrevi [Mullvad: privacidade sem marketing, sem conta e sem desculpas](/posts/mullvad-privacidade-sem-marketing-sem-conta-e-sem-desculpas/).) - **Não instale extensões.** Cada uma é um bit de entropia - **Não maximize nem redimensione** a janela. O letterboxing existe pra padronizar a resolução reportada, e quebrar isso te denuncia - **Não instale fontes** nem faça login em contas - **Não fuce no `about:config`.** O `resistFingerprinting` já vem afinado; você só vai desregular - Ajuste o nível de segurança pelo escudo (Standard/Safer/Safest) conforme o site - **Pareie com VPN.** O Mullvad Browser não tem anonimato de rede: sem VPN, seu provedor vê os destinos igual a qualquer outro navegador. Ele cuida do *browser*, a VPN cuida da *rede* Resumindo a divisão de trabalho: **Brave** pra vida diária (compatibilidade + bloqueio agressivo), **Mullvad** pra quando fingerprint importa mais que conveniência. ### E os outros? - **Firefox comum, Tor Browser e Mullvad Browser:** pra matar o WebRTC de vez, em `about:config` defina `media.peerconnection.enabled` como `false`. Chamada de vídeo no navegador para de funcionar, é o preço. No Mullvad e no Tor, isso é dos poucos ajustes que valem a pena, porque não altera fingerprint visível. - **Chrome e Chromium puros:** não existe controle nativo equivalente ao do Brave. A extensão oficial que fazia isso foi descontinuada. Se WebRTC te preocupa e você usa Chrome, o caminho realista é trocar de navegador ou bloquear UDP no firewall. ## Parte 3: Tor Browser, quando privacidade não basta e você quer anonimato Brave e Mullvad te dão **privacidade**. O Tor Browser te dá **anonimato de rede**, coisa que nenhuma VPN entrega, porque numa VPN você só troca o observador (o provedor) por outro (o provedor da VPN). O Tor roteia por três saltos, cada um cego pro anterior, e ninguém no caminho vê origem e destino ao mesmo tempo. O que ele faz que os outros não fazem: - **Fingerprint uniforme por design.** É a mesma base do Mullvad Browser, só que com a rede Tor acoplada. Timezone em UTC, locale en-US, tela padronizada. Aquele redirect regional automático simplesmente não acontece - **Sem correlação de IP.** O site vê o IP do exit node, que muda e não é seu - **Acesso a `.onion`.** Serviços que nem existem na web comum. Aliás, na minha opinião, [todo site deveria ter um endereço na rede Tor](/posts/todo-site-deve-ter-um-endereco-na-rede-tor/) Os contras: - **É lento.** Três saltos custam latência. Não é daily driver - **Muito site bloqueia exit nodes.** CAPTCHA infinito, 403, o pacote completo - **Não mexa nele.** Maximizar janela, instalar extensão, mudar `about:config`: cada ajuste quebra a uniformidade e te torna rastreável. A regra do Mullvad vale em dobro aqui - **Não te salva de você mesmo.** Logar na sua conta pessoal dentro do Tor mata o anonimato na hora. E o Tor protege o tráfego do *browser*, não do resto do device **Em rede hostil ou sob censura**, o Tor tem pontes e transportes plugáveis. O mais prático pra emprestar sua banda a quem precisa é o **Snowflake**, cobri o funcionamento em [Snowflake: proxy temporário para contornar a censura](/posts/snowflake-para-contornar-a-censura-na-internet/). **No Android**: **existe [Tor Browser for Android](https://www.torproject.org/download/#android)** (oficial, via F-Droid ou Play). Pra rotear o sistema inteiro pela rede Tor, o **[Orbot](https://guardianproject.info/apps/org.torproject.android/)** funciona como VPN local. Ambos já fazem parte do meu kit no celular. Regra prática de quando puxar cada um: | Preciso de… | Ferramenta | |---|---| | Navegação diária sem tracking | Brave | | Anti-fingerprint máximo, país indiferente | Mullvad (desktop) / Tor Browser (Android) | | Anonimato de rede real / `.onion` / driblar censura | Tor Browser + Orbot | | Exit num país específico sem vazar região | Brave endurecido + VPN | ## Parte 4: Android, Brave como principal, Fennec vs Cromite O Mullvad Browser não existe pra Android (sim, uma tristeza). O **Brave** segue como principal, com o mesmo endurecimento do desktop e mais dois detalhes que só existem no mobile: - **Idioma: coloque `en-US` no topo.** Isso muda o header `Accept-Language` e mata aquele redirect automático pro `.com.br` mesmo com VPN em outro país - **Trave o vazamento no nível do SO:** Configurações do Android → VPN → (seu provedor) → **VPN sempre ativa** + **Bloquear conexões sem VPN**. É isso que impede IPv6 nativo de escapar do túnel - WebRTC: **Disable non-proxied UDP**; Secure DNS desligado se a VPN cuida do DNS (ou aponte pro NextDNS). Pra DNS criptografado no Android sem depender do navegador, tem o [InviZible Pro e alternativas](/posts/dns-criptografado-no-android-invizible-pro-e-alternativas/) Mas todo mundo devia ter um **secundário**: pra compartimentar identidades, pra abrir o que quebra no principal, e pra não colocar todos os ovos num engine só. E é aqui que entram Fennec e Cromite. ### Os dois candidatos **[Cromite](https://github.com/uazo/cromite)**, sucessor espiritual do Bromite. Chromium **sem telemetria**, com **adblock nativo** e mitigações de fingerprint ligadas por padrão. Leve, rápido, "Chromium limpo" sem o circo de rewards/cripto do Brave. **[Fennec](https://f-droid.org/packages/org.mozilla.fennec_fdroid/)**, o Firefox do F-Droid, **degoogled**, sem os binários proprietários da Mozilla. Aceita `about:config` completo e **add-ons** (leia-se uBlock Origin). Engine Gecko, não Chromium. Ajustes que importam no Fennec (`about:config`): - `privacy.resistFingerprinting` → **true** (padroniza timezone pra UTC e locale pra en-US, de novo, mata o redirect regional na raiz) - `media.peerconnection.enabled` → **false** (desliga WebRTC de vez) - `webgl.disabled` → **true** se você tolera perder alguns sites - `geo.enabled` → **false** ### Qual escolher como secundário? **Fennec.** E o motivo é puramente estratégico: meu principal (Brave) é **Chromium/Blink**, e um bom secundário não deve repetir o mesmo engine. O Fennec é **Gecko**, e isso me dá duas coisas que o Cromite não dá: 1. **Diversidade de engine.** Fingerprint diferente por construção, e um fallback de renderização real pra quando um site quebra no Chromium (acontece mais do que deveria) 2. **`resistFingerprinting` de verdade.** A mesma linhagem de defesa do Tor/Mullvad, algo que nenhum Chromium mobile replica com a mesma profundidade O Cromite é ótimo, mas como secundário ele é **redundante no eixo que importa**: continua sendo Chromium, igual ao Brave. Ele brilha noutro papel: quando você precisa de um **segundo Chromium** especificamente (algum site que só vai bem em Blink) e não quer o peso nem os extras do Brave. Aí sim, Cromite entra como "Chromium limpo de plantão". | Critério | Fennec | Cromite | |---|---|---| | Engine | Gecko (diversifica) | Blink (repete o Brave) | | Anti-fingerprint | Alto (RFP) | Bom (padrão) | | Add-ons | Sim (uBO) | Não | | Peso | Médio | Leve | | Papel ideal | **Secundário** (compartimentar + fallback) | Terceiro / "Chromium de emergência" | Detalhe: `resistFingerprinting` pode brigar com layout e quebrar alguns sites. É o mesmo problema do Mullvad no desktop, padronização custa conveniência. Se isso te incomoda mais do que o ganho de privacidade compensa, aí o Cromite vira secundário aceitável. Mas pro objetivo de **secundário de verdade**, Fennec ganha. Uma nota: escolher Firefox em 2026 exige olhar pra Mozilla com ceticismo, o que motivou [Mozilla sob ataque: o que aconteceu com o Firefox](/posts/mozilla-sob-ataque-o-que-aconteceu-com-o-firefox/). A graça do Fennec é justamente pegar o **engine** do Firefox sem a bagagem corporativa da Mozilla. ## Bônus: testando pela linha de comando Pra checar o host rapidamente, sem abrir navegador: ```bash #!/usr/bin/env bash # leak-check.sh - auditoria rapida de saida da maquina echo "== IPv4 ==" curl -s -4 https://ip.me echo "== IPv6 ==" curl -s -6 https://ip.me || echo "sem saida IPv6" echo "== Cloudflare trace ==" curl -s https://crypto.cloudflare.com/cdn-cgi/trace | grep -E '^(ip|loc|tls|sni|warp)=' echo "== DNS (NextDNS) ==" curl -s https://test.nextdns.io echo "== JA4 do curl ==" curl -s https://tls.browserleaks.com/json | grep '"ja4"' ``` Uma ressalva importante: esse script mede o **host**, não o navegador. O JA4 retornado é o do `curl` e o DNS é o do sistema. Serve pra validar VPN, rota e resolver do sistema operacional, que é onde a maioria dos problemas de rede realmente está. Pro navegador, não tem jeito, é preciso abrir as páginas de teste nele. ## O que nenhum desses testes pega Vale delimitar o escopo, porque teste passado gera confiança, e confiança mal calibrada é pior que nenhuma. **Análise de tráfego.** Volume, tempo e padrão de acesso continuam visíveis mesmo com ECH e DNS cifrados. Existe pesquisa madura de website fingerprinting em cima só disso. **Tudo que não é navegador.** Aplicativo de celular, cliente de e-mail, atualizador do sistema, telemetria de IDE. No Android o PCAPdroid cobre esse buraco; no desktop, é wireshark e paciência. **Correlação por conta.** Login é identificação voluntária. Nenhum teste técnico protege contra isso. **Extensões.** Uma extensão maliciosa lê a página depois que o TLS terminou. Todos os testes vão passar. **O que muda amanhã.** Atualização de navegador reseta preferência, atualização de cliente de VPN muda regra de rota. Por isso teste é rotina, não evento único. ## Fechando as torneiras Não existe navegador perfeitamente privado, existe o navegador certo pro seu modelo de ameaça: - **Brave** endurecido, o dia a dia - **Fennec**, secundário, engine diferente, compartimentação - **Mullvad Browser** (desktop), quando fingerprint importa mais que conveniência - **Tor Browser + Orbot**, quando você precisa de anonimato, não só privacidade E o hábito importa: existe uma diferença enorme entre "configurei DNS criptografado" e "verifiquei que as consultas saem cifradas pelo resolver certo". A primeira frase é intenção e a segunda é engenharia. Reserve vinte minutos, rode a bateria uma vez, anote os resultados em algum lugar e repita a cada dois ou três meses, ou depois de qualquer mudança grande de navegador, VPN ou rede. Vazamento é quase sempre regressão: algo que já funcionou e parou, e você só descobre comparando com a medição anterior. Teste, compare, ajuste, teste de novo. E pare de confiar cegamente na barra "conectado" da VPN: ela esconde o IP, não o resto do seu device gritando quem você é. O IP era só o começo, agora você tem o encanamento inteiro mapeado. ### Leitura relacionada aqui no blog **Fundamentos** - [Não confie no seu provedor de internet](/posts/nao-confie-no-seu-provedor-de-internet/) - [VPN não é o suficiente](/posts/vpn-nao-e-o-suficiente/) - [Qual a melhor VPN](/posts/qual-melhor-vpn/) · [Qual o melhor navegador](/posts/qual-melhor-navegador/) - [Guias de privacidade e segurança online](/posts/privacidade-e-seguranca-online/) · [Privacidade e segurança em 2025](/posts/privacidade-e-seguranca-2025/) **DNS, SNI e a hora** - Série DNS criptografado: [parte 1, DNSCrypt e DNS Stamps](/posts/dnscrypt-dns-stamps-e-dns-criptografado-o-guia-que-faltava/) · [parte 2, dnscrypt-proxy no Linux](/posts/dnscrypt-proxy-no-linux-configurando-dns-criptografado/) · [parte 3, Android e InviZible Pro](/posts/dns-criptografado-no-android-invizible-pro-e-alternativas/) - [SNI Leak: o calcanhar de Aquiles do DNS seguro](/posts/sni-leak-o-calcanhar-de-aquiles-do-dns-seguro/) - [NTS: por que você precisa autenticar a hora do seu Linux](/posts/nts-por-que-voc-precisa-autenticar-a-hora-do-seu-linux/) **Navegadores e Tor** - [Explorando funções avançadas no Brave](/posts/funcoes-avancadas-no-brave-browser/) · [Sync não é backup no Brave](/posts/brave-browser-como-fazer-backup/) - [Mullvad: privacidade sem marketing](/posts/mullvad-privacidade-sem-marketing-sem-conta-e-sem-desculpas/) - [Mozilla sob ataque: o que aconteceu com o Firefox](/posts/mozilla-sob-ataque-o-que-aconteceu-com-o-firefox/) - [Snowflake: contornar a censura](/posts/snowflake-para-contornar-a-censura-na-internet/) · [Todo site deve ter um endereço na rede Tor](/posts/todo-site-deve-ter-um-endereco-na-rede-tor/) - [Como fugir das propagandas na internet](/posts/como-fugir-das-propaganda-na-internet/) **Contexto** - [Vigilância governamental: Five Eyes, MLAT e outros](/posts/vigilancia-governamental/) --- ## Qobuz on Linux - URL: https://esli.blog/posts/qobuz-on-linux/ - Date: 2026-07-29 - Series: tech - Tags: linux, audio, qobuz, streaming, hi-res, audiophile, pipewire, wine *Also available in Portuguese: [Qobuz no Linux](/posts/qobuz-no-linux/).* On Android I use the native Qobuz app and, every now and then, [USB Audio Player PRO](https://www.extreamsd.com/index.php/component/content/article?id=1) and HiBy Music (that one is rare, a test that stayed installed and a wishlist of their devices), both logged into the same Qobuz account, each with its own equalizer and direct access to the DAC, bypassing the Android mixer. I wrote about that arrangement in [Transform Old Smartphone in a Digital Audio Player (DAP)](https://esli.blog/posts/transform-old-smartphone-in-a-digital-audio-player-dap/), and the rest of the gear is in [My Music Setup](https://esli.blog/posts/my-music-setup/). I use it both on the 2019 phone (the subject of that article about turning a phone into a dedicated DAP), retired and SIM-less, with the iFi Go Link, and on my daily driver. For my Thinkpad laptop running Arch (Omarchy), another PC with EndeavourOS (Arch again) and a desktop with Fedora 44, 64 GB of RAM and an SSL2+ interface, what I get is... a browser tab. That bothers me more than it should. Qobuz has never shipped an official Linux client. There are apps for Windows, macOS, Android, iOS, smart TVs and integrations with half a dozen hardware manufacturers. For Linux there is `play.qobuz.com` and your own creativity. This article is about how far creativity gets you. ## The problem is not (entirely) Qobuz Before comparing browser vs Wine, it is worth agreeing on what is being measured. "Quality" here is not subjective: it is what leaves the server, what reaches the DAC and how much is lost along the way, from Linux to your ears. A 24-bit / 192 kHz stream that goes through a resampler down to 48 kHz is still a 192 kHz file at the source, but it becomes a 48 kHz signal at the converter. You pay for the Studio plan to burn bandwidth and CPU, and nothing else. Using YouTube Music (the worst quality) obviously makes good gear and good settings pointless. Same for listening on Spotify without setting it to the best quality ("Very High" under "Audio Quality"). With Qobuz the goal is to get close to the quality you are offered (and paying for), matched to the gear you actually have. On Linux there are three points where the chain can break: 1. **The application**, which may decode and resample on its own. 2. **The sound server** (PipeWire, or PulseAudio on older setups), which runs at a fixed rate and converts anything that does not match. 3. **The ALSA device you pick**, because `plughw:` converts silently and `hw:` does not. Those three are on my side of the fence, and all three have a fix. But it would be dishonest to stop here and pretend Qobuz carries no blame: the three only become my problem because there is no official Linux client. On every other platform the app is what handles this, negotiating the rate with the system and handing the stream to the hardware with nothing in between. On Linux that work has been outsourced to the subscriber, and the rest of this article is precisely the cost of that outsourcing: a compatibility layer, API credentials scraped out of a JavaScript bundle, third-party clients maintained by volunteers. It is not a lack of demand, and it is not a technical limitation. The catalogue is already served over HTTP to anything that knows how to ask. ## Option 1: the web player in a browser The Qobuz catalogue serves FLAC from 16/44.1 up to 24/192 depending on your plan, and the web player at `play.qobuz.com` reaches that same catalogue. The limit is not what the server sends, it is what the browser does with what it receives. Browsers route audio through the WebAudio API and the system mixer, which runs at a fixed rate, usually 48 kHz. A 192 kHz FLAC is decoded correctly and then resampled to fit the graph. There is no exclusive mode, no passthrough, no per-track rate negotiation. The QBZ project itself, an alternative native client, [documents this in its FAQ](https://github.com/vicrodh/qbz/wiki/FAQ) as its very reason to exist. ### Does the browser make any difference? No. | Browser | Engine | Behaviour | |---|---|---| | Chrome / Chromium | Blink | Outputs at the graph's fixed rate, typically 48 kHz. Broadest MSE and codec support, and the combination Qobuz actually tests | | Brave / Vivaldi / Edge / Opera | Blink | Same engine, same audio pipeline. The differences are tracker blocking and telemetry | | Firefox | Gecko | Also capped at the device's preferred rate. [Bug 1400731](https://bugzilla.mozilla.org/show_bug.cgi?id=1400731) was opened in 2017 with Qobuz as the test case, and it has aged well, in the bad sense | | Safari | WebKit | Irrelevant here, unless you keep a Mac around | Pick a browser for privacy, RAM usage or personal taste. For audio there is no difference, it is irrelevant and useless. Installing the web player as a PWA (`Install page as app` in Chromium) improves the experience: no address bar, its own icon and window, MPRIS integration in some cases. ![Qobuz web player installed as a PWA in Brave, showing the Black Sabbath artist page with the album grid, playlists and similar artists. The footer shows Walking in My Shoes by Depeche Mode playing, with a CD 16-bit 44.1 kHz Stereo indicator](/images/qobuz-no-linux/qobuz-pwa-brave.png) Look at the bottom right: that quality badge is the only hint the web player gives you about what is coming out. It describes the track, not what the DAC received. ![Qobuz PWA in Brave showing the Best of 192 kHz Soul/Funk/R&B playlist, 93 tracks with the Hi-Res 192 kHz badge on the cover, listing Chain of Fools, Killing Me Softly and Superfly with album, label and genre](/images/qobuz-no-linux/qobuz-pwa-playlist-brave.png) ### How to stop arguing and start measuring Play a Hi-Res track and ask the kernel what is actually happening: ```bash # find your DAC's card cat /proc/asound/cards # with the track playing, see what the hardware got cat /proc/asound/card*/pcm0p/sub0/hw_params ``` Typical output with the browser playing a 24/96 FLAC: ``` access: MMAP_INTERLEAVED format: S32_LE subformat: STD channels: 2 rate: 48000 (48000/1) period_size: 1024 buffer_size: 8192 ``` `rate: 48000` on a 96 kHz track settles the debate. To complement it: ```bash pw-metadata -n settings | grep clock # graph rate and allowed rates pw-top # per-node rate, in real time wpctl status # active devices and nodes ``` Where the browser is a perfectly reasonable choice: discovering music, reading the Qobuz editorial (which is good), building playlists, listening on the laptop while working. CD quality run through a decent resampler still sounds far above any lossy platform. The web player is only bad for the audiophile, not for the human being. It is imperceptible unless the rest of your setup has no other weak spots. ## Option 2: the Windows app via Wine, Bottles or CrossOver The official installer is Electron. Which means you are going to run Chromium packaged inside a compatibility layer, on top of Linux, in order to escape Chromium's audio limitations. Ironic. What saves the manoeuvre is that the Windows app speaks WASAPI, exclusive mode included. And Wine knows how to map WASAPI onto ALSA. The path of least effort is [Bottles](https://usebottles.com/): create an Application bottle, run the installer, done. There are [consistent reports](https://audiophilestyle.com/forums/topic/70489-whats-that-the-qobuz-app-running-and-working-in-linux/) of success in a few clicks. The annoying part: Bottles runs as a Flatpak, and the sandbox complicates direct ALSA access. You get the app and lose exactly the reason you went after the app. For real bit-perfect the reference material is the [Qobuz-wine-guide](https://github.com/logon84/Qobuz-wine-guide), which uses wine-staging with the sound backend set to ALSA (not PulseAudio), a launcher that can optionally start the app outside PipeWire in exclusive mode, and some sound server tuning. CrossOver is the same Wine with commercial support and ready-made profiles. If you already pay for it, give it a try. If you do not, you are not going to start paying because of this. ### Measuring the app under CrossOver, without exclusive WASAPI I installed the official app (version 8.2.0-b033, Electron) in a CrossOver bottle and measured the whole chain on Fedora 44 with PipeWire 1.6.8 and the SSL2+ Mk II. The result: **I needed neither exclusive mode nor Wine's ALSA backend**. The default path, `winepulse.drv` talking to PipeWire, already delivers the track's native rate to the hardware. ![The official Qobuz Windows app running under CrossOver on Fedora with KDE, showing The Best of 192 kHz playlist with 206 tracks. The footer shows School's Out by Alice Cooper with a Hi-Res 24-Bit 192 kHz Stereo badge and the SSL 2+ Mk II Line Output device](/images/qobuz-no-linux/qobuz-crossover-playlist-192khz.png) The `Hi-Res 24-Bit 192 kHz` badge next to the `SSL 2+ Mk II Line Output` device is what the app claims to be doing. The `/proc` output below is what confirms it. The first step is confirming there is no DSP in the path. The graph has to go from the app to the hardware in a straight line: ```bash pw-dump | jq -r '.[] | select(.type=="PipeWire:Interface:Link") | "\(.info."output-node-id") -> \(.info."input-node-id")"' pgrep -a easyeffects ``` In my case the path was `Qobuz (141) -> Line1 sink (64) -> split (63) -> alsa_output.hw_II_0 (59)`, with no `Audio/Filter` node in between. The `split` nodes are the SSL2+ UCM separating Line 1/2 from Line 3/4, channel routing, not processing. Then identify the app's stream and check what format it leaves Wine in: ```bash pactl list sink-inputs | grep -EA2 'application.name = "Qobuz"' pactl list sink-inputs | grep -E 'Sample Specification|Resample method|Volume:' ``` With a 24/96 playlist playing: ``` Sample Specification: float32le 2ch 96000Hz Volume: front-left: 65536 / 100% / 0.00 dB ``` And what actually matters, what the kernel handed to USB: ```bash cat /proc/asound/card2/pcm0p/sub0/hw_params grep -E 'Status|Momentary' /proc/asound/card2/stream0 ``` ``` format: S32_LE rate: 96000 (96000/1) ``` ``` Status: Running Momentary freq = 96000 Hz (0xc.0000) ``` Switching to a 192 kHz playlist and running the same commands, without touching anything: ``` format: S32_LE rate: 192000 (192000/1) period_size: 512 ``` ``` Momentary freq = 192000 Hz (0x18.0000) ``` That is the proof, and it is worth understanding why it is conclusive. My `default.clock.rate` is 48000. The hardware only leaves that value if something asks for another rate and the graph agrees to renegotiate, which only happens because `default.clock.allowed-rates` includes 88200, 96000, 176400 and 192000. If Wine were resampling to a fixed mix format, `hw_params` would stay pinned at 48000 regardless of the track. It followed 96 and then 192 kHz, so there is no rate conversion anywhere in the chain. To watch the switch happen live while skipping between tracks of different rates: ```bash watch -n1 'grep rate /proc/asound/card2/pcm0p/sub0/hw_params' ``` ![Expanded player mode of the Qobuz app under CrossOver, with the Cross Purposes cover by Black Sabbath, the track I Witness playing, the album queue on the right, the Autoplay toggle and a Hi-Res 24-Bit 44.1 kHz Stereo indicator above the SSL 2+ Mk II output](/images/qobuz-no-linux/qobuz-crossover-player.png) About bit depth: the stream leaves Wine as float32, PipeWire carries float32 and ALSA delivers S32_LE. With the volume at `100% / 0.00 dB` on the sink input, on the sink (`wpctl get-volume @DEFAULT_AUDIO_SINK@` returning `1.00`) and in the app itself, there is no floating-point attenuation, and the conversion from 24-bit to float32 and back to S32_LE is exact. Formally this is not bit-perfect in the strict, exclusive-mode sense, since the bus is float. In practice float32's 24-bit mantissa carries the content losslessly at unity gain. Any volume below 100% at any of those three points breaks the argument. Check the health of the stream too, because the right rate with an xrun every two seconds is not a win: ```bash pw-top -b -n 3 journalctl --user -u pipewire -u wireplumber --since "30 min ago" | grep -Ei 'xrun|underrun' ``` The `ERR` column in `pw-top` should stay at zero. In my case the driver ran with `WAIT 30.5us` against `BUSY 4.6us` at a quantum of 512, comfortable headroom. One detail: `pactl list sinks` reports `Sample Specification: float32le 2ch 48000Hz` even with the hardware at 192 kHz. That is the PulseAudio compatibility layer showing the graph's format, not the device's. Ignore it and look at `hw_params`. Two bottle settings are worth recording, because they affect the result. In `~/.cxoffice/Qobuz_Installer/drive_c/users/crossover/AppData/Roaming/Qobuz/settings-*.json`, the `currentDevice` field was empty, meaning shared WASAPI with no exclusive device selected. And `downloadQuality` was `7`, which in the Qobuz scheme caps at 24/96 (the 24/192 tier is `27`). That field governs downloads and imports, not streaming, but if the app looks stuck at 96 kHz it is the first place to look. ![The Cross Purposes album page in the Qobuz app under CrossOver, with the Hi-Res 24-Bit 44.1 kHz badge, the Downloads only toggle enabled and the tracks marked with the completed download icon](/images/qobuz-no-linux/qobuz-crossover-downloads.png) As a bonus, the official app is the only path on this list that gives you the subscription's offline downloads, with the `Downloads only` filter working normally inside the bottle. Verdict: yes, it works. Run the official app under Wine, and the default PipeWire path is enough as long as `allowed-rates` is configured. The wine-staging juggling with an ALSA backend and exclusive mode solves a problem modern PipeWire no longer has. ## Option 3: Strawberry [Strawberry](https://www.strawberrymusicplayer.org/) is the player I already use for my local collection. It is Qt, has a GStreamer backend with direct ALSA output, and ships native Qobuz support. It is the most elegant option in theory and the most annoying to configure in practice. The reason: Qobuz does not hand out API credentials to third-party clients. You have to supply an `App ID` and an `App Secret` that the web player itself uses, extracted from its JavaScript bundle. ### Extracting the credentials ```bash pipx install qobuz-dl python - <<'EOF' from qobuz_dl.bundle import Bundle b = Bundle() print("app_id:", b.get_app_id()) for k, v in b.get_secrets().items(): print("secret:", k, v) EOF ``` Out comes one `app_id` and several secrets. There is no documented way to tell which one works, so you will have to try them one by one until you hit the right one. ### Configuring Under `Settings > Qobuz`: - tick `Enable Qobuz` - `App ID` and `App Secret` as extracted - your account username and password - `Preferred audio format`: pick the top tier (24-bit / 192 kHz) and let the server negotiate down when a track does not have it - adjust the search limits, the default is low and makes it look like your library vanished Under `Settings > Backend`: - `Output`: `alsasink` - `Device`: your DAC's `hw:`, never `plughw:` (`plughw` converts silently, which is precisely what we are trying to avoid) - disable fading, replaygain and the equalizer if the goal is bit-perfect With everything in place, Qobuz becomes just another source in the sidebar, next to Library, Files and Radios: ![Strawberry Music Player with the Qobuz source selected in the sidebar, a search for Soul Men, the Sam & Dave album loaded in the playlist and the columns showing 192000 Hz, 24 Bit, 3233 kbps and FLAC format on the playing track](/images/qobuz-no-linux/strawberry-album.png) The `Sample Rate`, `Bit Depth` and `Source` columns are the reason to use Strawberry: `192000 Hz`, `24 Bit`, `FLAC` and `Stream` on the same row, without having to open `/proc`. ![A 93-track playlist loaded in Strawberry from Qobuz, with Killing Me Softly With His Song by Roberta Flack playing at 192000 Hz and 24 Bit, and the remaining tracks listed with Stream as their source](/images/qobuz-no-linux/strawberry-playlist.png) ### The error you will hit ``` Invalid Request Signature parameter (request_sig) (400) ``` Search works, playback does not. It means the secret you picked is not accepted to sign the stream request. Go back, swap the secret, repeat. This is not a Strawberry bug, and with the right secret everything works. The one gap is that Strawberry does not fetch playlists, but I solved that with a script that downloads all of mine as .xspf (maybe a second article just about that, but it is already versioned in my dotfiles repo). ## Option 4: alternative and native clients ### QBZ [QBZ](https://github.com/vicrodh/qbz) (https://qbz.lol/) is a native Qobuz client for Linux written in Rust, MIT licensed, no telemetry, and in version 2.0 it dropped webview and IPC: a single process with a Slint UI. Backends for PipeWire, ALSA (including a `Direct hw:` bypass mode), PulseAudio and JACK, with passthrough to the DAC and per-track sample rate switching, from 44.1 up to 192 kHz. It also has DSD with DoP and native passthrough, gapless, MPRIS, scrobbling to Last.fm and ListenBrainz, Chromecast and DLNA, plus a hardware configuration wizard. Installation: ```bash # Arch yay -S qbz-bin # Fedora / openSUSE (glibc 2.39+) sudo dnf install ./qbz-*.rpm # grab the RPM at github.com/vicrodh/qbz/releases # Flatpak flatpak install flathub com.blitzfc.qbz ``` Relevant warning: do not go with the Flatpak. Its sandbox limits interaction with the hardware. Login is OAuth through the browser, with no password typed inside the app, and there is an offline mode for anyone who only wants to play their local collection: ![QBZ start screen with the player logo, the Qobuz terms of service acceptance checked, the Sign in with your browser button and the link to start in offline mode](/images/qobuz-no-linux/qbz-login.png) After logging in the whole library shows up, favourites and playlists included, with per-type counters: ![QBZ library showing 2885 items split into 2326 tracks, 58 albums, 239 artists and 6 labels, with the cover grid and the playlist list in the left sidebar](/images/qobuz-no-linux/qbz-library.png) The album page has a per-track `Quality` column, which spares you the surprise of finding out mid-listen that this particular record is only CD quality: ![The Headless Cross album page by Black Sabbath in QBZ, with the Quality column reading HI-RES 24-bit 44.1 kHz on every track and the footer showing the SYST and DEFAULT backends](/images/qobuz-no-linux/qbz-album.png) And there is a full-screen mode, for when the laptop becomes the stereo: ![QBZ full-screen Now Playing mode, with the Headless Cross cover enlarged, a gradient background sampled from the artwork, the Hi-Res 24-bit 44.1 kHz badge and centred playback controls](/images/qobuz-no-linux/qbz-now-playing.png) An interesting detail for anyone with a spare Pi or mini PC: the project ships `qbzd`, a headless daemon that turns the machine into a Qobuz Connect endpoint, showing up in the official apps as if it were a hardware streamer. There is a [containerised fork](https://github.com/yet-another-quentin/qbzd) for people who prefer Docker or LXC on Proxmox. That covers the "Qobuz Connect on Linux" scenario. ### Qobine, formerly qobuz-player `SofusA/qobuz-player` is now [SofusA/qobine](https://github.com/SofusA/qobine) It started as a fork of hifi.rs and diverged quite a bit. Today it is a Rust monorepo, GPL-3.0, bundling a TUI, a GTK player for GNOME, a web server with its own UI, an RFID player and experimental Qobuz Connect. It supports up to 24-bit / 192 kHz, MPRIS (control it with `playerctl` or any D-Bus client) and gapless. The RFID mode is the fun part: tap a card, the album plays. A homemade jukebox for anyone who misses having a physical object and does not want to go back to cleaning a turntable stylus. The original [hifi.rs](https://github.com/iamdb/hifi.rs) has been stalled since 2024. ## Option 5: treat Qobuz as a network source ### Lyrion Music Server + squeezelite The old Logitech Media Server became [Lyrion](https://lyrion.org/). The server runs in Docker, `squeezelite` runs on the host that has the DAC and talks straight to ALSA, supporting 44.1 up to 384 kHz plus multiroom sync. ```bash docker run -d --name lyrion \ -p 9000:9000 -p 3483:3483 -p 3483:3483/udp \ -v lms-config:/config -v /srv/music:/music \ lmscommunity/lyrionmusicserver:latest # on the host with the DAC sudo dnf install squeezelite squeezelite -l # list the devices squeezelite -o hw:2,0 -n "living-room-fedora" ``` Then, under `Settings > Plugins`, install the Qobuz plugin and log in. ### upmpdcli An MPD-based UPnP renderer with a media server module for Qobuz. It is the right route for anyone who controls everything through BubbleUPnP. ### Music Assistant If you already run Home Assistant, [Music Assistant](https://www.music-assistant.io/music-providers/qobuz/) has a Qobuz provider with the Hi-Res catalogue, automatically picking the best available quality and syncing favourites both ways. It plays to Chromecast, Sonos, AirPlay and friends. ## What about the equalizer? On Android I have EQ inside the player itself, without leaving the bit chain, because UAPP and HiBy process before handing anything to the DAC. On Linux the route is [EasyEffects](https://github.com/wwmm/easyeffects) on top of PipeWire, which accepts AutoEQ profiles for known IEMs and headphones. EQ is processing, and processing happens in float, which requires conversion. I do not use it and do not miss it outside the phone. ## Comparison | Path | Real Hi-Res | Bit-perfect | Effort | Stability | |---|---|---|---|---| | Web player (any browser) | Receives up to 24/192 | No, resamples to the graph's rate | Zero | High | | Windows app via Wine/CrossOver | Yes, measured at 24/96 and 24/192 | Yes, with shared WASAPI and PipeWire `allowed-rates` | Medium | Medium, depends on Wine and the Electron updater | | Strawberry | Yes | Yes, with `alsasink` + `hw:` | Medium, the credentials are a pain | Medium, depends on the API not changing | | QBZ | Yes | Yes, PipeWire or ALSA Direct | Low | Good, actively developed | | Qobine (formerly qobuz-player) | Yes | Yes, via GStreamer/ALSA | Low | Good, smaller scope | | Lyrion + squeezelite | Yes | Yes | Medium | High | | upmpdcli | Yes | Yes | Medium | Depends on the version, keep it updated | | Music Assistant | Yes | Depends on the endpoint | Low if you already run HA | High | ## What I actually use Fedora 44 desktop with KDE, output through the SSL2+ or over TOSLink to the Edifier. Strawberry. Arch laptop with Hyprland, Fosi K5 Pro DAC: Strawberry. The PWA in the browser (Brave), for browsing the interface, discovering albums and playlists, reading the editorial... No guilt, and no pretending to be an audiophile. In all of the above I can control the player from my phone with KDE Connect or with the native app (currently the Qobuz Beta). The installer failed to run under plain Wine, but installed without a fight under CrossOver, so I did not bother troubleshooting the Wine path (I do not want to use it that way anyway)... QBZ is excellent, a great middle ground between Strawberry and the PWA. By the way, I am on Qobuz Club at https://club.qobuz.com/u/7ee25f7b And my playlists are public over there too. ## References - [QBZ](https://github.com/vicrodh/qbz) and its [project FAQ](https://github.com/vicrodh/qbz/wiki/FAQ) - [Qobine, formerly qobuz-player](https://github.com/SofusA/qobine) - [Qobuz-wine-guide](https://github.com/logon84/Qobuz-wine-guide) - [Strawberry Music Player](https://www.strawberrymusicplayer.org/) - [Lyrion Music Server](https://lyrion.org/) and [squeezelite](https://lyrion.org/players-and-controllers/squeezelite/) - [upmpdcli](https://www.lesbonscomptes.com/upmpdcli/) - [Music Assistant, Qobuz provider](https://www.music-assistant.io/music-providers/qobuz/) - [Bugzilla 1400731, sample rate in Firefox](https://bugzilla.mozilla.org/show_bug.cgi?id=1400731) --- ## Qobuz no Linux - URL: https://esli.blog/posts/qobuz-no-linux/ - Date: 2026-07-29 - Series: tech - Tags: linux, audio, qobuz, streaming, hi-res, audiophile, pipewire, wine *Também disponível em inglês: [Qobuz on Linux](/posts/qobuz-on-linux/).* No Android uso o app nativo do Qobuz e, volta e meia, o [USB Audio Player PRO](https://www.extreamsd.com/index.php/component/content/article?id=1) e o HiBy Music (bem raro mesmo - um teste que ficou instalado e uma wishlist dos devices deles), mas ambos logados na mesma conta Qobuz, cada um com seu equalizador e acesso direto ao DAC, contornando o mixer do Android. Escrevi sobre esse arranjo em [Transformando antigo smartphone em DAP](https://esli.blog/posts/transformando-antigo-smartphone-em-dap/), e o resto do parque de equipamentos está em [meu setup musical](https://esli.blog/posts/meu-setup-musical/). Uso tanto no celular de 2019 (material do artigo sobre transformando um celular em DAP dedicado), aposentado e sem SIM com o iFi Go Link, quanto no meu smartphone do dia-a-dia. Para o meu laptop Thinkpad com Arch (Omarchy), outro PC com EndevourOS (Arch novamente) e um desktop com Fedora 44, 64 GB de RAM e uma interface SSL2+ entrega... só uma aba de navegador. Isso incomoda mais do que deveria. O Qobuz nunca lançou cliente oficial para Linux. Existem apps para Windows, macOS, Android, iOS, SmartTVs e integração com meia dúzia de fabricantes de hardware. Para Linux, existe o `play.qobuz.com` e a sua criatividade. Este artigo é sobre até onde a criatividade chega. ## O problema não é (tanto) o Qobuz Antes de comparar navegador vs Wine, vale alinhar o que está sendo medido. "Qualidade" aqui não é subjetivo: é o que sai do servidor, o que chega ao DAC e o quanto se perde no caminho, do Linux até os ouvidos. Streaming em 24 bits / 192 kHz que passa por um resampler para 48 kHz continua sendo um arquivo de 192 kHz na origem, mas vira um sinal de 48 kHz no conversor. Paga-se o plano Studio para consumir banda e processamento, só. Usar o Youtube Music (a pior qualidade), obviamente não adianta em nada ter equipamento ou configurações boas... Como também, ouvir via Spotify e não configurar para a melhor qualidade ("Very High" em "Audio Quality"), o ideal com o Qobuz é chegar perto da qualidade oferecida (e paga!) alinhando com os equipamentos disponíveis. No Linux há três pontos onde a cadeia pode ser quebrada: 1. **A aplicação**, que pode decodificar e reamostrar por conta própria. 2. **O servidor de som** (PipeWire, ou PulseAudio em setups antigos), que roda numa taxa fixa e converte tudo que não bate. 3. **O device ALSA escolhido**, porque `plughw:` faz conversão silenciosa e `hw:` não faz. Esses três pontos são do lado de cá, e todos têm solução. Mas seria desonesto parar por aqui e fingir que o Qobuz não tem culpa nenhuma: os três só viram problema meu porque não existe cliente oficial para Linux. Nas outras plataformas quem resolve isso é o app, que negocia a taxa com o sistema e entrega o stream ao hardware sem intermediário. No Linux esse trabalho foi terceirizado para o assinante, e o resto deste artigo é exatamente o custo dessa terceirização: camada de compatibilidade, credenciais de API extraídas de bundle JavaScript, clientes de terceiros mantidos por voluntários. Não é falta de demanda nem limitação técnica, o próprio catálogo já é servido por HTTP para qualquer coisa que saiba pedir. ## Opção 1: web player no navegador O catálogo do Qobuz entrega FLAC de 16/44,1 a 24/192 conforme o plano, e o web player em `play.qobuz.com` acessa o mesmo catálogo. A limitação não está no que o servidor manda, está no que o navegador faz com o que recebe. Navegadores roteiam áudio pela WebAudio API e pelo mixer do sistema, que opera numa taxa fixa, normalmente 48 kHz. Um FLAC de 192 kHz é decodificado corretamente e depois reamostrado para caber no grafo. Não existe modo exclusivo, não existe passthrough, não existe negociação de taxa por faixa. O próprio projeto QBZ, que é um cliente nativo alternativo, [documenta isso na FAQ](https://github.com/vicrodh/qbz/wiki/FAQ) como a razão de existir. ### E entre navegadores, muda alguma coisa? Não. | Navegador | Motor | Comportamento | |---|---|---| | Chrome / Chromium | Blink | Sai na taxa fixa do grafo, tipicamente 48 kHz. Suporte mais amplo a MSE e codecs, é a combinação que o Qobuz testa de fato | | Brave / Vivaldi / Edge / Opera | Blink | Mesmo motor, mesmo pipeline de áudio. As diferenças são de bloqueio de tracker e telemetria | | Firefox | Gecko | Também limitado à taxa preferida do device. O [bug 1400731](https://bugzilla.mozilla.org/show_bug.cgi?id=1400731) foi aberto em 2017 justamente com o Qobuz como caso de teste e envelheceu bem, no sentido ruim | | Safari | WebKit | Irrelevante aqui, salvo se você mantém um Mac por perto | Escolha o navegador por privacidade, consumo de RAM ou gosto pessoal. Para áudio, a diferença não existe, irrelevante e inútil. Instalar o web player como PWA (`Instalar página como aplicativo` no Chromium) melhora a experiência, tira barra de endereço, ganha ícone e janela própria, integra MPRIS em alguns casos. ![Web player do Qobuz instalado como PWA no Brave, página do artista Black Sabbath com a grade de álbuns, playlists e artistas similares. O rodapé mostra Walking in My Shoes do Depeche Mode tocando, com o indicador CD 16 bits 44,1 kHz Estéreo](/images/qobuz-no-linux/qobuz-pwa-brave.png) Repare no canto inferior direito: o indicador de qualidade é a única pista que o web player dá sobre o que está saindo. Ele descreve a faixa, não o que o DAC recebeu. ![PWA do Qobuz no Brave exibindo a playlist Best of 192 kHz Soul/Funk/R&B, com 93 faixas e o selo Hi-Res 192 kHz na capa, listando Chain of Fools, Killing Me Softly e Superfly com álbum, selo e gênero](/images/qobuz-no-linux/qobuz-pwa-playlist-brave.png) ### Como parar de discutir e medir Toque uma faixa Hi-Res e pergunte ao kernel o que está realmente acontecendo: ```bash # descubra o card do seu DAC cat /proc/asound/cards # com a faixa tocando, veja o que o hardware recebeu cat /proc/asound/card*/pcm0p/sub0/hw_params ``` Saída típica com o navegador tocando um FLAC 24/96: ``` access: MMAP_INTERLEAVED format: S32_LE subformat: STD channels: 2 rate: 48000 (48000/1) period_size: 1024 buffer_size: 8192 ``` `rate: 48000` numa faixa de 96 kHz encerra o debate. Complementando: ```bash pw-metadata -n settings | grep clock # taxa e taxas permitidas do grafo pw-top # taxa por nó, em tempo real wpctl status # dispositivos e nós ativos ``` Onde o navegador é uma escolha perfeitamente razoável: descobrir música, ler o editorial do Qobuz (que é bom), montar playlist, ouvir no notebook enquanto trabalha. CD quality reamostrado por um resampler decente continua soando muito acima de qualquer plataforma lossy. O web player só é ruim para audiófilo, não para ser humano - imperceptível caso não tenha deficiências no restante do setup. ## Opção 2: o app do Windows via Wine, Bottles ou CrossOver O instalador oficial é Electron. Ou seja: você vai rodar Chromium empacotado dentro de uma camada de compatibilidade, em cima do Linux, para escapar das limitações de áudio do Chromium. Irônico. O que salva a manobra é que o app Windows fala WASAPI, incluindo modo exclusivo. E o Wine sabe mapear WASAPI para ALSA. O caminho de menor esforço é o [Bottles](https://usebottles.com/): criar uma garrafa do tipo Application, rodar o instalador, funcionar. Há [relatos consistentes](https://audiophilestyle.com/forums/topic/70489-whats-that-the-qobuz-app-running-and-working-in-linux/) de sucesso com poucos cliques. O detalhe chato: o Bottles roda como Flatpak, e o sandbox complica o acesso direto ao ALSA. Você ganha o app, e perde exatamente o motivo de ter ido atrás do app. Para bit-perfect de verdade, o material de referência é o [Qobuz-wine-guide](https://github.com/logon84/Qobuz-wine-guide), que usa wine-staging com o backend de som configurado em ALSA (não PulseAudio), um launcher que opcionalmente sobe o app fora do PipeWire em modo exclusivo, e ajustes no servidor de som. CrossOver é o mesmo Wine com suporte comercial e perfis prontos. Se você já paga, tente. Se não paga, não vai pagar por causa disso. ### Medindo o app via CrossOver, sem WASAPI exclusivo Instalei o app oficial (versão 8.2.0-b033, Electron) numa bottle do CrossOver e medi a cadeia inteira no Fedora 44 com PipeWire 1.6.8 e a SSL2+ Mk II. O resultado: **não precisei de modo exclusivo nem do backend ALSA do Wine**. O caminho padrão, `winepulse.drv` falando com o PipeWire, já entrega a taxa nativa da faixa ao hardware. ![App oficial do Qobuz para Windows rodando via CrossOver no Fedora com KDE, exibindo a playlist The Best of 192 kHz com 206 faixas. O rodapé mostra School's Out do Alice Cooper com o selo Hi-Res 24-Bit 192 kHz Stereo e a saída SSL 2+ Mk II Line Output](/images/qobuz-no-linux/qobuz-crossover-playlist-192khz.png) O selo `Hi-Res 24-Bit 192 kHz` ao lado do device `SSL 2+ Mk II Line Output` é o que o app afirma estar fazendo. O `/proc` abaixo é o que confirma. O primeiro passo é confirmar que não existe DSP no caminho. O grafo precisa ir do app ao hardware em linha reta: ```bash pw-dump | jq -r '.[] | select(.type=="PipeWire:Interface:Link") | "\(.info."output-node-id") -> \(.info."input-node-id")"' pgrep -a easyeffects ``` No meu caso o caminho foi `Qobuz (141) -> Line1 sink (64) -> split (63) -> alsa_output.hw_II_0 (59)`, sem nenhum nó `Audio/Filter` no meio. Os nós `split` são o UCM da SSL2+ separando Line 1/2 de Line 3/4, roteamento de canal, não processamento. Depois, identifique o stream do app e veja em que formato ele sai do Wine: ```bash pactl list sink-inputs | grep -EA2 'application.name = "Qobuz"' pactl list sink-inputs | grep -E 'Sample Specification|Resample method|Volume:' ``` Com uma playlist de 24/96 tocando: ``` Sample Specification: float32le 2ch 96000Hz Volume: front-left: 65536 / 100% / 0.00 dB ``` E o que interessa de verdade, o que o kernel entregou ao USB: ```bash cat /proc/asound/card2/pcm0p/sub0/hw_params grep -E 'Status|Momentary' /proc/asound/card2/stream0 ``` ``` format: S32_LE rate: 96000 (96000/1) ``` ``` Status: Running Momentary freq = 96000 Hz (0xc.0000) ``` Trocando para uma playlist de 192 kHz e repetindo o mesmo comando, sem mexer em nada: ``` format: S32_LE rate: 192000 (192000/1) period_size: 512 ``` ``` Momentary freq = 192000 Hz (0x18.0000) ``` Essa é a prova, e vale entender por que ela é conclusiva. Meu `default.clock.rate` é 48000. O hardware só sai desse valor se alguém pedir outra taxa e o grafo aceitar renegociar, o que só acontece porque `default.clock.allowed-rates` inclui 88200, 96000, 176400 e 192000. Se o Wine estivesse reamostrando para um mix format fixo, o `hw_params` ficaria cravado em 48000 independentemente da faixa. Ele acompanhou 96 e depois 192 kHz, então não há conversão de taxa em lugar nenhum da cadeia. Para acompanhar a troca em tempo real ao pular entre faixas de taxas diferentes: ```bash watch -n1 'grep rate /proc/asound/card2/pcm0p/sub0/hw_params' ``` ![Modo player expandido do app Qobuz no CrossOver, com a capa de Cross Purposes do Black Sabbath, a faixa I Witness em reprodução, a fila do álbum à direita, o toggle de Autoplay e o indicador Hi-Res 24-Bit 44.1 kHz Stereo sobre a saída SSL 2+ Mk II](/images/qobuz-no-linux/qobuz-crossover-player.png) Sobre profundidade de bits: o stream sai do Wine em float32, o PipeWire trafega em float32 e o ALSA entrega S32_LE. Com o volume em `100% / 0.00 dB` no sink input, no sink (`wpctl get-volume @DEFAULT_AUDIO_SINK@` retornando `1.00`) e no próprio app, não há atenuação em ponto flutuante, e a conversão de 24 bits para float32 e de volta para S32_LE é exata. Formalmente isso não é bit-perfect no sentido estrito do modo exclusivo, já que o barramento é float. Na prática a mantissa de 24 bits do float32 carrega o conteúdo sem perda quando o ganho é unitário. Qualquer volume abaixo de 100% em qualquer um desses três pontos quebra o argumento. Confira também a saúde do stream, porque taxa certa com xrun a cada dois segundos não é vitória: ```bash pw-top -b -n 3 journalctl --user -u pipewire -u wireplumber --since "30 min ago" | grep -Ei 'xrun|underrun' ``` A coluna `ERR` do `pw-top` deve ficar em zero. No meu caso o driver rodou com `WAIT 30.5us` contra `BUSY 4.6us` num quantum de 512, folga confortável. Um detalhe: o `pactl list sinks` reporta `Sample Specification: float32le 2ch 48000Hz` mesmo com o hardware em 192 kHz. Isso é a camada de compatibilidade PulseAudio mostrando o formato do grafo, não o do device. Ignore, e olhe o `hw_params`. Vale registrar duas configurações da bottle que afetam o resultado. Em `~/.cxoffice/Qobuz_Installer/drive_c/users/crossover/AppData/Roaming/Qobuz/settings-*.json`, o campo `currentDevice` estava vazio, ou seja, WASAPI compartilhado, nenhum device exclusivo selecionado. E `downloadQuality` estava em `7`, que no esquema do Qobuz é o teto de 24/96 (o tier de 24/192 é o `27`). Esse campo rege download e import, não streaming, mas se o app parecer preso em 96 kHz é o primeiro lugar para olhar. ![Página do álbum Cross Purposes no app Qobuz via CrossOver, com o selo Hi-Res 24-Bit 44.1 kHz, o toggle Downloads only ativo e as faixas marcadas com o ícone de download concluído](/images/qobuz-no-linux/qobuz-crossover-downloads.png) Como bônus, o app oficial é o único caminho da lista que dá acesso ao download offline da assinatura, com o filtro `Downloads only` funcionando normalmente dentro da bottle. Veredito: Sim, funciona! Rode o app oficial via Wine, e o caminho padrão via PipeWire já basta desde que `allowed-rates` esteja configurado. O malabarismo de wine-staging com backend ALSA e modo exclusivo resolve um problema que o PipeWire moderno já não tem. ## Opção 3: Strawberry O [Strawberry](https://www.strawberrymusicplayer.org/) é o player que já uso para a coleção local, é Qt, tem backend GStreamer com saída ALSA direta e traz suporte nativo ao Qobuz. É a opção mais elegante em teoria e a mais irritante de configurar na prática. O motivo: o Qobuz não distribui credenciais de API para clientes de terceiros. Você precisa fornecer um `App ID` e um `App Secret` que o próprio web player usa, extraídos do bundle JavaScript. ### Extraindo as credenciais ```bash pipx install qobuz-dl python - <<'EOF' from qobuz_dl.bundle import Bundle b = Bundle() print("app_id:", b.get_app_id()) for k, v in b.get_secrets().items(): print("secret:", k, v) EOF ``` Saem um `app_id` e vários secrets. Não existe critério documentado para saber qual funciona, terá que testar um por um até achar o correto. ### Configurando Em `Configurações > Qobuz`: - marque `Enable Qobuz` - `App ID` e `App Secret` conforme extraídos - usuário e senha da conta - `Preferred audio format`: escolha o topo (24-bit / 192 kHz) e deixe o servidor negociar para baixo quando a faixa não tiver - ajuste os limites de busca, o padrão é baixo e faz parecer que sua biblioteca sumiu Em `Configurações > Backend`: - `Output`: `alsasink` - `Device`: o `hw:` do seu DAC, nunca `plughw:` (o `plughw` converte em silêncio, que é precisamente o que estamos tentando evitar) - desative fading, replaygain e equalizador se o objetivo é bit-perfect Com tudo no lugar, o Qobuz vira mais uma fonte na barra lateral, ao lado de Biblioteca, Arquivos e Rádios: ![Strawberry Music Player com a fonte Qobuz selecionada na barra lateral, busca por Soul Men, o álbum de Sam & Dave carregado na playlist e as colunas mostrando 192000 Hz, 24 Bit, 3233 kbps e formato FLAC na faixa em reprodução](/images/qobuz-no-linux/strawberry-album.png) As colunas `Sample Rate`, `Profundidade de bits` e `Fonte` são o motivo de usar o Strawberry: `192000 Hz`, `24 Bit`, `FLAC` e `Stream` na mesma linha, sem precisar abrir o `/proc`. ![Playlist de 93 faixas carregada no Strawberry a partir do Qobuz, com Killing Me Softly With His Song da Roberta Flack tocando em 192000 Hz e 24 Bit, e o restante das faixas listadas com origem Stream](/images/qobuz-no-linux/strawberry-playlist.png) ### O erro que você vai encontrar ``` Invalid Request Signature parameter (request_sig) (400) ``` Busca funciona, playback não. Significa que o secret escolhido não é aceito para assinar a requisição de stream. Volte, troque o secret, repita. Isso não é bug do Strawberry, a secret correta funcionará tudo ok. Único ponto é que o Strawberry não busca playlists, mas resolvi isso com um script, baixando todas as minhas playlists no formato .xspf (talvez um segundo artigo somente com isso, mas já versionei no meu repo de dotfiles). ## Opção 4: clientes alternativos e nativos ### QBZ O [QBZ](https://github.com/vicrodh/qbz) (https://qbz.lol/) é um cliente Qobuz nativo para Linux escrito em Rust, MIT, sem telemetria, e na versão 2.0 abandonou webview e IPC: processo único, UI em Slint. Backends PipeWire, ALSA (com modo `Direct hw:` de bypass), PulseAudio e JACK, com passthrough para o DAC e troca de sample rate por faixa, de 44,1 a 192 kHz. Tem também DSD com DoP e passthrough nativo, gapless, MPRIS, scrobbling para Last.fm e ListenBrainz, Chromecast e DLNA, e um assistente de configuração de hardware. Instalação: ```bash # Arch yay -S qbz-bin # Fedora / openSUSE (glibc 2.39+) sudo dnf install ./qbz-*.rpm # baixe o RPM em github.com/vicrodh/qbz/releases # Flatpak flatpak install flathub com.blitzfc.qbz ``` Aviso relevante: Não vai de Flatpak! - o sandbox dele limita a interação com o hardware. O login é OAuth pelo navegador, sem senha digitada dentro do app, e existe o modo offline para quem só quer tocar a coleção local: ![Tela inicial do QBZ com o logo do player, o aceite dos termos de serviço do Qobuz marcado, o botão Sign in with your browser e o link para iniciar em modo offline](/images/qobuz-no-linux/qbz-login.png) Depois do login a biblioteca inteira aparece, favoritos e playlists incluídos, com contadores por tipo: ![Biblioteca do QBZ mostrando 2885 itens divididos em 2326 faixas, 58 álbuns, 239 artistas e 6 selos, com a grade de capas e a lista de playlists na barra lateral esquerda](/images/qobuz-no-linux/qbz-library.png) A página de álbum traz uma coluna `Quality` por faixa, o que evita a surpresa de descobrir no meio da audição que aquele disco específico é só CD quality: ![Página do álbum Headless Cross do Black Sabbath no QBZ, com a coluna Quality marcando HI-RES 24-bit 44.1 kHz em cada faixa e o rodapé indicando os backends SYST e DEFAULT](/images/qobuz-no-linux/qbz-album.png) E tem o modo tela cheia, para quando o notebook vira aparelho de som: ![Modo Now Playing em tela cheia do QBZ, com a capa de Headless Cross ampliada, fundo degradê extraído da arte, o selo Hi-Res 24-bit 44.1 kHz e os controles de reprodução centralizados](/images/qobuz-no-linux/qbz-now-playing.png) Detalhe interessante para quem tem um Pi ou um mini-PC sobrando: o projeto embarca o `qbzd`, um daemon headless que transforma a máquina num endpoint Qobuz Connect, aparecendo nos apps oficiais como se fosse um streamer de hardware. Existe [fork containerizado](https://github.com/yet-another-quentin/qbzd) para quem prefere Docker ou LXC no Proxmox. Isso resolve o cenário "Qobuz Connect no Linux". ### Qobine, ex qobuz-player `SofusA/qobuz-player`, agora é [SofusA/qobine](https://github.com/SofusA/qobine) Nasceu como fork do hifi.rs e divergiu bastante. Hoje é um monorepo em Rust, GPL-3.0, reunindo TUI, player GTK para GNOME, servidor web com UI própria, player RFID e Qobuz Connect experimental. Suporta até 24 bits / 192 kHz, MPRIS (controle via `playerctl` ou qualquer cliente D-Bus) e gapless. O modo RFID é a parte divertida: aproxime um cartão, toca o álbum. Jukebox caseiro para quem sente falta de objeto físico e não quer voltar a limpar agulha de toca-discos. O [hifi.rs](https://github.com/iamdb/hifi.rs) original está parado desde 2024. ## Opção 5: tratar o Qobuz como fonte de rede ### Lyrion Music Server + squeezelite O antigo Logitech Media Server virou [Lyrion](https://lyrion.org/). O servidor roda em Docker, o `squeezelite` roda no host que tem o DAC e fala ALSA direto, com suporte a 44,1 até 384 kHz e sincronismo multiroom. ```bash docker run -d --name lyrion \ -p 9000:9000 -p 3483:3483 -p 3483:3483/udp \ -v lms-config:/config -v /srv/musica:/music \ lmscommunity/lyrionmusicserver:latest # no host com o DAC sudo dnf install squeezelite squeezelite -l # lista os devices squeezelite -o hw:2,0 -n "sala-fedora" ``` Depois, em `Settings > Plugins`, instale o plugin do Qobuz e faça login. ### upmpdcli Renderer UPnP baseado em MPD, com módulo de media server para Qobuz. É a rota certa para quem controla tudo via BubbleUPnP. ### Music Assistant Se você já roda Home Assistant, o [Music Assistant](https://www.music-assistant.io/music-providers/qobuz/) tem provider de Qobuz com o catálogo Hi-Res, seleciona automaticamente a melhor qualidade disponível e sincroniza favoritos nos dois sentidos. Toca em Chromecast, Sonos, AirPlay e afins. ## E o equalizador? No Android eu tenho EQ dentro do próprio player, sem sair da cadeia de bits, porque o UAPP e o HiBy processam antes de entregar ao DAC. No Linux o caminho é o [EasyEffects](https://github.com/wwmm/easyeffects) sobre PipeWire, que aceita perfis do AutoEQ para IEMs e headphones conhecidos. EQ é processamento e o processamento acontece em float, que exige conversão. Não uso e nem sinto falta fora do smartphone. ## Comparativo | Caminho | Hi-Res real | Bit-perfect | Esforço | Estabilidade | |---|---|---|---|---| | Web player (qualquer navegador) | Recebe até 24/192 | Não, reamostra para a taxa do grafo | Zero | Alta | | App Windows via Wine/CrossOver | Sim, medido em 24/96 e 24/192 | Sim, com WASAPI compartilhado e PipeWire com `allowed-rates` | Médio | Média, depende do Wine e do updater do Electron | | Strawberry | Sim | Sim, com `alsasink` + `hw:` | Médio, credenciais chatas | Média, depende da API não mudar | | QBZ | Sim | Sim, PipeWire ou ALSA Direct | Baixo | Boa, projeto ativo | | Qobine (ex qobuz-player) | Sim | Sim, via GStreamer/ALSA | Baixo | Boa, escopo menor | | Lyrion + squeezelite | Sim | Sim | Médio | Alta | | upmpdcli | Sim | Sim | Médio | Depende da versão, atualize | | Music Assistant | Sim | Depende do endpoint | Baixo se já roda HA | Alta | ## O que eu uso Desktop Fedora 44 com KDE, saída pela SSL2+ ou pelo TOSlink para o Edifier. Strawberry. Notebook Arch com Hyprland, DAC Fosi K5 Pro: Strawberry. PWA no Navegador (Brave), para navegar na interface, descobrir álbuns e playlists, ler o editorial... Sem culpa, e sem fingir que é audiófilo. Em todos os casos acima, consigo controlar o player pelo celular com o KDE Connect ou pelo app nativo (atualmente estou usando o Qobuz Beta). O instalador falhou ao tentar rodar via Wine, porém, instalou tranquilamente no CrossOver, logo não tentei resolver nem fazer um tshot para instalar no wine (até porque, não quero usar nesse formato)... O QBZ é excelente, uma alternativa ótima entre Strawberry e o PWA. Ah, estou lá no Qobuz Club https://club.qobuz.com/u/7ee25f7b E minhas playlists são publicas por lá também. ## Referências - [QBZ](https://github.com/vicrodh/qbz) e [FAQ do projeto](https://github.com/vicrodh/qbz/wiki/FAQ) - [Qobine, ex qobuz-player](https://github.com/SofusA/qobine) - [Qobuz-wine-guide](https://github.com/logon84/Qobuz-wine-guide) - [Strawberry Music Player](https://www.strawberrymusicplayer.org/) - [Lyrion Music Server](https://lyrion.org/) e [squeezelite](https://lyrion.org/players-and-controllers/squeezelite/) - [upmpdcli](https://www.lesbonscomptes.com/upmpdcli/) - [Music Assistant, provider Qobuz](https://www.music-assistant.io/music-providers/qobuz/) - [Bugzilla 1400731, sample rate no Firefox](https://bugzilla.mozilla.org/show_bug.cgi?id=1400731) --- ## Meu Setup Musical - URL: https://esli.blog/posts/meu-setup-musical/ - Date: 2026-07-28 - Series: blogging - Tags: blogging, music, linux, streaming, audio, setup, headphones, earbuds, audiophile, daw, amplifiers, bass *Versão em português do post [My Music Setup](/posts/my-music-setup/).* ## Fones e earbuds - Samsung Galaxy Buds Pro (1º modelo) - earbuds - AKG K240 Mini II - headphone - AKG N9 Hybrid - headset - Edifier W820NB Plus - headset - KZ ZSN Pro 2 - IEM - Fosi IM4 In-ear Open-Back Monitor ## Dispositivos de áudio: interfaces, DACs e amplificadores - iFi Go Link - DAC USB + amplificador de fone (somente reprodução) - Fosi Audio K5 Pro - SSL2+ MKII - interface de áudio USB - Behringer Link UCG102 - interface de guitarra USB "portátil" ## Mini amplificadores de baixo - VOX AP2-BS - Blackstar Fly 103 Bass ## Caixas de som - Edifier S360DB Hi-Res - Sony HT-S700RF - soundbar ## Apps e serviços - Qobuz - Spotify - USB Audio Player Pro (Android) - HiByMusic (Android) - Strawberry - Linux (conectado ao Qobuz) - MusicPod - Linux (ótimo frontend para rádios e podcasts) - Guitar Pro 8 (Wine/Bottles) - ultimate-guitar.com (web) - Yousician (Android/Windows) ## Pedais de baixo - BOSS ME-20B - pedaleira e pré-amplificador - BOSS Chorus CEB-3 - Fender Drive Overdrive PR-2525 - TMiranda Bass Drive Bo-1 - Behringer Vintage Bass VB1 ## Baixos - Sire Marcus Miller V7 - Epiphone SG EB-3 - Epiphone Tobias IV - Memphis Tagima JB ## Outros instrumentos - Violão Stagg (aço, eletrico. Modelo Ovation Style) - Guitarra Epiphone SG 400 Pro E... - Gaitas Hering (G, C), modelo Vintage Harp 1923 - Flauta de pã - Flauta indígena de madeira Observação: mas eu não toco esses muito bem... me falta controle de respiração. ## Tralhas - Um monte de cabos Fender P10/P10, retos e angulados. - Um antigo e ótimo afinador com metrônomo Cherub WMT-568C, funcionando bem (provavelmente comprei em 2004). ## O que eu acho do meu setup Baixei o Wavelet no Android, mas como o HiByMusic e o USB Audio Player Pro se conectam ao Qobuz e têm equalizador próprio, estou explorando bastante esses dois antes de combinar com qualquer outro software (no caso do Wavelet, seria app do Qobuz + Wavelet). Dia a dia é só o Qobuz mesmo. Os produtos Behringer que tenho são terríveis. Foi dinheiro jogado fora, e eu teria vergonha de revender para alguém. Mas acho que devo ter sido enganado pela loja, que vendeu algo usado ou recondicionado (no caso do pedal), e tive muita dor de cabeça com as configurações do UCG102 no Linux no passado. O Galaxy Buds Pro é excelente, mas só para ouvir música no smartphone. Às vezes uso no Linux, mas sinto que esquenta mais do que o normal (ou provavelmente me desacostumei com in-ear). O Edifier W820NB Plus era o meu fone do dia a dia (2023, 2024 e 2025)... trabalho, reuniões, perfeito no Windows e no Linux, mas no futuro não vou comprar mais nada Bluetooth, só dispositivos com opção wireless de 2,4 GHz. Em 2025 (bf) comprei um AKG N9 Hybrid com dongle (conexão wifi e bt) - virou meu fone padrão para o trabalho. Os mini amplificadores são bons, mas muito limitados. Eu sabia disso quando comprei e estou tranquilo com eles, mas preciso de um amplificador padrão para o baixo (na verdade, algo que minha esposa pediu quando notou a diferença quando toco em outros modelos). Os pedais do fabricante brasileiro TMiranda são superiores aos da Fender, BOSS e outros que já usei. Os amplificadores deles estão na minha lista de desejos. Meu primeiro baixo foi um Memphis Tagima Jazz Bass sunburst. Tenho ele desde 2005, e continua sendo o meu favorito. ![](/images/my-music-setup/080cb0a2-c6c2-48dd-aeb5-6e7b411b6aad.png) A configuração das conexões é a seguinte: - Laptop (Omarchy/Arch) --> USB-C --> DAC Fosi K5 Pro --> coaxial (RCA L/R) --> S360DB (aux) - Workstation (Fedora 44 KDE) --> USB-C --> interface SSL 2+ MKII --> coaxial (RCA L/R) --> S360DB (pc) - Workstation (Fedora 44 KDE) --> TOSlink --> S360DB (opt) Opcionalmente, tenho um cabo RCA 3,5 mm <--> P2 para ligar o DAC, a interface ou os pedais à saída do amplificador BlackStar, deixando o setup mais portátil sem precisar mover o Edifier 360. A importância de bons cabos: procure sempre cabos blindados, com carcaça em liga de alumínio, conectores banhados a ouro, isolamento em PVC e condutores de cobre esmaltado... Essas características não são nada "extraordinário", mas existem cabos baratos que não atendem a esses padrões e podem degradar a qualidade do áudio. O mesmo vale para cabos de rede, cabos HDMI e por aí vai. Eles sempre serão o componente mais barato de todo o setup e, por isso, acabam sendo negligenciados. Não economize aqui. E a minha maior frustração: não conseguir montar uma workstation completa e funcional como home studio usando 100% Linux. Tentei por muito tempo, usando o Ubuntu Studio (porque ele já vem pronto para usar)... Usei bastante para gravar e editar vídeos (principalmente OBS e Kdenlive), mas nunca consegui capturar e gravar o baixo com total satisfação (para mim). Esses equipamentos foram comprados há muito tempo (exceto a SSL2+), então preciso atualizar esse ambiente: fones fechados para gravação e um microfone condensador. Vou montar meu novo home studio em 1 ano provavelmente (quando eu me mudar para um espaço novo e melhor, espero) e aí poderei melhorar a acústica do ambiente e investir mais nisso. Para usar a SSL2+ no Linux, estou testando vários apps, mas ultimamente tenho usado mais o REAPER na versão trial (e o Mixbus 11, cuja licença veio junto com a SSL2+ e também tem versão para Linux). O PC está conectado à SSL2+ (USB-C), mas também está ligado diretamente ao Edifier S360DB por Toslink (interface de cabo óptico). Usando o controle remoto do Edifier, consigo alterar entre as entradas/fontes de audio. Desde a pandemia eu não toquei nada. Toco e estudo baixo desde 2005, já toquei em algumas bandas, mas negligenciei completamente nos últimos 5 anos... A ideia de uma casa nova, um espaço para home studio, me devolveu o entusiasmo. :-) Espero muito atualizar este post em um futuro próximo e, se isso acontecer, ficarei imensamente feliz. Enquanto isso... Escrevi sobre transformar um smartphone antigo em um Digital Audio Player (DAP) dedicado, usando alguns apps, um launcher alternativo de Android, shell script para remover apps via comando adb e o meu iFi Go Link (amplificador e DAC USB-C): [transformando-antigo-smartphone-em-dap](/posts/transformando-antigo-smartphone-em-dap/) [transform-old-smartphone-in-a-digital-audio-player-dap](/posts/transform-old-smartphone-in-a-digital-audio-player-dap/) ## DAW Na maior parte do tempo, estou usando o Mixbus 11. Mas tenho instalado os trials do Mixbus 12, LiveTrax 2 (e depois instalei o LiveTrax 3 - recem lançado), REAPER, Ardour 9... E escrevi estes artigos sobre várias DAWs e especificamente sobre Ardour, Mixbus e LiveTrax: [daw-no-linux](/posts/daw-no-linux/) [daw-ardour-mixbus-e-livetrax](/posts/daw-ardour-mixbus-e-livetrax/) Em inglês: [daws-on-linux (EN)](/posts/daws-on-linux/) E, mais recentemente, escrevi dois artigos que saem do "o que eu tenho" e vão para o "como isso funciona na prática" na hora de tocar: Em [Contrabaixo no Linux](https://esli.blog/posts/contrabaixo-no-linux/) eu conto como deixei o desktop Fedora pronto para tocar, gravar e escutar contrabaixo com a SSL 2+ MkII: o PipeWire ajustado para 48 kHz, a busca pela latência mínima com ALSA exclusivo, launchers dedicados para cada programa e uma comparação honesta entre Ardour 9.7, Mixbus 11, Mixbus 12, LiveTrax 3 e REAPER 7.78. Já em [linux-daw-ssl-lowlatency](https://esli.blog/posts/linux-daw-ssl-lowlatency/) (em inglês e pt/br) eu explico o repositório que resolve a parte chata dessa história: um wrapper que tira a placa das mãos do PipeWire, entrega o dispositivo ao DAW em ALSA exclusivo e devolve tudo ao normal quando você fecha o programa, cobrindo inclusive o REAPER, que não implementa o protocolo de reserva de dispositivo e por isso brigava com o PipeWire. --- ## Guia prático: todas as formas de ver o ADS-B em tempo real - URL: https://esli.blog/posts/guia-visualizacao-adsb/ - Date: 2026-07-27 - Series: tech - Tags: tar1090, hardware, rf, linux, sdr, radio, dongle, rtl-sdr, adsb, readsb Sexto artigo da série, e este é diferente dos anteriores: menos narrativa, mais guia de consulta. Com o pipeline inteiro no ar, ficou a pergunta prática do dia a dia: "quero ver os aviões agora, qual comando eu rodo?". Este post é a resposta, com todas as interfaces que o `readsb` expõe, da mais técnica à mais amigável. A série até aqui: o [primeiro artigo](https://esli.blog/posts/sdr-radio-no-linux/) apresentou o projeto de gerar meus próprios dados do mundo físico; o [segundo](https://esli.blog/posts/adsb-sdr-radio-no-linux/) explicou o protocolo ADS-B e sua falta de autenticação; o [terceiro](https://esli.blog/posts/rtl-sdr-v4/) cobriu o hardware RTL-SDR v4 e a instalação do driver; o [quarto](https://esli.blog/posts/rtl-sdr-v4-adsb-1090/) montou o pipeline de decodificação com o `readsb` até as primeiras aeronaves no terminal; e o [quinto](https://esli.blog/posts/rtl-sdr-v4-tar1090/) trocou o fork do `readsb` e colocou o mapa web ao vivo no ar com o `tar1090`. O ponto de partida deste guia: o serviço `readsb` já roda em background (`systemctl status readsb`) e expõe os dados por várias portas TCP locais. Abaixo, os comandos pra cada forma de visualização, e os apps GUI instalados que servem de apoio. ## 1. Visualização interativa no terminal: viewadsb A forma mais direta de "ver os aviões" sem precisar de mapa web. É um cliente ncurses que já vem com o `readsb`, conecta em `127.0.0.1:30005` (Beast) por padrão. ```bash viewadsb ``` Mostra uma tabela ao vivo, com um cabeçalho assim: ``` Hex Mode Sqwk Flight Alt Spd Hdg Lat Long RSSI Msgs Seen ``` `Ctrl+C` pra sair. **Importante**: precisa rodar direto num terminal SSH de verdade (ele usa a tela inteira via ncurses), não funciona bem através de pontes de automação sem TTY. Variações úteis: ```bash viewadsb --metric # altitude/velocidade em métrico em vez de pés/nós viewadsb --show-only=E491F0 # filtra só uma aeronave específica pelo ICAO hex ``` ### Glossário das colunas (pra quem não é da área) | Coluna | Significado | | ------------ | ------------------------------------------------------------------------------ | | `Hex` | Código único da aeronave (ICAO), tipo uma "placa" do transponder | | `Mode` | Modo do transponder (S = Mode S, o padrão moderno) | | `Sqwk` | Squawk, código de 4 dígitos que o piloto configura (7700 = emergência, por ex.) | | `Flight` | Número do voo/callsign (ex: `TAM3994`) | | `Alt` | Altitude em pés | | `Spd` | Velocidade no solo, em nós | | `Hdg` | Rumo/direção (graus, 0-360) | | `Lat`/`Long` | Posição (latitude/longitude) | | `RSSI` | Força do sinal recebido (mais perto de 0 = sinal mais forte) | | `Msgs` | Quantas mensagens dessa aeronave já foram recebidas | | `Seen` | Segundos desde a última mensagem recebida | ### Alternativa mais amigável: radar-simples.sh Pra não depender do jeito abreviado do `viewadsb`, escrevi um script que lê `/run/readsb/aircraft.json` (via `jq`) e mostra uma tabela com nomes de coluna por extenso, em português, sem precisar decifrar sigla nenhuma: ```bash ~/radar-simples.sh # atualiza a cada 2s (padrão) ~/radar-simples.sh 5 # atualiza a cada 5s ``` Saída (exemplo real): ``` === Aviões ao vivo - 19:16:20 === 3 aeronaves detectadas, 1 com posição conhecida VOO ALTITUDE(pés) VELOCIDADE(nós) RUMO(°) LATITUDE LONGITUDE SINAL VISTO HÁ(s) PSEKR 3075 147 270 -23.538860 -46.553853 -17.7 0 Atualiza a cada 2s - Ctrl+C pra sair | Mapa: http://172.22.2.108/tar1090/ ``` É ele rodando no painel do meio aqui, entre o `viewadsb` (topo) e o stream SBS bruto (embaixo): ![Três terminais no servidor: viewadsb com a tabela técnica no topo, o radar-simples.sh com colunas em português no meio e o stream SBS bruto via nc embaixo](/images/guia-visualizacao-adsb/terminal_20260725_183615.png) Script completo (`~/radar-simples.sh`): ```bash #!/bin/bash # Visão simplificada em português dos aviões recebidos pelo readsb. # Lê /run/readsb/aircraft.json e mostra uma tabela com nomes de coluna claros. DATA=/run/readsb/aircraft.json INTERVAL="${1:-2}" if ! command -v jq &>/dev/null; then echo "Precisa do 'jq' instalado (sudo pacman -S jq)." exit 1 fi trap 'echo; echo "Saindo."; exit 0' INT while true; do clear if [[ ! -f "$DATA" ]]; then echo "Não achei $DATA - o serviço readsb está rodando?" sleep "$INTERVAL" continue fi TOTAL=$(jq '.aircraft | length' "$DATA") COM_POSICAO=$(jq '[.aircraft[] | select(.lat and .lon)] | length' "$DATA") echo "=== Aviões ao vivo - $(date '+%H:%M:%S') ===" echo "$TOTAL aeronaves detectadas, $COM_POSICAO com posição conhecida" echo { echo -e "VOO\tALTITUDE(pés)\tVELOCIDADE(nós)\tRUMO(°)\tLATITUDE\tLONGITUDE\tSINAL\tVISTO HÁ(s)" jq -r ' .aircraft | map(select(.lat and .lon)) | sort_by(.seen) | .[] | [ ((.flight // "(sem voo)") | gsub("^\\s+|\\s+$";"")), (if .alt_baro then (.alt_baro|tostring) else "-" end), (if .gs then (.gs|round|tostring) else "-" end), (if .track then (.track|round|tostring) else "-" end), (.lat|tostring), (.lon|tostring), (if .rssi then (.rssi|tostring) else "-" end), (if .seen then (.seen|round|tostring) else "-" end) ] | @tsv ' "$DATA" } | column -t -s $'\t' echo IP=$(ip route get 1.1.1.1 2>/dev/null | grep -o 'src [0-9.]*' | cut -d' ' -f2) echo "Atualiza a cada ${INTERVAL}s - Ctrl+C pra sair | Mapa: http://${IP}/tar1090/" sleep "$INTERVAL" done ``` ## 2. Stream de texto (SBS/BaseStation): bom pra logging ou grep ```bash nc localhost 30003 ``` Cada linha é uma mensagem decodificada em CSV (formato SBS). Foi assim que a captura foi validada lá no quarto artigo. Dá pra redirecionar pra arquivo: ```bash nc localhost 30003 | tee -a ~/adsb-log-$(date +%F).csv ``` Ou filtrar por aeronave/campo com `grep`/`awk` em tempo real: ```bash nc localhost 30003 | grep E491F0 ``` ## 3. Stream raw (Mode-S hex): porta 30002 ```bash nc localhost 30002 ``` Mensagens Mode-S em hexadecimal bruto, sem decodificação de posição/velocidade. Mais cru, útil pra depurar reconstrução de mensagens, não pra leitura direta. ## 4. Beast binário: porta 30005 ```bash nc localhost 30005 | xxd | head ``` Formato binário (usado pelo `viewadsb` e por feeders como o do ADSBExchange/FlightAware). Não é legível diretamente: só use se for alimentar outra ferramenta que fale o protocolo Beast. ## 5. Arquivos JSON em /run/readsb/ ```bash ls -la /run/readsb/ cat /run/readsb/aircraft.json | jq . ``` O `aircraft.json` (estado atual) é escrito periodicamente em **JSON puro**, legível direto ou com `jq`. Nota: isso só é verdade depois da troca pro fork `readsb-wiedehopf-git`; o fork Mictronics usado inicialmente gravava em protobuf binário, ilegível sem decodificar (a história completa da troca está no [quinto artigo](https://esli.blog/posts/rtl-sdr-v4-tar1090/)). É esse arquivo que alimenta o `radar-simples.sh` (seção 1) e o tar1090 (seção 7). ## 6. Apps GUI instalados no servidor (auxiliares/alternativos) O servidor tem uma sessão gráfica ativa (Wayland, seat0/tty1), então dá pra rodar interface gráfica se você tiver acesso à área de trabalho dessa máquina, local ou via alguma solução de acesso remoto gráfico (não incluído aqui). ### SDR++ (sdrpp-git, já instalado) ```bash sdrpp ``` É um receptor SDR de uso geral (waterfall, espectro, demodulação), **não decodifica ADS-B**. Os plugins instalados nessa build são pra source de hardware (rtl_sdr, hackrf, limesdr, etc.), demodulação de rádio (`radio.so`), M17, pager, ATV e meteor (satélite). Não há plugin de ADS-B nesse build específico do SDR++. **Uso como auxiliar**: sintonize 1090 MHz manualmente no SDR++ pra ver visualmente a energia RF chegando (pulsos no waterfall toda vez que um avião transmite). Bom pra confirmar visualmente se a antena está captando sinal, mas não substitui o `readsb` pra decodificação real. ### SatDump Instalado via AUR (`yay -S satdump`), como documentado no quinto artigo. É focado em satélites (137 MHz, NOAA, Meteor), não em ADS-B. Buscar esses dados de satélite será um projeto futuro do homelab. ## 7. Mapa web ao vivo: tar1090 ``` http://IP-DO-SERVIDOR/tar1090/ ``` Mostra um mapa ao vivo estilo FlightAware/ADSBExchange: ícone do avião na posição real, disponível pra qualquer dispositivo na rede do servidor (ou externamente, se você quiser expor). A opção mais amigável de todas: ![Mapa web do tar1090 no navegador com quatro aeronaves ao vivo sobre a região metropolitana de São Paulo, trilhas de voos anteriores e tabela com callsign, tipo, altitude e velocidade](/images/guia-visualizacao-adsb/tar1090web_20260725_184248.png) Clica no avião e vê voo, altitude e velocidade sem decifrar sigla nenhuma. Aqui um A350 da Ethiopian Airlines cruzando a região, com matrícula, tipo e RSSI no tooltip: ![Mapa do tar1090 com tooltip aberto pro voo ETH506, Airbus A350 da Ethiopian Airlines, mostrando matrícula ET-AVE, altitude de 14.400 pés e velocidade de 356 nós](/images/guia-visualizacao-adsb/tar1090_20260724_194127.png) ## Resumo rápido | Quero... | Comando | | ------------------------------------------- | ------------------------------------------ | | Ver tabela ao vivo no terminal (técnico) | `viewadsb` | | Ver tabela ao vivo em português (amigável) | `~/radar-simples.sh` | | Ver mensagens decodificadas (texto) | `nc localhost 30003` | | Gravar log em arquivo | `nc localhost 30003 \| tee -a log.csv` | | Ver mensagens Mode-S cruas | `nc localhost 30002` | | Confirmar visualmente RF em 1090 MHz (GUI) | `sdrpp` (sintonizar 1090 MHz manualmente) | | Mapa web ao vivo (mais amigável de todos) | `http://IP-DO-SERVIDOR/tar1090/` | --- ## Do terminal ao mapa: ADS-B ao vivo na web com tar1090 - URL: https://esli.blog/posts/rtl-sdr-v4-tar1090/ - Date: 2026-07-26 - Series: tech - Tags: tar1090, hardware, rf, linux, sdr, radio, dongle, rtl-sdr, adsb, readsb Quinto artigo da série, e parte 2 do hands-on. No [anterior](https://esli.blog/posts/rtl-sdr-v4-adsb-1090/) o pipeline terminou funcionando, mas só em texto no terminal. Agora chega a recompensa visual: o mapa web ao vivo, estilo Flightradar, servido pelo próprio homelab. Pra quem está chegando agora, o caminho até aqui em uma frase por artigo: o [primeiro](https://esli.blog/posts/sdr-radio-no-linux/) apresentou o projeto de gerar meus próprios dados do mundo físico morando embaixo da rota de Guarulhos; o [segundo](https://esli.blog/posts/adsb-sdr-radio-no-linux/) destrinchou o protocolo ADS-B, a mensagem de 112 bits que toda aeronave grita em 1090 MHz sem autenticação nenhuma; e o [terceiro](https://esli.blog/posts/rtl-sdr-v4/) cobriu o hardware RTL-SDR v4, o que o separa dos clones e a instalação do driver no Arch e no Fedora. O [quarto artigo](https://esli.blog/posts/rtl-sdr-v4-adsb-1090/) foi o hands-on de ponta a ponta no servidor EndeavourOS: validação do dongle em 1090 MHz com `rtl_sdr`, a escolha da antena certa pra polarização vertical do ADS-B, a compilação do `readsb` na unha (com dois patches pra domar o `-Werror` num código sem manutenção frente ao GCC atual), regra `udev` dedicada pro usuário de serviço, override do systemd com as flags corretas, e a prova final: aeronaves reais decodificadas com posição, altitude e velocidade via `nc localhost 30003`. Terminou com uma pendência declarada: os dados existiam só como streams brutos nas portas 30002 a 30005, sem interface visual. É essa pendência que este artigo resolve, e no caminho ela derruba uma decisão da parte 1: o fork do `readsb` vai ser trocado. ## 1. SDR++ (auxiliar visual de RF) Já estava instalado (`sdrpp-git`, AUR) desde o terceiro artigo. Só precisou ser executado. Como o servidor tem uma sessão gráfica Wayland ativa (`seat0`/`tty1`), o app abre normalmente na tela: ```bash sdrpp ``` Confirmado: os plugins dessa build são só fontes de hardware (rtl_sdr, hackrf, limesdr...) e demoduladores de rádio geral (FM, M17, pager, ATV, meteor). **Não existe decodificador de ADS-B no SDR++.** Ele serve pra visualizar o waterfall e o espectro em 1090 MHz manualmente, como conferência visual de que a antena está captando energia RF. Quem decodifica de fato é o `readsb`. ## 2. SatDump Instalado via AUR: ```bash yay -S satdump --answerclean All --answerdiff None --removemake --noconfirm ``` Optei pela versão estável (`satdump` 1.2.2), não a `-git`, por ser um projeto C++/CMake grande: menos chance de quebrar com o GCC do sistema. É focado em satélites (137 MHz, NOAA, Meteor), não em ADS-B; entra aqui só pra completar o stack que o primeiro artigo prometeu. O build (CMake, C++, bastante plugin: inmarsat, cubesat, meteosat, GOES, NOAA/METOP, todos os backends de SDR) rodou em background e levou bem mais tempo que o `readsb`, uns 45 minutos. Compilou sem nenhum patch necessário. Como sempre, a etapa final de instalação precisou rodar manualmente no terminal: ```bash sudo pacman -U ~/.cache/yay/satdump/satdump-1.2.2-5-x86_64.pkg.tar.zst ``` Apareceu um aviso de lint "pacote contém referência a \$srcdir" durante o empacotamento. É informativo, sem efeito prático: alguns binários referenciam o caminho de build nos metadados. Confirmado instalado: ```bash pacman -Q satdump # satdump 1.2.2-5 which satdump # /usr/bin/satdump ``` ## 3. Por que trocar o fork do readsb Ao começar a configurar o **tar1090** (o mapa web ao vivo), percebi que o `readsb-git` instalado na parte 1 (fork **Mictronics**, `readsb-protobuf`) grava os dados em **protobuf binário** (`aircraft.pb`), não em JSON: ```bash file /run/readsb/aircraft.pb # aircraft.pb: data xxd /run/readsb/aircraft.pb | head -5 # 00000000: 08a0 b88f d306 10a9 377a c701 08a2 8192 ........7z...... # 00000010: 0712 0854 414d 3335 3930 2018 f666 20a3 ...TAM3590 ..f . ``` Dá pra reconhecer o callsign `TAM3590` no meio dos bytes: os dados estão certos, só o formato é binário. O tar1090 (e a maioria dos frontends web de ADS-B) espera **JSON** (`aircraft.json`). O fork Mictronics simplesmente não grava nesse formato, não tem flag pra isso. A solução foi trocar para o **`readsb-wiedehopf-git`**, mantido pelo próprio autor do tar1090, que: - grava JSON nativamente - é mais ativamente mantido (última atualização em setembro de 2025, contra um fork Mictronics parado desde ~2020) - compilou **sem nenhum patch** no GCC atual (o fork antigo precisou de dois patches manuais pra contornar o `-Werror` e os novos erros padrão do GCC 14+, como visto na parte 1) ## 4. Removendo o fork antigo e instalando o novo Os dois pacotes entram em conflito, então o `readsb-git` sai primeiro: ```bash sudo pacman -R readsb-git ``` Build do novo fork, fora do cache do yay, mesma lição da parte 1 (dessa vez nem precisou de patch, compilou de primeira): ```bash yay -G readsb-wiedehopf-git cd ~/readsb-wiedehopf-git makepkg -s --noconfirm ``` Instalação: ```bash sudo pacman -U ~/readsb-wiedehopf-git/readsb-wiedehopf-git-3.16.15.r8.g0bfd047-1-x86_64.pkg.tar.zst ``` ## 5. Reconfigurando pro novo fork O pacote novo já vem com service e config próprios, com convenção de variáveis diferente do antigo (`RECEIVER_OPTIONS`/`DECODER_OPTIONS`/`NET_OPTIONS`/`JSON_OPTIONS` em vez do `USER_OPTIONS` único): ```ini # /etc/default/readsb (gerado pelo pacote) RECEIVER_OPTIONS="--device 0 --device-type rtlsdr --gain auto --ppm 0" DECODER_OPTIONS="--max-range 450 --write-json-every 1" NET_OPTIONS="--net --net-ri-port 30001 --net-ro-port 30002 --net-sbs-port 30003 --net-bi-port 30004,30104 --net-bo-port 30005" JSON_OPTIONS="--json-location-accuracy 2 --range-outline-hours 24" ``` O JSON já vem habilitado por padrão (`--write-json` está no próprio `readsb.service` do pacote), a diferença chave em relação ao fork anterior. Passos: ```bash # Remover o override do systemd criado na parte 1 (referenciava $USER_OPTIONS, # variável que não existe mais nesse pacote e quebraria o serviço novo) sudo rm /etc/systemd/system/readsb.service.d/override.conf sudo rmdir /etc/systemd/system/readsb.service.d # Adicionar lat/lon reais na config nova (use as suas coordenadas) sudo sed -i 's/--gain auto --ppm 0"/--gain auto --ppm 0 --lat -23.58 --lon -46.55"/' /etc/default/readsb sudo systemctl daemon-reload sudo systemctl restart readsb ``` A regra `udev` (`99-readsb-rtlsdr.rules`, `GROUP="readsb"`) e o usuário de sistema continuaram funcionando sem alteração: o novo pacote cria o mesmo usuário `readsb`, com o mesmo uid. ### Validação ```bash journalctl -u readsb -n 15 --no-pager ``` ``` Using lat: -23.58, lon: -46.55 (location accuracy: exact) rtlsdr: using device #0: Generic RTL2832U OEM (RTLSDRBlog, Blog V4, SN 00000001) Found Rafael Micro R828D tuner RTL-SDR Blog V4 Detected ``` ```bash cat /run/readsb/aircraft.json ``` ```json { "now" : 1784929956.000, "messages" : 270, "aircraft" : [ {"hex":"e48e77","flight":"GLO2178 ","alt_baro":29875,"gs":435.0,"lat":-23.344178,"lon":-46.254120, ...}, {"hex":"e49246","flight":"TAM3994 ","alt_baro":13125,"gs":308.4,"lat":-23.399901,"lon":-46.175283, ...} ``` Agora sim, JSON de verdade, com voos reais (GLO2178, TAM3994), pronto pro tar1090 ler. E as ferramentas de terminal da parte 1 continuam funcionando normalmente sobre o fork novo: ![Três terminais: viewadsb listando aeronaves com callsign e RSSI, a visão resumida ao vivo com três aeronaves e o stream SBS bruto via nc na porta 30003](/images/rtl-sdr-v4-tar1090/terminal_20260725_183615.png) ## 6. Instalando o tar1090 Não existe pacote `tar1090` no AUR. A instalação oficial é via script (`wiedehopf/tar1090/install.sh`), feito originalmente pra Debian/Ubuntu (usa `apt-get`). Antes de rodar, baixei e **revisei o script inteiro** (500 linhas) pra confirmar que era seguro e entender o que ele faz. Nunca rode um `curl | sudo bash` às cegas, ainda mais de script pensado pra outra distro. Achado importante na revisão: o bloco que usa `apt-get` só roda se faltar `git`, `jq` ou `curl` no sistema. Como já havia `git` e `curl`, só faltava o `jq`. O resto do script (clonar os repos do tar1090 e do banco de dados de aeronaves, gerar configuração de webserver, criar serviço systemd) é genérico o suficiente pra funcionar em qualquer distro com systemd. ### Dependências ```bash sudo pacman -S --needed jq lighttpd sudo mkdir -p /etc/lighttpd/conf.d ``` O `mkdir` é necessário porque o script só ativa o modo automático de configuração do lighttpd (`lighttpd=yes`) se achar esse diretório (convenção Debian de `conf.d`/`conf-enabled`/`conf-available` que o pacote do Arch não usa). ### Rodando o instalador oficial ```bash sudo bash ~/tar1090-install.sh ``` O script sozinho: clonou `wiedehopf/tar1090` e `wiedehopf/tar1090-db`, detectou `/run/readsb/aircraft.json` como fonte de dados, gerou `/etc/lighttpd/conf.d/88-tar1090.conf` (aliases de URL pro `/tar1090/`) e criou e habilitou o serviço `tar1090.service`, um script que processa e compacta os dados do readsb pro formato que o frontend web lê. ### Dois problemas encontrados depois de rodar o script **Problema 1: o lighttpd nunca subiu.** O script só reinicia o lighttpd se ele já estava rodando antes; não habilita nem inicia um lighttpd recém-instalado do zero. Resultado: `systemctl status lighttpd` mostrava `inactive (dead)`, apesar de tudo configurado. **Problema 2: o `lighttpd.conf` do Arch é minimalista e não carrega `conf-enabled`.** O `lighttpd.conf` padrão do pacote Arch tem só o essencial (document-root, index-file), sem nenhuma linha incluindo o diretório onde o script (e o `mkdir` feito antes) colocou as configurações do tar1090. Sem isso, mesmo com o lighttpd rodando, ele nunca leria o `88-tar1090.conf`. Correção: ```bash echo 'include_shell "cat /etc/lighttpd/conf-enabled/*.conf"' | sudo tee -a /etc/lighttpd/lighttpd.conf sudo lighttpd -tt -f /etc/lighttpd/lighttpd.conf # valida a config antes de subir sudo systemctl enable --now lighttpd ``` ### Validação final ```bash curl -sI http://localhost/tar1090/ # HTTP/1.1 200 OK curl -s http://localhost/tar1090/data/aircraft.json | head -c 200 # { "now" : ..., "aircraft" : [{"hex":"e49511","flight":"GLO1238 ", ... "lat":-23.613876,"lon":-46.559397, ... ``` Mapa ao vivo funcionando no IP interno do servidor, em `/tar1090/`. E a recompensa visual depois de cinco artigos: ![Interface web do tar1090 no navegador: mapa da região de Guarulhos com trilhas de voos, tabela com seis aeronaves e painel de detalhes do TAM8088, um Boeing 787-9 da LATAM, com foto, matrícula e telemetria completa](/images/rtl-sdr-v4-tar1090/tar1090.png) Clicando em qualquer aeronave, o painel lateral mostra tudo que o transponder transmite, enriquecido pelo banco de dados do tar1090: matrícula, companhia, tipo, rota, altitude com tendência, velocidade, proa e RSSI. Aqui um 787-9 da LATAM saindo de Guarulhos pra Amsterdã: ![Painel detalhado do tar1090 pro voo TAM8078, Boeing 787-9 com rota GRU-AMS, sobre o mapa com trilhas coloridas por altitude e o círculo do receptor](/images/rtl-sdr-v4-tar1090/tar1090-detalhado.png) O tooltip rápido, sem abrir o painel, já resolve a pergunta clássica de quem olha pro céu ("que avião é esse?"): um A330 da South African Airways subindo a 9.250 pés: ![Mapa do tar1090 com tooltip do voo SAA227, Airbus A330 da South African Airways, mostrando matrícula, altitude, velocidade e RSSI](/images/rtl-sdr-v4-tar1090/tar1090-voo.png) ## 7. Estado final dos serviços ```bash systemctl is-enabled lighttpd tar1090 readsb # enabled # enabled # enabled ``` Todos habilitados, sobrevivem a reboot do servidor. | Serviço | Função | | ---------- | ------------------------------------------------------ | | `readsb` | decodifica ADS-B (fork wiedehopf, JSON) | | `tar1090` | processa e compacta os dados do readsb pro frontend web | | `lighttpd` | serve a página web em `/tar1090/` | E o pipeline inteiro, do sinal RF ao browser, cabe numa foto só: o mapa web de um lado, e do outro os mesmos aviões no `viewadsb`, na visão resumida e no stream SBS bruto: ![Mapa web do tar1090 no navegador ao lado de três terminais no servidor: a visão ao vivo resumida, o viewadsb e o stream SBS bruto, todos mostrando as mesmas aeronaves](/images/rtl-sdr-v4-tar1090/tar1090-mais-terminal-cli.png) O que começou como "e se eu gerasse os meus próprios dados?" no primeiro artigo agora é um serviço de verdade no homelab: antena, RTL-SDR v4, readsb decodificando, tar1090 desenhando e lighttpd servindo, tudo habilitado no systemd. Ainda haverá novos artigos e irei evoluir este lab... --- ## Capturando ADS-B em 1090 MHz com a RTL-SDR v4 - URL: https://esli.blog/posts/rtl-sdr-v4-adsb-1090/ - Date: 2026-07-25 - Series: tech - Tags: tar1090, hardware, rf, linux, sdr, radio, dongle, rtl-sdr, adsb, readsb 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](https://esli.blog/posts/sdr-radio-no-linux/) 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](https://esli.blog/posts/adsb-sdr-radio-no-linux/) 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](https://esli.blog/posts/rtl-sdr-v4/) 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. ```bash 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. ```bash 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 ```bash 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: ```bash 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: ![Mini PC do homelab com o tripé flexível do kit montado sobre o gabinete e a antena estendida na vertical](/images/rtl-sdr-v4-adsb-1090/server-antena.png) ## 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) ```bash 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: ```bash 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()`: ```bash 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: ```bash 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 o `provides` do `rtl-sdr-blog-git`) - `-i`: instala o pacote gerado ao final via `pacman -U` - `--cleanbuild`: força reextração das fontes do zero (garante que o `prepare()` 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: ```bash 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`: ```bash 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: ```bash 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 reiniciar - `udevadm 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`) ```bash 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: ```bash 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 ```bash 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 ```bash 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,,...,,...,,,...,,...,,,,,,...` 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: ![Três terminais lado a lado: viewadsb listando aeronaves com callsign e RSSI, uma visão ao vivo resumida com quatro aeronaves detectadas, e o stream SBS bruto recebido via nc na porta 30003](/images/rtl-sdr-v4-adsb-1090/terminal-viewadsb-nc.png) **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. --- ## linux-daw-ssl-lowlatency: exclusive ALSA for DAWs under PipeWire - URL: https://esli.blog/posts/linux-daw-ssl-lowlatency/ - Date: 2026-07-23 - Series: tech - Tags: linux, audio, pipewire, alsa, wireplumber, daw, reaper, bass This is the technical companion to my [DAWs on Linux](https://esli.blog/posts/daws-on-linux/) post. The code lives at [github.com/Esl1h/linux-daw-ssl-lowlatency](https://github.com/Esl1h/linux-daw-ssl-lowlatency). Here I explain what it does and why, no hand-holding. ## The problem Under PipeWire, the sound card is never yours. PipeWire keeps a node on the ALSA device and mixes everything through it. For desktop audio that is correct. For playing an instrument through an amp simulator it adds a layer of buffering you do not want. The lowest latency path is the ALSA backend opening the hardware device directly (`hw:CARD`), in exclusive mode, which means PipeWire has to let go of the card first. Ardour and its derivatives (Mixbus, LiveTrax) request that release through the D-Bus device reservation protocol (`org.freedesktop.ReserveDevice1`), and PipeWire yields. REAPER does not implement reservation, so it collides with PipeWire and either fails to open the device or falls back to something else. The whole repository exists to close that gap uniformly for every DAW. ## The mechanism WirePlumber exposes the card as a device object with selectable profiles. For the SSL 2+ MkII those are `off` (index 0), `HiFi` (1) and `pro-audio` (2). Setting the profile to `off` makes PipeWire release the kernel PCM, which stays available to any ALSA client. So the wrapper is conceptually three steps: read the device id and its current profile, set the profile to `off`, run the DAW, and on exit set the profile back. ## The wrapper `bin/daw-alsa-ssl` is the whole thing. The interesting part is `read_dev`, which pulls the device id and active profile index out of `pw-dump` JSON, matching by `device.name`: ```bash read_dev() { local dump dump="$(pw-dump 2>/dev/null)" || return 0 [ -n "$dump" ] || return 0 if command -v python3 >/dev/null 2>&1; then printf '%s' "$dump" | python3 -c ' import json, sys match = sys.argv[1] try: data = json.load(sys.stdin) except Exception: sys.exit(0) for o in data: info = o.get("info") or {} props = info.get("props") or {} if props.get("device.name") == match: idx = 1 for p in (info.get("params") or {}).get("Profile", []): if p.get("index") is not None: idx = p["index"] print(o.get("id"), idx) break ' "$CARD_MATCH" elif command -v jq >/dev/null 2>&1; then printf '%s' "$dump" | jq -r --arg m "$CARD_MATCH" ' [ .[] | select(.info.props."device.name" == $m) ] as $x | if ($x | length) == 0 then empty else $x[0] as $o | (($o.info.params.Profile // []) | map(.index) | last) as $idx | "\($o.id) \(if $idx == null then 1 else $idx end)" end' fi } ``` Three decisions worth calling out. Matching is by `device.name` (`alsa_card.usb-...`), not by the numeric WirePlumber id, because that id changes across reboots and replugs. The name is stable. Parsing JSON needs a real parser. Pure bash with grep and sed on `pw-dump` output is fragile: the friendly name "SSL 2+ Mk II" contains digits, so a naive number extraction from `wpctl status` returns garbage (`572` instead of `57`). So the wrapper prefers `python3` (stdlib `json`, present on essentially every distro) and falls back to `jq`. Neither is a hard dependency you install for this; python3 is already there, and jq covers the rare case it is not. One bug worth documenting. The first version fed `pw-dump` on a pipe while passing the Python script as a heredoc to `python3 -`. With both a pipe and a heredoc, the pipe wins as stdin, so Python read the JSON as its program and threw a traceback. `read_dev` silently returned nothing on every run, which meant the card was never freed and REAPER never got exclusive access. The fix is to pass the program with `python3 -c` and keep stdin for the data. Obvious in hindsight, invisible until you check `/proc/asound`. The rest is bookkeeping. Read the id and profile, set a trap so the profile is restored on any exit (normal, INT or TERM), switch to `off`, then exec the DAW: ```bash DEV_LINE="$(read_dev)" DEV_ID="${DEV_LINE%% *}" PREV_PROFILE="${DEV_LINE##* }" restore() { [ -n "${DEV_ID:-}" ] && command -v wpctl >/dev/null 2>&1 \ && wpctl set-profile "$DEV_ID" "${PREV_PROFILE:-1}" >/dev/null 2>&1 } trap restore EXIT INT TERM if [ -n "${DEV_ID:-}" ] && command -v wpctl >/dev/null 2>&1; then wpctl set-profile "$DEV_ID" 0 >/dev/null 2>&1 sleep 0.4 fi "$@" ``` The target card is `DAW_ALSA_CARD`, defaulting to the SSL 2+ MkII. Point it at any interface by exporting the PipeWire `device.name` (find it with `pw-dump | grep device.name`). If the card is not found, or neither python3 nor jq nor wpctl is present, the wrapper degrades gracefully: it just runs the DAW, and the Ardour family still frees the card via D-Bus on its own. Only REAPER loses the automatic release. ## Launchers and installer The launchers are plain `.desktop` templates with a `__WRAPPER__` placeholder in the Exec line: ```ini Exec=__WRAPPER__ /opt/REAPER/reaper %F ``` `install.sh` substitutes the placeholder with the installed wrapper path, writes each launcher into `~/.local/share/applications/`, and skips any DAW whose binary is not present, so you do not get dead menu entries for software you never installed. It is XDG-aware (`~/.local/bin`, `~/.local/share`), needs no root, and runs `update-desktop-database` at the end. `uninstall.sh` reverses it. ## REAPER settings REAPER does not do device reservation, so it is the reason the wrapper exists, and it needs its audio backend pointed at the raw device. The relevant `reaper.ini` keys, after configuring ALSA in Preferences: ```ini linux_audio_mode=1 ; ALSA linux_audio_bsize=128 ; block size linux_audio_bufs=2 ; number of blocks linux_audio_srate=48000 ``` `mode=1` is ALSA. With a 128 sample block and 2 blocks at 48 kHz, the device buffer is 256 frames. Dropping from 3 blocks to 2 is the difference between a 384 and a 256 frame buffer, a real latency cut you take as far as your plugin load stays glitch free. ## System tuning Three things at the OS level matter more than any per-DAW knob. The CPU governor should be `performance`, not `schedutil`, so clocks do not ramp during a take. PipeWire's default clock rate belongs at 48000 (`default.clock.rate = 48000` in a `pipewire.conf.d` drop-in), with 44100 kept in `allowed-rates` so plain playback can still switch down. And realtime scheduling should be granted, either through RTKit (which PipeWire uses by default) or by adding your user to a group with an `rtprio` limit (on Fedora, the `@pipewire` group carries `rtprio 70` and `memlock`). ## Verifying it actually works Do not trust the DAW's own latency readout. Check the kernel. With the DAW open on the SSL, `hw_params` shows the live format: ``` $ cat /proc/asound/card2/pcm0p/sub0/hw_params format: S32_LE rate: 48000 (48000/1) period_size: 128 buffer_size: 256 ``` `fuser -v /dev/snd/pcmC2D0p /dev/snd/pcmC2D0c` shows `reaper` holding both the playback and capture PCM, and the SSL's PipeWire profile reads `off`. Together that is the proof: PipeWire has let go, the DAW owns the hardware directly, at the buffer you asked for. Close the DAW and the profile goes back to `HiFi`, desktop audio returns, and `hw_params` reads `closed`. ## Install ```bash git clone https://github.com/Esl1h/linux-daw-ssl-lowlatency.git cd linux-daw-ssl-lowlatency ./install.sh ``` Then set the ALSA device once in each DAW and open them from the generated launchers. --- ## PT/BR Este é o complemento técnico do meu post [DAWs no Linux](https://esli.blog/posts/daws-no-linux/). O código está em [github.com/Esl1h/linux-daw-ssl-lowlatency](https://github.com/Esl1h/linux-daw-ssl-lowlatency). ### O problema O PipeWire mantém um nó no dispositivo ALSA e mistura tudo por ele. Para o áudio do desktop isso é o certo. Para tocar um instrumento por um simulador de amplificador, adiciona uma camada de buffer que você não quer. O caminho de menor latência é o backend ALSA abrindo o dispositivo de hardware direto (`hw:CARD`), em modo exclusivo, o que exige que o PipeWire solte a placa antes. O Ardour e derivados (Mixbus, LiveTrax) pedem essa liberação pelo protocolo de reserva de dispositivo via D-Bus (`org.freedesktop.ReserveDevice1`), e o PipeWire cede. O REAPER não implementa reserva, então ele colide com o PipeWire e ou falha ao abrir o dispositivo ou cai em outro backend. O repositório inteiro existe para fechar essa lacuna de forma uniforme para todos os DAWs. ### O mecanismo O WirePlumber expõe a placa como um objeto device com perfis selecionáveis. Para a SSL 2+ MkII eles são `off` (índice 0), `HiFi` (1) e `pro-audio` (2). Colocar o perfil em `off` faz o PipeWire liberar o PCM do kernel, que fica disponível para qualquer cliente ALSA. Então o wrapper é, conceitualmente, três passos: ler o id do device e o perfil atual, pôr o perfil em `off`, rodar o DAW e, na saída, devolver o perfil. ### O wrapper O `bin/daw-alsa-ssl` é tudo. A parte interessante é o `read_dev`, que extrai o id do device e o índice do perfil ativo do JSON do `pw-dump`, casando por `device.name`. Três decisões merecem destaque. O casamento é por `device.name` (`alsa_card.usb-...`), não pelo id numérico do WirePlumber, porque esse id muda entre reboots e replugues. O nome é estável. Parsear JSON exige um parser de verdade. Bash puro com grep e sed na saída do `pw-dump` é frágil: o nome amigável "SSL 2+ Mk II" tem dígitos, então uma extração ingênua de número do `wpctl status` devolve lixo (`572` em vez de `57`). Por isso o wrapper prefere `python3` (o módulo `json` da stdlib, presente em praticamente toda distro) e cai para `jq`. Nenhum dos dois é dependência que você instala para isso; o python3 já está lá, e o jq cobre o caso raro de não estar. Um bug que vale documentar. A primeira versão passava o `pw-dump` por um pipe enquanto entregava o script Python por heredoc ao `python3 -`. Com pipe e heredoc juntos, o pipe vence como stdin, então o Python leu o JSON como programa e deu traceback. O `read_dev` retornava vazio silenciosamente a cada execução, o que significa que a placa nunca era liberada e o REAPER nunca ganhava acesso exclusivo. A correção é passar o programa com `python3 -c` e deixar o stdin para os dados. Óbvio em retrospecto, invisível até você conferir o `/proc/asound`. O resto é contabilidade: ler id e perfil, armar um `trap` que restaura o perfil em qualquer saída (normal, INT ou TERM), trocar para `off` e então executar o DAW. A placa alvo é a `DAW_ALSA_CARD`, com padrão SSL 2+ MkII. Aponte para qualquer interface exportando o `device.name` do PipeWire (ache com `pw-dump | grep device.name`). Se a placa não for encontrada, ou se faltar python3, jq e wpctl, o wrapper degrada com elegância: só roda o DAW, e a família Ardour ainda libera a placa via D-Bus sozinha. Só o REAPER perde a liberação automática. ### Launchers e instalador Os launchers são templates `.desktop` com um placeholder `__WRAPPER__` na linha Exec. O `install.sh` substitui o placeholder pelo caminho instalado do wrapper, escreve cada launcher em `~/.local/share/applications/` e pula qualquer DAW cujo binário não exista, então você não fica com entradas mortas no menu para software que nunca instalou. Ele respeita o XDG (`~/.local/bin`, `~/.local/share`), não precisa de root e roda o `update-desktop-database` no fim. O `uninstall.sh` reverte. ### Ajustes do REAPER O REAPER não faz reserva de dispositivo, por isso é a razão de o wrapper existir, e precisa do backend de áudio apontado para o dispositivo cru. As chaves relevantes do `reaper.ini`, depois de configurar ALSA nas Preferences: ```ini linux_audio_mode=1 ; ALSA linux_audio_bsize=128 ; block size linux_audio_bufs=2 ; número de blocos linux_audio_srate=48000 ``` `mode=1` é ALSA. Com bloco de 128 amostras e 2 blocos a 48 kHz, o buffer do dispositivo é 256 frames. Baixar de 3 para 2 blocos é a diferença entre um buffer de 384 e um de 256 frames, um corte real de latência que você leva até onde a carga de plugins ficar sem estalos. ### Ajustes de sistema Três coisas no nível do SO importam mais que qualquer botão por DAW. O governor da CPU deve ser `performance`, não `schedutil`, para os clocks não oscilarem durante uma gravação. O clock padrão do PipeWire deve ficar em 48000 (`default.clock.rate = 48000` num drop-in em `pipewire.conf.d`), com 44100 mantido em `allowed-rates` para a reprodução comum ainda poder cair para 44.1. E o escalonamento em tempo real deve ser concedido, seja pelo RTKit (que o PipeWire usa por padrão) ou adicionando seu usuário a um grupo com limite `rtprio` (no Fedora, o grupo `@pipewire` carrega `rtprio 70` e `memlock`). ### Verificando que funciona de verdade Não confie no medidor de latência do próprio DAW. Confira o kernel. Com o DAW aberto na SSL, o `hw_params` mostra o formato ao vivo: ``` $ cat /proc/asound/card2/pcm0p/sub0/hw_params format: S32_LE rate: 48000 (48000/1) period_size: 128 buffer_size: 256 ``` O `fuser -v /dev/snd/pcmC2D0p /dev/snd/pcmC2D0c` mostra o `reaper` detendo o PCM de saída e o de entrada, e o perfil do SSL no PipeWire lê `off`. Juntos, isso é a prova: o PipeWire soltou, o DAW é dono do hardware direto, no buffer que você pediu. Feche o DAW e o perfil volta para `HiFi`, o áudio do desktop retorna e o `hw_params` lê `closed`. ### Instalação ```bash git clone https://github.com/Esl1h/linux-daw-ssl-lowlatency.git cd linux-daw-ssl-lowlatency ./install.sh ``` Depois, ajuste o dispositivo ALSA uma vez em cada DAW e abra pelos launchers gerados. --- ## DAWs on Linux: Ardour, Mixbus, LiveTrax and REAPER, and a low-latency bass rig with the SSL 2+ MkII - URL: https://esli.blog/posts/daws-on-linux/ - Date: 2026-07-23 - Series: tech - Tags: linux, audio, daw, ardour, mixbus, livetrax, reaper, pipewire, bass I have written about this a few times in Portuguese, always in pieces. This post pulls those pieces together in English and brings the numbers up to date. If you want the originals, here they are: a general [overview of DAWs on Linux](https://esli.blog/posts/daw-no-linux/), a [deeper look at Ardour, Mixbus and LiveTrax](https://esli.blog/posts/daw-ardour-mixbus-e-livetrax/), and the [hands-on writeup of my bass setup](https://esli.blog/posts/contrabaixo-no-linux/) with the SSL 2+ MkII. The scripts and launchers from that last one live in a repository, [linux-daw-ssl-lowlatency](https://github.com/Esl1h/linux-daw-ssl-lowlatency), and I come back to them near the end. My goal here is the one thing all three articles were circling around: on a Linux desktop, which DAW do I actually open to play, record and listen to bass lines, and how do I make the machine get out of the way. ## What a DAW is, briefly A DAW (Digital Audio Workstation) is the digital studio where recording, editing, mixing and mastering happen. It gives you multitrack recording, non-destructive editing, effects and plugins (VST, LV2, AU), automation, virtual instruments and signal routing, all in one place. You capture sound from microphones and instruments, move audio and MIDI around, stack takes across tracks, process in real time or offline, and export the result in professional formats. That description fits every option below. What separates them, on Linux specifically, is which ones run natively, how heavy they are, what they cost, and whether they color the sound on purpose. ## The Linux DAW landscape in 2026 The first honest thing to say is that the famous names do not run natively on Linux. Pro Tools, Logic Pro, Cubase, Studio One Pro 7 (around 199 USD), FL Studio and Ableton Live are Windows or macOS software. Some of them can be coaxed to run through Wine or yabridge, but that is a different article and a different set of headaches. What does run natively, and well, is a shorter but very capable list. Proprietary, native on Linux: - REAPER 7 (Cockos), 60 USD personal, 225 USD commercial. - Bitwig Studio 6, 99 USD Essentials, 199 USD Producer, 399 USD full Studio. - Renoise 3.5, 88 USD, the tracker that never left. - Waveform Pro 13 (Tracktion), a free tier plus paid editions from roughly 149 USD. - Harrison Mixbus 12, 29.99 USD, with Mixbus 12 Pro at 99.99 USD. - Harrison LiveTrax 3, 39.99 USD. Free and open source, native on Linux: - Ardour 9.7, pay what you want starting at 1 USD for a ready to run build, or build the source for free. - LMMS, for electronic music and beatmaking. - Qtractor, a lean MIDI and audio sequencer. - MusE and Rosegarden, MIDI focused, Rosegarden with real notation. - Zrythm, the modern newcomer. - Audacity, more an audio editor than a full DAW, but everyone ends up using it. Prices are for the standard tier. Several of these sell extended versions with more plugins. The point of the list is that a Linux user is not choosing between crumbs. Four of these, Ardour, REAPER, Mixbus and LiveTrax, are the ones I keep installed, and the rest of this post is about them. ## REAPER, the quiet professional REAPER is the outlier of the group and the one I am closest to buying. For 60 USD (personal use, or businesses under 20 thousand USD a year) it gives you a full professional DAW that runs on almost anything, with a famously small CPU and RAM footprint and native builds for Windows, macOS and Linux. The commercial license is 225 USD. A single license includes free upgrades all the way through version 8.99, and the 60 day evaluation is the complete program, not a crippled demo. It is still called the secret DAW of professionals, and the reasons are not technical. There is no aggressive marketing, no celebrity endorsements, the first run looks plainer than FL Studio, and it ships without a bundled instrument library, which VSTs solve in five minutes. What you get instead is total control: the interface, the routing, the keyboard shortcuts and the behavior are all customizable, and it scripts in Lua and ReaScript. For anyone who values power, stability and value over gloss, it is one of the best choices on any platform, and on Linux its native ALSA backend gives the lowest monitoring latency of the whole set. One thing worth stating plainly, because I looked: REAPER does not run sales or hand out coupons. The 60 USD price is the permanent discounted rate for people who qualify, so if you are waiting for a promo code, it does not exist. The trial is the discount. ![REAPER running on my Fedora desktop](/images/daws-on-linux/reaper.png) ## Harrison Audio, and why Mixbus and LiveTrax exist Both Harrison DAWs are built on Ardour's source code, so it helps to know who Harrison is. It is a fifty year old company that invented the in-line console design the industry standardized on, folding the record and monitor paths into a single channel strip. Harrison desks mixed Thriller, Nevermind, Bohemian Rhapsody and Aja, and film work from Transformers to Harry Potter to Marvel. Sony Pictures alone runs thirteen Harrison consoles, and shows like The Simpsons and CSI are mixed on them. Mixbus was the move that brought that analog Harrison sound into software. In October 2023, Solid State Logic acquired Harrison, which matters here because my interface is an SSL. In July 2024 Ben Loftis of Harrison put it on record that Harrison, SSL and parent company Audiotonix committed to supporting Linux for Mixbus and LiveTrax, and called it the strongest Linux support of any pro audio company. The full DAWs, plus the integrated XT plugins, stay supported on Linux, with SSL EQ and Dolby Atmos reserved for Mixbus Pro. The asterisk is that in 2024 Harrison discontinued the standalone AVA plugins for Linux, blaming low Linux license volume and incompatibility with the iLok DRM that SSL requires. The philosophy now is to pour resources into one complete DAW rather than a scattered plugin lineup. So on Linux you get Mixbus and LiveTrax, well supported, and not much else from Harrison as separate plugins. ## Mixbus and LiveTrax today LiveTrax 3 (39.99 USD) is the specialist. It is a multitrack live recorder and virtual soundcheck tool, stripped down and optimized for capturing performances quickly and reliably, with seamless integration for Allen & Heath, SSL Live and, new in version 3, full DiGiCo console integration. It keeps the things a live engineer needs, a real time RTA analyzer, direct recording to FLAC, setlist markers and a Big Clock for monitoring from across the room, and drops everything else. That less is more approach is exactly what makes it stable and fast when a recording cannot be redone. Mixbus 12 (29.99 USD, or 99.99 USD for Pro) is the opposite: the full production DAW. It emulates the workflow of Harrison analog consoles, baking a 32C style EQ, analog saturation and compression into every channel strip, plus tone, tape saturation and sidechain compression across eight stereo mixbuses. It records, edits MIDI, mixes and masters, ships with Harrison's XT plugins, and in the Pro version adds Dolby Atmos with 3D panning and certified export. When I registered my SSL 2+ MkII online, the bundle included a Mixbus 11 license, which is how I ended up owning it. Both LiveTrax and Mixbus can be downloaded and run in demo mode to try, and REAPER has its 60 day trial, so nothing here requires buying blind. ![Mixbus open on my Fedora desktop](/images/daws-on-linux/mixbus.png) ![LiveTrax open on my Fedora desktop](/images/daws-on-linux/livetrax.png) ## Ardour, the foundation Ardour is the crown jewel of free audio and the base that both Harrison DAWs inherit. It is the most complete and professionally viable FOSS DAW, routinely compared to Pro Tools for multitrack recording and mixing. Paul Davis and an active community have built robust multitrack recording, non-destructive editing, advanced mixing and full VST, LV2 and AU plugin support, running natively on Linux, macOS and Windows. It does sample accurate automation, flexible routing, serious MIDI editing and video sync for scoring. Its funding model is unusual and worth respecting: the source is free to build, and ready to run binaries are pay what you want from 1 USD, either as a one time payment or a 1 USD per month subscription that keeps the updates coming. That is what sustains the development. ![Ardour open on my Fedora desktop](/images/daws-on-linux/ardour.png) ### Ardour versus Mixbus Mixbus inherits all of Ardour's recording, editing and sequencing, then adds the modeled Harrison analog processors. The differences are mostly interface and sound coloring: Mixbus gives you a modeled three band analog EQ with a high pass filter, three compressor types, saturation and summing on every strip, plus the eight stereo mixbuses. It also brings the redesigned interface with dedicated Cue, Record, Edit and Mix pages, the Focus Channel for reaching every Harrison tool in one place, commercial support, manuals and video tutorials, and features that reach Mixbus before they reach Ardour. Ardour, by contrast, stays clean and neutral. It is pure DAW function with no built in coloring, completely free and open, but without the commercial support and the analog processors that make Mixbus the pick for anyone chasing that vintage console sound. ### Ardour versus LiveTrax The difference between LiveTrax and plain Ardour is that LiveTrax was deliberately cut down and tuned for live recording and virtual soundcheck. Where Ardour hands you the full editing, mixing, MIDI, plugin and automation toolset, LiveTrax removes all of that and keeps only the essentials for the job: efficient multitrack capture, playback for soundcheck, setlist markers, live specific meters like the RTA analyzer and phase correlation, and direct console integration. Simplicity is the feature. ### Performance Real performance depends on the number of simultaneous tracks, how many plugins you load, the buffer size, the quality of the audio interface and how well the OS is tuned. For serious work, a modern i5 or Ryzen 5, 16 GB of RAM and an SSD for projects is a sane floor. But hold the workload constant and there is no meaningful difference between Ardour, Mixbus and LiveTrax, because they share an engine. The choice between them is simplicity versus features versus specific plugins, not speed. ## What changed recently Since I first wrote about these, the versions moved, so here is the current state. Mixbus 12 refines the redesign that Mixbus 11 introduced and adds real tools. The headline is a DeEsser and a DeNoiser on every channel, processors Harrison says come from its motion picture console work, which folds cleanup into the strip instead of making it a separate step. Cue launching grew to sixteen rows and now records audio and MIDI clips straight into the slots. MIDI editing gained a chord tool in the piano roll, pick the type and click the root. And the interface went darker and higher contrast, leaning into the 32 Classic console look. For bass specifically, the DeEsser tames aggressive slap attacks and the DeNoiser cleans single coil hum, which is a concrete reason to move up. Since I already own the licensed 11 and it sounds just as good, I am waiting for a worthwhile upgrade coupon before jumping. Ardour 9.7 (released 5 June 2026) is a bugfix release with a few real additions. The MIDI Tools sidebar, once piano roll only, now shows up in the main editor through the Editor List (Shift+L), with chord editing and quantization, and the cross cursor for MIDI and automation is on by default in the inline editor. Outside MIDI it added an optional vertical summary, natural sort order across the interface, multitouch on Linux and Windows, saving and recalling port connections per backend or device, and uninterrupted recording even when LTC sync drops. An empty binding map for generic MIDI controllers makes unknown surfaces easier to use. REAPER 7.78 is the current build, and the difference from the Harrison family is philosophy more than features. REAPER is the lightest engine and the most efficient, the near blank canvas you shape with scripting and routing. The Ardour family is opinionated and ships a ready workflow, and Mixbus in particular hands you console tone from the first channel, which for bass is a shortcut: less time building a plugin chain, more time playing. ## The part that actually matters for playing: latency Everything above is about which program to open. This is about making the computer disappear so the bass feels connected to your hands. My machine runs Fedora with a Ryzen 9 3900X and 64 GB of RAM, and the interface is a Solid State Logic SSL 2+ MkII over USB. On any modern Linux desktop, PipeWire owns the sound card at all times. That is great for everyday listening, the browser, Qobuz and notifications all coexisting, but it adds latency the moment you want to play an instrument through an amp simulator inside a DAW. There are two ways to route a DAW to the interface. The first is through PipeWire's own JACK layer, which is convenient because DAW audio coexists with the browser, but it stacks PipeWire's latency on top of the DAW's. The second is the ALSA backend straight to the card, in exclusive mode: the DAW takes the interface for itself, PipeWire releases the device, and round trip latency drops to the floor. The cost is that while the DAW is open no sound comes out of the browser through the SSL. For playing bass through an amp sim, that is the right trade, and it is the one I use. The catch is that PipeWire keeps the card busy by default, and not every DAW asks for it back. Ardour and its family request the release over D-Bus and PipeWire yields, but REAPER does not. So I wrote a small wrapper that switches the SSL's PipeWire profile off before launching the DAW, giving it exclusive ALSA access, and restores the profile when the DAW closes so desktop audio comes back. It finds the card by name rather than by a numeric id, because the id changes across reboots. Around that wrapper I made desktop launchers for all five DAWs, so opening any of them from the menu already lands in exclusive ALSA. I packaged the wrapper, the five launchers and an installer in a public repository: [linux-daw-ssl-lowlatency](https://github.com/Esl1h/linux-daw-ssl-lowlatency). Clone it, run `./install.sh`, and it drops the script in place, creates launchers only for the DAWs it actually finds, and exposes a `DAW_ALSA_CARD` variable for anyone using an interface other than the SSL. Inside each DAW the settings are the same, chosen once: ALSA backend, the SSL as the device, 48000 Hz, a 128 sample buffer, 2 periods. In REAPER that lives under Options, Preferences, Audio, Device; in Ardour, Mixbus 11 and 12, and LiveTrax 3, it is the Audio/MIDI Setup dialog at startup. A 128 sample buffer at 48 kHz gives roughly 6 to 9 ms round trip in practice, comfortably playable, and if the machine holds up without glitches, 64 samples gets close to 3 ms. For monitoring the bass there are two honest options. Direct hardware monitoring uses the SSL's MONITOR MIX knob and has zero latency, which is what I want when I am recording a clean DI and not relying on a plugin for the sound. Software monitoring runs the signal through an amp sim in the DAW, and there the trick is to turn MONITOR MIX all the way to the USB side, otherwise you hear the dry DI stacked on top of the processed tone. For a free bass amp that runs in all five, Guitarix has usable models; in Mixbus, the console channel strip already gives the low end body without any external plugin. ## The verdict for my machine With a CPU this comfortable, none of the five is a bottleneck, so the decision comes down to latency, tone and licensing. For playing live with the lowest latency, REAPER is untouchable, and that is why it is the one I will actually buy. For recording and walking away with a finished bass tone, the Mixbus 11 I already own is the natural base, with the console doing the heavy lifting, and the move to 12 waits for a discount. Ardour 9.7 stays the best integrated native Linux citizen and the best free fallback, and LiveTrax is what I open when the goal is to capture a whole rehearsal fast without sculpting anything. In the end it is not one against the others. It is REAPER to play, Mixbus to give it tone, and Ardour holding the line. ## Sources - [Ardour 9.7 - What's new](https://ardour.org/whatsnew.html) - [Ardour 9.7 released (Ardour forum)](https://discourse.ardour.org/t/ardour-9-7-released/113377) - [The Ardour FAQ (pricing model)](https://ardour.org/faq.html) - [Mixbus 12 (Harrison store)](https://store.harrisonaudio.com/all-products/mixbus-12) - [What's New in Harrison Mixbus 12 (Levels Music Production)](https://www.levelsmusicproduction.com/blog/what-s-new-in-harrison-mixbus-12) - [Harrison Audio launches LiveTrax 3](https://harrisonaudio.com/explore/media/harrison-launches-livetrax-3) - [Why Harrison is no longer developing plugins for Linux](https://support.harrisonaudio.com/hc/en-gb/articles/19840057916957-Why-is-Harrison-no-longer-developing-plugins-for-Linux) - [REAPER Purchase](https://www.reaper.fm/purchase.php) - [linux-daw-ssl-lowlatency (code)](https://github.com/Esl1h/linux-daw-ssl-lowlatency) --- ## Contrabaixo no Linux: configurando Ardour, Mixbus, LiveTrax e REAPER com a SSL 2+ MkII - URL: https://esli.blog/posts/contrabaixo-no-linux/ - Date: 2026-07-23 - Series: tech - Tags: linux, audio, daw, pipewire, alsa, ardour, mixbus, reaper, contrabaixo Este é o registro de como deixei minha estação de trabalho pronta para o que mais faço nela depois de ouvir música: tocar contrabaixo elétrico. A máquina roda Fedora 44 (KDE Plasma) num Ryzen 9 3900X com 64 GB de RAM, e a interface de áudio é uma Solid State Logic SSL 2+ MkII conectada via USB. Instalei cinco DAWs para comparar e escolher onde vivo o dia a dia: Ardour 9.7, Mixbus 11 (licenciado), Mixbus 12 (trial), LiveTrax 3 (trial) e REAPER 7.78 (trial, mas quase comprado). Este texto é a continuação prática de dois artigos anteriores. Em [DAW no Linux](https://esli.blog/posts/daw-no-linux/) eu faço o panorama geral do assunto: o que é um DAW, as opções proprietárias, as multiplataforma e o mundo livre nativo do Linux, para situar quem está chegando agora. Já em [DAW: Ardour, Mixbus e LiveTrax](https://esli.blog/posts/daw-ardour-mixbus-e-livetrax/) eu mergulho nesses três irmãos da família Ardour, comparando um a um o Ardour com o Mixbus e com o LiveTrax. Aqui eu parto desse ponto e desço para o setup real na minha máquina: latência, ALSA exclusivo e como cada DAW se comporta para tocar contrabaixo, agora com o REAPER também na mesa. ## O ponto de partida Antes de mexer em qualquer DAW, olhei o estado real do sistema. A CPU já estava com o governor em `performance` e o RTKit ativo entregando prioridade de tempo real ao PipeWire, então a base estava boa. Dois detalhes, porém, mereciam ajuste. O primeiro: o PipeWire estava com clock padrão em 44100 Hz. Para produção musical o padrão de fato é 48000 Hz, então mudei o default para 48000 mantendo 44100 na lista de rates permitidos. Assim, quando nenhum DAW está segurando a placa, o Qobuz consegue voltar a 44.1 dinamicamente para tocar bit a bit, e quando abro um DAW tudo fica em 48000. O segundo: os DAWs estavam configurados de forma inconsistente. O Ardour apontava para a placa onboard da placa mãe, e não para a SSL. O REAPER estava num preset estranho de 192 kHz com buffer de 1024 amostras, o pior dos mundos, latência alta e CPU desperdiçado ao mesmo tempo. Nada disso serve para tocar baixo. ## Duas estratégias de áudio no Linux Com PipeWire no comando, existem dois caminhos para um DAW conversar com a interface. O primeiro é passar pela camada JACK do próprio PipeWire. É cômodo, porque o áudio do DAW convive com o navegador e o Qobuz ao mesmo tempo, mas soma a latência do PipeWire por cima da latência do DAW. O segundo é usar o backend ALSA direto na SSL, em modo exclusivo. O DAW toma a placa só para ele, o PipeWire solta o dispositivo, e a latência cai para o mínimo possível. O custo é que, enquanto o DAW está aberto, não sai áudio do navegador pela SSL. Para tocar contrabaixo através de simuladores de amplificador, essa é a escolha certa, e foi a que adotei. ## Liberando a SSL para o ALSA de forma automática O detalhe chato é que o PipeWire mantém a placa ocupada por padrão. O Ardour e derivados pedem a liberação do dispositivo via D-Bus e o PipeWire cede sozinho, mas o REAPER não faz esse pedido. Para não ter que lembrar de nada e padronizar o comportamento em todos os DAWs, escrevi um wrapper que desliga o perfil da SSL no PipeWire antes de abrir o programa e o devolve ao fechar. O script vive em `~/.local/bin/daw-alsa-ssl`: ```bash #!/usr/bin/env bash # daw-alsa-ssl - Estrategia A # Libera o SSL 2+ MkII do PipeWire (perfil off), abre o DAW em ALSA # exclusivo (hw:II) e devolve a placa ao PipeWire quando o DAW fecha. set -uo pipefail CARD_MATCH="alsa_card.usb-Solid_State_Logic_SSL_2__Mk_II-00" read_dev() { pw-dump 2>/dev/null | python3 - "$CARD_MATCH" <<'PY' import json, sys match = sys.argv[1] try: data = json.load(sys.stdin) except Exception: sys.exit(0) for o in data: info = o.get("info") or {} props = info.get("props") or {} if props.get("device.name") == match: idx = 1 for p in (info.get("params") or {}).get("Profile", []): if p.get("index") is not None: idx = p["index"] print(o.get("id"), idx) break PY } DEV_LINE="$(read_dev)" DEV_ID="${DEV_LINE%% *}" PREV_PROFILE="${DEV_LINE##* }" [ -z "${PREV_PROFILE//[0-9]/}" ] || PREV_PROFILE=1 restore() { [ -n "${DEV_ID:-}" ] && command -v wpctl >/dev/null 2>&1 \ && wpctl set-profile "$DEV_ID" "${PREV_PROFILE:-1}" >/dev/null 2>&1 } trap restore EXIT INT TERM if [ -n "${DEV_ID:-}" ] && command -v wpctl >/dev/null 2>&1; then wpctl set-profile "$DEV_ID" 0 >/dev/null 2>&1 sleep 0.4 fi "$@" ``` Ele descobre o id da placa no PipeWire pelo nome ALSA, guarda o perfil ativo (o HiFi, no meu caso), troca para o perfil `off` que libera o `hw:II` para o ALSA, abre o DAW e, ao encerrar, restaura o perfil original. Como o id do PipeWire pode mudar entre reboots, a descoberta é sempre pelo nome, nunca por número fixo. ## Launchers dedicados Com o wrapper pronto, criei um atalho para cada DAW em `~/.local/share/applications/`, todos chamando o wrapper. O do REAPER, por exemplo: ```ini [Desktop Entry] Type=Application Name=REAPER (SSL · ALSA) Comment=REAPER em ALSA exclusivo no SSL 2+ MkII Exec=/home/USUARIO/.local/bin/daw-alsa-ssl /opt/REAPER/reaper %F Icon=cockos-reaper Terminal=false Categories=AudioVideo;AudioVideoEditing;Audio;Recorder; ``` Os outros quatro seguem o mesmo padrão, cada um apontando para seu binário: `/usr/bin/ardour9`, `/usr/local/bin/Mixbus11`, `/usr/local/bin/Mixbus12` e `/usr/local/bin/LiveTrax3`. Assim, o menu do sistema ganha entradas como "Ardour 9 (SSL · ALSA)" ou "Mixbus 11 (SSL · ALSA)", e abrir por elas já entra na stratégia acima sem pensar. Para não copiar e colar nada à mão, empacotei o wrapper, os cinco launchers e um `install.sh` num repositório público: [linux-daw-ssl-lowlatency](https://github.com/Esl1h/linux-daw-ssl-lowlatency). Clonar e rodar `./install.sh` já instala o script, cria só os atalhos dos DAWs presentes no sistema e ainda traz um `DAW_ALSA_CARD` para quem usa outra interface que não a SSL. ## Configuração de áudio dentro de cada DAW A escolha do dispositivo é persistente em cada programa, então é um passo único na primeira abertura. Os valores que uso para tocar são sempre os mesmos: 48000 Hz, buffer de 128 amostras e 2 períodos. Isso rende algo entre 6 e 9 ms de ida e volta na prática, plenamente tocável. Se a máquina aguentar sem estalos, dá para descer o buffer para 64 e chegar perto de 3 ms. No REAPER a configuração fica em Options, Preferences, Audio, Device: sistema ALSA, device `hw:II`, sample rate 48000, block size 128, 2 blocks. No Ardour, no Mixbus 11, no Mixbus 12 e no LiveTrax 3, todos irmãos do mesmo motor, a tela é a mesma janela de Audio/MIDI Setup na abertura: Audio System ALSA, device SSL 2+ Mk II, 48000, 128, 2 períodos. ## Monitoração do contrabaixo Há duas formas de ouvir o instrumento enquanto se toca. A monitoração direta por hardware usa o knob MONITOR MIX da própria SSL e tem latência zero, ideal quando gravo o sinal limpo e não dependo de plugin no som. A monitoração por software passa o sinal por um simulador de amplificador dentro do DAW, e aí o segredo é girar o MONITOR MIX totalmente para o lado USB, senão a gente ouve o sinal seco somado ao processado. Para timbre de baixo, o Guitarix traz modelos de amplificador em LV2 que rodam em qualquer um dos cinco. No Mixbus, o próprio channel strip estilo console já dá corpo sem precisar de plugin externo. ## Diferenças entre Mixbus 11 e Mixbus 12 O Mixbus 11 foi o que trouxe o fluxo redesenhado e o Focus Channel. O Mixbus 12 refina esse sistema e adiciona ferramentas novas. A mais chamativa é a inclusão de um DeEsser e um DeNoiser em cada canal, processadores que a Harrison diz derivar da experiência com consoles de cinema, trazendo limpeza de ruído para dentro do canal em vez de ser um passo separado. O sistema de cue launching cresceu para até 16 fileiras e passou a permitir gravar clipes de áudio e de MIDI direto nos slots. A edição MIDI ganhou uma ferramenta de acordes no piano roll, em que se escolhe o tipo e basta clicar na fundamental. E a interface mudou para um tema mais escuro e de maior contraste, reforçando a pegada do console 32 Classic e facilitando distinguir canais e buses. Para quem toca e grava baixo, o ganho concreto do 12 é o channel strip mais completo (o DeEsser ajuda a domar ataques agressivos de slap e o DeNoiser limpa hum de captação single coil) e o cue launching maior para montar bases. Como já tenho a licença do 11 e ele soa igualmente bem, meu plano é migrar só quando aparecer um cupom de upgrade que valha a pena. ## O que há de novo no Ardour 9.7 O Ardour 9.7 saiu em 5 de junho de 2026 como um release de correções com alguns acréscimos que interessam. As melhorias de edição MIDI continuaram: a barra lateral de MIDI Tools, que antes vivia só no piano roll, agora aparece também no editor principal ao ativar a Editor List com Shift+L, com edição de acordes e quantização. O cursor em cruz para edição de MIDI e automação passou a estar disponível no editor inline e vem ligado por padrão. Fora o MIDI, o 9.7 trouxe um sumário vertical opcional para complementar o painel de sumário horizontal reformulado, ordenação natural de listas pela interface, suporte a multitoque no Linux e no Windows, a possibilidade de salvar e restaurar conexões de portas por backend ou dispositivo, e gravação ininterrupta mesmo com perda de sincronismo LTC. Um mapa de bindings vazio para controladores MIDI genéricos facilita usar superfícies que o Ardour ainda não conhece. ## Como esses três se comparam ao REAPER atual Ardour, Mixbus e LiveTrax são o mesmo motor com roupas diferentes: Ardour é a base livre e nativa do Linux, o Mixbus adiciona a emulação de console analógico da Harrison, e o LiveTrax é uma versão enxuta para captura ao vivo. O REAPER 7.78, a versão instalada e também a mais recente, joga em outra categoria de filosofia. A diferença mais sentida ao tocar é o motor. O REAPER é notoriamente o mais leve e eficiente em CPU, e o backend ALSA nativo entrega a menor latência de monitoração através de plugin. Numa máquina folgada como esta, a família Ardour também roda tranquila, mas para live monitoring com simulador de amplificador o REAPER larga na frente. Em filosofia de produto, o REAPER é uma tela quase em branco, absurdamente customizável, com scripting em Lua e ReaScript, roteamento livre e um ecossistema de ações e temas. Já a família Ardour é mais opinativa e vem com um fluxo pronto, e o Mixbus em particular entrega timbre de console desde o primeiro canal, o que para baixo é um atalho e tanto: menos tempo montando cadeia de plugin, mais tempo tocando. Em licenciamento a diferença também é grande. O Ardour é livre e de código aberto. O Mixbus e o LiveTrax são pagos e por versão. O REAPER tem só uma licença, com preço que varia pelo uso: 60 dólares para uso pessoal ou pequeno negócio com faturamento anual abaixo de 20 mil dólares, e 225 dólares para uso comercial acima disso. Vale notar que o REAPER não faz promoções nem distribui cupons: o preço de 60 dólares já é o valor permanente para quem se enquadra, e uma licença nova inclui upgrades gratuitos até a versão 8.99. Ou seja, se a ideia é comprar o REAPER, não adianta esperar cupom, ele simplesmente não existe. O trial de 60 dias funciona completo justamente para você decidir com calma. ## Veredito para esta máquina Como nenhum dos cinco chega a ser gargalo de CPU aqui, a decisão passa por latência, timbre e licença. Para tocar ao vivo com a menor latência, o REAPER é imbatível, e é por isso que ele deve mesmo virar compra. Para gravar e já sair com timbre de baixo pronto, o Mixbus 11 que já está pago é a base natural, com o console fazendo o trabalho pesado, e o upgrade para o 12 fica para quando o desconto aparecer. O Ardour 9.7 segue como o cidadão Linux mais bem integrado e o melhor plano B gratuito, e o LiveTrax entra quando o objetivo é registrar um ensaio inteiro rápido, sem lapidar nada. No fim, não é um contra os outros: é o REAPER para tocar, o Mixbus para dar timbre, e o Ardour de retaguarda. ## Fontes - [Ardour 9.7 - What's new](https://ardour.org/whatsnew.html) - [Ardour 9.7 released (fórum Ardour)](https://discourse.ardour.org/t/ardour-9-7-released/113377) - [Ardour 9.7 Open-Source DAW Improves MIDI Editing (9to5Linux)](https://9to5linux.com/ardour-9-7-open-source-daw-improves-midi-editing-adds-new-vertical-summary) - [What's New in Harrison Mixbus 12 (Levels Music Production)](https://www.levelsmusicproduction.com/blog/what-s-new-in-harrison-mixbus-12) - [Harrison Audio Mixbus 12: New Post-Production and MIDI Features (gearnews)](https://www.gearnews.com/harrison-audio-mixbus/) - [Mixbus 12 (loja Harrison)](https://store.harrisonaudio.com/all-products/mixbus-12) - [REAPER Purchase](https://www.reaper.fm/purchase.php) --- ## RTL-SDR v4 no Linux - URL: https://esli.blog/posts/rtl-sdr-v4/ - Date: 2026-07-14 - Series: tech - Tags: hardware, rf, linux, sdr, radio, dongle, rtl-sdr, adsb, readsb 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: RF · HARDWARE SOFTWARE · LINUX aeronave transponder 1090ES 1090 MHz · PPM · 1 Mbps antena dipolo do kit cabo coaxial · SMA RTL-SDR v4 tuner R828D → ADC RTL2832U USB · IQ de 8 bits 2,4 MS/s · ~4,8 MB/s dump1090 acha preâmbulo, decodifica, valida CRC TCP · HTTP consumidores mapa web · SBS :30003 · JSON ← artigo 4, o hands-on 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. ![Dongle RTL-SDR Blog V4 sobre o manual, com RTL2832U, R828D, TCXO, bias-T e HF marcados na serigrafia da caixa de alumínio](/images/rtl-sdr-v4/dongle-e-manual.jpeg) ## 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. ![Caixa preta lacrada com o selo vermelho da Open Source SDR Lab](/images/rtl-sdr-v4/caixa-lacrada.jpeg) 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. ![Caixa aberta, com o kit ainda embalado em plástico bolha e saco antiestático](/images/rtl-sdr-v4/unboxing.jpeg) ![Conteúdo do kit na caixa: tripé flexível, suporte de ventosa, cabo de extensão e elementos telescópicos](/images/rtl-sdr-v4/kit-antenas.jpeg) ![Tripé flexível, suporte de ventosa e os dois pares de elementos telescópicos do dipolo](/images/rtl-sdr-v4/tripe-e-hastes.jpeg) ## 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: tar1090, hardware, rf, linux, sdr, radio, dongle, rtl-sdr, adsb, readsb [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: quadro 1090ES · downlink format 17 DF · 5 bits CA · 3 bits preâmbulo · 8 µs ICAO 24 bits identidade única payload (ME) 56 bits · 7 bytes posição, altitude, velocidade CRC 24 bits confere erros 8D 4840D6 202CC371C32CE0 576098 112 bits · 14 bytes · 112 µs no ar (120 µs com o preâmbulo) 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: tar1090, hardware, rf, linux, sdr, radio, dongle, rtl-sdr, adsb, readsb 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/ ![climabr - curl](../../../assets/blog/covers/climabr-curl.png) 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 (`