"use strict"; const $ = (id) => document.getElementById(id); let recargaPendente = null; let recargaInicio = 0; let appConfig = null; let imagemURL = null; let recebendoImagem = false; let salvandoConfig = false; let sequencia = -1; let ultimoStatus = null; let consultandoStatus = false; async function api(rota, dados) { const r = await fetch(rota, {cache: 'no-store', signal: AbortSignal.timeout(rota === '/app/connect' ? 65000 : 3500), ...(dados === undefined ? {} : {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(dados)})}); const body = await r.json(); if (!r.ok) throw new Error(body.erro || 'Servidor respondeu HTTP ' + r.status); return body; } function ajustarCamera() { const ligada = appConfig && appConfig.preview; $('toggleCamera').textContent = ligada ? 'Desligar câmera' : 'Ligar câmera'; if (!ligada) esconderImagem('Transmissão desligada. O rosto e os sensores continuam funcionando.'); if (window.CarinhaVideo && !ligada) window.CarinhaVideo.parar(); } function esconderImagem(texto) { $('cameraImagem').hidden = true; $('cameraImagem').removeAttribute('src'); $('cameraMensagem').hidden = false; $('cameraMensagem').textContent = texto; if (imagemURL) URL.revokeObjectURL(imagemURL); imagemURL = null; sequencia = -1; } async function salvarApp(parte) { if (salvandoConfig) return; salvandoConfig = true; CAMPOS.forEach(([chave]) => { $('ajuste-' + chave).disabled = true; }); $('configEstado').textContent = 'Salvando…'; $('toggleCamera').disabled = true; try { appConfig = (await api('/app/config', parte)).config; atualizarCampos(); ajustarCamera(); $('configEstado').textContent = 'Salvo no servidor. Aguardando o celular aplicar.'; } catch (e) { $('configEstado').textContent = 'Não foi possível salvar: ' + e.message; atualizarCampos(); } finally { salvandoConfig = false; $('toggleCamera').disabled = false; CAMPOS.forEach(([chave]) => { $('ajuste-' + chave).disabled = false; }); } } const CAMPOS = [ ['rastrearRosto', 'Rastrear rosto'], ['rastrearMaos', 'Rastrear mãos'], ['seguirRastreamento', 'Olhos acompanharem o alvo'], ['mostrarRastreamento', 'Mostrar rastreamento no vídeo'], ['sensorMic', 'Microfone ligado'], ['sensorCamera', 'Câmera ligada'], ['micSono', 'Permitir microfone durante o sono'], ['cameraSono', 'Permitir câmera durante o sono'], ['cameraLado', 'Câmera', [['user','Frontal'],['environment','Traseira']]], ['videoResolucao', 'Resolução do vídeo', [['auto','Automática (padrão)'],['max','Máxima disponível'],['480','480p'],['720','720p'],['1080','1080p'],['2160','4K'],['4320','8K']]], ['videoFPS', 'FPS do vídeo', [[0,'Automático (padrão)'],[-1,'Máximo disponível'],[15,'15'],[24,'24'],[30,'30'],[60,'60'],[120,'120'],[240,'240']]], ['boca', 'Mostrar boca'], ['sobrancelhas', 'Mostrar sobrancelhas'], ['auto', 'Gestos espontâneos'], ['audio', 'Sons da Carinha'], ['ouvir', 'Reagir aos sons'], ['reagir', 'Reagir ao movimento'], ['volume', 'Volume dos sons', 0, 1, 0.05], ['sompausa', 'Silêncio para dormir (s)', 1, 120, 1], ['economia', 'Reduzir brilho em repouso'], ['brilho', 'Brilho normal', 0.01, 1, 0.01], ['brilhoRepouso', 'Brilho em repouso', 0.01, 1, 0.01], ['repousoTela', 'Espera para reduzir brilho (s)', 5, 300, 5], ['somlim', 'Nível para perceber som', 0, 1, 0.01], ['somganho', 'Aumento de som para susto', 0, 1, 0.01], ['somaltolim', 'Som mínimo para susto', 0, 1, 0.01], ['somcooldown', 'Intervalo entre sustos por som (s)', 0, 120, 1], ['movlim', 'Movimento mínimo', 0, 1, 0.01], ['movforte', 'Movimento para susto', 0, 1, 0.01], ['movcooldown', 'Intervalo entre sustos por movimento (s)', 0, 120, 1], ['movolhar', 'Quanto os olhos acompanham', 0, 100, 1], ]; function montarCampos() { CAMPOS.forEach(([chave, texto, min, max, passo], i) => { const area = document.createElement('div'); const label = document.createElement('label'); const select = Array.isArray(min); const input = document.createElement(select ? 'select' : 'input'); input.id = 'ajuste-' + chave; label.htmlFor = input.id; if (!select) input.type = min === undefined ? 'checkbox' : 'range'; label.textContent = texto; if (select) { min.forEach(([v,t]) => input.add(new Option(t, String(v)))); area.className = 'ajuste'; area.append(label, input); } else if (input.type === 'range') { input.min = min; input.max = max; input.step = passo; area.className = 'ajuste-range'; const output = document.createElement('output'); output.id = 'valor-' + chave; label.appendChild(output); area.append(label, input); input.oninput = () => { output.textContent = input.value; }; } else { area.className = 'ajuste'; area.append(label, input); } input.onchange = () => salvarApp({[chave]: input.type === 'checkbox' ? input.checked : select && chave !== 'videoFPS' ? input.value : Number(input.value)}); $(i < 4 ? 'ajustesRastreamento' : i < 23 ? 'ajustesBasicos' : 'ajustesAvancados').appendChild(area); }); } function atualizarCampos() { if (!appConfig) return; CAMPOS.forEach(([chave]) => { const input = $('ajuste-' + chave); if (input.type === 'checkbox') input.checked = appConfig[chave]; else if (document.activeElement !== input) { input.value = appConfig[chave]; if ($('valor-' + chave)) $('valor-' + chave).textContent = appConfig[chave]; } }); } async function statusApp() { if (consultandoStatus) return; consultandoStatus = true; try { const s = await api('/app/status'); ultimoStatus = s; if (!salvandoConfig) { appConfig = await api('/app/config'); atualizarCampos(); ajustarCamera(); } $('conexaoEstado').textContent = s.online ? 'Conectado' : 'Sem conexão'; $('conexaoEstado').classList.toggle('ativo', s.online); if(s.online && s.celular_ip && document.activeElement !== $('celularIP')) $('celularIP').value=s.celular_ip; if(s.servidor_ip && document.activeElement !== $('pcIP')) $('pcIP').value=s.servidor_ip; if(recargaPendente && s.recarga?.id===recargaPendente && s.recarga.confirmada){$('conectarEstado').textContent='Tela atualizada: confirmação recebida do celular.';recargaPendente=null;} else if(recargaPendente && Date.now()-recargaInicio>12000){$('conectarEstado').textContent='O celular não confirmou a recarga. Feche e reabra o Carinha uma vez para carregar o suporte atualizado.';recargaPendente=null;} const e = s.estado || {}; const bateria = e.bateria; const nivel = bateria?.nivel; $('bateriaCelular').textContent = s.online && typeof nivel === 'number' && nivel >= 0 && nivel <= 100 ? `Bateria do celular: ${Math.round(nivel)}%${bateria.carregando ? ' · carregando' : ''}` : 'Bateria do celular: aguardando dados do app.'; $('rostoConfirmado').textContent = s.online ? 'No celular: ' + (e.rosto || '?') + (e.dormindo ? ', dormindo.' : ', acordado.') : 'Aguardando confirmação do celular.'; ['mic', 'mov'].forEach((nome) => { const chave = nome === 'mic' ? 'microfone' : 'movimento'; const ativo = s.online && e[nome === 'mic' ? 'microfone_ativo' : 'camera_ativa']; const v = ativo ? Number(e[chave] || 0) : 0; $(nome + 'Nivel').value = v; $(nome + 'Valor').textContent = ativo ? Math.round(v * 100) + '%' : '—'; }); $('sensorEstado').textContent = s.online ? (e.camera_ativa ? ('Câmera ' + (appConfig?.cameraLado === 'environment' ? 'traseira' : 'frontal')) : 'Câmera desativada') + (e.centro ? ' · centro do movimento: ' + e.centro.map(v => Math.round(v * 100) + '%').join(', ') : '') + (typeof e.brilho === 'number' ? ' · brilho: ' + Math.round(e.brilho * 100) + '%' : '') : 'Abra o app no celular ou use Conectar e abrir o app.'; if (appConfig && !salvandoConfig && e.config && Object.keys(appConfig).every(k => e.config[k] === appConfig[k])) $('configEstado').textContent = s.online ? 'Configurações aplicadas no celular.' : 'Configurações salvas no servidor. Celular sem conexão.'; if (appConfig && appConfig.videoRTC && window.CarinhaVideo) window.CarinhaVideo.atualizar(s, appConfig); if (appConfig && window.CarinhaRastreamento) window.CarinhaRastreamento.atualizar(s, appConfig); if (e.video && e.video.aviso) $('sensorEstado').textContent += ' · ' + e.video.aviso; if (appConfig && !appConfig.videoRTC && appConfig.preview && (!s.online || !s.camera_recente)) esconderImagem(s.online ? 'Aguardando imagem da câmera. Confira a permissão de câmera no celular.' : 'Celular sem conexão. A imagem antiga foi removida.'); } catch (e) { ultimoStatus = null; $('bateriaCelular').textContent = 'Bateria do celular: sem conexão.'; $('conexaoEstado').textContent = 'Servidor indisponível'; $('conexaoEstado').classList.remove('ativo'); esconderImagem('O servidor não respondeu. Confira o serviço no Orange Pi.'); if (window.CarinhaVideo) window.CarinhaVideo.parar(); if (window.CarinhaRastreamento) window.CarinhaRastreamento.parar(); } finally { consultandoStatus = false; } } async function imagem() { if (appConfig && appConfig.videoRTC) return; if (recebendoImagem || !appConfig || !appConfig.preview || !ultimoStatus || !ultimoStatus.camera_recente) return; recebendoImagem = true; try { const r = await fetch('/camera/frame?t=' + Date.now(), {cache:'no-store', signal:AbortSignal.timeout(2500)}); if (!r.ok || r.status === 204) return; const blob = await r.blob(); if (!appConfig.preview) return; const nova = URL.createObjectURL(blob); const anterior = imagemURL; imagemURL = nova; $('cameraImagem').src = nova; $('cameraImagem').hidden = false; $('cameraMensagem').hidden = true; if (anterior) URL.revokeObjectURL(anterior); sequencia = ultimoStatus.frame_seq; } catch (e) { esconderImagem('Imagem indisponível. Aguardando o próximo quadro.'); } finally { recebendoImagem = false; } } $('toggleCamera').onclick = () => salvarApp({preview: !appConfig.preview}); $('conectarForm').onsubmit = async (ev) => { ev.preventDefault(); $('conectarBotao').disabled = true; $('conectarEstado').textContent = 'Conectando ao celular…'; try { const r = await api('/app/connect', {ip: $('celularIP').value.trim(), pc: $('pcIP').value.trim(), porta:5555}); $('conectarEstado').textContent = r.mensagem; } catch (e) { $('conectarEstado').textContent = e.message; } finally { $('conectarBotao').disabled = false; } }; montarCampos(); (async () => { try { appConfig = await api('/app/config'); atualizarCampos(); ajustarCamera(); $('toggleCamera').disabled = false; await statusApp(); } catch (e) { $('configEstado').textContent = 'Falha ao carregar: ' + e.message; } })(); setInterval(statusApp, 1000); setInterval(imagem, 250); (function(){ const iaActive=document.getElementById('iaAtiva'),iaState=document.getElementById('iaEstado'),iaTest=document.getElementById('iaTestar'); let updating=false; async function request(path,data){const r=await fetch('/ia/'+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);return d;} async function refresh(){if(updating)return;try{const s=await request('status');iaActive.checked=s.ativo;iaTest.disabled=!s.ativo||s.ocupado;iaState.textContent=s.erro?'IA: '+s.erro:s.ocupado?'IA local pensando…':s.ultima?`${s.modelo} · ${s.executor} · última expressão: ${s.ultima.gesto} (${s.ultima.segundos} s)`:`${s.modelo} · ${s.executor} · ${s.ativo?'aguardando eventos':'desativado'}`;}catch(e){iaState.textContent=e.message;}} iaActive.onchange=async()=>{updating=true;try{await request('config',{ativo:iaActive.checked});}catch(e){iaState.textContent=e.message;}finally{updating=false;refresh();}}; iaTest.onclick=async()=>{iaTest.disabled=true;try{await request('evento',{evento:document.getElementById('iaEvento').value});iaState.textContent='Evento enviado para a IA local…';}catch(e){iaState.textContent=e.message;}}; refresh();setInterval(refresh,2000); })(); document.getElementById('recarregarTela').onclick=async()=>{ const button=document.getElementById('recarregarTela');button.disabled=true; try{const r=await api('/app/recarregar',{});document.getElementById('conectarEstado').textContent=r.mensagem;recargaPendente=(await api('/app/status')).recarga.id;recargaInicio=Date.now();} catch(e){document.getElementById('conectarEstado').textContent=e.message;} finally{button.disabled=false;} };