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:
+70
@@ -0,0 +1,70 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user