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:
+15
-7
@@ -1,6 +1,7 @@
|
||||
"""Rotina, diário e reações procedurais. Não treina pesos nem controla motores."""
|
||||
import json,sqlite3,threading,time,math
|
||||
from contextlib import contextmanager
|
||||
from comum import ler_json
|
||||
from gemini import gerar_json
|
||||
from figurinhas import criar_desenho,validar_desenho,montar_prompt
|
||||
|
||||
@@ -28,8 +29,20 @@ def validar_receita(r):
|
||||
return r
|
||||
|
||||
class Treinamento:
|
||||
# Mudanças de expressão/ocupação viram eventos SSE sem caçar cada atribuição.
|
||||
@property
|
||||
def expressao(self):return self._expressao
|
||||
@expressao.setter
|
||||
def expressao(self,valor):
|
||||
self._expressao=valor;self.publicar('reacao',valor)
|
||||
@property
|
||||
def busy(self):return self._busy
|
||||
@busy.setter
|
||||
def busy(self,valor):
|
||||
self._busy=valor;self.publicar('treinamento',dict(ocupado=valor,erro=self.erro))
|
||||
def __init__(self,path,iniciar=True):
|
||||
self.path=str(path);self.lock=threading.RLock();self.busy=False;self.erro='';self.expressao=None
|
||||
self.publicar=lambda tipo,dados:None
|
||||
self.path=str(path);self.lock=threading.RLock();self.erro='';self.busy=False;self.expressao=None
|
||||
with self.db() as db:
|
||||
db.execute('CREATE TABLE IF NOT EXISTS rotina(id INTEGER PRIMARY KEY,texto TEXT)')
|
||||
db.execute('CREATE TABLE IF NOT EXISTS diario(id INTEGER PRIMARY KEY,horario REAL,papel TEXT,texto TEXT)')
|
||||
@@ -178,12 +191,7 @@ class Treinamento:
|
||||
elif method=='GET' and route=='/treinamento/expressao':
|
||||
with self.lock:result=self.expressao
|
||||
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')
|
||||
size=int(h.headers.get('Content-Length','0'))
|
||||
if not 0<size<=4096 or h.headers.get('Content-Type','').split(';')[0]!='application/json':raise ValueError('Use JSON até 4096 bytes')
|
||||
d=json.loads(h.rfile.read(size))
|
||||
if not isinstance(d,dict):raise ValueError('Objeto obrigatório')
|
||||
d=ler_json(h,4096)
|
||||
with self.lock,self.db() as db:
|
||||
if route=='/treinamento/biblioteca':self.alterar_reacao(d.get('acao'),d.get('id'),d)
|
||||
elif route=='/treinamento/parar':self.expressao=dict(id=time.time_ns(),horario=time.time(),receita=dict(limpar=True))
|
||||
|
||||
Reference in New Issue
Block a user