Files
carinha-control-panel/eventos.py
T
julioandClaude Opus 5.5 35d443a358 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>
2026-10-05 19:34:35 -03:00

71 lines
2.5 KiB
Python

"""Hub Server-Sent Events: um único fluxo /eventos substitui os pollings do painel."""
from collections import deque
import json
import threading
import time
from urllib.parse import parse_qs, urlsplit
PING = 15
class Eventos:
def __init__(self, buffer=200):
self.cond = threading.Condition()
self.seq = 0
self.buffer = deque(maxlen=buffer)
self.fechado = False
def publicar(self, tipo, dados):
corpo = json.dumps(dados, ensure_ascii=False)
with self.cond:
self.seq += 1
self.buffer.append((self.seq, tipo, corpo))
self.cond.notify_all()
return self.seq
def desde(self, seq):
with self.cond:
return [e for e in self.buffer if e[0] > seq]
def fechar(self):
with self.cond:
self.fechado = True
self.cond.notify_all()
def atender(self, h):
url = urlsplit(h.path)
try:
ultimo = int(h.headers.get('Last-Event-ID') or parse_qs(url.query).get('desde', ['0'])[0])
except ValueError:
ultimo = 0
with self.cond:
# Cliente novo (ou que perdeu demais): começa do agora e lê /estado.
if ultimo <= 0 or (self.buffer and ultimo < self.buffer[0][0] - 1) or ultimo > self.seq:
ultimo = self.seq
h.send_response(200)
h.send_header('Content-Type', 'text/event-stream; charset=utf-8')
h.send_header('Cache-Control', 'no-store')
h.send_header('X-Accel-Buffering', 'no')
h.end_headers()
try:
h.wfile.write(f'retry: 3000\nevent: pronto\nid: {ultimo}\ndata: {{"seq": {ultimo}}}\n\n'.encode())
while True:
limite = time.monotonic() + PING
with self.cond:
while not self.fechado and (not self.buffer or self.buffer[-1][0] <= ultimo):
resta = limite - time.monotonic()
if resta <= 0:
break
self.cond.wait(resta)
if self.fechado:
return
novos = [e for e in self.buffer if e[0] > ultimo]
if not novos:
h.wfile.write(b': ping\n\n')
continue
for seq, tipo, corpo in novos:
h.wfile.write(f'id: {seq}\nevent: {tipo}\ndata: {corpo}\n\n'.encode())
ultimo = seq
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError, OSError):
return