- carinha.html assina /eventos (expressao, app_config, conversa, reacao, recarregar) e volta ao polling de /fila sozinho se o SSE cair; foto dos ajustes a cada (re)conexao; token (?t=) vai em todo fetch ao servidor - APK 0.8-token: token nas preferencias e no menu de toque longo, enviado na pagina, no HTTP do Java e ao llama-server (--api-key); trocar servidor por Intent exige o token salvo; camera/microfone so para o servidor configurado ou a copia local - cerebro/conversa mandam a chave ao llama-server do S10 - servidor: evento SSE 'recarregar'; remove /camera/frame e /app/connect; enderecos antigos redirecionam para a secao do painel novo - remove dashboard, painel, conversa, emojis, treinamento, braco, servos, api, scripts e CSS antigos, audio-robot.js e servir-local.bat - logica pura do painel em web/src/lib/nucleo.ts com testes node --test (substitui test_movimentos.js e test_rastreamento.js) - testes de navegador portados para o painel novo (rtc, rosto, rastreamento) - README, docs/PAINEL.md e docs/SEGURANCA.md reescritos; versao 0.8.0; carinha-pc.service le /etc/carinha.env Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
214 lines
10 KiB
Java
214 lines
10 KiB
Java
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);
|
|
}
|
|
private volatile String token = "";
|
|
/** Token do servidor (CARINHA_TOKEN); vai como Authorization em todo pedido. */
|
|
public void token(String valor) { token = valor == null ? "" : valor.trim(); }
|
|
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);
|
|
if (!token.isEmpty()) c.setRequestProperty("Authorization", "Bearer " + token);
|
|
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(); }
|
|
}
|
|
}
|