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
+85 -4
View File
@@ -1,7 +1,88 @@
<script lang="ts">
import Card from '../lib/componentes/Card.svelte';
import { Power } from '@lucide/svelte';
import { onDestroy, onMount } from 'svelte';
import { braco } from '../lib/braco.svelte';
import Abas from '../lib/componentes/Abas.svelte';
import Selo from '../lib/componentes/Selo.svelte';
import { estado } from '../lib/estado.svelte';
import { registrarNaPaleta } from '../lib/paleta.svelte';
import Calibracao from './movimento/Calibracao.svelte';
import Controle from './movimento/Controle.svelte';
import Diagnostico from './movimento/Diagnostico.svelte';
type Aba = 'controle' | 'calibracao' | 'diagnostico';
let aba = $state<Aba>('controle');
const motores = $derived(estado.motores);
onMount(() => {
braco.carregar();
});
// Posturas na paleta: carregar como alvo (mover exige segurar o botão).
const sair = registrarNaPaleta(() =>
(braco.status?.posturas ?? []).map((p, i) => ({
id: 'postura-' + i,
grupo: 'Posturas do braço',
titulo: 'Carregar ' + p.nome,
fazer: () => (braco.alvo = [...p.angulos]),
})),
);
onDestroy(() => {
sair();
braco.soltar();
});
</script>
<Card titulo="Movimento" descricao="Braço, calibração e diagnóstico migram na fase 4.">
<p class="pequeno-texto">Enquanto isso, use a tela antiga: <a href="/braco">abrir Movimento (versão anterior)</a>.</p>
</Card>
<div class="movimento">
<div class="estado">
<div class="linha">
{#if !motores?.disponivel}
<Selo tom="aviso">PCA9685 indisponível neste servidor</Selo>
{:else if estado.estop.ativo || motores.travado}
<Selo tom="perigo">Travado pelo E-Stop</Selo>
{:else if motores.ativos.length}
<Selo tom="ok">Motores ligados · {motores.dono === 'braco' ? 'braço' : 'diagnóstico'} · canais {motores.ativos.join(', ')}</Selo>
{:else}
<Selo>Motores desligados</Selo>
{/if}
{#if motores?.erro}<span class="erro pequeno-texto">{motores.erro}</span>{/if}
</div>
<button class="btn" onclick={() => braco.pararMotores()} disabled={!motores?.ativos.length}>
<Power size={16} /> Desligar motores
</button>
</div>
<Abas
rotulo="Movimento"
bind:ativa={aba}
abas={[
{ id: 'controle', titulo: 'Controle' },
{ id: 'calibracao', titulo: 'Calibração' },
{ id: 'diagnostico', titulo: 'Diagnóstico' },
]}
/>
{#if aba === 'controle'}
<Controle />
{:else if aba === 'calibracao'}
<Calibracao />
{:else}
<Diagnostico />
{/if}
</div>
<style>
.movimento {
display: grid;
gap: var(--e-4);
}
.estado {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--e-3);
flex-wrap: wrap;
}
.erro {
color: var(--perigo);
}
</style>