From 91c64e5b188c63201bf832b89f61d077e6070b62 Mon Sep 17 00:00:00 2001 From: julio Date: Sat, 10 Oct 2026 03:04:25 -0300 Subject: [PATCH] feat(rede): placa em carinha.local e app que acha a placa sozinho MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sistema/rede: a placa vira "carinha" na rede (avahi), http://carinha.local na porta 80 (nftables redireciona para a 8100, sem tocar no 192.168.4.1 do portal Carinha-Config) e anúncio "_carinha._tcp" na 8100; - app 0.9.1: quando o endereço salvo não responde, procura o anúncio na rede (NsdManager), testa o endereço achado e passa a usar e guardar. Co-Authored-By: Claude Opus 5.5 --- android/app/AndroidManifest.xml | 4 +- .../app/java/br/carinha/rosto/Descoberta.java | 93 +++++++++++++++++++ .../java/br/carinha/rosto/MainActivity.java | 34 ++++++- sistema/rede/carinha.nft | 12 +++ sistema/rede/carinha.service.avahi | 9 ++ sistema/rede/instalar.sh | 15 +++ 6 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 android/app/java/br/carinha/rosto/Descoberta.java create mode 100644 sistema/rede/carinha.nft create mode 100644 sistema/rede/carinha.service.avahi create mode 100644 sistema/rede/instalar.sh diff --git a/android/app/AndroidManifest.xml b/android/app/AndroidManifest.xml index 0c5b39f..3a44d88 100644 --- a/android/app/AndroidManifest.xml +++ b/android/app/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="13" + android:versionName="0.9.1"> diff --git a/android/app/java/br/carinha/rosto/Descoberta.java b/android/app/java/br/carinha/rosto/Descoberta.java new file mode 100644 index 0000000..0e37fa1 --- /dev/null +++ b/android/app/java/br/carinha/rosto/Descoberta.java @@ -0,0 +1,93 @@ +package br.carinha.rosto; + +import android.content.Context; +import android.net.nsd.NsdManager; +import android.net.nsd.NsdServiceInfo; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +/** + * Acha a placa na rede pelo anúncio mDNS "_carinha._tcp" (avahi na Orange Pi, o mesmo do + * carinha.local). Assim, se o IP da placa mudar, o app se reconecta sem ninguém digitar endereço. + * Uma busca por vez; desiste depois de alguns segundos e responde no fio principal. + */ +final class Descoberta { + interface Resposta { void achou(String endereco); } // null = não achou + + private static final String TAG = "CarinhaDescoberta"; + private static final String TIPO = "_carinha._tcp."; + private static final long LIMITE = 8000; + + private final NsdManager nsd; + private final Handler principal = new Handler(Looper.getMainLooper()); + private NsdManager.DiscoveryListener ouvinte; + private Resposta resposta; + + Descoberta(Context c) { + nsd = (NsdManager) c.getApplicationContext().getSystemService(Context.NSD_SERVICE); + } + + boolean procurando() { return ouvinte != null; } + + void procurar(Resposta r) { + if (nsd == null) { r.achou(null); return; } + if (ouvinte != null) return; + resposta = r; + ouvinte = new NsdManager.DiscoveryListener() { + @Override public void onDiscoveryStarted(String tipo) { } + @Override public void onDiscoveryStopped(String tipo) { } + @Override public void onStartDiscoveryFailed(String tipo, int erro) { terminar(null); } + @Override public void onStopDiscoveryFailed(String tipo, int erro) { } + @Override public void onServiceLost(NsdServiceInfo s) { } + @Override public void onServiceFound(NsdServiceInfo s) { + if (!s.getServiceType().startsWith("_carinha._tcp")) return; + resolver(s); + } + }; + try { + nsd.discoverServices(TIPO, NsdManager.PROTOCOL_DNS_SD, ouvinte); + } catch (Exception e) { + Log.w(TAG, "busca falhou: " + e); + ouvinte = null; + r.achou(null); + return; + } + principal.postDelayed(new Runnable() { + @Override public void run() { terminar(null); } + }, LIMITE); + } + + @SuppressWarnings("deprecation") + private void resolver(NsdServiceInfo s) { + try { + nsd.resolveService(s, new NsdManager.ResolveListener() { + @Override public void onResolveFailed(NsdServiceInfo s, int erro) { Log.w(TAG, "não resolveu: " + erro); } + @Override public void onServiceResolved(NsdServiceInfo s) { + if (s.getHost() == null || s.getHost().isLoopbackAddress()) return; + String ip = s.getHost().getHostAddress(); + if (ip == null || ip.contains(":")) return; // só IPv4: o servidor ouve em 0.0.0.0 + terminar("http://" + ip + ":" + s.getPort()); + } + }); + } catch (Exception e) { + Log.w(TAG, "resolver falhou: " + e); + } + } + + private void terminar(final String endereco) { + principal.post(new Runnable() { + @Override public void run() { + if (ouvinte == null) return; // já respondeu + try { nsd.stopServiceDiscovery(ouvinte); } catch (Exception e) { /* já parou */ } + ouvinte = null; + Resposta r = resposta; + resposta = null; + Log.i(TAG, endereco == null ? "placa não encontrada na rede" : "placa encontrada: " + endereco); + if (r != null) r.achou(endereco); + } + }); + } + + void parar() { terminar(null); } +} diff --git a/android/app/java/br/carinha/rosto/MainActivity.java b/android/app/java/br/carinha/rosto/MainActivity.java index 68aff44..9118cab 100644 --- a/android/app/java/br/carinha/rosto/MainActivity.java +++ b/android/app/java/br/carinha/rosto/MainActivity.java @@ -108,6 +108,7 @@ public class MainActivity extends Activity { private String servidor; private String token = ""; // CARINHA_TOKEN do servidor; vazio = servidor aberto private final CerebroLocal cerebroLocal = new CerebroLocal(); + private Descoberta descoberta; // acha a placa na rede quando o endereço salvo não responde private boolean jaCaiuParaLocal = false; private boolean jaRecarregou = false; private boolean usandoLocal = false; // servidor caiu: ficamos na copia do APK @@ -191,6 +192,7 @@ public class MainActivity extends Activity { usarServidorDoPedido(getIntent()); // permite apontar o servidor ao lancar cerebroLocal.chave = token; cerebroLocal.iniciar(this, servidor); + descoberta = new Descoberta(this); remoto = new ControleRemoto(new Runnable() { @Override public void run() { runOnUiThread(new Runnable() { @@ -582,7 +584,7 @@ public class MainActivity extends Activity { runOnUiThread(new Runnable() { @Override public void run() { if (!usandoLocal || !alvo.equals(servidor)) return; - if (!voltou) { procurarServidor(); return; } + if (!voltou) { procurarNaRede(); return; } Log.i(TAG, "servidor voltou: " + servidor); usandoLocal = false; jaCaiuParaLocal = false; @@ -594,6 +596,36 @@ public class MainActivity extends Activity { } }; + /** + * O endereço salvo não respondeu: procura a placa pelo anúncio na rede (carinha.local). + * Achou em outro endereço que responde, passa a usar esse e guarda; senão tenta de novo daqui a pouco. + */ + private void procurarNaRede() { + if (descoberta.procurando()) { procurarServidor(); return; } + descoberta.procurar(new Descoberta.Resposta() { + @Override public void achou(final String endereco) { + if (!usandoLocal || endereco == null || endereco.equals(servidor)) { procurarServidor(); return; } + new Thread(new Runnable() { + @Override public void run() { + final boolean responde = servidorResponde(endereco); + runOnUiThread(new Runnable() { + @Override public void run() { + if (!responde || !usandoLocal) { procurarServidor(); return; } + Log.i(TAG, "placa achada na rede: " + servidor + " -> " + endereco); + servidor = endereco; + getSharedPreferences(PREFS, MODE_PRIVATE).edit().putString(CHAVE, servidor).apply(); + cerebroLocal.iniciar(MainActivity.this, servidor); + usandoLocal = false; + jaCaiuParaLocal = false; + abrirRosto(); + } + }); + } + }, "testa-placa-achada").start(); + } + }); + } + /** Qualquer resposta HTTP (até 401 de token) quer dizer que o servidor está no ar. */ private static boolean servidorResponde(String endereco) { java.net.HttpURLConnection c = null; diff --git a/sistema/rede/carinha.nft b/sistema/rede/carinha.nft new file mode 100644 index 0000000..7e5c558 --- /dev/null +++ b/sistema/rede/carinha.nft @@ -0,0 +1,12 @@ +# Carinha: http://carinha.local (porta 80) leva ao painel na 8100. +# O 192.168.4.1 fica de fora: é a página de configuração do Wi-Fi no modo ponto de acesso. +table ip carinha { + chain entrada { + type nat hook prerouting priority dstnat; policy accept; + ip daddr != 192.168.4.1 tcp dport 80 redirect to :8100 + } + chain local { + type nat hook output priority -100; policy accept; + ip daddr 127.0.0.0/8 tcp dport 80 redirect to :8100 + } +} diff --git a/sistema/rede/carinha.service.avahi b/sistema/rede/carinha.service.avahi new file mode 100644 index 0000000..d8bf61f --- /dev/null +++ b/sistema/rede/carinha.service.avahi @@ -0,0 +1,9 @@ + + + + + Carinha + _http._tcp80 + _carinha._tcp8100 + diff --git a/sistema/rede/instalar.sh b/sistema/rede/instalar.sh new file mode 100644 index 0000000..552c67c --- /dev/null +++ b/sistema/rede/instalar.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Nome na rede: a placa vira "carinha" e responde em http://carinha.local (rodar como root, desta pasta). +set -e +cd "$(dirname "$0")" +apt-get install -y --no-install-recommends avahi-daemon libnss-mdns nftables +hostnamectl set-hostname carinha +sed -i "s/^127\.0\.1\.1.*/127.0.1.1 carinha/" /etc/hosts +grep -q "^127.0.1.1 carinha" /etc/hosts || echo "127.0.1.1 carinha" >> /etc/hosts +install -m 644 carinha.service.avahi /etc/avahi/services/carinha.service +install -m 644 carinha.nft /etc/nftables.d-carinha.nft +grep -q "nftables.d-carinha.nft" /etc/nftables.conf || echo 'include "/etc/nftables.d-carinha.nft"' >> /etc/nftables.conf +nft -c -f /etc/nftables.conf +systemctl enable --now avahi-daemon nftables +systemctl restart avahi-daemon nftables +echo "pronto: http://carinha.local"