Stream e controles do Furbo direto da sua rede: vídeo em 2K, pan, petisco e ajustes, sem app e sem nuvem.
O nome é o duplo sentido de sempre, off the leash e offline na mesma palavra: a câmera sai da coleira e o vídeo sai da nuvem.
A câmera Furbo é boa. O que está em volta dela é que não. O vídeo passa pelo cloud, o clipe que você grava sai com marca d'água, o histórico bom fica atrás de assinatura, o alerta de latido é uma caixa preta que você não pode auditar nem ajustar, e não existe forma de gravar na sua máquina, plugar no seu NVR ou automatizar qualquer coisa.
Só que a câmera já está na sua rede, e o que ela faz é software. A ambição aqui não é clonar o app: é ser melhor em tudo o que depende de rodar na sua casa em vez de no servidor de outra pessoa.
| Furbo | offleash | |
|---|---|---|
| latência | relay pelo cloud, segundos | P2P na sua LAN, fração de segundo |
| onde o vídeo vive | servidor deles | a sua rede |
| gravação | clipes no cloud, com marca d'água | arquivo seu, em 2K, sem marca |
| histórico | atrás de assinatura | o seu disco, sem limite artificial |
| alerta de latido | caixa preta no cloud | modelo local, e você vê o que ele faz |
| integração | nenhuma | RTSP, API HTTP, MQTT, webhooks |
| automação | nenhuma | qualquer coisa, porque é software livre |
| quantas pessoas assistindo | uma | uma também, por enquanto: é limite da câmera |
O que ainda não é melhor: o app deles é polido e o nosso é uma página com botões. Isso é a parte fácil de melhorar.
Funciona, e foi medido numa câmera de verdade, não num emulador: Furbo Mini
360°, produto MC0030, firmware 045.
| vídeo | 2560x1440, H.264 Main, 15 fps, pela sessão P2P na própria LAN |
| saídas | RTSP, WebRTC e uma web app com player embutido |
| controles | pan, petisco, som do petisco, volume, visão noturna, sensibilidade ao latido, autotracking, autozoom, qualidade |
| sem | app no celular, relay, e nenhuma conta no cloud depois da colheita inicial |
São quatro camadas, e vale saber quem é dono de cada uma, porque é isso que decide o que dá para cortar e o que não dá.
flowchart LR
cam["câmera Furbo<br/>nenhuma porta TCP aberta"]
subgraph tutk["ThroughTek, a TUTK/Kalay"]
masters["servidores mestres<br/>m1/m2/m3.iotcplatform.com"]
relay["relay"]
end
cloud["cloud da Furbo<br/>product.furbo.co"]
bridge["bridge offleash"]
go2rtc["go2rtc"]
viewer["navegador, mpv, Home Assistant"]
cam <-->|"login do aparelho<br/>UID + AuthKey"| masters
bridge -->|"resolve o UID<br/>só no começo"| masters
cam <-->|"sessão P2P na LAN<br/>o caminho do vídeo"| bridge
cam -.->|"por relay, quando não há LAN"| relay
relay -.-> bridge
cloud -.->|"emite a credencial,<br/>e reemite a cada consulta"| cam
bridge -.->|"colhe uma vez,<br/>e-mail + senha + código"| cloud
bridge -->|"H.264 cru"| go2rtc
go2rtc --> viewer
- A câmera roda um servidor TUTK e não abre porta nenhuma. Ela só fala para fora e responde à busca em LAN.
- A ThroughTek dá endereço à câmera (o
uid), autentica o transporte (aauth_key) e, quando não há LAN, intermedeia por relay. - O cloud da Furbo não toca no vídeo. Ele cuida da conta, do provisionamento e de emitir a credencial que as duas pontas usam.
- O nosso stack abre a sessão P2P, lê o H.264 cru e publica.
O caminho do vídeo é local e o caminho da credencial passa pela nuvem. Essa separação é o que permite usar a nuvem uma única vez.
flowchart LR
cam["câmera<br/>uma sessão de vídeo por vez"] -->|"H.264"| bridge["bridge"]
bridge --> go2rtc["go2rtc"]
go2rtc --> a["navegador<br/>WebRTC"]
go2rtc --> b["mpv / Home Assistant<br/>RTSP"]
go2rtc -.-> c["gravador<br/>roadmap"]
go2rtc -.-> d["IA local<br/>roadmap"]
A câmera aceita uma sessão de vídeo por vez. É por isso que o bridge mantém uma só e o go2rtc é quem distribui: quem quiser ver, gravar ou analisar consome do go2rtc, nunca da câmera. Qualquer arquitetura que abra uma segunda sessão quebra a primeira, e esse é o erro mais comum de quem tenta fazer isso.
Precisa de Docker e de uma câmera Furbo já configurada na sua rede.
git clone https://github.com/felipeadeildo/offleash.git
cd offleash
# a imagem traz python, ffmpeg, go2rtc e a lib nativa do TUTK.
# ela é construída hoje a partir do projeto de onde a camada de bridge vem
# (ver Dependências); o roadmap é trazer esse código para cá.
git clone https://github.com/willbeeching/ha-furbo ~/ha-furbo
docker build -t furbo-bridge:local ~/ha-furbo/furbo-bridge
# colhe as credenciais uma vez (pede e-mail, senha e o código que chega por e-mail)
./harvest.sh send
./harvest.sh code 123456
./run.sh # só loopback: nada na rede alcança a câmera
./run.sh tailnet # também no IP do Tailscale, para ver do celular
./run.sh logs
./run.sh stopDepois disso:
| endereço | para quê |
|---|---|
http://127.0.0.1:8080/ |
a web app, com player e controles |
rtsp://furbo:<senha>@127.0.0.1:8554/furbo |
mpv, VLC, Home Assistant |
A senha do RTSP é derivada do token da API, não é o token: uma URL de vídeo vazada dá vídeo e não dá acesso aos controles.
Esta é a parte que quase nenhum projeto documenta, e é o que decide se o seu acesso vai funcionar amanhã.
sequenceDiagram
participant F as câmera
participant C as cloud da Furbo
participant P as app oficial
participant V as você
Note over F,C: no provisionamento
C->>F: UID + AuthKey + senha do canal AV
C->>P: os mesmos valores
Note over V,C: a colheita, uma vez
V->>C: e-mail + senha + código
C-->>V: UID, AuthKey, conta e senha atuais
Note over P,C: e é isso que quebra a nossa cópia
P->>C: pede credencial nova
C-->>P: senha NOVA, a anterior morre
Note over V,F: a nossa senha morreu. a sessão IOTC conecta, o AV falha com -20011
A câmera não aceita conexão local por RTSP nem ONVIF. Todo o vídeo sai por TUTK/Kalay, um P2P da ThroughTek que autentica nas duas pontas com quatro valores:
| campo | o que é | muda ao longo do tempo? |
|---|---|---|
uid |
o P2PUuid da câmera, 20 caracteres | não |
auth_key |
a AuthKey do transporte IOTC, 8 caracteres | não, medido em duas colheitas |
account |
o P2PAccountId, que no Mini 360 é o próprio MAC da câmera | não |
password |
o P2PAccountKey da camada de áudio e vídeo | sim, é reemitido a cada consulta |
E daí vem a regra que governa tudo:
Só existe uma sessão de vídeo por vez, e cada consulta ao cloud reemite a senha, invalidando a anterior. Abrir a live view no app oficial derruba o
offleash, e usar ooffleashderruba o app.
O sintoma de senha vencida é enganoso e vale conhecer, porque imita câmera
travada: a sessão de transporte conecta (ela usa a auth_key, que continua
válida) e o canal de vídeo falha com avClientStartEx -20011 AV_ER_TIMEOUT.
Reiniciar a câmera não resolve. Recuperar custa um minuto:
./harvest.sh send
./harvest.sh code 123456Dá, e é decisão de arquitetura, não de código. Os quatro valores existem em dois lugares além da nuvem:
| de onde | como | o que custa |
|---|---|---|
| cloud, uma vez | o ./harvest.sh, com e-mail, senha e o código |
um minuto, e a conta precisa existir |
| pelo BLE do aparelho | o P2PUuid e o DeviceToken saem em duas características GATT |
ainda passa pelo cloud, mas sem conta e sem código |
| do próprio aparelho | UART 115200 no flash, em /mnt/flash/furbo/p2p_auth.json |
abrir a câmera, e no 360 o UART tinha senha |
O caminho do meio é o mais elegante: ler o flash com uma garra, sem dessoldar
nada, devolve a imagem inteira e os arquivos de credencial que o aparelho
escreve na partição gravável. Se você quiser seguir por aí, o guia de como falar
com o hardware, do multímetro ao bootloader, está em
docs/hardware.md.
O que não dá para evitar, nem com engenharia reversa perfeita:
- A
auth_keyé um segredo de 8 caracteres. Não é derivável do MAC, do número de série ou de qualquer coisa que a câmera exponha, porque se fosse não seria segredo. Engenharia reversa entrega o formato da fechadura, não a combinação. - A câmera só fala TUTK. Não existe RTSP nem ONVIF para onde cair, e o
uidvive na infraestrutura da ThroughTek. A câmera faz login lá de qualquer maneira, mesmo quando o vídeo vem direto pela LAN. - A SDK do TUTK é um binário fechado. Dá para reimplementar o protocolo, e existe quem tenha feito boa parte disso em Python puro, mas é um projeto bem maior do que o ganho.
O resumo honesto: a engenharia reversa já foi feita, e é ela que nos deixa falar com a câmera sem passar pelo app. O que sobra não é falta de conhecimento, é distribuição de uma chave.
O comportamento documentado aqui saiu da leitura do bytecode do app oficial (versão 7.84.1), e corrige um erro que está em outras implementações.
FurboP2pCmdImplTutkV3.setRotate monta seis bytes:
[0] = direção [1] = velocidade [2..4] = zero [5] = terceiro argumento
Só que o valor do segundo byte é velocidade, não graus, e a classe
LiveViewModel.e2 mostra isso:
isHwMini3() ? 50 : 60 # 50 no Mini 3, 60 nos outros modelos
if (soltando) esse valor = 0
Ou seja, o app prende o comando enquanto o botão está pressionado e manda
[direção, 0, 0, 0, 0, 0] ao soltar. O zero é o pare. Quem manda o comando
uma vez só, sem o pare, vê a câmera girar até o batente, que foi exatamente o
bug que originou este projeto.
A câmera também reporta a posição no opcode NOTIFY_PAN (0x30007), que as
implementações existentes ignoram: cinco bytes com um estado e um inteiro de 32
bits, na faixa de -668 a +668. É por ele que dá para medir o movimento de
verdade em vez de adivinhar.
Como o passo é escolhido aqui: em tempo, não em graus. A taxa medida é de aproximadamente 110 unidades por segundo na escala da câmera, mas a conversão disso para graus não foi medida com confiança, então a interface oferece passos de 0,2 s a 1,6 s em vez de prometer graus.
O vídeo nasce na sua LAN, então ver de fora é uma decisão de arquitetura, e as respostas custam coisas bem diferentes.
flowchart LR
subgraph casa["sua casa"]
cam["câmera"] --> bridge["bridge"]
bridge --> go2rtc["go2rtc"]
end
go2rtc -->|"1. tailnet privada"| ts["Tailscale"]
ts --> celular["celular e notebook"]
go2rtc -->|"2. push de saída"| vps["VPS sua<br/>relay + TLS + auth"]
vps --> navegador["qualquer navegador"]
go2rtc -->|"3. túnel de saída"| tun["túnel gerenciado"]
tun --> navegador
| qualidade | custo | quem alcança | |
|---|---|---|---|
| Tailscale | a melhor: caminho direto WireGuard | zero | só quem está na sua tailnet |
| VPS sua | você controla: WebRTC sub-segundo ou HLS | uns 5 dólares por mês | pública, com a sua autenticação |
| Túnel gerenciado | boa, mas passa pela rede de terceiro | zero | pública, autenticada pelo provedor |
O limite que não muda em nenhuma das três: a câmera aceita uma sessão só, então quem distribui é o go2rtc lá de casa. Um relay nunca abre uma segunda sessão na câmera; ele é só mais um consumidor do go2rtc.
./run.sh tailnetIsso amarra a web app, o player, o RTSP e a mídia WebRTC ao IP da tailnet, e a
nada mais. Com o Tailscale ligado no celular, abra http://<ip-da-tailnet>:8080/.
Aviso importante: a web app não tem autenticação própria, porque ela mesma injeta o token da API. Numa tailnet só sua isso é você. Se a tailnet for compartilhada, qualquer máquina dela alcança a câmera, inclusive para controlar. Nesse caso: restrinja por ACL no Tailscale, use um tailnet próprio, ou espere a autenticação da web app entrar no roadmap.
A ideia é o caminho de saída, nunca uma porta aberta em casa:
flowchart LR
subgraph casa["casa"]
cam["câmera"] --> bridge["bridge"] --> go2rtc["go2rtc"]
end
subgraph vps["VPS"]
mediamtx["MediaMTX ou go2rtc<br/>recebe o push"] --> proxy["Caddy<br/>HTTPS + autenticação"]
end
go2rtc -->|"push RTSP, de dentro para fora"| mediamtx
proxy --> web["web app, de qualquer lugar"]
- a casa faz push para a VPS, então não existe porta aberta nem IP fixo em casa
- a VPS roda o fan-out: WebRTC para latência baixa, HLS para compatibilidade
- o TLS e a autenticação ficam na VPS, que é a única coisa pública
- com uma tailnet ligando a casa à VPS, nem o push precisa sair para a internet aberta
Se a sua ambição é o serviço self-hosted do roadmap, é aqui que ele mora: o mesmo desenho que serve você serve vários usuários, um bridge por casa e um relay central.
Serviços como o Cloudflare Tunnel publicam um endereço HTTPS sem abrir porta e com autenticação do provedor. Resolve sem infraestrutura nenhuma, mas o vídeo passa pela rede de terceiro, e as condições de uso desses serviços restringem transmissão contínua de vídeo. Vale ler antes de usar como caminho principal.
- Uma sessão de vídeo por vez, e o app oficial compete com este projeto.
- Petisco e som do petisco não foram testados no hardware, e o primeiro ejetaria um petisco de verdade.
- Áudio não foi testado; o codec é escolhido por tentativa.
- Stream com a câmera sem internet não foi medido. O SDK resolve o UID pelos servidores da ThroughTek e a busca em LAN costuma bastar, mas isso é hipótese.
- Uma câmera. O código do bridge suporta várias, a interface ainda não.
- A lib nativa do TUTK é x86_64 ou arm64; não roda em Windows nem em macOS.
O código deste repositório é MIT. Duas dependências merecem atenção antes de qualquer uso que não seja pessoal:
- A camada de bridge é hoje importada de
willbeeching/ha-furbo(MIT), com as correções deste projeto aplicadas em volta, sem modificar o original. O objetivo do roadmap é assumir esse código aqui dentro, mantendo a atribuição. - A biblioteca nativa do TUTK (
libIOTCAPIs_ALL.so) é da ThroughTek, e é baixada em tempo de build domrlt8/docker-wyze-bridgecom commit fixo e verificação de SHA-256. Ela não está sob a licença MIT deste repositório. Para uso pessoal é o caminho que a comunidade usa; para distribuir como produto, o correto é licenciar a SDK com a ThroughTek.
O que falta para ser melhor que o Furbo de verdade, na ordem em que faz sentido construir.
Primeiras vitórias, cada uma é uma coisa que o fabricante não faz:
- Gravar em disco: 2K contínuo ou por evento, sem marca d'água, com retenção configurável e um índice de eventos. É o pedido mais óbvio de quem já tem a câmera.
- Latido local: detector de áudio rodando aqui, com limiar ajustável e sem caixa preta. O Furbo esconde isso atrás de assinatura.
- Aviso onde você já está: Telegram, WhatsApp ou webhook, com o clipe anexado, em vez de notificação genérica que só abre o app deles.
- Assumir o código: trazer a camada de bridge para dentro do repo, com atribuição, e eliminar a dependência de importação.
Depois, quando as primeiras estiverem de pé:
- IA na sua máquina: detecção de cão, pessoa e movimento com modelo local, e resumo do dia em linguagem natural a partir dos eventos.
- Busca no histórico: "quando ele latiu ontem à noite" resolvido por descrição, em vez de arrastar uma linha do tempo.
- Treino com reforço: petisco com tempo de clique, sessões registradas, e o reforço certo na hora certa.
- Pan em graus de verdade e posições salvas ("olhar o sofá").
- Várias câmeras e vários endereços, com uma interface só.
- Virar serviço: sobe com um comando, autentica na interface, faz backup da configuração.
- Talkback e agendamento de petisco.
O trabalho pesado de reverter o protocolo é de
willbeeching/ha-furbo, que leu o
protocolo no app e publicou sob MIT. A leitura do pan neste repositório veio do
APK do app oficial, e as pesquisas públicas que abriram o caminho foram as da
Somerset Recon (partes I a III) e da
Software Secured (partes 1
a 6). Para qualquer coisa que o bridge não cubra,
Fredde87/cuboai-tutk é uma
implementação de TUTK em Python puro e ótima referência.