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:
+281
-113
@@ -1,38 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
kiosk-server.py — servidor local do PC: painel, carinha e fila de ordens.
|
||||
kiosk-server.py — servidor do Carinha (Orange Pi, porta 8100): painel, rosto e controle.
|
||||
|
||||
Alem de entregar os arquivos da pasta (padrao /root/carinha), ele expõe:
|
||||
Estado em tempo real:
|
||||
GET /eventos -> Server-Sent Events (expressao, telemetria, conversa, reacao,
|
||||
ia, motores, estop, aparencia, app_config, treinamento)
|
||||
GET /estado -> foto de tudo para o primeiro carregamento (inclui 'seq')
|
||||
|
||||
GET /fila -> devolve e limpa a fila, em JSON: ["feliz","piscar"]
|
||||
GET /gesto/<nome> -> poe <nome> na fila (ex.: /gesto/coracao)
|
||||
POST /gesto -> corpo = nome do gesto
|
||||
Comandos:
|
||||
POST /comando -> {"tipo": "gesto"|"rosto"|"cor"|"reacao"|"parar_reacao"|"recarregar", "valor": ...}
|
||||
POST /parar-tudo -> E-Stop: desliga todos os servos, para reações e acalma o rosto
|
||||
POST /rearmar -> libera os motores depois do E-Stop
|
||||
GET /fila?aparelho=1 -> ordens pendentes para o celular (até o rosto migrar para /eventos)
|
||||
|
||||
A pagina da carinha pergunta /fila a cada meio segundo; assim da para mandar
|
||||
um gesto do PC com um curl, igual ao antigo FIFO /tmp/eilik.fifo:
|
||||
Autenticação: se CARINHA_TOKEN (ambiente) ou seguranca.json {"token": ...} existir, toda
|
||||
rota de controle/dados exige "Authorization: Bearer <token>" (ou ?t=<token> no EventSource
|
||||
e no app). Sem token configurado o servidor fica aberto como antes.
|
||||
|
||||
curl http://orangepi:8099/gesto/feliz
|
||||
|
||||
Alem disso guarda a APARENCIA da carinha (rosto + cor dos olhos), para o painel
|
||||
personalizar e a pagina aplicar sozinha ao carregar:
|
||||
|
||||
GET /config -> {"rosto":"carinha","cor":[92,214,255]}
|
||||
POST /config -> corpo JSON {"rosto":"miudo","cor":[255,120,0]}
|
||||
|
||||
E serve o painel de controle em http://<maquina>:8099/painel .
|
||||
|
||||
So usa a biblioteca padrao (http.server). Nada de instalacao extra.
|
||||
Só usa a biblioteca padrão. Nada de instalação extra.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections import deque
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import unquote
|
||||
from pathlib import Path
|
||||
from controle_pc import ControlePC
|
||||
from comum import autorizado, carregar_token, gravar_atomico, ler_json
|
||||
from controle_pc import ControlePC, versao
|
||||
from eventos import Eventos
|
||||
from servos import ControleServos
|
||||
from braco import Braco
|
||||
from cerebro import Cerebro
|
||||
@@ -41,7 +42,6 @@ from treinamento import Treinamento
|
||||
from api_config import ConfigAPI
|
||||
|
||||
TRAVA = threading.Lock()
|
||||
FILA = []
|
||||
|
||||
# Espelho da biblioteca de rostos do carinha.html (para /faces funcionar)
|
||||
ROSTOS = ["eilik", "carinha", "miudo"]
|
||||
@@ -50,12 +50,65 @@ ROSTOS = ["eilik", "carinha", "miudo"]
|
||||
GESTOS = ["piscar", "piscar2", "olhar_esq", "olhar_dir", "olhar_baixo", "feliz",
|
||||
"surpreso", "bravo", "coracao", "triste", "piscadinha", "alegre",
|
||||
"dormir", "acordar", "aleatorio", "parar"]
|
||||
# Ordens internas que o rosto entende mas não viram botão.
|
||||
ORDENS_INTERNAS = {"calma"}
|
||||
|
||||
# Apar┌┐ncia atual da carinha (personalizacao pelo painel). Fica gravada num
|
||||
# Aparência atual da carinha (personalizacao pelo painel). Fica gravada num
|
||||
# arquivo ao lado do servidor, para sobreviver a reinicios.
|
||||
APARENCIA = {"rosto": "carinha", "cor": [92, 214, 255]}
|
||||
ARQUIVO_APARENCIA = "aparencia.json"
|
||||
|
||||
# Rotas que não exigem token: só o necessário para a página do rosto abrir e o app se achar.
|
||||
PUBLICAS_GET = {"/app/health", "/faces", "/gestos", "/config", "/ia/modelo", "/app.apk"}
|
||||
PREFIXOS_API = ("/eventos", "/estado", "/fila", "/rtc/", "/camera/", "/conversa/", "/treinamento/",
|
||||
"/app/", "/braco/", "/servos/", "/ia/", "/api/", "/gesto/", "/face/", "/diag/")
|
||||
|
||||
|
||||
class Fila:
|
||||
"""Ordens para o celular: limitada e com validade, para não repetir ordens velhas
|
||||
quando o aparelho volta depois de um tempo offline."""
|
||||
|
||||
def __init__(self, maximo=20, validade=10.0):
|
||||
self.lock = threading.Lock()
|
||||
self.itens = deque(maxlen=maximo)
|
||||
self.validade = validade
|
||||
|
||||
def adicionar(self, ordem, agora=None):
|
||||
with self.lock:
|
||||
if ordem == "recarregar" and any(o == ordem for _, o in self.itens):
|
||||
return
|
||||
self.itens.append((agora if agora is not None else time.monotonic(), ordem))
|
||||
|
||||
def consumir(self, aparelho=True, agora=None):
|
||||
"""O celular leva tudo; um rosto aberto no navegador (kiosk) leva tudo menos 'recarregar'."""
|
||||
agora = agora if agora is not None else time.monotonic()
|
||||
with self.lock:
|
||||
validos = [(t, o) for t, o in self.itens if agora - t <= self.validade]
|
||||
pendentes = [o for _, o in validos if aparelho or o != "recarregar"]
|
||||
self.itens.clear()
|
||||
if not aparelho:
|
||||
self.itens.extend((t, o) for t, o in validos if o == "recarregar")
|
||||
return pendentes
|
||||
|
||||
def ordens(self):
|
||||
with self.lock:
|
||||
return [o for _, o in self.itens]
|
||||
|
||||
def limpar(self):
|
||||
with self.lock:
|
||||
self.itens.clear()
|
||||
|
||||
|
||||
FILA = Fila()
|
||||
EVENTOS = Eventos()
|
||||
ESTOP = {"ativo": False, "desde": None}
|
||||
|
||||
|
||||
def enfileirar(ordem):
|
||||
"""Caminho único de uma ordem para o rosto: fila do celular + evento para os painéis."""
|
||||
FILA.adicionar(ordem)
|
||||
EVENTOS.publicar("expressao", {"ordem": ordem})
|
||||
|
||||
|
||||
def carregar_aparencia():
|
||||
try:
|
||||
@@ -73,14 +126,20 @@ def carregar_aparencia():
|
||||
|
||||
def salvar_aparencia():
|
||||
try:
|
||||
with open(ARQUIVO_APARENCIA, "w", encoding="utf-8") as f:
|
||||
json.dump(APARENCIA, f, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
gravar_atomico(ARQUIVO_APARENCIA, APARENCIA)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def validar_cor(valor):
|
||||
cor = [max(0, min(255, int(v))) for v in list(valor)[:3]]
|
||||
if len(cor) != 3:
|
||||
raise ValueError("Cor precisa de três números")
|
||||
return cor
|
||||
|
||||
|
||||
def aplicar_aparencia(dados):
|
||||
"""Guarda rosto/cor e poe as ordens na fila para a pagina aplicar na hora."""
|
||||
"""Guarda rosto/cor e manda as ordens para a página aplicar na hora."""
|
||||
ordens = []
|
||||
with TRAVA:
|
||||
if "rosto" in dados:
|
||||
@@ -90,24 +149,43 @@ def aplicar_aparencia(dados):
|
||||
ordens.append("rosto " + nome)
|
||||
if "cor" in dados:
|
||||
try:
|
||||
cor = [max(0, min(255, int(v))) for v in list(dados["cor"])[:3]]
|
||||
if len(cor) == 3:
|
||||
APARENCIA["cor"] = cor
|
||||
ordens.append("cor %d,%d,%d" % tuple(cor))
|
||||
except Exception:
|
||||
APARENCIA["cor"] = validar_cor(dados["cor"])
|
||||
ordens.append("cor %d,%d,%d" % tuple(APARENCIA["cor"]))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
FILA.extend(ordens)
|
||||
salvar_aparencia()
|
||||
aparencia = dict(APARENCIA)
|
||||
for ordem in ordens:
|
||||
enfileirar(ordem)
|
||||
if ordens:
|
||||
salvar_aparencia()
|
||||
EVENTOS.publicar("aparencia", aparencia)
|
||||
return ordens
|
||||
|
||||
|
||||
def ordem_de_comando(tipo, valor):
|
||||
"""Traduz {tipo, valor} para a ordem de texto que o rosto entende; ValueError se inválido."""
|
||||
if tipo == "gesto":
|
||||
nome = str(valor or "").strip().lower()
|
||||
if nome not in GESTOS and nome not in ORDENS_INTERNAS:
|
||||
raise ValueError("Gesto desconhecido: " + nome)
|
||||
return nome
|
||||
if tipo == "rosto":
|
||||
nome = str(valor or "").strip().lower()
|
||||
if nome not in ROSTOS:
|
||||
raise ValueError("Rosto desconhecido: " + nome)
|
||||
return "rosto " + nome
|
||||
raise ValueError("Tipo de comando inválido")
|
||||
|
||||
|
||||
class Handler(SimpleHTTPRequestHandler):
|
||||
extensoes_map = {".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".woff2": "font/woff2",
|
||||
".mp4": "video/mp4"}
|
||||
|
||||
def guess_type(self, caminho):
|
||||
@@ -120,32 +198,71 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
self.send_header("Content-Type", tipo)
|
||||
self.send_header("Content-Length", str(len(dados)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
if codigo == 401:
|
||||
self.send_header("WWW-Authenticate", 'Bearer realm="carinha"')
|
||||
self.end_headers()
|
||||
self.wfile.write(dados)
|
||||
|
||||
def json(self, dados, codigo=200):
|
||||
self.enviar(json.dumps(dados, ensure_ascii=False), codigo=codigo)
|
||||
|
||||
def rota(self):
|
||||
return self.path.split("?", 1)[0]
|
||||
|
||||
def acesso_negado(self, metodo):
|
||||
"""True (e já respondeu 401) se a rota exige token e o pedido não trouxe o certo."""
|
||||
rota = self.rota()
|
||||
protegida = metodo == "POST" or (rota not in PUBLICAS_GET and rota.startswith(PREFIXOS_API))
|
||||
if protegida and not autorizado(self):
|
||||
self.json({"erro": "Token ausente ou inválido"}, 401)
|
||||
return True
|
||||
return False
|
||||
|
||||
def modulos(self):
|
||||
s = self.server
|
||||
return [getattr(s, nome) for nome in ("api_config", "treinamento", "conversa", "cerebro",
|
||||
"braco", "servos", "controle") if hasattr(s, nome)]
|
||||
|
||||
def estado(self):
|
||||
s = self.server
|
||||
host = self.headers.get("Host", "").split(":")[0]
|
||||
with TRAVA:
|
||||
aparencia = dict(APARENCIA)
|
||||
dados = dict(versao=versao(), seq=EVENTOS.seq, autenticacao=bool(getattr(s, "token", None)),
|
||||
estop=dict(ESTOP), aparencia=aparencia, gestos=GESTOS, rostos=ROSTOS)
|
||||
if hasattr(s, "controle"):
|
||||
dados.update(app=s.controle.resumo(host), app_config=s.controle.config.copy())
|
||||
if hasattr(s, "servos"):
|
||||
dados["motores"] = s.servos.motores()
|
||||
if hasattr(s, "conversa"):
|
||||
dados["conversa"] = s.conversa.status()
|
||||
if hasattr(s, "cerebro"):
|
||||
dados["ia"] = s.cerebro.status()
|
||||
if hasattr(s, "treinamento"):
|
||||
dados["reacao"] = s.treinamento.expressao
|
||||
dados["treinamento"] = dict(ocupado=s.treinamento.busy, erro=s.treinamento.erro)
|
||||
if hasattr(s, "api_config"):
|
||||
dados["api"] = s.api_config.status()
|
||||
return dados
|
||||
|
||||
def do_GET(self):
|
||||
if hasattr(self.server, "api_config") and self.server.api_config.rotas(self, "GET"):return
|
||||
if hasattr(self.server, 'treinamento') and self.server.treinamento.rotas(self, 'GET'):
|
||||
if self.acesso_negado("GET"):
|
||||
return
|
||||
if hasattr(self.server, 'conversa') and self.server.conversa.rotas(self, 'GET'):
|
||||
return
|
||||
if hasattr(self.server, 'cerebro') and self.server.cerebro.rotas(self, 'GET'):
|
||||
return
|
||||
if hasattr(self.server, 'braco') and self.server.braco.rotas(self, 'GET'):
|
||||
return
|
||||
if hasattr(self.server, 'servos') and self.server.servos.rotas(self, 'GET'):
|
||||
return
|
||||
if self.server.controle.rotas(self, 'GET'):
|
||||
return
|
||||
caminho = self.path.split("?", 1)[0]
|
||||
for modulo in self.modulos():
|
||||
if modulo.rotas(self, "GET"):
|
||||
return
|
||||
caminho = self.rota()
|
||||
partes = [p for p in caminho.split("/") if p]
|
||||
|
||||
if caminho == "/eventos":
|
||||
return EVENTOS.atender(self)
|
||||
|
||||
if caminho == "/estado":
|
||||
return self.json(self.estado())
|
||||
|
||||
if partes and partes[0] == "fila":
|
||||
with TRAVA:
|
||||
aparelho = 'aparelho=1' in self.path
|
||||
pendentes = [x for x in FILA if x!='recarregar' or aparelho]
|
||||
FILA[:] = [x for x in FILA if x=='recarregar' and not aparelho]
|
||||
return self.enviar(json.dumps(pendentes))
|
||||
# Painéis não leem a fila: observam o rosto por /eventos.
|
||||
return self.enviar(json.dumps(FILA.consumir(aparelho="aparelho=1" in self.path)))
|
||||
|
||||
if partes and partes[0] == "faces":
|
||||
return self.enviar(json.dumps({"rostos": ROSTOS}))
|
||||
@@ -156,12 +273,6 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
if partes and partes[0] == "gestos":
|
||||
return self.enviar(json.dumps({"gestos": GESTOS}))
|
||||
|
||||
if len(partes) == 2 and partes[0] == "face":
|
||||
nome = unquote(partes[1]).lower()
|
||||
with TRAVA:
|
||||
FILA.append("rosto " + nome)
|
||||
return self.enviar(json.dumps({"ok": True, "rosto": nome}))
|
||||
|
||||
if partes and partes[0] == "diag":
|
||||
# a propria pagina relata o tamanho que recebeu (ajuda a achar
|
||||
# barras/enquadramento errado no kiosk). Aparece no journal.
|
||||
@@ -169,11 +280,15 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
print("DIAG kiosk:", info, flush=True)
|
||||
return self.enviar(json.dumps({"ok": True}))
|
||||
|
||||
if len(partes) == 2 and partes[0] == "gesto":
|
||||
nome = unquote(partes[1]).lower() # aceita %20 para espacos
|
||||
with TRAVA:
|
||||
FILA.append(nome)
|
||||
return self.enviar(json.dumps({"ok": True, "gesto": nome}))
|
||||
# Obsoletos (GET que muda estado); ficam até o painel antigo sair. Use POST /comando.
|
||||
if len(partes) == 2 and partes[0] in ("gesto", "face"):
|
||||
tipo = "gesto" if partes[0] == "gesto" else "rosto"
|
||||
try:
|
||||
ordem = ordem_de_comando(tipo, unquote(partes[1]))
|
||||
except ValueError as e:
|
||||
return self.json({"erro": str(e)}, 400)
|
||||
enfileirar(ordem)
|
||||
return self.json({"ok": True, partes[0]: ordem})
|
||||
|
||||
if caminho in ("/dashboard", "/emojis", "/api"):
|
||||
self.path = caminho + ".html"
|
||||
@@ -201,60 +316,103 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
elif caminho in ('/dashboard.html','/emojis.html','/api.html','/dashboard.css','/dashboard.js','/overview.js','/biblioteca.js','/api-painel.js','/painel.css','/conversa.css','/treinamento.css','/servos.css','/braco.css','/audio-robot.js'):
|
||||
pass
|
||||
elif caminho not in ('/conversa.html', '/treinamento.html', '/treinamento.js', '/reacoes-render.js', '/painel-conversa.js', '/braco-calibracao.js', '/movimentos-core.js', '/braco-movimentos.js', '/braco.html', '/braco.js', '/servos.html', '/servos.js', '/carinha.html', '/camera.html', '/painel.html', '/painel-controle.js', '/painel-video.js',
|
||||
'/rastreamento-core.js', '/rastreador-worker.js', '/painel-rastreamento.js', '/audio-robot.js'):
|
||||
'/rastreamento-core.js', '/rastreador-worker.js', '/painel-rastreamento.js'):
|
||||
return self.enviar(json.dumps({'erro': 'Arquivo não disponível'}), codigo=404)
|
||||
return super().do_GET()
|
||||
|
||||
def do_POST(self):
|
||||
if hasattr(self.server, "api_config") and self.server.api_config.rotas(self, "POST"):return
|
||||
if hasattr(self.server, 'treinamento') and self.server.treinamento.rotas(self, 'POST'):
|
||||
if self.acesso_negado("POST"):
|
||||
return
|
||||
if hasattr(self.server, 'conversa') and self.server.conversa.rotas(self, 'POST'):
|
||||
return
|
||||
if hasattr(self.server, 'cerebro') and self.server.cerebro.rotas(self, 'POST'):
|
||||
return
|
||||
if hasattr(self.server, 'braco') and self.server.braco.rotas(self, 'POST'):
|
||||
return
|
||||
if hasattr(self.server, 'servos') and self.server.servos.rotas(self, 'POST'):
|
||||
return
|
||||
if self.server.controle.rotas(self, 'POST'):
|
||||
return
|
||||
rota = self.path.split("?", 1)[0].rstrip("/")
|
||||
tamanho = int(self.headers.get("Content-Length") or 0)
|
||||
for modulo in self.modulos():
|
||||
if modulo.rotas(self, "POST"):
|
||||
return
|
||||
rota = self.rota().rstrip("/")
|
||||
try:
|
||||
if rota == "/comando":
|
||||
return self.comando(ler_json(self, 2048))
|
||||
if rota == "/parar-tudo":
|
||||
ler_json(self, 2048)
|
||||
return self.json(self.parar_tudo())
|
||||
if rota == "/rearmar":
|
||||
ler_json(self, 2048)
|
||||
return self.json(self.rearmar())
|
||||
if rota == '/app/recarregar':
|
||||
ler_json(self, 2048)
|
||||
return self.recarregar()
|
||||
if rota == "/config":
|
||||
return self.json({"ok": True, "ordens": aplicar_aparencia(ler_json(self, 2048)),
|
||||
"aparencia": APARENCIA})
|
||||
if rota in ("/gesto", "/face"):
|
||||
# Obsoleto: corpo é o nome cru. Use POST /comando.
|
||||
tamanho = int(self.headers.get("Content-Length") or 0)
|
||||
nome = self.rfile.read(min(tamanho, 200)).decode("utf-8", "ignore")
|
||||
ordem = ordem_de_comando("gesto" if rota == "/gesto" else "rosto", nome)
|
||||
enfileirar(ordem)
|
||||
return self.json({"ok": True, "ordem": ordem})
|
||||
except ValueError as e:
|
||||
return self.json({"erro": str(e)}, 400)
|
||||
return self.json({"erro": "Rota POST desconhecida"}, 404)
|
||||
|
||||
if rota == '/app/recarregar':
|
||||
origin=self.headers.get('Origin')
|
||||
if origin and origin not in ('http://'+self.headers.get('Host',''),'https://'+self.headers.get('Host','')):
|
||||
return self.enviar(json.dumps(dict(erro='Origem inválida')),codigo=400)
|
||||
import time
|
||||
with self.server.controle.lock:
|
||||
online=time.monotonic()-self.server.controle.status_at<5
|
||||
import uuid
|
||||
self.server.controle.recarga = dict(id=uuid.uuid4().hex, confirmada=False)
|
||||
if not online:return self.enviar(json.dumps(dict(erro='Abra o Carinha e conecte ao servidor primeiro')),codigo=409)
|
||||
with TRAVA:
|
||||
if 'recarregar' not in FILA:FILA.append('recarregar')
|
||||
return self.enviar(json.dumps(dict(ok=True,mensagem='Solicitação enviada. Aguardando o celular recarregar a tela.')))
|
||||
def comando(self, dados):
|
||||
tipo, valor = dados.get("tipo"), dados.get("valor")
|
||||
s = self.server
|
||||
if tipo == "cor":
|
||||
if not aplicar_aparencia({"cor": valor}):
|
||||
raise ValueError("Cor inválida")
|
||||
return self.json({"ok": True})
|
||||
if tipo == "recarregar":
|
||||
return self.recarregar()
|
||||
if tipo in ("reacao", "parar_reacao"):
|
||||
if not hasattr(s, "treinamento"):
|
||||
raise ValueError("Reações indisponíveis")
|
||||
if tipo == "reacao":
|
||||
if type(valor) is not int:
|
||||
raise ValueError("ID de reação inválido")
|
||||
s.treinamento.testar(valor)
|
||||
else:
|
||||
with s.treinamento.lock:
|
||||
s.treinamento.expressao = dict(id=time.time_ns(), horario=time.time(), receita=dict(limpar=True))
|
||||
return self.json({"ok": True})
|
||||
ordem = ordem_de_comando(tipo, valor)
|
||||
enfileirar(ordem)
|
||||
return self.json({"ok": True, "ordem": ordem})
|
||||
|
||||
if rota == "/config":
|
||||
try:
|
||||
dados = json.loads(self.rfile.read(tamanho).decode("utf-8", "ignore") or "{}")
|
||||
if not isinstance(dados, dict):
|
||||
raise ValueError("esperava um objeto JSON")
|
||||
except Exception as e:
|
||||
return self.enviar(json.dumps({"erro": "json invalido: %s" % e}), codigo=400)
|
||||
ordens = aplicar_aparencia(dados)
|
||||
return self.enviar(json.dumps({"ok": True, "aparencia": APARENCIA,
|
||||
"ordens": ordens}))
|
||||
def parar_tudo(self):
|
||||
s = self.server
|
||||
if hasattr(s, "braco"):
|
||||
s.braco.cancelar()
|
||||
if hasattr(s, "servos"):
|
||||
with s.servos.lock:
|
||||
s.servos.travado = True
|
||||
s.servos.stop()
|
||||
if hasattr(s, "treinamento"):
|
||||
with s.treinamento.lock:
|
||||
s.treinamento.expressao = dict(id=time.time_ns(), horario=time.time(), receita=dict(limpar=True))
|
||||
FILA.limpar()
|
||||
enfileirar("calma")
|
||||
ESTOP.update(ativo=True, desde=time.time())
|
||||
EVENTOS.publicar("estop", dict(ESTOP))
|
||||
return {"ok": True, "estop": dict(ESTOP)}
|
||||
|
||||
if rota not in ("/gesto", "/face"):
|
||||
return self.enviar(json.dumps({"erro": "rotas POST: /gesto, /face, /config"}), codigo=404)
|
||||
corpo = self.rfile.read(tamanho).decode("utf-8", "ignore").strip().lower()
|
||||
if not corpo:
|
||||
return self.enviar(json.dumps({"erro": "corpo vazio"}), codigo=400)
|
||||
with TRAVA:
|
||||
FILA.append(corpo)
|
||||
return self.enviar(json.dumps({"ok": True, "ordem": corpo}))
|
||||
def rearmar(self):
|
||||
s = self.server
|
||||
if hasattr(s, "servos"):
|
||||
with s.servos.lock:
|
||||
s.servos.travado = False
|
||||
EVENTOS.publicar("motores", s.servos.motores())
|
||||
ESTOP.update(ativo=False, desde=None)
|
||||
EVENTOS.publicar("estop", dict(ESTOP))
|
||||
return {"ok": True, "estop": dict(ESTOP)}
|
||||
|
||||
def recarregar(self):
|
||||
controle = self.server.controle
|
||||
with controle.lock:
|
||||
online = time.monotonic() - controle.status_at < 5
|
||||
controle.recarga = dict(id=uuid.uuid4().hex, confirmada=False)
|
||||
if not online:
|
||||
return self.json({"erro": "Abra o Carinha e conecte ao servidor primeiro"}, 409)
|
||||
FILA.adicionar("recarregar")
|
||||
return self.json({"ok": True, "mensagem": "Solicitação enviada. Aguardando o celular recarregar a tela."})
|
||||
|
||||
def log_message(self, formato, *args):
|
||||
# silencio por padrao (roda em segundo plano); --log liga o registro
|
||||
@@ -270,6 +428,13 @@ class Servidor(ThreadingHTTPServer):
|
||||
so o desligamos no Windows."""
|
||||
daemon_threads = True
|
||||
allow_reuse_address = (os.name != "nt")
|
||||
token = None
|
||||
|
||||
|
||||
def ligar_eventos(servidor):
|
||||
for nome in ("controle", "servos", "cerebro", "conversa", "treinamento"):
|
||||
if hasattr(servidor, nome):
|
||||
getattr(servidor, nome).publicar = EVENTOS.publicar
|
||||
|
||||
|
||||
def main():
|
||||
@@ -282,21 +447,24 @@ def main():
|
||||
os.chdir(args.pasta)
|
||||
carregar_aparencia()
|
||||
servidor = Servidor(("0.0.0.0", args.porta), Handler)
|
||||
servidor.token = carregar_token(Path.cwd())
|
||||
servidor.api_config = ConfigAPI(Path.cwd() / 'api-privada.json')
|
||||
servidor.controle = ControlePC(Path.cwd() / 'app-config.json')
|
||||
servidor.servos = ControleServos(Path.cwd() / 'servos.json')
|
||||
servidor.braco = Braco(Path.cwd() / 'braco.json', servidor.servos)
|
||||
def expressao_ia(gesto):
|
||||
with TRAVA: FILA.append(gesto)
|
||||
servidor.cerebro = Cerebro(Path.cwd() / 'ia-config.json', servidor.controle, expressao_ia)
|
||||
servidor.cerebro = Cerebro(Path.cwd() / 'ia-config.json', servidor.controle, enfileirar)
|
||||
servidor.conversa = Conversa(Path.cwd() / 'memoria.db', servidor.cerebro)
|
||||
servidor.treinamento = Treinamento(Path.cwd() / 'treinamento.db')
|
||||
servidor.conversa.memoria.on_registrar = servidor.treinamento.registrar
|
||||
servidor.conversa.treinamento = servidor.treinamento
|
||||
ligar_eventos(servidor)
|
||||
servidor.registrar = args.log
|
||||
print(f"servindo {args.pasta} em http://0.0.0.0:{args.porta}/carinha.html")
|
||||
print(f"painel de controle em http://0.0.0.0:{args.porta}/painel")
|
||||
servidor.serve_forever()
|
||||
print(f"servindo {args.pasta} em http://0.0.0.0:{args.porta}/")
|
||||
print("autenticação:", "token obrigatório" if servidor.token else "DESLIGADA (defina CARINHA_TOKEN)")
|
||||
try:
|
||||
servidor.serve_forever()
|
||||
finally:
|
||||
EVENTOS.fechar()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user