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:
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="br.carinha.rosto"
|
||||
android:versionCode="10"
|
||||
android:versionName="0.8.2">
|
||||
android:versionCode="11"
|
||||
android:versionName="0.8.3">
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||
|
||||
|
||||
@@ -93,6 +93,22 @@ addEventListener("resize", ajustar);
|
||||
const PRETO = [6, 8, 14];
|
||||
const CIANO = [92, 214, 255];
|
||||
const EXTRAS = { boca: true, sobrancelhas: false, iris: false, subirBoca: 0, distSobrancelha: 16 };
|
||||
const SONO = { apagar: true, minutos: 5, desde: 0, apagada: false };
|
||||
/* Dormindo há tempo demais: apaga a tela (preto total) e o app baixa o brilho ao mínimo.
|
||||
Qualquer coisa que acorde o rosto (som, movimento, toque, comando, PIR) acende de novo. */
|
||||
function cuidarDaTela() {
|
||||
const agora = performance.now();
|
||||
if (!r.dormindo) SONO.desde = 0;
|
||||
else if (!SONO.desde) SONO.desde = agora;
|
||||
const apagar = SONO.apagar && r.dormindo && SONO.desde && agora - SONO.desde > SONO.minutos * 60000;
|
||||
if (apagar !== SONO.apagada) {
|
||||
SONO.apagada = apagar;
|
||||
window.carinhaTelaApagada = apagar;
|
||||
document.body.style.visibility = apagar ? "hidden" : "";
|
||||
console.log(apagar ? "tela apagada (dormindo ha " + SONO.minutos + " min)" : "tela acesa");
|
||||
}
|
||||
}
|
||||
setInterval(cuidarDaTela, 1000);
|
||||
const CIANO_E = [150, 232, 255];
|
||||
const VERM = [255, 78, 78];
|
||||
const ROSA = [255, 112, 176];
|
||||
@@ -716,6 +732,16 @@ function checarSilencio(agora) {
|
||||
journal do carinha-web (so quando o microfone esta ativo). */
|
||||
function relatarSom(evento, n) {
|
||||
try { fetch("diag/mic=" + evento + " nivel=" + n.toFixed(2)).catch(() => {}); } catch (e) {}
|
||||
if (evento !== "adormeceu") registrarEvento("som", evento, n);
|
||||
}
|
||||
|
||||
/* Histórico de detecções no servidor (o app tira a foto do evento). Só o celular registra. */
|
||||
function registrarEvento(tipo, evento, nivel) {
|
||||
if (new URLSearchParams(location.search).get("aparelho") !== "1") return;
|
||||
try {
|
||||
fetch("/historico/evento", { method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ tipo, evento, nivel }) }).catch(() => {});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------ presenca (movimento)
|
||||
@@ -804,6 +830,7 @@ function movimento(vel, cx, cy) {
|
||||
/* Conta ao servidor o que a presenca percebeu (aparece no journal do carinha-web). */
|
||||
function relatarMov(evento, n) {
|
||||
try { fetch("diag/mov=" + evento + " nivel=" + n.toFixed(2)).catch(() => {}); } catch (e) {}
|
||||
registrarEvento("movimento", evento, n);
|
||||
}
|
||||
|
||||
// ------------------------------------------------- camera oculta (sensor)
|
||||
@@ -936,6 +963,8 @@ let configRecebida = null;
|
||||
function configurarApp(c) {
|
||||
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true; EXTRAS.iris = c.iris === true;
|
||||
if (typeof c.subirBoca === 'number') EXTRAS.subirBoca = c.subirBoca;
|
||||
SONO.apagar = c.apagarTela !== false;
|
||||
if (typeof c.apagarTelaMin === 'number') SONO.minutos = c.apagarTelaMin;
|
||||
if (typeof c.distSobrancelha === 'number') EXTRAS.distSobrancelha = c.distSobrancelha;
|
||||
AUTO = c.auto;
|
||||
AUDIO.ligado = c.audio && par.get('espelho') !== '1' && par.get('audio') !== '0'; AUDIO.volume = c.volume;
|
||||
@@ -966,6 +995,7 @@ async function sincronizarApp() {
|
||||
movimento: MOV.vel, camera_ativa: window.carinhaSensores ? window.carinhaSensores.camera : MOV.ativo && agora - MOV.visto < 2,
|
||||
centro: [MOV.cx, MOV.cy], config: configRecebida,
|
||||
brilho: window.carinhaBrilho,
|
||||
tela_apagada: !!window.carinhaTelaApagada,
|
||||
bateria: window.carinhaBateria,
|
||||
aparelho: window.carinhaAparelho,
|
||||
video: window.carinhaVideo,
|
||||
|
||||
@@ -152,6 +152,15 @@ window.rtcReceber = packet => {
|
||||
});
|
||||
};
|
||||
window.rtcParar = () => { allowed = false; parar(); sid = null; };
|
||||
/* Foto do evento para o histórico, tirada do vídeo ao vivo (câmera de movimento parada). */
|
||||
window.rtcFoto = () => {
|
||||
if (!stream || video.readyState < 2 || !video.videoWidth) return;
|
||||
const escala = Math.min(1, 1280 / video.videoWidth);
|
||||
const tela = document.createElement('canvas');
|
||||
tela.width = Math.round(video.videoWidth * escala); tela.height = Math.round(video.videoHeight * escala);
|
||||
tela.getContext('2d').drawImage(video, 0, 0, tela.width, tela.height);
|
||||
CarinhaRTC.foto(tela.toDataURL('image/jpeg', 0.82));
|
||||
};
|
||||
setInterval(() => {
|
||||
if (!movementEnabled || !stream || video.readyState < 2 || video.currentTime===lastMotionFrame) return;
|
||||
lastMotionFrame=video.currentTime;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user