'use strict'; const cards = document.querySelector('#cards'), statusEl = document.querySelector('#status'); let rows = [], available = false, dirty = false, busy = false; const live = document.querySelector('#live'); let pending = null, liveRequest = null, liveTimer = null, lastSent = 0; function cancelLive() { live.checked = false; pending = null; clearTimeout(liveTimer); liveTimer = null; } function queueLive(index, pulse) { if (!live.checked || !available || dirty || busy) return; pending = {indice:index,pulso:pulse}; pumpLive(); } function pumpLive() { if (!pending || liveRequest || liveTimer || !live.checked) return; const wait = 100 - (performance.now() - lastSent); if (wait > 0) { liveTimer = setTimeout(() => {liveTimer = null; pumpLive();}, wait); return; } const command = pending; pending = null; lastSent = performance.now(); liveRequest = api('mover', command).then(() => { statusEl.textContent = `Ao vivo: canal ${rows[command.indice].canal}, ${command.pulso} µs. Desligamento 2 s após o último comando.`; }).catch(e => { cancelLive(); statusEl.textContent = e.message; }).finally(() => { liveRequest = null; pumpLive(); }); } async function stopLive() { cancelLive(); if (liveRequest) await liveRequest; await api('parar',{}); } async function api(path, data) { const response = await fetch('/servos/' + path, data === undefined ? {} : {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}); const result = await response.json(); if (!response.ok) throw Error(result.erro || 'Falha no servidor'); return result; } function mark() { cancelLive(); dirty = true; document.querySelector('#dirty').textContent = 'Alterações não salvas'; updateButtons(); } function updateButtons() { cards.querySelectorAll('[data-test]').forEach(b => b.disabled = !available || dirty || busy); document.querySelector('#add').disabled = rows.length >= 16 || busy; document.querySelector('#save').disabled = busy; live.disabled = !available || dirty || busy; } function render() { cards.replaceChildren(); rows.forEach((row, index) => { const section = document.createElement('section'); section.className = 'card'; const h = document.createElement('h2'); h.textContent = row.nome; section.append(h); const fields = document.createElement('div'); fields.className = 'fields'; for (const [key, title, min, max] of [['nome','Nome'],['canal','Canal',0,15],['minimo','Mínimo (µs)',1000,2000],['maximo','Máximo (µs)',1000,2000],['centro','Centro (µs)',1000,2000]]) { const label = document.createElement('label'); label.textContent = title; const input = document.createElement('input'); input.type = key === 'nome' ? 'text' : 'number'; input.value = row[key]; if (key === 'nome') input.maxLength = 40; else { input.min = min; input.max = max; input.step = 1; } input.addEventListener('input', () => { row[key] = key === 'nome' ? input.value : Number(input.value); mark(); if (key === 'nome') h.textContent = input.value; }); label.append(input); fields.append(label); } section.append(fields); const label = document.createElement('label'); label.textContent = 'Pulso de teste'; const range = document.createElement('input'); range.type = 'range'; range.min = row.minimo; range.max = row.maximo; range.step = 5; range.value = row.centro; const out = document.createElement('output'); out.textContent = range.value + ' µs'; range.addEventListener('input', () => {out.textContent = range.value + ' µs'; queueLive(index, Number(range.value));}); label.append(range, out); section.append(label); const actions = document.createElement('div'); actions.className = 'actions'; for (const [text, center] of [['Testar por 2 s',false],['Testar centro',true]]) { const button = document.createElement('button'); button.textContent = text; button.dataset.test = '1'; button.addEventListener('click', () => run(async () => { await api('mover',{indice:index,pulso:center ? row.centro : Number(range.value)}); statusEl.textContent = `${row.nome}: comando enviado ao canal ${row.canal}. Sinal será desligado em até 2 s.`; })); actions.append(button); } section.append(actions); cards.append(section); }); updateButtons(); } async function run(action) { busy = true; updateButtons(); try { await action(); } catch (e) { statusEl.textContent = e.message; } finally { busy = false; updateButtons(); } } document.querySelector('#stop').onclick = () => run(async () => { await stopLive(); statusEl.textContent = 'Sinais desligados em todos os canais.'; }); live.onchange = () => { if (!live.checked) run(async () => {await stopLive(); statusEl.textContent = 'Controle ao vivo desligado. Sinais desligados.';}); else statusEl.textContent = 'Controle ao vivo ativado. Arraste um deslizador para mover.'; }; document.addEventListener('visibilitychange', () => { if (document.hidden && live.checked) run(stopLive); }); document.querySelector('#save').onclick = () => run(async () => { cancelLive(); if (liveRequest) await liveRequest; const data = await api('config',{servos:rows}); rows = data.servos; dirty = false; document.querySelector('#dirty').textContent = ''; render(); statusEl.textContent = 'Configurações salvas. Nenhum movimento enviado.'; }); document.querySelector('#add').onclick = () => { const channel = Array.from({length:16},(_,i)=>i).find(i=>!rows.some(r=>r.canal===i)); if (channel === undefined) return; rows.push({nome:`Servo ${rows.length+1}`,canal:channel,minimo:1400,maximo:1600,centro:1500}); mark(); render(); }; run(async () => { const data = await api('status'); rows = data.servos; available = data.disponivel; render(); statusEl.textContent = available ? `PCA9685 disponível. ${data.ativos.length ? 'Teste em andamento; desligamento automático em até 2 s.' : 'Sem testes em andamento.'}` : 'Controle indisponível neste servidor. Abra a aba no Orange Pi. ' + data.erro; });