feat: initial commit - Carinha Control Panel v1.0 com UI glassmorphism, watch-bar, emojis, braco robotico e conversa IA
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user