Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43770c6d67 | ||
|
|
f8d2ebf705 |
@@ -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="11"
|
||||
android:versionName="0.8.3">
|
||||
android:versionCode="12"
|
||||
android:versionName="0.9.0">
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||
|
||||
@@ -36,6 +36,23 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!-- no modo quiosque a carinha é a tela inicial do aparelho -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- dono do aparelho (modo quiosque): adb shell dpm set-device-owner br.carinha.rosto/.Admin -->
|
||||
<receiver
|
||||
android:name=".Admin"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_DEVICE_ADMIN">
|
||||
<meta-data android:name="android.app.device_admin" android:resource="@xml/admin" />
|
||||
<intent-filter>
|
||||
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
+156
-11
@@ -20,6 +20,52 @@
|
||||
@keyframes aparecerFigura{from{opacity:0}to{opacity:1}}
|
||||
@media(prefers-reduced-motion:reduce){.iconeOlho,.figurinhaDinamica{animation:none}}
|
||||
#fps{position:fixed;left:4px;top:2px;color:#2b3a4a;font:12px monospace;display:none}
|
||||
/* sem a placa: placa ✕ celular na cor dos olhos. Deitado, o rosto dormindo fica onde está e o
|
||||
desenho fica em pé no canto direito; em pé, o rosto sobe e o desenho vai embaixo. */
|
||||
#semServidor{position:fixed;right:2.5vw;top:50%;transform:translateY(-50%);width:min(24vw,42vh);
|
||||
z-index:5;pointer-events:none;text-align:center;opacity:.82;font-family:system-ui,sans-serif;
|
||||
animation:semServidorEntra 1.2s ease-out}
|
||||
#semServidor svg{display:block;margin:0 auto;overflow:visible;filter:drop-shadow(0 0 3px currentColor)}
|
||||
#semServidor svg.horizontal{display:none}
|
||||
#semServidor svg.vertical{height:58vh;width:auto;max-width:100%}
|
||||
#semServidor svg.vertical .olhinhos{animation-name:semServidorOlharCima}
|
||||
@media (orientation:portrait){
|
||||
body.semServidor #tela,body.semServidor #rostoMotor{transform:translateY(-20%) scale(.8)}
|
||||
#semServidor{right:auto;top:auto;left:50%;bottom:3.5vh;transform:translateX(-50%);width:min(80vw,400px)}
|
||||
#semServidor svg.horizontal{display:block;width:100%;height:auto}
|
||||
#semServidor svg.vertical{display:none}
|
||||
}
|
||||
#semServidor .titulo{text-wrap:balance;font-weight:600;font-size:clamp(12px,3.2vh,20px);margin-top:.4em;letter-spacing:.02em}
|
||||
#semServidor .sub{font-size:clamp(10px,2.4vh,15px);opacity:.6;margin-top:.25em;font-variant-numeric:tabular-nums}
|
||||
#semServidor .placa rect,#semServidor .celular>rect{fill:none;stroke:currentColor;stroke-width:3}
|
||||
#semServidor .chip{fill:currentColor;fill-opacity:.25}
|
||||
#semServidor .pino{fill:currentColor;opacity:.7}
|
||||
#semServidor .led{fill:#ff5a5a;animation:semServidorLed 1.6s steps(1) infinite}
|
||||
#semServidor .cabo{stroke:currentColor;stroke-width:3;stroke-dasharray:5 6;opacity:.5;fill:none}
|
||||
#semServidor .arco{fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:round;opacity:0;
|
||||
animation:semServidorArco 2.4s ease-out infinite}
|
||||
#semServidor .a2{animation-delay:.3s}
|
||||
#semServidor .a3{animation-delay:.6s}
|
||||
#semServidor .xis path{stroke:#ff5a5a;stroke-width:5;stroke-linecap:round;fill:none}
|
||||
#semServidor .xis{transform-box:fill-box;transform-origin:center;animation:semServidorXis 2.4s ease-in-out infinite}
|
||||
#semServidor .olhinhos{fill:currentColor;transform-box:fill-box;transform-origin:center;
|
||||
animation:semServidorOlhar 5s ease-in-out infinite}
|
||||
#semServidor .boquinha{fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round}
|
||||
#semServidor .celular{transform-box:fill-box;transform-origin:50% 100%;animation:semServidorProcura 4.8s ease-in-out infinite}
|
||||
#semServidor.cutucado .celular{animation:semServidorChacoalha .5s ease-in-out}
|
||||
@keyframes semServidorEntra{from{opacity:0}to{opacity:.82}}
|
||||
/* em pé no canto direito: o celularzinho olha para cima, para a placa */
|
||||
@keyframes semServidorOlharCima{0%,30%{transform:translateY(-3px) scaleY(1)}34%{transform:translateY(-3px) scaleY(.1)}38%,60%{transform:translateY(-3px) scaleY(1)}
|
||||
70%,90%{transform:translateY(1px) scaleY(1)}100%{transform:translateY(-3px)}}
|
||||
@keyframes semServidorArco{0%{opacity:0}25%{opacity:.9}70%,100%{opacity:0}}
|
||||
@keyframes semServidorXis{0%,55%,100%{transform:scale(1);opacity:.75}65%{transform:scale(1.25);opacity:1}80%{transform:scale(1);opacity:.85}}
|
||||
@keyframes semServidorLed{0%{opacity:1}50%{opacity:.15}}
|
||||
/* o celularzinho olha para a placa, olha de volta e pisca */
|
||||
@keyframes semServidorOlhar{0%,30%{transform:translateX(-3px) scaleY(1)}34%{transform:translateX(-3px) scaleY(.1)}38%,60%{transform:translateX(-3px) scaleY(1)}
|
||||
70%,90%{transform:translateX(1px) scaleY(1)}100%{transform:translateX(-3px)}}
|
||||
@keyframes semServidorProcura{0%,100%{transform:rotate(0)}20%{transform:rotate(-5deg)}40%{transform:rotate(0)}}
|
||||
@keyframes semServidorChacoalha{0%,100%{transform:rotate(0)}25%{transform:rotate(-9deg)}50%{transform:rotate(7deg)}75%{transform:rotate(-4deg)}}
|
||||
@media(prefers-reduced-motion:reduce){#semServidor *,#semServidor{animation:none!important}#semServidor .arco{opacity:.6}}
|
||||
</style>
|
||||
<body>
|
||||
<svg id="tela" viewBox="0 0 480 320" preserveAspectRatio="xMidYMid meet">
|
||||
@@ -106,7 +152,7 @@ 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;
|
||||
const apagar = SONO.apagar && r.dormindo && !window.carinhaSemServidor && SONO.desde && agora - SONO.desde > SONO.minutos * 60000;
|
||||
if (apagar !== SONO.apagada) {
|
||||
SONO.apagada = apagar;
|
||||
window.carinhaTelaApagada = apagar;
|
||||
@@ -725,6 +771,11 @@ function trata(ordem) {
|
||||
r.dormindo = true;
|
||||
atividade(null);
|
||||
} else if (ordem === "acordar") {
|
||||
if (window.carinhaSemServidor) { // sem a placa continua dormindo; o aviso chacoalha
|
||||
const aviso = document.getElementById("semServidor");
|
||||
if (aviso) { aviso.classList.remove("cutucado"); void aviso.offsetWidth; aviso.classList.add("cutucado"); }
|
||||
return;
|
||||
}
|
||||
r.dormindo = false; animar("surpreso");
|
||||
} else if (ordem === "aleatorio") {
|
||||
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
|
||||
@@ -777,27 +828,44 @@ async function buscarOrdens() {
|
||||
try {
|
||||
const resp = await fetch("fila"+(par.get("aparelho")==="1"?"?aparelho=1":""), { cache: "no-store" });
|
||||
if (!resp.ok) return;
|
||||
conexaoOk();
|
||||
const lista = await resp.json();
|
||||
for (const nome of lista) trata(nome);
|
||||
} catch (e) { /* sem servidor (file://) — ignora */ }
|
||||
}
|
||||
setInterval(buscarOrdens, 500);
|
||||
|
||||
// O app (window.CarinhaApp) guarda a ultima aparencia e os ajustes vindos do servidor:
|
||||
// a copia do APK (file://) nao enxerga o armazenamento da pagina do servidor.
|
||||
const LOCAL = location.protocol === "file:";
|
||||
function lembrar(chave, valor) {
|
||||
try { if (window.CarinhaApp && !LOCAL) window.CarinhaApp.guardar(chave, JSON.stringify(valor)); } catch (e) {}
|
||||
}
|
||||
function lembrado(chave) {
|
||||
try { const s = window.CarinhaApp && window.CarinhaApp.lembrado(chave); return s ? JSON.parse(s) : null; } catch (e) { return null; }
|
||||
}
|
||||
|
||||
// A aparencia guardada no servidor (rosto + cor) e aplicada ao carregar: o
|
||||
// painel de controle personaliza e a carinha ja abre do jeito escolhido. Sem
|
||||
// servidor (file://) isso falha calado e vale o padrao.
|
||||
// servidor vale a ultima que o app guardou (ou o padrao).
|
||||
async function aplicarAparencia() {
|
||||
try {
|
||||
const resp = await fetch("config", { cache: "no-store" });
|
||||
if (!resp.ok) return;
|
||||
const c = await resp.json();
|
||||
if (c.rosto && ROSTOS[c.rosto]) trocarRosto(c.rosto);
|
||||
if (FORMATOS.includes(c.formato)) FORMATO = c.formato;
|
||||
if (Array.isArray(c.cor) && c.cor.length === 3 && c.cor.every((n) => !isNaN(n)))
|
||||
{ CIANO.splice(0, 3, ...c.cor.map(Number)); r.cor = c.cor.map(Number); }
|
||||
} catch (e) { /* sem servidor — segue com o padrao */ }
|
||||
let c = null;
|
||||
if (!LOCAL) {
|
||||
try {
|
||||
const resp = await fetch("config", { cache: "no-store" });
|
||||
if (resp.ok) c = await resp.json();
|
||||
} catch (e) { /* sem servidor */ }
|
||||
}
|
||||
if (!c) c = lembrado("aparencia");
|
||||
if (!c) return;
|
||||
if (c.rosto && ROSTOS[c.rosto]) trocarRosto(c.rosto);
|
||||
if (FORMATOS.includes(c.formato)) FORMATO = c.formato;
|
||||
if (Array.isArray(c.cor) && c.cor.length === 3 && c.cor.every((n) => !isNaN(n)))
|
||||
{ CIANO.splice(0, 3, ...c.cor.map(Number)); r.cor = c.cor.map(Number); }
|
||||
}
|
||||
setTimeout(aplicarAparencia, 200);
|
||||
// O painel troca rosto/cor por ordens: guarda a aparencia atual de tempos em tempos.
|
||||
setInterval(() => { if (!window.carinhaSemServidor) lembrar("aparencia", { rosto: ROSTO_NOME, formato: FORMATO, cor: CIANO.slice() }); }, 15000);
|
||||
// ?painel=1: prévia dentro do painel novo. Nada de polling; o painel repassa os eventos
|
||||
// do servidor (SSE) por postMessage.
|
||||
const MODO_PAINEL = new URLSearchParams(location.search).get('painel') === '1';
|
||||
@@ -1238,6 +1306,7 @@ function configurarApp(c) {
|
||||
MOV.cooldown = c.movcooldown; MOV.olharMax = c.movolhar;
|
||||
if (!c.reagir) { MOV.olharX = MOV.olharY = 0; MOV.ultimoMov = 0; }
|
||||
configRecebida = c;
|
||||
lembrar('config', c);
|
||||
if(!c.seguirRastreamento || !c.sensorCamera || !c.reagir) TRACK.alvo=null;
|
||||
}
|
||||
let sincronizando = false;
|
||||
@@ -1363,6 +1432,7 @@ function laco(t) {
|
||||
if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null);
|
||||
document.body.classList.toggle("atividade", !!ATIV && !ATIV.saindo);
|
||||
|
||||
if (window.carinhaSemServidor && !r.dormindo) { r.fila = []; r.kf = null; r.dormindo = true; } // sem a placa: só dorme
|
||||
const ocupado = congelado ? true : passo(dt);
|
||||
if (!ocupado) {
|
||||
descanso += dt;
|
||||
@@ -1434,6 +1504,81 @@ setTimeout(relatar, 1500);
|
||||
setInterval(poll,1000);poll();
|
||||
})();
|
||||
|
||||
/* ---------------------------------------------------------------- sem servidor
|
||||
Sem a placa (copia do APK, ou o servidor caiu com a pagina aberta) a carinha dorme com a
|
||||
ultima aparencia e ajustes guardados e mostra placa ✕ celular. Volta sozinha quando a placa
|
||||
responde (o app recarrega a pagina do servidor; aberta pelo servidor, o SSE reconecta). */
|
||||
let ultimoOk = performance.now();
|
||||
const TOLERANCIA_SEM_SERVIDOR = 15000; // ms sem resposta antes de dormir
|
||||
function conexaoOk() { ultimoOk = performance.now(); }
|
||||
|
||||
function mostrarSemServidor(ligar) {
|
||||
let el = document.getElementById("semServidor");
|
||||
document.body.classList.toggle("semServidor", ligar);
|
||||
if (!ligar) { if (el) el.hidden = true; return; }
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = "semServidor";
|
||||
// placa e celular pelo canto de cima à esquerda; o desenho vertical vai no canto direito com o celular deitado
|
||||
const placa = (px, py) =>
|
||||
'<g class="placa">' +
|
||||
'<rect x="' + px + '" y="' + py + '" width="84" height="56" rx="6"/>' +
|
||||
'<rect class="chip" x="' + (px + 29) + '" y="' + (py + 15) + '" width="26" height="26" rx="3"/>' +
|
||||
[0,1,2,3,4,5,6,7].map((i) => '<circle class="pino" cx="' + (px + 11 + i * 9) + '" cy="' + (py + 7) + '" r="2"/>').join("") +
|
||||
'<circle class="pino" cx="' + (px + 7) + '" cy="' + (py + 50) + '" r="2.4"/><circle class="pino" cx="' + (px + 77) + '" cy="' + (py + 50) + '" r="2.4"/>' +
|
||||
'<circle class="led" cx="' + (px + 72) + '" cy="' + (py + 22) + '" r="2.6"/>' +
|
||||
'</g>';
|
||||
const celular = (cx, cy) =>
|
||||
'<g class="celular">' +
|
||||
'<rect x="' + cx + '" y="' + cy + '" width="40" height="76" rx="8"/>' +
|
||||
'<g class="olhinhos"><ellipse cx="' + (cx + 13) + '" cy="' + (cy + 34) + '" rx="3.2" ry="4.2"/><ellipse cx="' + (cx + 27) + '" cy="' + (cy + 34) + '" rx="3.2" ry="4.2"/></g>' +
|
||||
'<path class="boquinha" d="M' + (cx + 14) + ' ' + (cy + 48) + ' Q' + (cx + 20) + ' ' + (cy + 44) + ' ' + (cx + 26) + ' ' + (cy + 48) + '"/>' +
|
||||
'</g>';
|
||||
const xis = (x, y) => '<g class="xis"><path d="M' + (x - 10) + ' ' + (y - 10) + ' L' + (x + 10) + ' ' + (y + 10) + ' M' + (x + 10) + ' ' + (y - 10) + ' L' + (x - 10) + ' ' + (y + 10) + '"/></g>';
|
||||
el.innerHTML =
|
||||
'<svg class="horizontal" viewBox="0 0 300 104" aria-hidden="true">' + placa(18, 30) +
|
||||
'<path class="cabo" d="M104 58 H146"/>' +
|
||||
'<g class="arcos">' +
|
||||
'<path class="arco a1" d="M212 50 A10 10 0 0 0 212 66"/>' +
|
||||
'<path class="arco a2" d="M203 44 A18 18 0 0 0 203 72"/>' +
|
||||
'<path class="arco a3" d="M194 38 A26 26 0 0 0 194 78"/>' +
|
||||
'</g>' + xis(160, 58) + celular(220, 18) +
|
||||
'</svg>' +
|
||||
'<svg class="vertical" viewBox="0 0 120 262" aria-hidden="true">' + placa(18, 8) +
|
||||
'<path class="cabo" d="M60 66 V108"/>' +
|
||||
'<g class="arcos">' +
|
||||
'<path class="arco a1" d="M52 172 A10 10 0 0 1 68 172"/>' +
|
||||
'<path class="arco a2" d="M46 163 A18 18 0 0 1 74 163"/>' +
|
||||
'<path class="arco a3" d="M40 154 A26 26 0 0 1 80 154"/>' +
|
||||
'</g>' + xis(60, 122) + celular(40, 182) +
|
||||
'</svg>' +
|
||||
'<div class="titulo">sem conexão com a placa</div>' +
|
||||
'<div class="sub"></div>';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
let alvo = "";
|
||||
try { alvo = (window.CarinhaApp && window.CarinhaApp.servidor()) || (LOCAL ? "" : location.host); } catch (e) {}
|
||||
el.querySelector(".sub").textContent = "tentando de novo" + (alvo ? " · " + String(alvo).replace(/^https?:\/\//, "") : "") + "…";
|
||||
el.style.color = "rgb(" + CIANO.map(Math.round).join(",") + ")";
|
||||
el.hidden = false;
|
||||
}
|
||||
|
||||
function checarServidor() {
|
||||
if (MODO_PAINEL || par.get("espelho") === "1") return;
|
||||
if (window.carinhaSSE) conexaoOk();
|
||||
const fora = LOCAL || performance.now() - ultimoOk > TOLERANCIA_SEM_SERVIDOR;
|
||||
if (window.carinhaSemServidor) mostrarSemServidor(true); // acompanha a cor guardada
|
||||
if (fora === !!window.carinhaSemServidor) return;
|
||||
window.carinhaSemServidor = fora;
|
||||
console.log(fora ? "sem servidor: dormindo" : "servidor de volta");
|
||||
mostrarSemServidor(fora);
|
||||
if (fora) { atividade(null); r.fila = []; r.kf = null; r.dormindo = true; }
|
||||
else trata("acordar");
|
||||
}
|
||||
// Copia do APK: os ultimos ajustes guardados (boca, sobrancelhas, sono...) valem também aqui.
|
||||
setTimeout(() => { if (LOCAL) { const c = lembrado("config"); if (c) configurarApp(c); } checarServidor(); }, 300);
|
||||
setInterval(checarServidor, 1000);
|
||||
|
||||
</script>
|
||||
<script src="/reacoes-render.js"></script>
|
||||
<script>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package br.carinha.rosto;
|
||||
|
||||
import android.app.admin.DeviceAdminReceiver;
|
||||
|
||||
/**
|
||||
* Administrador do aparelho. Vira "dono" (device owner) uma vez, pelo ADB:
|
||||
* adb shell dpm set-device-owner br.carinha.rosto/.Admin
|
||||
* A partir daí o app trava a tela nele mesmo (ver Quiosque).
|
||||
*/
|
||||
public final class Admin extends DeviceAdminReceiver { }
|
||||
@@ -85,7 +85,11 @@ public class MainActivity extends Activity {
|
||||
private static final String PREFS = "carinha";
|
||||
private static final String CHAVE = "servidor";
|
||||
private static final String CHAVE_TOKEN = "token";
|
||||
private static final String PADRAO = "http://192.168.1.15:8100";
|
||||
private static final String PADRAO = "http://192.168.1.35:8100"; // a Orange Pi
|
||||
private static final long ESPERA_SERVIDOR = 10000; // ms entre tentativas sem servidor
|
||||
/** O que a pagina pode guardar no app (sobrevive sem servidor; a copia do APK le). */
|
||||
private static final java.util.Set<String> LEMBRAVEIS =
|
||||
new java.util.HashSet<String>(Arrays.asList("aparencia", "config"));
|
||||
|
||||
private static final String TAG = "CarinhaMic";
|
||||
private static final String TAG_JS = "CarinhaJS"; // console.log da pagina
|
||||
@@ -247,6 +251,7 @@ public class MainActivity extends Activity {
|
||||
cfg.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||
}
|
||||
|
||||
web.addJavascriptInterface(new PonteApp(), "CarinhaApp");
|
||||
web.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public void onReceivedError(WebView vista, WebResourceRequest pedido,
|
||||
@@ -414,7 +419,8 @@ public class MainActivity extends Activity {
|
||||
private void menu() {
|
||||
final String[] opcoes = {"Ver a camera", "Voltar para a carinha",
|
||||
(movimentoLigado ? "Desligar reacao ao movimento" : "Ligar reacao ao movimento"),
|
||||
"Mudar o servidor", "Sensores acordado / dormindo", "Camera e qualidade do video", "Token do servidor"};
|
||||
"Mudar o servidor", "Sensores acordado / dormindo", "Camera e qualidade do video", "Token do servidor",
|
||||
"Modo quiosque"};
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Carinha")
|
||||
.setItems(opcoes, new DialogInterface.OnClickListener() {
|
||||
@@ -426,12 +432,55 @@ public class MainActivity extends Activity {
|
||||
else if (i == 3) pedirServidor();
|
||||
else if (i == 4) menuSensores();
|
||||
else if (i == 5) menuVideo();
|
||||
else pedirToken();
|
||||
else if (i == 6) pedirToken();
|
||||
else menuQuiosque();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
/** Manutenção: soltar a tela para usar o Android, ou deixar de ser dono do aparelho. */
|
||||
private void menuQuiosque() {
|
||||
if (!Quiosque.dono(this)) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Modo quiosque desligado")
|
||||
.setMessage("Para ligar, pelo PC:\nadb shell dpm set-device-owner br.carinha.rosto/.Admin")
|
||||
.setPositiveButton("OK", null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
final boolean pausado = Quiosque.pausado();
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(pausado ? "Quiosque pausado" : "Quiosque ligado")
|
||||
.setItems(new String[]{
|
||||
pausado ? "Travar de novo" : "Pausar até reiniciar (abre o Android)",
|
||||
"Desligar o modo quiosque de vez"}, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface d, int i) {
|
||||
if (i == 0) {
|
||||
if (pausado) { Quiosque.retomar(MainActivity.this); aviso("quiosque ligado"); }
|
||||
else { Quiosque.pausar(MainActivity.this); aviso("quiosque pausado: Home e barra liberados até reiniciar"); }
|
||||
} else confirmarRemoverQuiosque();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void confirmarRemoverQuiosque() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Desligar o modo quiosque?")
|
||||
.setMessage("A carinha deixa de ser dona do aparelho. Para voltar, será preciso o ADB de novo.")
|
||||
.setPositiveButton("Desligar", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface d, int w) {
|
||||
Quiosque.remover(MainActivity.this);
|
||||
aviso("modo quiosque desligado");
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancelar", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
/** Liga/desliga a reacao ao movimento (guardado nas preferencias). */
|
||||
private void salvarAjuste(String chave, Object valor) {
|
||||
try { JSONObject c = new JSONObject(); c.put(chave, valor); remoto.configurar(c);
|
||||
@@ -500,14 +549,63 @@ public class MainActivity extends Activity {
|
||||
Toast.makeText(this, texto, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
/** Servidor fora do ar: mostra a carinha embutida no APK (sem controle remoto). */
|
||||
/**
|
||||
* Servidor fora do ar: a copia do APK mostra a carinha dormindo, com a ultima aparencia
|
||||
* guardada, e o desenho "placa x celular". Enquanto isso o app tenta o servidor de novo.
|
||||
*/
|
||||
private void cairParaLocal(WebView vista) {
|
||||
if (jaCaiuParaLocal) return;
|
||||
jaCaiuParaLocal = true;
|
||||
usandoLocal = true;
|
||||
naCamera = false;
|
||||
aviso("sem servidor " + servidor + " — usando o rosto local");
|
||||
Log.i(TAG, "sem servidor " + servidor + ": rosto local dormindo, tentando de novo");
|
||||
vista.loadUrl("file:///android_asset/carinha.html");
|
||||
procurarServidor();
|
||||
}
|
||||
|
||||
/** Sem servidor: a cada 10 s pergunta se ele voltou; voltou, abre o rosto dele de novo. */
|
||||
private void procurarServidor() {
|
||||
web.removeCallbacks(tentarServidor);
|
||||
web.postDelayed(tentarServidor, ESPERA_SERVIDOR);
|
||||
}
|
||||
|
||||
private final Runnable tentarServidor = new Runnable() {
|
||||
@Override public void run() {
|
||||
if (!usandoLocal) return;
|
||||
if (!emPrimeiroPlano) { procurarServidor(); return; }
|
||||
final String alvo = servidor;
|
||||
new Thread(new Runnable() {
|
||||
@Override public void run() {
|
||||
final boolean voltou = servidorResponde(alvo);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override public void run() {
|
||||
if (!usandoLocal || !alvo.equals(servidor)) return;
|
||||
if (!voltou) { procurarServidor(); return; }
|
||||
Log.i(TAG, "servidor voltou: " + servidor);
|
||||
usandoLocal = false;
|
||||
jaCaiuParaLocal = false;
|
||||
abrirRosto();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, "procura-servidor").start();
|
||||
}
|
||||
};
|
||||
|
||||
/** Qualquer resposta HTTP (até 401 de token) quer dizer que o servidor está no ar. */
|
||||
private static boolean servidorResponde(String endereco) {
|
||||
java.net.HttpURLConnection c = null;
|
||||
try {
|
||||
c = (java.net.HttpURLConnection) new java.net.URL(endereco + "/config").openConnection();
|
||||
c.setConnectTimeout(2500);
|
||||
c.setReadTimeout(2500);
|
||||
c.setUseCaches(false);
|
||||
return c.getResponseCode() > 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (c != null) c.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void pedirServidor() {
|
||||
@@ -742,12 +840,14 @@ public class MainActivity extends Activity {
|
||||
rtcWeb.onResume();
|
||||
web.evaluateJavascript("window.carinhaAtiva=true", null);
|
||||
telaCheia();
|
||||
Quiosque.aplicar(this);
|
||||
// voltou do segundo plano: atualiza o rosto — mas sem expulsar quem
|
||||
// estava vendo a pagina da camera
|
||||
if (jaRecarregou && !naCamera) {
|
||||
abrirRosto();
|
||||
}
|
||||
jaRecarregou = true;
|
||||
if (usandoLocal) procurarServidor();
|
||||
pedirPermissoes(); // religa o microfone (onPause desligou)
|
||||
if (!naCamera) iniciarMovimento(); // e volta a enxergar o movimento
|
||||
}
|
||||
@@ -1106,6 +1206,20 @@ public class MainActivity extends Activity {
|
||||
});
|
||||
}
|
||||
|
||||
/** window.CarinhaApp: guarda a ultima aparencia/ajustes vindos do servidor para a copia do APK. */
|
||||
private final class PonteApp {
|
||||
@JavascriptInterface public void guardar(String chave, String json) {
|
||||
if (!LEMBRAVEIS.contains(chave) || json == null || json.length() > 50000) return;
|
||||
try { new JSONObject(json); } catch (Exception e) { return; }
|
||||
getSharedPreferences(PREFS, MODE_PRIVATE).edit().putString("ultimo_" + chave, json).apply();
|
||||
}
|
||||
@JavascriptInterface public String lembrado(String chave) {
|
||||
if (!LEMBRAVEIS.contains(chave)) return null;
|
||||
return getSharedPreferences(PREFS, MODE_PRIVATE).getString("ultimo_" + chave, null);
|
||||
}
|
||||
@JavascriptInterface public String servidor() { return servidor; }
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package br.carinha.rosto;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.app.admin.SystemUpdatePolicy;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Modo quiosque: com o app como dono do aparelho, a carinha é a tela inicial,
|
||||
* abre sozinha ao ligar e ninguém sai dela (sem barra de status, sem tela de bloqueio).
|
||||
* Sem ser dono, tudo aqui é ignorado e o app funciona como antes.
|
||||
*/
|
||||
public final class Quiosque {
|
||||
private static final String TAG = "CarinhaQuiosque";
|
||||
/** Pausado pelo menu para manutenção; volta sozinho quando o celular reinicia. */
|
||||
private static boolean pausado;
|
||||
|
||||
private Quiosque() { }
|
||||
|
||||
private static DevicePolicyManager dpm(Context c) {
|
||||
return (DevicePolicyManager) c.getSystemService(Context.DEVICE_POLICY_SERVICE);
|
||||
}
|
||||
|
||||
private static ComponentName admin(Context c) { return new ComponentName(c, Admin.class); }
|
||||
|
||||
public static boolean dono(Context c) {
|
||||
DevicePolicyManager d = dpm(c);
|
||||
return d != null && d.isDeviceOwnerApp(c.getPackageName());
|
||||
}
|
||||
|
||||
public static boolean pausado() { return pausado; }
|
||||
|
||||
/** Chamado no onResume: reaplica as travas (idempotente). */
|
||||
public static void aplicar(Activity a) {
|
||||
if (pausado || !dono(a)) return;
|
||||
DevicePolicyManager d = dpm(a);
|
||||
ComponentName adm = admin(a);
|
||||
String pacote = a.getPackageName();
|
||||
try {
|
||||
d.setLockTaskPackages(adm, new String[]{pacote});
|
||||
// a carinha é a tela inicial: botão Home e o fim do boot caem nela
|
||||
IntentFilter inicio = new IntentFilter(Intent.ACTION_MAIN);
|
||||
inicio.addCategory(Intent.CATEGORY_HOME);
|
||||
inicio.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
d.addPersistentPreferredActivity(adm, inicio, new ComponentName(a, MainActivity.class));
|
||||
// tela ligada enquanto estiver na tomada, USB ou carregador sem fio
|
||||
d.setGlobalSetting(adm, Settings.Global.STAY_ON_WHILE_PLUGGED_IN, String.valueOf(
|
||||
BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS));
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
d.setKeyguardDisabled(adm, true);
|
||||
d.setStatusBarDisabled(adm, true);
|
||||
// atualização do sistema só de madrugada (3h–5h), nunca no meio do dia
|
||||
d.setSystemUpdatePolicy(adm, SystemUpdatePolicy.createWindowedInstallPolicy(180, 300));
|
||||
// câmera e microfone sem perguntar, inclusive depois de reinstalar o app
|
||||
for (String p : new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO})
|
||||
d.setPermissionGrantState(adm, pacote, p, DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
|
||||
}
|
||||
// o botão Ligar continua abrindo o menu de desligar/reiniciar
|
||||
if (Build.VERSION.SDK_INT >= 28)
|
||||
d.setLockTaskFeatures(adm, DevicePolicyManager.LOCK_TASK_FEATURE_GLOBAL_ACTIONS);
|
||||
if (!travado(a)) a.startLockTask();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "aplicar: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Solta a tela até o próximo reinício, para mexer nas configurações do Android. */
|
||||
public static void pausar(Activity a) {
|
||||
if (!dono(a)) return;
|
||||
pausado = true;
|
||||
DevicePolicyManager d = dpm(a);
|
||||
ComponentName adm = admin(a);
|
||||
try {
|
||||
if (travado(a)) a.stopLockTask();
|
||||
d.clearPackagePersistentPreferredActivities(adm, a.getPackageName());
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
d.setKeyguardDisabled(adm, false);
|
||||
d.setStatusBarDisabled(adm, false);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "pausar: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Volta a travar sem precisar reiniciar. */
|
||||
public static void retomar(Activity a) {
|
||||
pausado = false;
|
||||
aplicar(a);
|
||||
}
|
||||
|
||||
/** Deixa de ser dono do aparelho de vez (para desinstalar ou trocar de app). */
|
||||
@SuppressWarnings("deprecation")
|
||||
public static void remover(Activity a) {
|
||||
if (!dono(a)) return;
|
||||
pausar(a);
|
||||
try {
|
||||
dpm(a).clearDeviceOwnerApp(a.getPackageName());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "remover: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean travado(Activity a) {
|
||||
ActivityManager am = (ActivityManager) a.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (am == null) return false;
|
||||
if (Build.VERSION.SDK_INT >= 23) return am.getLockTaskModeState() != ActivityManager.LOCK_TASK_MODE_NONE;
|
||||
return am.isInLockTaskMode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Como dono do aparelho, o quiosque não precisa de políticas de administrador antigas -->
|
||||
<device-admin>
|
||||
<uses-policies />
|
||||
</device-admin>
|
||||
@@ -0,0 +1,37 @@
|
||||
# Desativa no S10 os apps que não servem ao robô (reversível: .\enxugar-s10.ps1 -Reativar)
|
||||
param([switch]$Reativar)
|
||||
$adb = 'C:/Users/JullioPC/Documents/Open/orange-pi-zero3/android/tools/android-sdk/platform-tools/adb.exe'
|
||||
$pacotes = @(
|
||||
# Bixby
|
||||
'com.samsung.android.bixby.agent', 'com.samsung.android.bixby.service', 'com.samsung.android.bixby.wakeup',
|
||||
'com.samsung.systemui.bixby2', 'com.samsung.android.app.settings.bixby', 'com.samsung.android.visionintelligence',
|
||||
'com.samsung.android.app.spage', 'com.samsung.android.app.routines',
|
||||
# enfeites da Samsung
|
||||
'com.samsung.android.app.tips', 'com.samsung.android.aremoji', 'com.samsung.android.ardrawing', 'com.samsung.android.arzone',
|
||||
'com.samsung.android.livestickers', 'com.samsung.android.stickercenter', 'com.samsung.android.app.camera.sticker.facearavatar.preload',
|
||||
'com.samsung.android.app.dressroom', 'com.samsung.android.app.ledbackcover', 'com.samsung.android.app.watchmanagerstub',
|
||||
'com.samsung.android.themestore', 'com.samsung.android.app.aodservice', 'com.samsung.android.game.gamehome',
|
||||
'com.samsung.android.game.gametools', 'com.samsung.android.app.reminder', 'com.samsung.android.forest',
|
||||
'com.sec.android.app.vepreload', 'com.samsung.android.mateagent', 'com.samsung.android.beaconmanager',
|
||||
'com.samsung.android.kidsinstaller',
|
||||
# contas e loja Samsung
|
||||
'com.sec.android.app.samsungapps', 'com.sec.android.app.billing', 'com.samsung.android.scloud',
|
||||
'com.samsung.android.samsungpass', 'com.samsung.android.samsungpassautofill', 'com.samsung.android.spayfw',
|
||||
'com.samsung.android.smartswitchassistant', 'com.sec.android.easyMover.Agent', 'com.samsung.android.easysetup',
|
||||
'com.samsung.android.app.sharelive', 'com.sec.android.app.desktoplauncher', 'com.sec.android.desktopmode.uiservice',
|
||||
'com.samsung.android.mdx', 'com.samsung.android.mdx.kit', 'com.samsung.android.mdx.quickboard',
|
||||
# apps de uso pessoal
|
||||
'com.samsung.android.messaging', 'com.samsung.android.calendar', 'com.google.android.gm', 'com.google.android.youtube',
|
||||
'com.google.android.apps.maps', 'com.google.android.apps.tachyon', 'com.google.android.googlequicksearchbox',
|
||||
# pré-instalados
|
||||
'com.facebook.katana', 'com.facebook.services', 'com.facebook.system', 'com.facebook.appmanager',
|
||||
'com.microsoft.skydrive', 'com.microsoft.appmanager', 'br.com.bemobi.appsclub.vivo'
|
||||
)
|
||||
$acao = if ($Reativar) { 'enable' } else { 'disable-user --user 0' }
|
||||
$ok = 0; $falhas = @()
|
||||
foreach ($p in $pacotes) {
|
||||
$r = & $adb -d shell "pm $acao $p" 2>&1 | Out-String
|
||||
if ($r -match 'new state') { $ok++ } else { $falhas += "$p -> $($r.Trim())" }
|
||||
}
|
||||
"$ok de $($pacotes.Count) " + $(if ($Reativar) { 'reativados' } else { 'desativados' })
|
||||
$falhas
|
||||
+156
-11
@@ -20,6 +20,52 @@
|
||||
@keyframes aparecerFigura{from{opacity:0}to{opacity:1}}
|
||||
@media(prefers-reduced-motion:reduce){.iconeOlho,.figurinhaDinamica{animation:none}}
|
||||
#fps{position:fixed;left:4px;top:2px;color:#2b3a4a;font:12px monospace;display:none}
|
||||
/* sem a placa: placa ✕ celular na cor dos olhos. Deitado, o rosto dormindo fica onde está e o
|
||||
desenho fica em pé no canto direito; em pé, o rosto sobe e o desenho vai embaixo. */
|
||||
#semServidor{position:fixed;right:2.5vw;top:50%;transform:translateY(-50%);width:min(24vw,42vh);
|
||||
z-index:5;pointer-events:none;text-align:center;opacity:.82;font-family:system-ui,sans-serif;
|
||||
animation:semServidorEntra 1.2s ease-out}
|
||||
#semServidor svg{display:block;margin:0 auto;overflow:visible;filter:drop-shadow(0 0 3px currentColor)}
|
||||
#semServidor svg.horizontal{display:none}
|
||||
#semServidor svg.vertical{height:58vh;width:auto;max-width:100%}
|
||||
#semServidor svg.vertical .olhinhos{animation-name:semServidorOlharCima}
|
||||
@media (orientation:portrait){
|
||||
body.semServidor #tela,body.semServidor #rostoMotor{transform:translateY(-20%) scale(.8)}
|
||||
#semServidor{right:auto;top:auto;left:50%;bottom:3.5vh;transform:translateX(-50%);width:min(80vw,400px)}
|
||||
#semServidor svg.horizontal{display:block;width:100%;height:auto}
|
||||
#semServidor svg.vertical{display:none}
|
||||
}
|
||||
#semServidor .titulo{text-wrap:balance;font-weight:600;font-size:clamp(12px,3.2vh,20px);margin-top:.4em;letter-spacing:.02em}
|
||||
#semServidor .sub{font-size:clamp(10px,2.4vh,15px);opacity:.6;margin-top:.25em;font-variant-numeric:tabular-nums}
|
||||
#semServidor .placa rect,#semServidor .celular>rect{fill:none;stroke:currentColor;stroke-width:3}
|
||||
#semServidor .chip{fill:currentColor;fill-opacity:.25}
|
||||
#semServidor .pino{fill:currentColor;opacity:.7}
|
||||
#semServidor .led{fill:#ff5a5a;animation:semServidorLed 1.6s steps(1) infinite}
|
||||
#semServidor .cabo{stroke:currentColor;stroke-width:3;stroke-dasharray:5 6;opacity:.5;fill:none}
|
||||
#semServidor .arco{fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:round;opacity:0;
|
||||
animation:semServidorArco 2.4s ease-out infinite}
|
||||
#semServidor .a2{animation-delay:.3s}
|
||||
#semServidor .a3{animation-delay:.6s}
|
||||
#semServidor .xis path{stroke:#ff5a5a;stroke-width:5;stroke-linecap:round;fill:none}
|
||||
#semServidor .xis{transform-box:fill-box;transform-origin:center;animation:semServidorXis 2.4s ease-in-out infinite}
|
||||
#semServidor .olhinhos{fill:currentColor;transform-box:fill-box;transform-origin:center;
|
||||
animation:semServidorOlhar 5s ease-in-out infinite}
|
||||
#semServidor .boquinha{fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round}
|
||||
#semServidor .celular{transform-box:fill-box;transform-origin:50% 100%;animation:semServidorProcura 4.8s ease-in-out infinite}
|
||||
#semServidor.cutucado .celular{animation:semServidorChacoalha .5s ease-in-out}
|
||||
@keyframes semServidorEntra{from{opacity:0}to{opacity:.82}}
|
||||
/* em pé no canto direito: o celularzinho olha para cima, para a placa */
|
||||
@keyframes semServidorOlharCima{0%,30%{transform:translateY(-3px) scaleY(1)}34%{transform:translateY(-3px) scaleY(.1)}38%,60%{transform:translateY(-3px) scaleY(1)}
|
||||
70%,90%{transform:translateY(1px) scaleY(1)}100%{transform:translateY(-3px)}}
|
||||
@keyframes semServidorArco{0%{opacity:0}25%{opacity:.9}70%,100%{opacity:0}}
|
||||
@keyframes semServidorXis{0%,55%,100%{transform:scale(1);opacity:.75}65%{transform:scale(1.25);opacity:1}80%{transform:scale(1);opacity:.85}}
|
||||
@keyframes semServidorLed{0%{opacity:1}50%{opacity:.15}}
|
||||
/* o celularzinho olha para a placa, olha de volta e pisca */
|
||||
@keyframes semServidorOlhar{0%,30%{transform:translateX(-3px) scaleY(1)}34%{transform:translateX(-3px) scaleY(.1)}38%,60%{transform:translateX(-3px) scaleY(1)}
|
||||
70%,90%{transform:translateX(1px) scaleY(1)}100%{transform:translateX(-3px)}}
|
||||
@keyframes semServidorProcura{0%,100%{transform:rotate(0)}20%{transform:rotate(-5deg)}40%{transform:rotate(0)}}
|
||||
@keyframes semServidorChacoalha{0%,100%{transform:rotate(0)}25%{transform:rotate(-9deg)}50%{transform:rotate(7deg)}75%{transform:rotate(-4deg)}}
|
||||
@media(prefers-reduced-motion:reduce){#semServidor *,#semServidor{animation:none!important}#semServidor .arco{opacity:.6}}
|
||||
</style>
|
||||
<body>
|
||||
<svg id="tela" viewBox="0 0 480 320" preserveAspectRatio="xMidYMid meet">
|
||||
@@ -106,7 +152,7 @@ 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;
|
||||
const apagar = SONO.apagar && r.dormindo && !window.carinhaSemServidor && SONO.desde && agora - SONO.desde > SONO.minutos * 60000;
|
||||
if (apagar !== SONO.apagada) {
|
||||
SONO.apagada = apagar;
|
||||
window.carinhaTelaApagada = apagar;
|
||||
@@ -725,6 +771,11 @@ function trata(ordem) {
|
||||
r.dormindo = true;
|
||||
atividade(null);
|
||||
} else if (ordem === "acordar") {
|
||||
if (window.carinhaSemServidor) { // sem a placa continua dormindo; o aviso chacoalha
|
||||
const aviso = document.getElementById("semServidor");
|
||||
if (aviso) { aviso.classList.remove("cutucado"); void aviso.offsetWidth; aviso.classList.add("cutucado"); }
|
||||
return;
|
||||
}
|
||||
r.dormindo = false; animar("surpreso");
|
||||
} else if (ordem === "aleatorio") {
|
||||
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
|
||||
@@ -777,27 +828,44 @@ async function buscarOrdens() {
|
||||
try {
|
||||
const resp = await fetch("fila"+(par.get("aparelho")==="1"?"?aparelho=1":""), { cache: "no-store" });
|
||||
if (!resp.ok) return;
|
||||
conexaoOk();
|
||||
const lista = await resp.json();
|
||||
for (const nome of lista) trata(nome);
|
||||
} catch (e) { /* sem servidor (file://) — ignora */ }
|
||||
}
|
||||
setInterval(buscarOrdens, 500);
|
||||
|
||||
// O app (window.CarinhaApp) guarda a ultima aparencia e os ajustes vindos do servidor:
|
||||
// a copia do APK (file://) nao enxerga o armazenamento da pagina do servidor.
|
||||
const LOCAL = location.protocol === "file:";
|
||||
function lembrar(chave, valor) {
|
||||
try { if (window.CarinhaApp && !LOCAL) window.CarinhaApp.guardar(chave, JSON.stringify(valor)); } catch (e) {}
|
||||
}
|
||||
function lembrado(chave) {
|
||||
try { const s = window.CarinhaApp && window.CarinhaApp.lembrado(chave); return s ? JSON.parse(s) : null; } catch (e) { return null; }
|
||||
}
|
||||
|
||||
// A aparencia guardada no servidor (rosto + cor) e aplicada ao carregar: o
|
||||
// painel de controle personaliza e a carinha ja abre do jeito escolhido. Sem
|
||||
// servidor (file://) isso falha calado e vale o padrao.
|
||||
// servidor vale a ultima que o app guardou (ou o padrao).
|
||||
async function aplicarAparencia() {
|
||||
try {
|
||||
const resp = await fetch("config", { cache: "no-store" });
|
||||
if (!resp.ok) return;
|
||||
const c = await resp.json();
|
||||
if (c.rosto && ROSTOS[c.rosto]) trocarRosto(c.rosto);
|
||||
if (FORMATOS.includes(c.formato)) FORMATO = c.formato;
|
||||
if (Array.isArray(c.cor) && c.cor.length === 3 && c.cor.every((n) => !isNaN(n)))
|
||||
{ CIANO.splice(0, 3, ...c.cor.map(Number)); r.cor = c.cor.map(Number); }
|
||||
} catch (e) { /* sem servidor — segue com o padrao */ }
|
||||
let c = null;
|
||||
if (!LOCAL) {
|
||||
try {
|
||||
const resp = await fetch("config", { cache: "no-store" });
|
||||
if (resp.ok) c = await resp.json();
|
||||
} catch (e) { /* sem servidor */ }
|
||||
}
|
||||
if (!c) c = lembrado("aparencia");
|
||||
if (!c) return;
|
||||
if (c.rosto && ROSTOS[c.rosto]) trocarRosto(c.rosto);
|
||||
if (FORMATOS.includes(c.formato)) FORMATO = c.formato;
|
||||
if (Array.isArray(c.cor) && c.cor.length === 3 && c.cor.every((n) => !isNaN(n)))
|
||||
{ CIANO.splice(0, 3, ...c.cor.map(Number)); r.cor = c.cor.map(Number); }
|
||||
}
|
||||
setTimeout(aplicarAparencia, 200);
|
||||
// O painel troca rosto/cor por ordens: guarda a aparencia atual de tempos em tempos.
|
||||
setInterval(() => { if (!window.carinhaSemServidor) lembrar("aparencia", { rosto: ROSTO_NOME, formato: FORMATO, cor: CIANO.slice() }); }, 15000);
|
||||
// ?painel=1: prévia dentro do painel novo. Nada de polling; o painel repassa os eventos
|
||||
// do servidor (SSE) por postMessage.
|
||||
const MODO_PAINEL = new URLSearchParams(location.search).get('painel') === '1';
|
||||
@@ -1238,6 +1306,7 @@ function configurarApp(c) {
|
||||
MOV.cooldown = c.movcooldown; MOV.olharMax = c.movolhar;
|
||||
if (!c.reagir) { MOV.olharX = MOV.olharY = 0; MOV.ultimoMov = 0; }
|
||||
configRecebida = c;
|
||||
lembrar('config', c);
|
||||
if(!c.seguirRastreamento || !c.sensorCamera || !c.reagir) TRACK.alvo=null;
|
||||
}
|
||||
let sincronizando = false;
|
||||
@@ -1363,6 +1432,7 @@ function laco(t) {
|
||||
if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null);
|
||||
document.body.classList.toggle("atividade", !!ATIV && !ATIV.saindo);
|
||||
|
||||
if (window.carinhaSemServidor && !r.dormindo) { r.fila = []; r.kf = null; r.dormindo = true; } // sem a placa: só dorme
|
||||
const ocupado = congelado ? true : passo(dt);
|
||||
if (!ocupado) {
|
||||
descanso += dt;
|
||||
@@ -1434,6 +1504,81 @@ setTimeout(relatar, 1500);
|
||||
setInterval(poll,1000);poll();
|
||||
})();
|
||||
|
||||
/* ---------------------------------------------------------------- sem servidor
|
||||
Sem a placa (copia do APK, ou o servidor caiu com a pagina aberta) a carinha dorme com a
|
||||
ultima aparencia e ajustes guardados e mostra placa ✕ celular. Volta sozinha quando a placa
|
||||
responde (o app recarrega a pagina do servidor; aberta pelo servidor, o SSE reconecta). */
|
||||
let ultimoOk = performance.now();
|
||||
const TOLERANCIA_SEM_SERVIDOR = 15000; // ms sem resposta antes de dormir
|
||||
function conexaoOk() { ultimoOk = performance.now(); }
|
||||
|
||||
function mostrarSemServidor(ligar) {
|
||||
let el = document.getElementById("semServidor");
|
||||
document.body.classList.toggle("semServidor", ligar);
|
||||
if (!ligar) { if (el) el.hidden = true; return; }
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = "semServidor";
|
||||
// placa e celular pelo canto de cima à esquerda; o desenho vertical vai no canto direito com o celular deitado
|
||||
const placa = (px, py) =>
|
||||
'<g class="placa">' +
|
||||
'<rect x="' + px + '" y="' + py + '" width="84" height="56" rx="6"/>' +
|
||||
'<rect class="chip" x="' + (px + 29) + '" y="' + (py + 15) + '" width="26" height="26" rx="3"/>' +
|
||||
[0,1,2,3,4,5,6,7].map((i) => '<circle class="pino" cx="' + (px + 11 + i * 9) + '" cy="' + (py + 7) + '" r="2"/>').join("") +
|
||||
'<circle class="pino" cx="' + (px + 7) + '" cy="' + (py + 50) + '" r="2.4"/><circle class="pino" cx="' + (px + 77) + '" cy="' + (py + 50) + '" r="2.4"/>' +
|
||||
'<circle class="led" cx="' + (px + 72) + '" cy="' + (py + 22) + '" r="2.6"/>' +
|
||||
'</g>';
|
||||
const celular = (cx, cy) =>
|
||||
'<g class="celular">' +
|
||||
'<rect x="' + cx + '" y="' + cy + '" width="40" height="76" rx="8"/>' +
|
||||
'<g class="olhinhos"><ellipse cx="' + (cx + 13) + '" cy="' + (cy + 34) + '" rx="3.2" ry="4.2"/><ellipse cx="' + (cx + 27) + '" cy="' + (cy + 34) + '" rx="3.2" ry="4.2"/></g>' +
|
||||
'<path class="boquinha" d="M' + (cx + 14) + ' ' + (cy + 48) + ' Q' + (cx + 20) + ' ' + (cy + 44) + ' ' + (cx + 26) + ' ' + (cy + 48) + '"/>' +
|
||||
'</g>';
|
||||
const xis = (x, y) => '<g class="xis"><path d="M' + (x - 10) + ' ' + (y - 10) + ' L' + (x + 10) + ' ' + (y + 10) + ' M' + (x + 10) + ' ' + (y - 10) + ' L' + (x - 10) + ' ' + (y + 10) + '"/></g>';
|
||||
el.innerHTML =
|
||||
'<svg class="horizontal" viewBox="0 0 300 104" aria-hidden="true">' + placa(18, 30) +
|
||||
'<path class="cabo" d="M104 58 H146"/>' +
|
||||
'<g class="arcos">' +
|
||||
'<path class="arco a1" d="M212 50 A10 10 0 0 0 212 66"/>' +
|
||||
'<path class="arco a2" d="M203 44 A18 18 0 0 0 203 72"/>' +
|
||||
'<path class="arco a3" d="M194 38 A26 26 0 0 0 194 78"/>' +
|
||||
'</g>' + xis(160, 58) + celular(220, 18) +
|
||||
'</svg>' +
|
||||
'<svg class="vertical" viewBox="0 0 120 262" aria-hidden="true">' + placa(18, 8) +
|
||||
'<path class="cabo" d="M60 66 V108"/>' +
|
||||
'<g class="arcos">' +
|
||||
'<path class="arco a1" d="M52 172 A10 10 0 0 1 68 172"/>' +
|
||||
'<path class="arco a2" d="M46 163 A18 18 0 0 1 74 163"/>' +
|
||||
'<path class="arco a3" d="M40 154 A26 26 0 0 1 80 154"/>' +
|
||||
'</g>' + xis(60, 122) + celular(40, 182) +
|
||||
'</svg>' +
|
||||
'<div class="titulo">sem conexão com a placa</div>' +
|
||||
'<div class="sub"></div>';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
let alvo = "";
|
||||
try { alvo = (window.CarinhaApp && window.CarinhaApp.servidor()) || (LOCAL ? "" : location.host); } catch (e) {}
|
||||
el.querySelector(".sub").textContent = "tentando de novo" + (alvo ? " · " + String(alvo).replace(/^https?:\/\//, "") : "") + "…";
|
||||
el.style.color = "rgb(" + CIANO.map(Math.round).join(",") + ")";
|
||||
el.hidden = false;
|
||||
}
|
||||
|
||||
function checarServidor() {
|
||||
if (MODO_PAINEL || par.get("espelho") === "1") return;
|
||||
if (window.carinhaSSE) conexaoOk();
|
||||
const fora = LOCAL || performance.now() - ultimoOk > TOLERANCIA_SEM_SERVIDOR;
|
||||
if (window.carinhaSemServidor) mostrarSemServidor(true); // acompanha a cor guardada
|
||||
if (fora === !!window.carinhaSemServidor) return;
|
||||
window.carinhaSemServidor = fora;
|
||||
console.log(fora ? "sem servidor: dormindo" : "servidor de volta");
|
||||
mostrarSemServidor(fora);
|
||||
if (fora) { atividade(null); r.fila = []; r.kf = null; r.dormindo = true; }
|
||||
else trata("acordar");
|
||||
}
|
||||
// Copia do APK: os ultimos ajustes guardados (boca, sobrancelhas, sono...) valem também aqui.
|
||||
setTimeout(() => { if (LOCAL) { const c = lembrado("config"); if (c) configurarApp(c); } checarServidor(); }, 300);
|
||||
setInterval(checarServidor, 1000);
|
||||
|
||||
</script>
|
||||
<script src="/reacoes-render.js"></script>
|
||||
<script>
|
||||
|
||||
@@ -98,6 +98,16 @@ class Wifi:
|
||||
link = sh('iw', 'dev', IFACE, 'link').stdout
|
||||
return 'Connected to' in link
|
||||
|
||||
def gateway_wlan(self):
|
||||
m = re.search(r'default via (\S+)', sh('ip', '-4', 'route', 'show', 'dev', IFACE).stdout)
|
||||
return m.group(1) if m else (GATEWAY if (self.rede() or {}).get('ip_fixo', True) else '')
|
||||
|
||||
def chega_na_rede(self):
|
||||
"""Associar não basta: o roteador tem que responder PELO Wi-Fi. Uma rede que não é a da casa
|
||||
(outro roteador, rede de visitante) associa normalmente e deixaria a placa inacessível."""
|
||||
gw = self.gateway_wlan()
|
||||
return bool(gw) and sh('ping', '-c', '1', '-W', '2', '-I', IFACE, gw, timeout=6).returncode == 0
|
||||
|
||||
def ip_wlan(self):
|
||||
m = re.search(r'inet (\d+\.\d+\.\d+\.\d+)', sh('ip', '-4', 'addr', 'show', IFACE).stdout)
|
||||
return m.group(1) if m else ''
|
||||
@@ -161,9 +171,11 @@ class Wifi:
|
||||
self.filhos['wpa'] = subprocess.Popen(['wpa_supplicant', '-i', IFACE, '-D', 'nl80211', '-c', str(RUN / 'wpa.conf')],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
if rede.get('ip_fixo', True):
|
||||
conteudo = f'[Match]\nName={IFACE}\n\n[Network]\nAddress={IP_FIXO}/24\nGateway={GATEWAY}\nDNS={DNS}\n'
|
||||
# rotas do Wi-Fi com métrica maior: com o cabo ligado, o cabo sempre ganha
|
||||
conteudo = (f'[Match]\nName={IFACE}\n\n[Network]\nDNS={DNS}\n\n[Address]\nAddress={IP_FIXO}/24\nRouteMetric=600\n'
|
||||
f'\n[Route]\nGateway={GATEWAY}\nMetric=600\n')
|
||||
else:
|
||||
conteudo = f'[Match]\nName={IFACE}\n\n[Network]\nDHCP=ipv4\n\n[DHCPv4]\nRouteMetric=100\n'
|
||||
conteudo = f'[Match]\nName={IFACE}\n\n[Network]\nDHCP=ipv4\n\n[DHCPv4]\nRouteMetric=600\n'
|
||||
gravar(NETWORK, conteudo, 0o644) # o systemd-networkd põe o endereço quando conectar
|
||||
sh('networkctl', 'reload')
|
||||
sh('networkctl', 'reconfigure', IFACE)
|
||||
@@ -342,7 +354,7 @@ button{{padding:14px;border:0;border-radius:10px;background:var(--azul);color:#0
|
||||
return
|
||||
agora = time.time()
|
||||
if self.modo == 'cliente':
|
||||
if self.conectado() and self.ip_wlan():
|
||||
if self.conectado() and self.ip_wlan() and self.chega_na_rede():
|
||||
if not self.ok_em:
|
||||
log('conectado em', self.rede()['ssid'], 'com', self.ip_wlan())
|
||||
if self.rede().get('ip_fixo', True):
|
||||
@@ -355,8 +367,12 @@ button{{padding:14px;border:0;border-radius:10px;background:var(--azul);color:#0
|
||||
limite = QUEDA_TOLERADA if self.ok_em else ESPERA_CONECTAR
|
||||
if agora - (self.ok_em or self.desde) > limite:
|
||||
if not self.ok_em:
|
||||
self.erro = (f'Não consegui conectar em “{self.rede()["ssid"]}”. Confira a senha e se a rede está '
|
||||
'ligada, e tente de novo.')
|
||||
if self.conectado():
|
||||
self.erro = (f'Conectei em “{self.rede()["ssid"]}”, mas por ela não chego ao roteador da casa '
|
||||
f'({self.gateway_wlan() or GATEWAY}). Escolha a rede onde ficam o PC e o celular do Carinha.')
|
||||
else:
|
||||
self.erro = (f'Não consegui conectar em “{self.rede()["ssid"]}”. Confira a senha e se a rede está '
|
||||
'ligada, e tente de novo.')
|
||||
if self.anterior is not False: # a rede nova falhou: a antiga continua salva
|
||||
if self.anterior:
|
||||
gravar(REDE, self.anterior)
|
||||
|
||||
@@ -11,7 +11,8 @@ install -d -m 700 /etc/carinha-wifi
|
||||
install -m 755 carinha_wifi.py /usr/local/sbin/carinha-wifi
|
||||
install -m 644 carinha-wifi.service /etc/systemd/system/carinha-wifi.service
|
||||
# Cabo e Wi-Fi na mesma rede: cada placa de rede só responde pelo próprio endereço (sem "ARP flux")
|
||||
printf 'net.ipv4.conf.all.arp_ignore=1\nnet.ipv4.conf.all.arp_announce=2\n' > /etc/sysctl.d/90-carinha-arp.conf
|
||||
# e cabo desconectado não segura rota: o tráfego passa direto para o Wi-Fi
|
||||
printf 'net.ipv4.conf.all.arp_ignore=1\nnet.ipv4.conf.all.arp_announce=2\nnet.ipv4.conf.all.ignore_routes_with_linkdown=1\nnet.ipv4.conf.default.ignore_routes_with_linkdown=1\n' > /etc/sysctl.d/90-carinha-arp.conf
|
||||
sysctl -q --system
|
||||
systemctl daemon-reload
|
||||
systemctl enable carinha-wifi
|
||||
|
||||
Reference in New Issue
Block a user