Files
carinha-control-panel/braco.js
T

50 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const $=id=>document.getElementById(id);
let config, target=Array(7).fill(90), current=target.slice(), enabled=false, dirty=false, inflight=null, needed=false, playIndex=-1, pauseUntil=0, lastTick=performance.now();
let sequence=null;
async function api(path,data){const r=await fetch('/braco/'+path,data===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});const d=await r.json();if(!r.ok)throw Error(d.erro||'Falha');return d;}
function message(text){$('console').textContent=text;}
function pulseAngle(p,c){const center=c.centro??1500;return p<=center?90*(p-500)/(center-500):90+90*(p-center)/(2500-center);}
function bounds(i){const ch=config.articulacoes[i].canal,c=config.calibracao[ch];let lo=pulseAngle(c.minimo,c),hi=pulseAngle(c.maximo,c);if(ch===5){const pair=config.calibracao[6];lo=Math.max(lo,180-pulseAngle(pair.maximo,pair));hi=Math.min(hi,180-pulseAngle(pair.minimo,pair));}return [Math.ceil(lo),Math.floor(hi)];}
function controls(){ $('enable').disabled=!config?.disponivel||dirty; document.querySelectorAll('[data-execute]').forEach(b=>{b.disabled=!enabled||dirty;b.title=b.disabled?'Marque Ativar movimento e salve a calibração':'Executar nos oito canais';}); $('play').disabled=!enabled||!config.posturas.length; $('loadPose').disabled=!config.posturas.length; $('deletePose').disabled=!config.posturas.length; }
function render(){
$('sliders').replaceChildren();
config.articulacoes.forEach((a,i)=>{const [lo,hi]=bounds(i);target[i]=Math.max(lo,Math.min(hi,target[i]));const row=document.createElement('div');row.className='row';const wrap=document.createElement('div'),label=document.createElement('label'),range=document.createElement('input'),out=document.createElement('output');label.textContent=`${a.nome} · canal ${a.canal}${a.canal===5?' + 6 invertido':''} · limite ${lo}–${hi}°`;range.type='range';range.min=lo;range.max=hi;range.step=1;range.value=target[i];range.id='joint'+i;label.htmlFor=range.id;out.id='value'+i;out.textContent=String(Math.round(target[i])).padStart(3,'0');range.oninput=()=>{playIndex=-1;target[i]=Number(range.value);out.textContent=String(target[i]).padStart(3,'0');if(enabled)needed=true;else message('Posição escolhida na tela. Movimento desligado.');};wrap.append(label,range);row.append(wrap,out);$('sliders').append(row);});
$('calibration').replaceChildren();config.calibracao.forEach((c,i)=>{const card=document.createElement('article');card.textContent=`Canal ${i}`;for(const [key,name] of [['minimo','Mínimo (µs)'],['centro','Centro (µs)'],['maximo','Máximo (µs)']]){const label=document.createElement('label');label.textContent=name;const input=document.createElement('input');input.type='number';input.min=500;input.max=2500;input.step=10;input.value=c[key]??1500;input.oninput=()=>{c[key]=Number(input.value);dirty=true;stop().catch(e=>message(e.message));controls();message('Calibração alterada. Salve para habilitar movimento.');};label.append(input);card.append(label);}$('calibration').append(card);});
$('count').textContent=config.posturas.length+' posturas';$('poses').replaceChildren();config.posturas.forEach((pose,i)=>{const option=document.createElement('option');option.value=i;option.textContent=pose.nome;$('poses').append(option);});if(!config.posturas.length){const o=document.createElement('option');o.textContent='Sem posturas';$('poses').append(o);}controls();
}
function showTarget(){target.forEach((v,i)=>{$('joint'+i).value=v;$('value'+i).textContent=String(Math.round(v)).padStart(3,'0');});}
async function stop(){enabled=false;needed=false;playIndex=-1;$('enable').checked=false;controls();if(inflight)await inflight;await api('parar',{});message('Sinais desligados. Movimento desativado.');}
async function persist(){await stop();config=await api('config',{calibracao:config.calibracao,posturas:config.posturas});dirty=false;render();}
$('enable').onchange=()=>{if($('enable').checked){enabled=true;needed=false;current=target.slice();lastTick=performance.now();controls();message('Ativado. Arraste uma articulação para enviar a postura aos oito canais.');}else stop().catch(e=>message(e.message));};
$('stop').onclick=()=>stop().catch(e=>message(e.message));
$('speed').oninput=()=>$('speedValue').textContent=$('speed').value;
$('saveCalibration').onclick=async()=>{try{await persist();message('Calibração salva. Movimento desativado.');}catch(e){message(e.message);}};
$('savePose').onclick=async()=>{try{const name=prompt('Nome da postura:',`Postura ${config.posturas.length+1}`);if(!name)return;config.posturas.push({nome:name,angulos:target.slice()});await persist();message('Postura salva, sem movimento.');}catch(e){config=await api('status');render();message(e.message);}};
$('deletePose').onclick=async()=>{try{config.posturas.splice(Number($('poses').value),1);await persist();message('Postura excluída.');}catch(e){message(e.message);}};
$('loadPose').onclick=()=>{playIndex=-1;target=config.posturas[Number($('poses').value)].angulos.slice();showTarget();if(enabled)needed=true;message(enabled?'Movendo para a postura escolhida.':'Postura carregada apenas na tela.');};
$('play').onclick=()=>{if(!enabled||!config.posturas.length)return;sequence=config.posturas.map(p=>p.angulos.slice());playIndex=0;target=sequence[0].slice();needed=true;pauseUntil=0;showTarget();message('Reproduzindo posturas salvas uma vez.');};
$('reset').onclick=async()=>{try{await stop();target=Array(7).fill(90);current=target.slice();showTarget();message('Centro selecionado apenas na tela.');}catch(e){message(e.message);}};
$('export').onclick=()=>{const blob=new Blob([JSON.stringify({formato:'carinha-braco-v1',calibracao:config.calibracao,posturas:config.posturas},null,2)],{type:'application/json'});const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='braco-posturas.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};
$('import').onclick=()=>$('importFile').click();
$('importFile').onchange=async()=>{try{const f=$('importFile').files[0];if(!f)return;if(f.size>65536)throw Error('Arquivo maior que 64 KB');const data=JSON.parse(await f.text());if(data.formato!=='carinha-braco-v1')throw Error('Use arquivo exportado por esta aba');await stop();config=await api('config',{calibracao:data.calibracao,posturas:data.posturas});dirty=false;target=Array(7).fill(90);render();message('Importado, sem movimento.');}catch(e){message(e.message);}finally{$('importFile').value='';}};
setInterval(()=>{
const now=performance.now(),dt=Math.min(.2,(now-lastTick)/1000);lastTick=now;
if(!enabled||dirty||inflight||!needed)return;
const step=Number($('speed').value)*dt;
const distance=Math.max(...current.map((v,i)=>Math.abs(target[i]-v)));
const fraction=distance?Math.min(1,step/distance):1;
current=current.map((v,i)=>v+(target[i]-v)*fraction);
const pose=current.map(v=>Math.round(v*100)/100);
inflight=api('mover',{angulos:pose}).then(()=>{
if(!enabled)return;
const reached=current.every((v,i)=>Math.abs(v-target[i])<.01);
if(reached&&playIndex>=0){if(!pauseUntil)pauseUntil=performance.now()+700;if(performance.now()>=pauseUntil){playIndex++;pauseUntil=0;if(playIndex>=sequence.length){playIndex=-1;needed=false;message('Sequência concluída. Sinais desligam após 2 s.');}else{target=sequence[playIndex].slice();showTarget();}}}
else if(reached){needed=false;message('Comando enviado. Sinais desligam após 2 s.');}
}).catch(async e=>{enabled=false;needed=false;playIndex=-1;$('enable').checked=false;controls();message(e.message);try{await api('parar',{});}catch{}}).finally(()=>{inflight=null;});
},100);
document.addEventListener('visibilitychange',()=>{if(document.hidden&&enabled)stop().catch(()=>{});});
api('status').then(data=>{config=data;render();$('connection').textContent=data.disponivel?'PCA9685 · conectado':'PCA9685 indisponível';message('Pronto. Movimento desativado. Calibre antes de ampliar o curso.');}).catch(e=>message(e.message));
window.BracoUI={calibration:()=>config?.calibracao,markCalibration(ch,key,us){config.calibracao[ch][key]=us;if(ch===5||ch===6){const other=11-ch,sum=(config.calibracao[5].centro??1500)+(config.calibracao[6].centro??1500);if(key!=='centro')config.calibracao[other][key==='minimo'?'maximo':'minimo']=sum-us;}dirty=true;render();message('Limite marcado. Clique Salvar calibração para gravar.');},canExecute:()=>enabled&&!dirty,ready:()=>!!config,bounds:()=>config.articulacoes.map((a,i)=>bounds(i)),pose:()=>enabled?current.slice():target.slice(),stop,
execute(steps){if(!enabled||dirty)throw Error('Ative o movimento e salve a calibração primeiro');sequence=steps.map(s=>s.slice());playIndex=0;target=sequence[0].slice();needed=true;pauseUntil=0;showTarget();message('Executando movimento sincronizado.');}};