- carinha.html assina /eventos (expressao, app_config, conversa, reacao, recarregar) e volta ao polling de /fila sozinho se o SSE cair; foto dos ajustes a cada (re)conexao; token (?t=) vai em todo fetch ao servidor - APK 0.8-token: token nas preferencias e no menu de toque longo, enviado na pagina, no HTTP do Java e ao llama-server (--api-key); trocar servidor por Intent exige o token salvo; camera/microfone so para o servidor configurado ou a copia local - cerebro/conversa mandam a chave ao llama-server do S10 - servidor: evento SSE 'recarregar'; remove /camera/frame e /app/connect; enderecos antigos redirecionam para a secao do painel novo - remove dashboard, painel, conversa, emojis, treinamento, braco, servos, api, scripts e CSS antigos, audio-robot.js e servir-local.bat - logica pura do painel em web/src/lib/nucleo.ts com testes node --test (substitui test_movimentos.js e test_rastreamento.js) - testes de navegador portados para o painel novo (rtc, rosto, rastreamento) - README, docs/PAINEL.md e docs/SEGURANCA.md reescritos; versao 0.8.0; carinha-pc.service le /etc/carinha.env Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
4.8 KiB
TypeScript
175 lines
4.8 KiB
TypeScript
/**
|
|
* 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<StatusBraco | null>(null);
|
|
alvo = $state<number[]>(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<StatusBraco>('/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<StatusBraco>('/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;
|
|
}
|
|
});
|
|
});
|