feat(celular): rosto e app usam SSE e token; remove telas legadas
- 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>
This commit is contained in:
+5
-66
@@ -1,10 +1,7 @@
|
||||
"""Configuração persistente, telemetria e prévia da câmera do celular."""
|
||||
import ipaddress
|
||||
"""Configuração persistente e telemetria do celular; sinalização do vídeo WebRTC."""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from comum import origem_valida
|
||||
@@ -33,8 +30,6 @@ LIMITES = dict(volume=(0, 1), somlim=(0, 1), somganho=(0, 1),
|
||||
somaltolim=(0, 1), sompausa=(1, 3600), somcooldown=(0, 120),
|
||||
movlim=(0, 1), movforte=(0, 1), movcooldown=(0, 120), movolhar=(0, 100),
|
||||
brilho=(0.01, 1), brilhoRepouso=(0.01, 1), repousoTela=(5, 3600))
|
||||
TOOLS = Path(os.environ.get('CARINHA_TOOLS',
|
||||
'C:/Users/JullioPC/Documents/Open/orange-pi-zero3/android/tools'))
|
||||
|
||||
|
||||
def validar(dados):
|
||||
@@ -65,9 +60,6 @@ class ControlePC:
|
||||
self.status_at = 0
|
||||
self.celular_ip = None
|
||||
self.recarga = {"id": None, "confirmada": False}
|
||||
self.frame = b''
|
||||
self.frame_at = 0
|
||||
self.frame_seq = 0
|
||||
self.rtc = Sinalizacao()
|
||||
self.publicar = lambda tipo, dados: None
|
||||
try:
|
||||
@@ -85,8 +77,6 @@ class ControlePC:
|
||||
self.config = novo
|
||||
if not novo['preview']:
|
||||
self.rtc.encerrar()
|
||||
self.frame = b''
|
||||
self.frame_at = 0
|
||||
return novo.copy()
|
||||
|
||||
def resumo(self, host=''):
|
||||
@@ -96,15 +86,13 @@ class ControlePC:
|
||||
idade = agora - self.status_at
|
||||
return dict(online=idade < 5, idade=round(idade, 1) if self.status_at else None,
|
||||
celular_ip=self.celular_ip, servidor_ip=host, recarga=self.recarga.copy(),
|
||||
estado=self.estado.copy(), preview=self.config['preview'],
|
||||
camera_recente=bool(self.frame) and agora - self.frame_at < 3 and self.config['preview'],
|
||||
frame_seq=self.frame_seq)
|
||||
estado=self.estado.copy(), preview=self.config['preview'])
|
||||
|
||||
def rotas(self, h, metodo):
|
||||
if self.rtc.atender(h, metodo, self.config['preview']):
|
||||
return True
|
||||
rota = h.path.split('?', 1)[0].rstrip('/')
|
||||
if rota not in ('/app/config', '/app/padrao', '/app/status', '/camera/frame', '/app/connect', '/app/health'):
|
||||
if rota not in ('/app/config', '/app/padrao', '/app/status', '/app/health'):
|
||||
return False
|
||||
|
||||
def enviar(dados, codigo=200):
|
||||
@@ -117,14 +105,13 @@ class ControlePC:
|
||||
if rota == '/app/config' and h.headers.get('Content-Type', '').split(';')[0] != 'application/json':
|
||||
raise ValueError('Use JSON')
|
||||
tamanho = int(h.headers.get('Content-Length', '0'))
|
||||
limite = 1_000_000 if rota == '/camera/frame' else 16384
|
||||
if tamanho <= 0 or tamanho > limite:
|
||||
if tamanho <= 0 or tamanho > 16384:
|
||||
enviar({'erro': 'Corpo vazio ou grande demais'}, 413)
|
||||
return True
|
||||
corpo = h.rfile.read(tamanho)
|
||||
if len(corpo) != tamanho:
|
||||
raise ValueError('Corpo incompleto')
|
||||
dados = None if rota == '/camera/frame' else json.loads(corpo)
|
||||
dados = json.loads(corpo)
|
||||
else:
|
||||
dados = None
|
||||
|
||||
@@ -155,56 +142,8 @@ class ControlePC:
|
||||
enviar({'ok': True})
|
||||
else:
|
||||
enviar(self.resumo(h.headers.get("Host", "").split(":")[0]))
|
||||
elif rota == '/camera/frame':
|
||||
with self.lock:
|
||||
if metodo == 'POST':
|
||||
if not self.config['preview']:
|
||||
enviar({'erro': 'Transmissão desligada'}, 409)
|
||||
elif h.headers.get('Content-Type', '').split(';')[0] != 'image/jpeg' or not corpo.startswith(b'\xff\xd8') or not corpo.endswith(b'\xff\xd9'):
|
||||
enviar({'erro': 'Esperava uma imagem JPEG'}, 400)
|
||||
else:
|
||||
self.frame = corpo
|
||||
self.frame_at = time.monotonic()
|
||||
self.frame_seq += 1
|
||||
enviar({'ok': True})
|
||||
else:
|
||||
recente = self.config['preview'] and self.frame and time.monotonic() - self.frame_at < 3
|
||||
h.enviar(self.frame if recente else b'', 'image/jpeg', 200 if recente else 204)
|
||||
elif rota == '/app/connect':
|
||||
if metodo != 'POST':
|
||||
enviar({'erro': 'Use POST'}, 405)
|
||||
elif h.client_address[0] not in ('127.0.0.1', '::1'):
|
||||
enviar({'erro': 'Abra o painel em localhost no PC para conectar'}, 403)
|
||||
else:
|
||||
self.conectar(h, dados, enviar)
|
||||
except (ValueError, TypeError) as e:
|
||||
enviar({'erro': str(e)}, 400)
|
||||
except OSError as e:
|
||||
enviar({'erro': str(e)}, 503)
|
||||
return True
|
||||
|
||||
def conectar(self, h, dados, enviar):
|
||||
ip = str(ipaddress.IPv4Address(dados.get('ip', '192.168.1.16')))
|
||||
porta = dados.get('porta', 5555)
|
||||
if isinstance(porta, bool) or not isinstance(porta, int) or not 1 <= porta <= 65535:
|
||||
raise ValueError('Porta ADB inválida')
|
||||
pc = str(ipaddress.IPv4Address(dados.get('pc', '192.168.1.15')))
|
||||
adb = str(TOOLS / 'android-sdk/platform-tools/adb.exe')
|
||||
serial = '%s:%s' % (ip, porta)
|
||||
url = 'http://%s:%s' % (pc, h.server.server_port)
|
||||
comandos = [[adb, 'connect', serial], [adb, '-s', serial, 'shell', 'input', 'keyevent', 'KEYCODE_WAKEUP'],
|
||||
[adb, '-s', serial, 'shell', 'wm', 'dismiss-keyguard'],
|
||||
[adb, '-s', serial, 'shell', 'am', 'start', '-n', 'br.carinha.rosto/.MainActivity', '-e', 'servidor', url]]
|
||||
saidas = []
|
||||
for cmd in comandos:
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=15,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
|
||||
except subprocess.TimeoutExpired:
|
||||
enviar({'erro': 'ADB não respondeu. Confira a depuração Wi-Fi no celular.'}, 504)
|
||||
return
|
||||
saidas.append((r.stdout + r.stderr).strip())
|
||||
if r.returncode or 'cannot connect' in r.stdout.lower() or 'failed' in r.stdout.lower() or 'error:' in r.stdout.lower():
|
||||
enviar({'erro': '\n'.join(saidas)}, 502)
|
||||
return
|
||||
enviar({'ok': True, 'servidor': url, 'mensagem': 'App aberto. Aguarde a confirmação de conexão no painel.'})
|
||||
|
||||
Reference in New Issue
Block a user