216 lines
12 KiB
JavaScript
216 lines
12 KiB
JavaScript
"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;}
|
|
};
|