feat(painel): Ao vivo em duas colunas e página "Ligações da placa"

Ao vivo no PC: a câmera deixa de ficar presa no topo e ganha cartões
embaixo (Gestos, Celular, Câmera, Acontecimentos, Aparência e
Personalidade); à direita ficam Vida e O que ele aprendeu. No celular
segue uma coluna só, na mesma ordem de antes (display: contents +
order).

Ligações da placa (#/ligacoes): mapa do conector de 26 pinos do
Orange Pi Zero 3 com os dispositivos registrados (lib/ligacoes.ts):
- PIR: VCC no 5 V (2), OUT no pino que a placa informa (CARINHA_PIR,
  padrão gpiochip1:70 = PC6 = pino 11), GND no 9; avisos de 3,3 V nos
  GPIOs e da ordem dos terminais que muda entre módulos;
- PCA9685: 3,3 V (1), SDA (3), SCL (5), GND (6); servos na fonte
  externa com terra comum;
- tela SPI do overlay tft35: pinos 18, 19, 21, 22, 23 e 24 reservados.
Estado ao vivo de cada um (sinal do PIR, PCA9685 no I²C), destaque dos
pinos ao tocar no dispositivo, atalhos em Ajustes, Ao vivo e Ctrl+K e
tópico no guia.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-09 18:18:39 -03:00
co-authored by Claude Opus 5.5
parent b4cf13df11
commit 47a836276a
8 changed files with 547 additions and 42 deletions
+77 -38
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {
Brain, Camera, Download, Eye, Footprints, Gamepad2, Hand, History, Palette, PenLine, RefreshCw, Repeat, ScanFace, Settings2, Smile, Sparkles, Video, Wand2,
Brain, Cable, Camera, Download, Eye, Footprints, Gamepad2, Hand, History, Palette, PenLine, RefreshCw, Repeat, ScanFace, Settings2, Smile, Sparkles, Video, Wand2,
} from '@lucide/svelte';
import { onDestroy, onMount } from 'svelte';
import { postar } from '../lib/api';
@@ -125,16 +125,16 @@ const pirAtivo = $derived(
</div>
</section>
<div class="lista">
<VidaCartao />
{#if estado.vida?.config.ligado}<AprendizadoCartao />{/if}
<!-- No PC: à direita Vida e o que ele aprendeu; embaixo da câmera, o resto.
No celular as duas colunas se desfazem (display: contents) e a ordem vem do `order`. -->
<div class="coluna direita">
<div class="c-vida"><VidaCartao /></div>
{#if estado.vida?.config.ligado}<div class="c-aprendizado"><AprendizadoCartao /></div>{/if}
<Grupo titulo="Celular" ajuda="celular">
<Celular />
<Linha icone={RefreshCw} cor="#0a84ff" rotulo="Recarregar tela do celular" onclick={recarregarCelular} disabled={!estado.celularOnline} />
</Grupo>
</div>
<Grupo titulo="Câmera" ajuda="camera" rodape={transmitindo && rastreio ? rastreio.texto : 'O vídeo vai direto do celular para este navegador (WebRTC); nada é gravado.'}>
<div class="coluna esquerda">
<div class="c-camera"><Grupo titulo="Câmera" ajuda="camera" rodape={transmitindo && rastreio ? rastreio.texto : 'O vídeo vai direto do celular para este navegador (WebRTC); nada é gravado.'}>
<Linha icone={Video} cor="#34c759" rotulo="Transmitir câmera do celular" detalhe={video?.exibindo ? video.qualidade : ''}>
{#snippet acessorio()}
<Interruptor rotulo="Transmitir câmera do celular" checked={transmitindo} disabled={!cfg} onchange={(v) => salvarApp({ preview: v })} />
@@ -163,9 +163,9 @@ const pirAtivo = $derived(
</Linha>
{/if}
<Linha icone={Settings2} cor="#8e8e93" rotulo="Qualidade, câmera frontal/traseira e sensores" href="#/ajustes" />
</Grupo>
</Grupo></div>
<Grupo titulo="Gestos" ajuda="gestos" rodape="Teclas 1 a 9 disparam os nove primeiros. Ctrl+K busca qualquer gesto.">
<div class="c-gestos"><Grupo titulo="Gestos" ajuda="gestos" rodape="Teclas 1 a 9 disparam os nove primeiros. Ctrl+K busca qualquer gesto.">
<Linha icone={Smile} cor="#ffcc00" rotulo="Favoritos">
<Gestos />
</Linha>
@@ -188,9 +188,31 @@ const pirAtivo = $derived(
</Linha>
<Linha icone={Wand2} cor="#af52de" rotulo="Criar com IA" detalhe="Expressões, atividades e figurinhas novas" href="#/criar" />
<Linha icone={Sparkles} cor="#ff2d55" rotulo="Todos os gestos e reações" href="#/reacoes" />
</Grupo>
</Grupo></div>
<Grupo titulo="Aparência" ajuda="aparencia">
<div class="c-acontecimentos"><Grupo titulo="Acontecimentos" ajuda="acontecimentos">
<Linha icone={Footprints} cor="#34c759" rotulo="Sensor de presença (PIR)">
{#snippet acessorio()}<PresencaPir />{/snippet}
</Linha>
<Linha icone={History} cor="#30b0c7" rotulo="Eventos" detalhe="Presença, movimento e som, com foto" href="#/eventos" />
<Linha icone={Cable} cor="#8e8e93" rotulo="Ligações da placa" detalhe="Em que pino vai cada fio do PIR e dos servos" href="#/ligacoes" />
{#if estado.feed.length}
<ol class="feed">
{#each estado.feed.slice(0, 12) as item (item.id)}
<li class={item.tipo}><time class="mono">{hora(item.quando)}</time> <span>{item.texto}</span></li>
{/each}
</ol>
{:else}
<p class="vazio-feed">Gestos, respostas e reações aparecem aqui na hora.</p>
{/if}
</Grupo></div>
<div class="c-celular"><Grupo titulo="Celular" ajuda="celular">
<Celular />
<Linha icone={RefreshCw} cor="#0a84ff" rotulo="Recarregar tela do celular" onclick={recarregarCelular} disabled={!estado.celularOnline} />
</Grupo></div>
<div class="c-aparencia"><Grupo titulo="Aparência" ajuda="aparencia">
<Linha
icone={Palette}
cor="#ff9500"
@@ -202,9 +224,9 @@ const pirAtivo = $derived(
<span class="amostra-cor" style="--c: {hex(estado.aparencia.cor)}" aria-hidden="true"></span>
{/snippet}
</Linha>
</Grupo>
</Grupo></div>
<Grupo titulo="Personalidade" ajuda="personalidade" rodape={estado.ia?.erro || 'Qwen escolhe expressões a partir de sons e movimentos do celular.'}>
<div class="c-personalidade"><Grupo titulo="Personalidade" ajuda="personalidade" rodape={estado.ia?.erro || 'Qwen escolhe expressões a partir de sons e movimentos do celular.'}>
<Linha
icone={Brain}
cor="#5e5ce6"
@@ -215,30 +237,14 @@ const pirAtivo = $derived(
<Interruptor rotulo="Reagir sozinho com IA local" checked={!!estado.ia?.ativo} disabled={!estado.ia} onchange={iaLocal} />
{/snippet}
</Linha>
</Grupo>
<Grupo titulo="Acontecimentos" ajuda="acontecimentos">
<Linha icone={Footprints} cor="#34c759" rotulo="Sensor de presença (PIR)">
{#snippet acessorio()}<PresencaPir />{/snippet}
</Linha>
<Linha icone={History} cor="#30b0c7" rotulo="Eventos" detalhe="Presença, movimento e som, com foto" href="#/eventos" />
{#if estado.feed.length}
<ol class="feed">
{#each estado.feed.slice(0, 12) as item (item.id)}
<li class={item.tipo}><time class="mono">{hora(item.quando)}</time> <span>{item.texto}</span></li>
{/each}
</ol>
{:else}
<p class="vazio-feed">Gestos, respostas e reações aparecem aqui na hora.</p>
{/if}
</Grupo>
</Grupo></div>
</div>
</div>
<style>
.ao-vivo {
display: grid;
gap: var(--e-4);
gap: var(--e-5);
align-items: start;
}
.atividades {
@@ -380,12 +386,22 @@ const pirAtivo = $derived(
0%, 100% { box-shadow: 0 0 0 0 rgb(52 199 89 / 0.7); }
50% { box-shadow: 0 0 0 7px rgb(52 199 89 / 0); }
}
/* ---------- lista */
.lista {
display: grid;
gap: var(--e-5);
/* ---------- cartões: uma coluna no celular (na ordem abaixo), duas no PC */
.coluna {
display: contents;
}
.coluna > div {
min-width: 0;
}
.palco { order: 0; }
.c-vida { order: 1; }
.c-aprendizado { order: 2; }
.c-celular { order: 3; }
.c-camera { order: 4; }
.c-gestos { order: 5; }
.c-aparencia { order: 6; }
.c-personalidade { order: 7; }
.c-acontecimentos { order: 8; }
.amostra-cor {
width: 22px;
height: 22px;
@@ -429,10 +445,33 @@ const pirAtivo = $derived(
@media (min-width: 1024px) {
.ao-vivo {
grid-template-columns: minmax(0, 1.5fr) minmax(340px, 1fr);
grid-template-areas: 'palco direita' 'esquerda direita';
grid-template-rows: auto 1fr;
gap: var(--e-5);
}
/* com cartões embaixo, a câmera deixa de ficar presa no topo (rolaria por cima deles) */
.palco {
top: calc(var(--topo-altura) + var(--e-5));
grid-area: palco;
position: relative;
top: auto;
}
.coluna {
display: grid;
align-content: start;
gap: var(--e-5);
min-width: 0;
}
.esquerda {
grid-area: esquerda;
}
.direita {
grid-area: direita;
}
.esquerda > .c-gestos { order: 1; }
.esquerda > .c-celular { order: 2; }
.esquerda > .c-camera { order: 3; }
.esquerda > .c-acontecimentos { order: 4; }
.esquerda > .c-aparencia { order: 5; }
.esquerda > .c-personalidade { order: 6; }
}
</style>