feat(web): migra Movimento com homem-morto e trava de hardware

- Controle: opt-in de movimento fisico, botao "segure para mover"
  (mouse, toque e teclado), sliders das articulacoes tambem so movem
  enquanto pressionados; velocidade e interpolacao ficam no servidor
- sequencias prontas e posturas salvas: previa so na tela ou execucao
  segurando o botao ate o fim; soltar interrompe na hora
- posturas com nome inline (sem prompt), exclusao confirmada inline,
  exportar/importar; posturas aparecem na paleta (Ctrl+K)
- Calibracao: testes de fim de curso por canal, marcar limites e tabela
  editavel com validacao antes de salvar
- Diagnostico (antigo Servos): ao vivo limitado a 10/s, bloqueado quando
  o braco esta com os motores
- desenho esquematico do braco em SVG com previa animada
- servidor: evento 'motores' tambem ao ligar canais; renovar no alvo
  devolve movendo=false para a sequencia avancar
- aba escondida ou E-Stop soltam o braco e desativam o movimento

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-05 19:56:48 -03:00
co-authored by Claude Opus 5.5
parent 947c0a3127
commit 673dea62e8
10 changed files with 1153 additions and 9 deletions
+10 -1
View File
@@ -88,6 +88,15 @@ class ControleServos:
with self.lock:
return dict(disponivel=self.hw is not None, erro=self.error, ativos=sorted(self.active),
dono=self.dono, travado=self.travado)
def ligar(self, canais_prazos):
"""Registra canais energizados até o prazo; avisa o painel quando algum canal liga."""
with self.lock:
novos = False
for canal, prazo in canais_prazos:
novos = novos or canal not in self.active
self.active[canal] = prazo
if novos:
self.publicar('motores', self.motores())
def reservar(self, nome):
"""Braço e diagnóstico usam o mesmo PCA9685 com limites diferentes: um de cada vez."""
with self.lock:
@@ -110,7 +119,7 @@ class ControleServos:
if self.hw is None: raise OSError('PCA9685 indisponível neste servidor')
self.reservar('diagnostico')
self.hw.pulse(row['canal'], pulse)
self.active[row['canal']] = time.monotonic() + 2
self.ligar([(row['canal'], time.monotonic() + 2)])
return self.status()
def expire(self, now):
with self.lock: