feat(rede): placa em carinha.local e app que acha a placa sozinho

- 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 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-10 03:04:25 -03:00
co-authored by Claude Opus 5.5
parent ccd89d125e
commit 91c64e5b18
6 changed files with 164 additions and 3 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.carinha.rosto"
android:versionCode="12"
android:versionName="0.9.0">
android:versionCode="13"
android:versionName="0.9.1">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
@@ -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); }
}
@@ -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;