/** * Braço (/braco/*) e diagnóstico (/servos/*). Segurança: * - homem-morto: só há movimento enquanto o botão/slider está pressionado; cada pedido leva * um prazo curto (ttl_ms) e o servidor desliga os motores se ele vencer sem renovação; * - a interpolação e o limite de velocidade são do servidor, não deste navegador. */ import { pedir, postar } from './api'; import { avisos } from './avisos.svelte'; import { estado } from './estado.svelte'; import { limites } from './nucleo'; export { sequencias } from './nucleo'; export interface Calibracao { canal: number; minimo: number; maximo: number; centro: number; } export interface Postura { nome: string; angulos: number[]; } export interface StatusBraco { calibracao: Calibracao[]; posturas: Postura[]; disponivel: boolean; erro: string; dono: string | null; travado: boolean; atual: number[] | null; movendo: boolean; articulacoes: { nome: string; canal: number }[]; } export const PRAZO_MS = 450; const RENOVAR_MS = 150; class Braco { status = $state(null); alvo = $state(Array(7).fill(90)); velocidade = $state(60); /** Opt-in explícito antes de qualquer movimento físico. */ ativado = $state(false); segurando = $state(false); passoSequencia = $state<{ nome: string; indice: number; total: number } | null>(null); private relogio = 0; private emVoo = false; private sequencia: number[][] | null = null; private pausaAte = 0; faixas = $derived(this.status ? limites(this.status.calibracao, this.status.articulacoes) : []); async carregar() { try { this.status = await pedir('/braco/status'); this.limitarAlvo(); } catch (e) { avisos.erro(e); } } limitarAlvo() { if (!this.faixas.length) return; this.alvo = this.alvo.map((v, i) => Math.max(this.faixas[i][0], Math.min(this.faixas[i][1], v))); } /** Começa a segurar: renova o movimento a cada 150 ms até soltar. */ segurar(sequencia?: { nome: string; passos: number[][] }) { if (!this.ativado || this.segurando) return; if (sequencia) { this.sequencia = sequencia.passos; this.passoSequencia = { nome: sequencia.nome, indice: 0, total: sequencia.passos.length }; this.alvo = [...sequencia.passos[0]]; this.pausaAte = 0; } this.segurando = true; this.renovar(); this.relogio = window.setInterval(() => this.renovar(), RENOVAR_MS); } /** Soltou: para de renovar. O servidor desliga os motores quando o prazo vence (≤ 450 ms). */ soltar() { if (!this.segurando) return; this.segurando = false; clearInterval(this.relogio); if (this.passoSequencia) avisos.mostrar(`${this.passoSequencia.nome} interrompida ao soltar.`); this.sequencia = null; this.passoSequencia = null; } private async renovar() { if (this.emVoo || !this.segurando) return; this.emVoo = true; try { const r = await postar<{ movendo: boolean }>('/braco/mover', { angulos: $state.snapshot(this.alvo), velocidade: this.velocidade, ttl_ms: PRAZO_MS, }); if (!r.movendo && this.sequencia && this.passoSequencia) this.avancarSequencia(); } catch (e) { this.segurando = false; clearInterval(this.relogio); this.sequencia = null; this.passoSequencia = null; avisos.erro(e); } finally { this.emVoo = false; } } private avancarSequencia() { if (!this.pausaAte) { this.pausaAte = performance.now() + 700; return; } if (performance.now() < this.pausaAte) return; this.pausaAte = 0; const p = this.passoSequencia!; if (p.indice + 1 >= p.total) { avisos.ok(`${p.nome} concluída.`); this.sequencia = null; this.passoSequencia = null; this.segurando = false; clearInterval(this.relogio); return; } this.passoSequencia = { ...p, indice: p.indice + 1 }; this.alvo = [...this.sequencia![p.indice + 1]]; } async pararMotores() { this.soltar(); try { await postar('/braco/parar'); } catch (e) { avisos.erro(e); } } async desativar() { this.ativado = false; await this.pararMotores(); } async salvar(dados: { calibracao: Calibracao[]; posturas: Postura[] }, sucesso: string) { this.soltar(); try { await postar('/braco/parar'); this.status = await postar('/braco/config', dados); this.limitarAlvo(); avisos.ok(sucesso); return true; } catch (e) { avisos.erro(e); return false; } } } export const braco = new Braco(); // Segurança: aba escondida ou E-Stop param de segurar na hora. document.addEventListener('visibilitychange', () => document.hidden && braco.soltar()); $effect.root(() => { $effect(() => { if (estado.estop.ativo) { braco.soltar(); braco.ativado = false; } }); });