- 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>
71 lines
2.5 KiB
Python
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
|