- 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>
185 lines
6.5 KiB
Svelte
185 lines
6.5 KiB
Svelte
<script lang="ts">
|
||
import { Plus, Save, Trash2, Zap } from '@lucide/svelte';
|
||
import { onDestroy, onMount } from 'svelte';
|
||
import { pedir, postar } from '../../lib/api';
|
||
import { avisos, tentar } from '../../lib/avisos.svelte';
|
||
import Card from '../../lib/componentes/Card.svelte';
|
||
import Selo from '../../lib/componentes/Selo.svelte';
|
||
import Slider from '../../lib/componentes/Slider.svelte';
|
||
import Toggle from '../../lib/componentes/Toggle.svelte';
|
||
import { estado } from '../../lib/estado.svelte';
|
||
|
||
interface Servo {
|
||
nome: string;
|
||
canal: number;
|
||
minimo: number;
|
||
maximo: number;
|
||
centro: number;
|
||
}
|
||
let servos = $state<Servo[]>([]);
|
||
let pulsos = $state<number[]>([]);
|
||
let disponivel = $state(false);
|
||
let sujo = $state(false);
|
||
let aoVivo = $state(false);
|
||
|
||
async function carregar() {
|
||
const st = await tentar(() => pedir<{ servos: Servo[]; disponivel: boolean }>('/servos/status'));
|
||
if (!st) return;
|
||
servos = st.servos;
|
||
pulsos = st.servos.map((s) => s.centro);
|
||
disponivel = st.disponivel;
|
||
sujo = false;
|
||
}
|
||
onMount(carregar);
|
||
|
||
// Ao vivo: no máximo 10 comandos/s, sempre o último valor.
|
||
let pendente: { indice: number; pulso: number } | null = null;
|
||
let emVoo = false;
|
||
let ultimo = 0;
|
||
let espera = 0;
|
||
function enfileirar(indice: number, pulso: number) {
|
||
pulsos[indice] = pulso;
|
||
if (!aoVivo || sujo) return;
|
||
pendente = { indice, pulso };
|
||
bombear();
|
||
}
|
||
async function bombear() {
|
||
if (!pendente || emVoo || espera) return;
|
||
const falta = 100 - (performance.now() - ultimo);
|
||
if (falta > 0) {
|
||
espera = window.setTimeout(() => {
|
||
espera = 0;
|
||
bombear();
|
||
}, falta);
|
||
return;
|
||
}
|
||
const cmd = pendente;
|
||
pendente = null;
|
||
ultimo = performance.now();
|
||
emVoo = true;
|
||
try {
|
||
await postar('/servos/mover', cmd);
|
||
} catch (e) {
|
||
aoVivo = false;
|
||
avisos.erro(e);
|
||
} finally {
|
||
emVoo = false;
|
||
bombear();
|
||
}
|
||
}
|
||
|
||
async function parar() {
|
||
aoVivo = false;
|
||
pendente = null;
|
||
clearTimeout(espera);
|
||
espera = 0;
|
||
await tentar(() => postar('/servos/parar'));
|
||
}
|
||
onDestroy(() => {
|
||
if (aoVivo) parar();
|
||
});
|
||
|
||
const testar = (i: number, pulso: number) =>
|
||
tentar(() => postar('/servos/mover', { indice: i, pulso }), `${servos[i].nome}: ${pulso} µs por 2 s.`);
|
||
|
||
function editar(i: number, campo: keyof Servo, v: string) {
|
||
servos[i] = { ...servos[i], [campo]: campo === 'nome' ? v : Number(v) };
|
||
sujo = true;
|
||
aoVivo = false;
|
||
}
|
||
function adicionar() {
|
||
const canal = Array.from({ length: 16 }, (_, i) => i).find((c) => !servos.some((s) => s.canal === c));
|
||
if (canal === undefined) return;
|
||
servos = [...servos, { nome: `Servo ${servos.length + 1}`, canal, minimo: 1400, maximo: 1600, centro: 1500 }];
|
||
pulsos = [...pulsos, 1500];
|
||
sujo = true;
|
||
}
|
||
function remover(i: number) {
|
||
servos = servos.filter((_, k) => k !== i);
|
||
pulsos = pulsos.filter((_, k) => k !== i);
|
||
sujo = true;
|
||
}
|
||
async function salvar() {
|
||
await parar();
|
||
const r = await tentar(() => postar<{ servos: Servo[] }>('/servos/config', { servos: $state.snapshot(servos) }), 'Servos salvos. Nenhum movimento enviado.');
|
||
if (r) {
|
||
servos = r.servos;
|
||
pulsos = r.servos.map((s) => s.centro);
|
||
sujo = false;
|
||
}
|
||
}
|
||
|
||
const ocupadoPeloBraco = $derived(estado.motores?.dono === 'braco' && !!estado.motores.ativos.length);
|
||
</script>
|
||
|
||
<div class="diagnostico">
|
||
<Card titulo="Diagnóstico do PCA9685" descricao="Teste canal a canal, com limites conservadores (1000–2000 µs). Use com o braço parado.">
|
||
{#snippet acoes()}
|
||
{#if sujo}<Selo tom="aviso">Alterações não salvas</Selo>{/if}
|
||
<button class="btn pequeno" onclick={adicionar} disabled={servos.length >= 16}><Plus size={14} /> Servo</button>
|
||
<button class="btn pequeno primario" onclick={salvar} disabled={!sujo}><Save size={14} /> Salvar</button>
|
||
{/snippet}
|
||
{#if ocupadoPeloBraco}
|
||
<p class="aviso pequeno-texto">O braço está usando os motores. Pare o braço antes de testar aqui.</p>
|
||
{/if}
|
||
<Toggle
|
||
rotulo="Controle ao vivo"
|
||
ajuda="Arrastar o pulso envia ao motor (até 10 vezes por segundo)"
|
||
checked={aoVivo}
|
||
disabled={!disponivel || sujo || estado.estop.ativo || ocupadoPeloBraco}
|
||
onchange={(v) => (v ? (aoVivo = true) : parar())}
|
||
/>
|
||
</Card>
|
||
|
||
<div class="servos">
|
||
{#each servos as s, i (i)}
|
||
<Card>
|
||
<div class="campos">
|
||
<label class="campo nome"><span>Nome</span><input type="text" maxlength="40" value={s.nome} onchange={(e) => editar(i, 'nome', e.currentTarget.value)} /></label>
|
||
<label class="campo"><span>Canal</span><input type="number" min="0" max="15" value={s.canal} onchange={(e) => editar(i, 'canal', e.currentTarget.value)} /></label>
|
||
<label class="campo"><span>Mín. µs</span><input type="number" min="1000" max="2000" value={s.minimo} onchange={(e) => editar(i, 'minimo', e.currentTarget.value)} /></label>
|
||
<label class="campo"><span>Centro</span><input type="number" min="1000" max="2000" value={s.centro} onchange={(e) => editar(i, 'centro', e.currentTarget.value)} /></label>
|
||
<label class="campo"><span>Máx. µs</span><input type="number" min="1000" max="2000" value={s.maximo} onchange={(e) => editar(i, 'maximo', e.currentTarget.value)} /></label>
|
||
</div>
|
||
<Slider rotulo="Pulso" valor={pulsos[i] ?? s.centro} min={s.minimo} max={s.maximo} passo={5} unidade=" µs" oninput={(v) => enfileirar(i, v)} />
|
||
<div class="linha">
|
||
<button class="btn pequeno" disabled={!disponivel || sujo} onclick={() => testar(i, pulsos[i])}><Zap size={14} /> Testar 2 s</button>
|
||
<button class="btn pequeno" disabled={!disponivel || sujo} onclick={() => testar(i, s.centro)}>Centro</button>
|
||
<span class="espaco"></span>
|
||
<button class="btn pequeno icone fantasma" aria-label="Remover {s.nome}" onclick={() => remover(i)}><Trash2 size={14} /></button>
|
||
</div>
|
||
</Card>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.diagnostico {
|
||
display: grid;
|
||
gap: var(--e-4);
|
||
}
|
||
.servos {
|
||
display: grid;
|
||
gap: var(--e-4);
|
||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
|
||
}
|
||
.campos {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: var(--e-2);
|
||
}
|
||
.campos .nome {
|
||
grid-column: 1 / -1;
|
||
}
|
||
.campos input {
|
||
min-height: 34px;
|
||
padding: var(--e-1) var(--e-2);
|
||
}
|
||
.espaco {
|
||
flex: 1;
|
||
}
|
||
.aviso {
|
||
color: var(--aviso);
|
||
}
|
||
</style>
|