feat: initial commit - Carinha Control Panel v1.0 com UI glassmorphism, watch-bar, emojis, braco robotico e conversa IA

This commit is contained in:
julio
2026-10-05 16:21:57 -03:00
commit 8217292893
114 changed files with 13145 additions and 0 deletions
@@ -0,0 +1,9 @@
package br.carinha.rosto;
public final class BrilhoRepouso {
public static float nivel(boolean economia, float normal, float repouso,
long inativo, long espera, boolean ocupado, boolean dormindo) {
if (economia && !ocupado && (dormindo || inativo >= espera)) return Math.min(normal, repouso);
return normal;
}
}
@@ -0,0 +1,57 @@
package br.carinha.rosto;
import android.content.Context;
import android.util.Log;
import java.io.*;
import java.net.*;
import java.security.MessageDigest;
/** Executor nativo no telefone. Preparação fora da thread da interface. */
public final class CerebroLocal {
private volatile boolean fechado;
private Process processo;
public void iniciar(final Context contexto, final String servidor) {
new Thread(() -> {
try {
File libs = new File(contexto.getApplicationInfo().nativeLibraryDir);
File executavel = new File(libs, "libcarinha_ai.so");
if (!executavel.exists()) { Log.w("CarinhaIA", "Runtime ARM64 indisponivel"); return; }
File modelo = new File(contexto.getFilesDir(), "qwen3-q4.gguf");
if (!modelo.exists()) {
Log.i("CarinhaIA", "Baixando modelo do servidor local");
HttpURLConnection con = (HttpURLConnection)new URL(servidor.replaceAll("/+$", "") + "/ia/modelo").openConnection();
con.setConnectTimeout(10000); con.setReadTimeout(60000);
File temporario = new File(contexto.getFilesDir(), "qwen3-q4.partial");
MessageDigest hash = MessageDigest.getInstance("SHA-256");
try (InputStream input = con.getInputStream(); FileOutputStream output = new FileOutputStream(temporario)) {
byte[] buffer = new byte[65536]; int n;
while ((n = input.read(buffer)) != -1) {
if (fechado) return;
output.write(buffer, 0, n); hash.update(buffer, 0, n);
}
} finally { con.disconnect(); }
StringBuilder digest = new StringBuilder();
for (byte b : hash.digest()) digest.append(String.format("%02x", b & 255));
if (!digest.toString().equals("ac2d97712095a558e31573f62f466a3f9d93990898b0ec79d7c974c1780d524a")) throw new IOException("Checksum do modelo invalido");
if (!temporario.renameTo(modelo)) throw new IOException("Falha ao guardar modelo");
}
if (fechado) return;
// Um executor já ativo (teste ADB) pode ser reutilizado durante a transição.
try {
HttpURLConnection health = (HttpURLConnection)new URL("http://127.0.0.1:8101/health").openConnection();
health.setConnectTimeout(500); health.setReadTimeout(500);
try { if (health.getResponseCode() == 200) { Log.i("CarinhaIA", "Executor existente pronto"); return; } }
finally { health.disconnect(); }
} catch (IOException ignored) { }
ProcessBuilder builder = new ProcessBuilder(executavel.getAbsolutePath(), "-m", modelo.getAbsolutePath(), "--host", "0.0.0.0", "--port", "8101", "-c", "1024", "-t", "2", "-np", "1", "--jinja");
builder.environment().put("LD_LIBRARY_PATH", libs.getAbsolutePath());
builder.environment().put("GGML_BACKEND_DL_PATH", libs.getAbsolutePath());
builder.redirectErrorStream(true);
builder.redirectOutput(new File(contexto.getFilesDir(), "ia-runtime.log"));
synchronized (this) { if (!fechado) processo = builder.start(); }
Log.i("CarinhaIA", "Executor nativo iniciado");
} catch (Exception e) { Log.e("CarinhaIA", "IA local: " + e); }
}, "CarinhaIA").start();
}
public synchronized void fechar() { fechado = true; if (processo != null) processo.destroy(); }
}
@@ -0,0 +1,209 @@
package br.carinha.rosto;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.media.Image;
import android.util.Log;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentLinkedQueue;
/** One network worker and one pending JPEG; camera never waits on the network. */
public final class ControleRemoto {
private volatile String servidor;
private volatile boolean preview;
private volatile JSONObject config = new JSONObject();
private volatile byte[] pendente;
private volatile ScheduledExecutorService worker;
private long consultarEm;
private long quadroEm;
private long logEm;
private final Runnable mudanca;
public interface RTCListener { boolean receber(JSONObject pacote); }
private RTCListener rtcListener;
private final ConcurrentLinkedQueue<String> sinais = new ConcurrentLinkedQueue<>();
private String rtcId = "";
private int rtcDesde;
private long rtcConsultarEm;
private final ConcurrentLinkedQueue<JSONObject> ajustes = new ConcurrentLinkedQueue<>();
public JSONObject config() { return config; }
public void configurar(JSONObject parte) { ajustes.offer(parte); }
public ControleRemoto(Runnable mudanca) { this.mudanca = mudanca; }
public void rtcListener(RTCListener listener) { rtcListener = listener; }
public boolean videoRTC() { return config.optBoolean("videoRTC", true); }
public void sinalRTC(String json) {
if (sinais.size() < 64) sinais.offer(json);
}
public void servidor(String valor) {
servidor = valor;
pendente = null;
consultarEm = 0;
rtcId = ""; rtcDesde = 0; rtcConsultarEm = 0;
}
public boolean preview() { return preview && worker != null; }
public boolean economia() { return config.optBoolean("economia", true); }
public float brilho() { return (float) config.optDouble("brilho", 0.7); }
public float brilhoRepouso() { return (float) config.optDouble("brilhoRepouso", 0.08); }
public long repousoTela() { return (long) (config.optDouble("repousoTela", 30) * 1000); }
public double somlim() { return config.optDouble("somlim", 0.35); }
public double movlim() { return config.optDouble("movlim", 0.05); }
public synchronized void iniciar() {
if (worker != null) return;
consultarEm = 0;
worker = Executors.newSingleThreadScheduledExecutor();
worker.scheduleWithFixedDelay(new Runnable() {
@Override public void run() { tick(); }
}, 0, 200, TimeUnit.MILLISECONDS);
}
public synchronized void parar() {
if (worker != null) worker.shutdownNow();
worker = null;
preview = false;
pendente = null;
sinais.clear();
}
private void tick() {
String base = servidor;
if (base == null || worker == null) return;
HttpURLConnection conn = null;
try {
long agora = System.currentTimeMillis();
JSONObject ajuste = ajustes.peek();
if (ajuste != null) {
conn = abrir(base + "/app/config");
conn.setRequestMethod("POST"); conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json");
byte[] dados = ajuste.toString().getBytes("UTF-8");
try (java.io.OutputStream out = conn.getOutputStream()) { out.write(dados); }
if (conn.getResponseCode() != 200) throw new java.io.IOException("Ajuste não salvo");
ajustes.poll(); consultarEm = 0;
conn.disconnect(); conn = null;
}
if (agora >= consultarEm) {
consultarEm = agora + 1000;
conn = abrir(base + "/app/config");
String texto;
try (InputStream input = conn.getInputStream()) {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int n;
while ((n = input.read(buffer)) != -1 && bytes.size() < 16384) bytes.write(buffer, 0, n);
texto = bytes.toString("UTF-8");
}
conn.disconnect(); conn = null;
JSONObject recebido = new JSONObject(texto);
boolean novo = recebido.optBoolean("preview", false);
if (worker == null || !base.equals(servidor)) return;
config = recebido;
if (novo != preview) {
preview = novo;
if (!novo) pendente = null;
Log.i("CarinhaPC", "transmissao=" + novo);
}
mudanca.run();
}
if (videoRTC()) rtcTick(base);
byte[] jpeg = pendente;
pendente = null;
if (jpeg != null && preview()) {
conn = abrir(base + "/camera/frame");
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "image/jpeg");
conn.setFixedLengthStreamingMode(jpeg.length);
try (java.io.OutputStream out = conn.getOutputStream()) { out.write(jpeg); }
int code = conn.getResponseCode();
if (code == 409) { preview = false; pendente = null; }
else if (code != 200) throw new java.io.IOException("HTTP " + code);
}
} catch (Exception e) {
pendente = null;
if (System.currentTimeMillis() >= logEm) {
logEm = System.currentTimeMillis() + 5000;
Log.w("CarinhaPC", "servidor indisponivel: " + e);
}
} finally { if (conn != null) conn.disconnect(); }
}
private HttpURLConnection abrir(String url) throws Exception {
HttpURLConnection c = (HttpURLConnection) new URL(url).openConnection();
c.setConnectTimeout(1500); c.setReadTimeout(1500);
c.setUseCaches(false);
return c;
}
public void quadro(Image image, int rotacao) {
if (videoRTC()) return;
long agora = System.currentTimeMillis();
if (!preview() || pendente != null || agora < quadroEm) return;
quadroEm = agora + 200;
try {
Image.Plane[] p = image.getPlanes();
ByteBuffer[] buffers = {p[0].getBuffer(), p[1].getBuffer(), p[2].getBuffer()};
int[] rows = {p[0].getRowStride(), p[1].getRowStride(), p[2].getRowStride()};
int[] pixels = {p[0].getPixelStride(), p[1].getPixelStride(), p[2].getPixelStride()};
int w = image.getWidth(), h = image.getHeight();
byte[] nv21 = YuvFrame.nv21(w, h, buffers, rows, pixels);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
new YuvImage(nv21, ImageFormat.NV21, w, h, null)
.compressToJpeg(new Rect(0, 0, w, h), 70, bytes);
byte[] jpeg = bytes.toByteArray();
if (rotacao != 0) {
Bitmap b = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length);
Matrix m = new Matrix(); m.postRotate(rotacao);
Bitmap rotated = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
bytes.reset(); rotated.compress(Bitmap.CompressFormat.JPEG, 70, bytes);
jpeg = bytes.toByteArray();
if (rotated != b) rotated.recycle();
b.recycle();
}
if (preview()) pendente = jpeg;
} catch (Exception e) { Log.w("CarinhaPC", "quadro descartado: " + e); }
}
private void rtcTick(String base) throws Exception {
String json;
while ((json = sinais.poll()) != null) {
HttpURLConnection c = abrir(base + "/rtc/send");
try {
byte[] body = json.getBytes("UTF-8");
c.setRequestMethod("POST"); c.setDoOutput(true);
c.setRequestProperty("Content-Type", "application/json");
c.setFixedLengthStreamingMode(body.length);
try (java.io.OutputStream out = c.getOutputStream()) { out.write(body); }
int code = c.getResponseCode();
if (code != 200 && code != 409) throw new java.io.IOException("RTC HTTP " + code);
} finally { c.disconnect(); }
}
long agora = System.currentTimeMillis();
if (agora < rtcConsultarEm || rtcListener == null) return;
rtcConsultarEm = agora + 400;
HttpURLConnection c = abrir(base + "/rtc/poll?lado=app&desde=" + rtcDesde + "&id=" + rtcId);
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (InputStream in = c.getInputStream()) {
byte[] buffer = new byte[4096]; int n;
while ((n = in.read(buffer)) != -1 && bytes.size() < 262144) bytes.write(buffer, 0, n);
}
JSONObject packet = new JSONObject(bytes.toString("UTF-8"));
if (worker == null || !base.equals(servidor)) return;
if (rtcListener.receber(packet)) {
String id = packet.optString("id", "");
if (!id.equals(rtcId)) { rtcDesde = 0; rtcId = id; }
org.json.JSONArray messages = packet.optJSONArray("mensagens");
if (messages != null) for (int i = 0; i < messages.length(); i++)
rtcDesde = Math.max(rtcDesde, messages.getJSONObject(i).getInt("seq"));
}
} finally { c.disconnect(); }
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,7 @@
package br.carinha.rosto;
public final class Sensores {
private Sensores() {}
public static boolean ativo(boolean ligado, boolean duranteSono, boolean dormindo) {
return ligado && (!dormindo || duranteSono);
}
}
@@ -0,0 +1,22 @@
package br.carinha.rosto;
import java.nio.ByteBuffer;
/** Converts Camera2 planes to NV21, respecting padding and interleaved chroma. */
public final class YuvFrame {
public static byte[] nv21(int w, int h, ByteBuffer[] planes, int[] rows, int[] pixels) {
if (w <= 0 || h <= 0 || (w & 1) != 0 || (h & 1) != 0)
throw new IllegalArgumentException("YUV requires positive even dimensions");
byte[] out = new byte[w * h * 3 / 2];
int at = 0;
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
out[at++] = planes[0].get(planes[0].position() + y * rows[0] + x * pixels[0]);
for (int y = 0; y < h / 2; y++)
for (int x = 0; x < w / 2; x++) {
out[at++] = planes[2].get(planes[2].position() + y * rows[2] + x * pixels[2]);
out[at++] = planes[1].get(planes[1].position() + y * rows[1] + x * pixels[1]);
}
return out;
}
}