- historico.py: eventos de PIR (gpiomon no pino 11, gpiochip1 linha 70), movimento e som do celular em SQLite; ate 200 eventos e 100 fotos; cada sensor e a foto ligam/desligam; intervalo minimo por tipo - PIR acorda o Carinha quando ele esta dormindo - APK 0.8.3: o app consulta o pedido de foto e envia uma foto da camera frontal (quadro da camera de movimento ou do video ao vivo) - tela apaga depois de X minutos dormindo (preto total + brilho minimo no AMOLED); acende com som, movimento, toque, comando ou PIR - painel: tela Historico (sensores, fotos, filtros, apagar), atalho em Ao vivo e no Ctrl+K, ajustes "Apagar a tela quando dormir" - testes do historico (servico e rotas HTTP) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
261 lines
12 KiB
Java
261 lines
12 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;
|
|
// Foto do evento (histórico): o servidor pede, o app tira e envia.
|
|
private volatile long fotoPedida = 0, fotoFeita = 0, fotoId = 0, pedidoEm = 0;
|
|
private volatile byte[] fotoJpeg = null;
|
|
/** Id do evento que está esperando foto (0 = nenhum). */
|
|
public long fotoPendente() { return fotoJpeg == null ? fotoPedida : 0; }
|
|
/** Foto vinda do vídeo ao vivo (quando a câmera de movimento está desligada). */
|
|
public void fotoRTC(byte[] jpeg) {
|
|
if (fotoPedida > 0 && fotoJpeg == null && jpeg != null && jpeg.length > 0) { fotoId = fotoPedida; fotoJpeg = jpeg; fotoPedida = 0; }
|
|
}
|
|
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);
|
|
fotoTick(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) {
|
|
boolean querFoto = fotoPedida > 0 && fotoJpeg == null;
|
|
long agora = System.currentTimeMillis();
|
|
if (!querFoto) {
|
|
if (videoRTC()) return;
|
|
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 (querFoto) { fotoId = fotoPedida; fotoJpeg = jpeg; fotoPedida = 0; }
|
|
else if (preview()) pendente = jpeg;
|
|
} catch (Exception e) { Log.w("CarinhaPC", "quadro descartado: " + e); }
|
|
}
|
|
|
|
/** Consulta o pedido de foto (1x/s) e envia a foto pronta para o histórico. */
|
|
private void fotoTick(String base) throws Exception {
|
|
byte[] jpeg = fotoJpeg;
|
|
if (jpeg != null) {
|
|
long id = fotoId;
|
|
fotoJpeg = null;
|
|
HttpURLConnection c = abrir(base + "/historico/foto?id=" + id);
|
|
try {
|
|
c.setRequestMethod("POST"); c.setDoOutput(true);
|
|
c.setRequestProperty("Content-Type", "image/jpeg");
|
|
c.setFixedLengthStreamingMode(jpeg.length);
|
|
try (java.io.OutputStream out = c.getOutputStream()) { out.write(jpeg); }
|
|
int code = c.getResponseCode();
|
|
Log.i("CarinhaPC", "foto do evento " + id + ": HTTP " + code);
|
|
fotoFeita = id;
|
|
} finally { c.disconnect(); }
|
|
}
|
|
long agora = System.currentTimeMillis();
|
|
if (agora < pedidoEm) return;
|
|
pedidoEm = agora + 1000;
|
|
HttpURLConnection c = abrir(base + "/historico/pedido");
|
|
try {
|
|
if (c.getResponseCode() != 200) return;
|
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
|
try (InputStream in = c.getInputStream()) {
|
|
byte[] buffer = new byte[512]; int n;
|
|
while ((n = in.read(buffer)) != -1 && bytes.size() < 4096) bytes.write(buffer, 0, n);
|
|
}
|
|
long id = new JSONObject(bytes.toString("UTF-8")).optLong("id", 0);
|
|
if (id > 0 && id != fotoFeita && id != fotoId && fotoJpeg == null) fotoPedida = id;
|
|
} finally { c.disconnect(); }
|
|
}
|
|
|
|
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(); }
|
|
}
|
|
}
|