feat(servidor): adiciona SSE, token, E-Stop real e trava de hardware
- comum.py: ler_json (Origin/JSON/tamanho), gravar_atomico e token (CARINHA_TOKEN ou seguranca.json), substituindo 6 copias nos modulos - eventos.py: hub SSE em /eventos com heartbeat e Last-Event-ID; modulos publicam expressao, telemetria, conversa, reacao, ia, motores - /estado agrega o snapshot inicial; /comando unifica gesto/rosto/cor/reacao - /parar-tudo desliga o PCA9685, para reacoes e trava motores ate /rearmar - braco: lease (ttl_ms) e interpolacao com limite de velocidade no servidor - braco e diagnostico nao disputam o PCA9685 (409 para o segundo dono) - fila limitada (20) com validade de 10 s; POST /face usa prefixo "rosto" - /app/config passa a checar Origin e JSON; versao vem de version.json - figurinhas automaticas do Gemini limitadas a 6 por hora - rosto entende a ordem "calma" enviada pelo E-Stop Token so e exigido quando configurado (docs/SEGURANCA.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+30
-11
@@ -7,8 +7,16 @@ from pathlib import Path
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from comum import origem_valida
|
||||
from sinalizacao import Sinalizacao
|
||||
|
||||
|
||||
def versao():
|
||||
try:
|
||||
return json.loads((Path(__file__).with_name('version.json')).read_text(encoding='utf-8'))['versao']
|
||||
except (OSError, ValueError, KeyError):
|
||||
return 'desconhecida'
|
||||
|
||||
PADRAO = dict(preview=False, auto=True, audio=True, volume=0.35,
|
||||
ouvir=True, reagir=True, somlim=0.35, somganho=0.4,
|
||||
somaltolim=0.55, sompausa=20, somcooldown=5,
|
||||
@@ -61,6 +69,7 @@ class ControlePC:
|
||||
self.frame_at = 0
|
||||
self.frame_seq = 0
|
||||
self.rtc = Sinalizacao()
|
||||
self.publicar = lambda tipo, dados: None
|
||||
try:
|
||||
self.config.update(validar(json.loads(self.arquivo.read_text(encoding='utf-8'))))
|
||||
except (OSError, ValueError):
|
||||
@@ -80,6 +89,17 @@ class ControlePC:
|
||||
self.frame_at = 0
|
||||
return novo.copy()
|
||||
|
||||
def resumo(self, host=''):
|
||||
"""Estado do celular para o painel (GET /app/status, /estado e evento 'telemetria')."""
|
||||
with self.lock:
|
||||
agora = time.monotonic()
|
||||
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)
|
||||
|
||||
def rotas(self, h, metodo):
|
||||
if self.rtc.atender(h, metodo, self.config['preview']):
|
||||
return True
|
||||
@@ -92,6 +112,10 @@ class ControlePC:
|
||||
|
||||
try:
|
||||
if metodo == 'POST':
|
||||
if not origem_valida(h):
|
||||
raise ValueError('Origem inválida')
|
||||
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:
|
||||
@@ -105,10 +129,12 @@ class ControlePC:
|
||||
dados = None
|
||||
|
||||
if rota == '/app/health':
|
||||
enviar({'projeto': 'carinha-pc', 'versao': '0.7.0'})
|
||||
enviar({'projeto': 'carinha-pc', 'versao': versao()})
|
||||
elif rota == '/app/config':
|
||||
if metodo == 'POST':
|
||||
enviar({'ok': True, 'config': self.atualizar(dados)})
|
||||
config = self.atualizar(dados)
|
||||
self.publicar('app_config', config)
|
||||
enviar({'ok': True, 'config': config})
|
||||
else:
|
||||
with self.lock:
|
||||
config = self.config.copy()
|
||||
@@ -123,17 +149,10 @@ class ControlePC:
|
||||
if self.recarga["id"] and str(dados.get("tela_atualizada", "")) == self.recarga["id"]:
|
||||
self.recarga["confirmada"] = True
|
||||
self.status_at = time.monotonic()
|
||||
self.publicar('telemetria', self.resumo(h.headers.get("Host", "").split(":")[0]))
|
||||
enviar({'ok': True})
|
||||
else:
|
||||
with self.lock:
|
||||
idade = time.monotonic() - self.status_at
|
||||
frame_idade = time.monotonic() - self.frame_at
|
||||
resultado = dict(online=idade < 5, idade=round(idade, 1) if self.status_at else None,
|
||||
celular_ip=self.celular_ip, servidor_ip=h.headers.get("Host", "").split(":")[0], recarga=self.recarga.copy(),
|
||||
estado=self.estado.copy(), preview=self.config['preview'],
|
||||
camera_recente=bool(self.frame) and frame_idade < 3 and self.config['preview'],
|
||||
frame_seq=self.frame_seq)
|
||||
enviar(resultado)
|
||||
enviar(self.resumo(h.headers.get("Host", "").split(":")[0]))
|
||||
elif rota == '/camera/frame':
|
||||
with self.lock:
|
||||
if metodo == 'POST':
|
||||
|
||||
Reference in New Issue
Block a user