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:
+7
-9
@@ -5,6 +5,7 @@ from pathlib import Path
|
||||
import threading
|
||||
import time
|
||||
from urllib.request import Request,urlopen
|
||||
from comum import gravar_atomico, ler_json
|
||||
|
||||
GESTOS=('piscar','piscadinha','feliz','surpreso','coracao','olhar_esq','olhar_dir','olhar_baixo')
|
||||
EVENTOS=('som','movimento','saudacao','sozinho')
|
||||
@@ -20,15 +21,17 @@ class Cerebro:
|
||||
self.executor='S10' if '192.168.1.16' in self.endpoint else 'Orange Pi'
|
||||
self.lock=threading.Lock();self.ativo=False;self.pending=None;self.busy=False
|
||||
self.conversando=False;self.history=[];self.result=None;self.error='';self.last=0;self.generation=0
|
||||
self.publicar=lambda tipo,dados:None
|
||||
try:self.ativo=json.loads(self.path.read_text())['ativo'] is True
|
||||
except (OSError,ValueError,KeyError,TypeError):pass
|
||||
threading.Thread(target=self.loop,daemon=True).start()
|
||||
def config(self,ativo):
|
||||
if type(ativo) is not bool:raise ValueError('ativo deve ser booleano')
|
||||
with self.lock:
|
||||
tmp=self.path.with_suffix('.tmp');tmp.write_text(json.dumps(dict(ativo=ativo)));tmp.replace(self.path)
|
||||
gravar_atomico(self.path,dict(ativo=ativo))
|
||||
self.ativo=ativo;self.generation+=1;self.pending=None
|
||||
return self.status()
|
||||
status=self.status();self.publicar('ia',status)
|
||||
return status
|
||||
def status(self):
|
||||
with self.lock:return dict(ativo=self.ativo,ocupado=self.busy,modelo='Qwen3-0.6B Q4_K_M',executor=self.executor,ultima=self.result,erro=self.error)
|
||||
def event(self,event):
|
||||
@@ -77,6 +80,7 @@ class Cerebro:
|
||||
finally:
|
||||
self.last=time.monotonic()
|
||||
with self.lock:self.busy=False
|
||||
self.publicar('ia',self.status())
|
||||
def rotas(self,h,method):
|
||||
route=h.path.split('?',1)[0]
|
||||
if route=='/ia/modelo' and method=='GET':
|
||||
@@ -95,13 +99,7 @@ class Cerebro:
|
||||
try:
|
||||
if method=='GET' and route=='/ia/status':result=self.status()
|
||||
elif method=='POST':
|
||||
origin=h.headers.get('Origin')
|
||||
if origin and origin not in ('http://'+h.headers.get('Host',''),'https://'+h.headers.get('Host','')):raise ValueError('Origem inválida')
|
||||
if h.headers.get('Content-Type','').split(';')[0]!='application/json':raise ValueError('Use JSON')
|
||||
size=int(h.headers.get('Content-Length','0'))
|
||||
if not 0<size<=2048:raise ValueError('Tamanho inválido')
|
||||
data=json.loads(h.rfile.read(size))
|
||||
if not isinstance(data,dict):raise ValueError('Objeto obrigatório')
|
||||
data=ler_json(h,2048)
|
||||
if route=='/ia/config':result=self.config(data.get('ativo'))
|
||||
elif route=='/ia/evento':result=self.event(data.get('evento'))
|
||||
else:raise ValueError('Rota inválida')
|
||||
|
||||
Reference in New Issue
Block a user