feat(historico): deteccoes com foto, sensor PIR na placa e tela apagando no sono

- 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>
This commit is contained in:
julio
2026-10-06 03:40:13 -03:00
co-authored by Claude Opus 5.5
parent 98c9e8803e
commit 788215b49e
20 changed files with 934 additions and 16 deletions
@@ -28,6 +28,15 @@ public final class ControleRemoto {
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); }
@@ -118,6 +127,7 @@ public final class ControleRemoto {
mudanca.run();
}
if (videoRTC()) rtcTick(base);
fotoTick(base);
byte[] jpeg = pendente;
pendente = null;
if (jpeg != null && preview()) {
@@ -147,10 +157,13 @@ public final class ControleRemoto {
return c;
}
public void quadro(Image image, int rotacao) {
if (videoRTC()) return;
boolean querFoto = fotoPedida > 0 && fotoJpeg == null;
long agora = System.currentTimeMillis();
if (!preview() || pendente != null || agora < quadroEm) return;
quadroEm = agora + 200;
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()};
@@ -171,10 +184,44 @@ public final class ControleRemoto {
if (rotated != b) rotated.recycle();
b.recycle();
}
if (preview()) pendente = jpeg;
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) {
@@ -198,6 +198,8 @@ public class MainActivity extends Activity {
}
atualizarSensores();
atualizarBrilho();
if (remoto.fotoPendente() > 0 && rtcAtivo)
rtcWeb.evaluateJavascript("window.rtcFoto&&window.rtcFoto()", null);
}
}
});
@@ -1079,7 +1081,7 @@ public class MainActivity extends Activity {
web.evaluateJavascript("window.carinhaBateria={nivel:" + percent + ",carregando:" + charging + "}", null);
}
}
web.evaluateJavascript("window.eilik?({dormindo:window.eilik.face.dormindo,comando:window.carinhaUltimoComando||0,ocupado:(Date.now()-(window.carinhaUltimoComando||0)<3000)&&!!(window.eilik.face.kf||window.eilik.face.fila.length)}):null",
web.evaluateJavascript("window.eilik?({dormindo:window.eilik.face.dormindo,comando:window.carinhaUltimoComando||0,ocupado:(Date.now()-(window.carinhaUltimoComando||0)<3000)&&!!(window.eilik.face.kf||window.eilik.face.fila.length),apagada:!!window.carinhaTelaApagada}):null",
new ValueCallback<String>() {
@Override public void onReceiveValue(String valor) {
if (!emPrimeiroPlano) return;
@@ -1091,6 +1093,7 @@ public class MainActivity extends Activity {
float nivel = BrilhoRepouso.nivel(remoto.economia(), remoto.brilho(), remoto.brilhoRepouso(),
System.currentTimeMillis() - ultimoUsoTela, remoto.repousoTela(),
estado.optBoolean("ocupado"), estado.optBoolean("dormindo"));
if (estado.optBoolean("apagada")) nivel = 0f; // tela preta + brilho mínimo
WindowManager.LayoutParams attr = getWindow().getAttributes();
if (Math.abs(attr.screenBrightness - nivel) > 0.005f) {
attr.screenBrightness = nivel;
@@ -1104,6 +1107,14 @@ public class MainActivity extends Activity {
}
private final class PonteRTC {
/** Foto do vídeo ao vivo (data:image/jpeg;base64,...) para o histórico. */
@JavascriptInterface public void foto(final String dataUrl) {
try {
int virgula = dataUrl == null ? -1 : dataUrl.indexOf(',');
if (virgula < 0 || dataUrl.length() > 4_000_000) return;
remoto.fotoRTC(android.util.Base64.decode(dataUrl.substring(virgula + 1), android.util.Base64.DEFAULT));
} catch (Exception e) { Log.w("CarinhaPC", "foto do video falhou: " + e); }
}
@JavascriptInterface public void rastrear(final String json) {
if (!emPrimeiroPlano || !cameraPermitida() || !remoto.config().optBoolean("seguirRastreamento", true)) return;
try {