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>
315 lines
9.5 KiB
Svelte
315 lines
9.5 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { pedir } from '../lib/api';
|
|
import Card from '../lib/componentes/Card.svelte';
|
|
import { estado } from '../lib/estado.svelte';
|
|
import { dispositivos, NOME_TIPO, pinoDaLinha, PINOS, type Dispositivo, type Pino, type TipoPino } from '../lib/ligacoes';
|
|
|
|
// Conector de 26 pinos do Orange Pi Zero 3 com o que está ligado em cada pino. O pino do PIR
|
|
// vem da placa (CARINHA_PIR); o estado ao vivo do PIR e da PCA9685 vem do servidor.
|
|
let pir = $state<{ linha: string; disponivel: boolean; erro: string; ativo?: boolean } | null>(null);
|
|
let foco = $state<string | null>(null);
|
|
|
|
onMount(async () => {
|
|
try {
|
|
pir = (await pedir<{ pir: typeof pir }>('/historico/status')).pir;
|
|
} catch {
|
|
pir = null;
|
|
}
|
|
});
|
|
|
|
const linhaPir = $derived(pir ? Number(pir.linha.split(':')[1]) : 70);
|
|
const pinoPir = $derived(pinoDaLinha(linhaPir));
|
|
const lista = $derived(dispositivos(pinoPir));
|
|
const pirAtivo = $derived(estado.pir?.ativo ?? pir?.ativo ?? false);
|
|
|
|
// pino -> fios ligados nele (um pino pode ter mais de um dispositivo só no caso dos GND/5 V)
|
|
const fiosPorPino = $derived.by(() => {
|
|
const m = new Map<number, { d: Dispositivo; terminal: string; cor: string }[]>();
|
|
for (const d of lista) for (const f of d.fios) m.set(f.pino, [...(m.get(f.pino) ?? []), { d, terminal: f.terminal, cor: f.cor }]);
|
|
return m;
|
|
});
|
|
const linhas = $derived(Array.from({ length: 13 }, (_, i) => [PINOS[i * 2], PINOS[i * 2 + 1]] as [Pino, Pino]));
|
|
|
|
function apagado(p: Pino) {
|
|
if (!foco) return false;
|
|
return !(fiosPorPino.get(p.num) ?? []).some((f) => f.d.id === foco);
|
|
}
|
|
function estadoDe(d: Dispositivo): { texto: string; tom: 'ok' | 'aviso' | 'neutro' } {
|
|
if (d.id === 'pir') {
|
|
if (!pir) return { texto: 'sem resposta da placa', tom: 'neutro' };
|
|
if (!pir.disponivel) return { texto: pir.erro || 'sensor indisponível', tom: 'aviso' };
|
|
return pirAtivo ? { texto: 'sinal alto agora (vendo movimento)', tom: 'ok' } : { texto: 'ligado, sem movimento agora', tom: 'ok' };
|
|
}
|
|
if (d.id === 'pca9685') {
|
|
const m = estado.motores;
|
|
if (!m) return { texto: 'sem resposta da placa', tom: 'neutro' };
|
|
return m.disponivel ? { texto: 'respondendo no I²C', tom: 'ok' } : { texto: m.erro || 'não encontrada no I²C', tom: 'aviso' };
|
|
}
|
|
return { texto: 'pinos reservados', tom: 'neutro' };
|
|
}
|
|
const CORES: Record<TipoPino, string> = {
|
|
'5v': '#ff3b30', '3v3': '#ff9500', gnd: '#636366', gpio: '#34c759', i2c: '#0a84ff', spi: '#af52de', uart: '#30b0c7',
|
|
};
|
|
</script>
|
|
|
|
<div class="pagina">
|
|
<Card titulo="Conector da placa" ajuda="ligacoes" descricao="Orange Pi Zero 3, conector de 26 pinos em 2 fileiras: ímpares de um lado, pares do outro. Para achar o pino 1, procure o número 1 impresso na placa ou a ilha de solda quadrada no verso. Toque num dispositivo para destacar os pinos dele.">
|
|
<div class="rolagem">
|
|
<div class="conector" role="img" aria-label="Mapa do conector de 26 pinos e o que está ligado em cada pino">
|
|
{#each linhas as [impar, par] (impar.num)}
|
|
{@const fe = fiosPorPino.get(impar.num) ?? []}
|
|
{@const fd = fiosPorPino.get(par.num) ?? []}
|
|
<div class="fios esq" class:apagado={apagado(impar)}>
|
|
{#each fe as f (f.d.id)}
|
|
<span class="fio" style="--c: {f.cor}" title={f.d.nome}>{f.d.emoji} {f.terminal}</span>
|
|
{/each}
|
|
</div>
|
|
<div class="pino esq" class:apagado={apagado(impar)} class:usado={fe.length} title={impar.funcao ?? NOME_TIPO[impar.tipo]}>
|
|
<span class="nome">{impar.nome}</span>
|
|
<span class="bola" style="--c: {CORES[impar.tipo]}" class:quadrada={impar.num === 1}>{impar.num}</span>
|
|
</div>
|
|
<div class="pino dir" class:apagado={apagado(par)} class:usado={fd.length} title={par.funcao ?? NOME_TIPO[par.tipo]}>
|
|
<span class="bola" style="--c: {CORES[par.tipo]}">{par.num}</span>
|
|
<span class="nome">{par.nome}</span>
|
|
</div>
|
|
<div class="fios dir" class:apagado={apagado(par)}>
|
|
{#each fd as f (f.d.id)}
|
|
<span class="fio" style="--c: {f.cor}" title={f.d.nome}>{f.d.emoji} {f.terminal}</span>
|
|
{/each}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
<p class="legenda">
|
|
{#each Object.entries(NOME_TIPO) as [tipo, nome] (tipo)}
|
|
<span><i style="--c: {CORES[tipo as TipoPino]}"></i>{nome}</span>
|
|
{/each}
|
|
</p>
|
|
</Card>
|
|
|
|
<div class="lista">
|
|
{#each lista as d (d.id)}
|
|
{@const st = estadoDe(d)}
|
|
<Card>
|
|
<button
|
|
class="cabeca"
|
|
class:ativo={foco === d.id}
|
|
onclick={() => (foco = foco === d.id ? null : d.id)}
|
|
aria-pressed={foco === d.id}
|
|
title="Destacar os pinos deste dispositivo"
|
|
>
|
|
<span class="emoji" aria-hidden="true">{d.emoji}</span>
|
|
<span class="txt">
|
|
<strong>{d.nome}</strong>
|
|
<span class="st {st.tom}">{d.situacao === 'reservado' ? 'reservado' : 'em uso'} · {st.texto}</span>
|
|
</span>
|
|
</button>
|
|
<p class="desc">{d.descricao}</p>
|
|
<table>
|
|
<thead><tr><th>No dispositivo</th><th>Pino da placa</th><th>O que é</th></tr></thead>
|
|
<tbody>
|
|
{#each d.fios as f (f.pino + f.terminal)}
|
|
{@const p = PINOS[f.pino - 1]}
|
|
<tr>
|
|
<td><span class="ponto" style="--c: {f.cor}"></span>{f.terminal}</td>
|
|
<td class="mono">{f.pino}</td>
|
|
<td>{p.funcao ?? NOME_TIPO[p.tipo]}{p.tipo === 'gpio' ? ` (${p.nome})` : ''}</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
{#if d.avisos.length}
|
|
<ul class="avisos">
|
|
{#each d.avisos as a (a)}<li>{a}</li>{/each}
|
|
</ul>
|
|
{/if}
|
|
{#if d.id === 'pir' && pir}
|
|
<p class="nota mono">Configuração da placa: CARINHA_PIR={pir.linha} → pino {pinoPir ?? '?'}</p>
|
|
{/if}
|
|
</Card>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.pagina {
|
|
display: grid;
|
|
gap: var(--e-4);
|
|
align-items: start;
|
|
}
|
|
@media (min-width: 1024px) {
|
|
.pagina {
|
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
}
|
|
}
|
|
.lista {
|
|
display: grid;
|
|
gap: var(--e-4);
|
|
}
|
|
.rolagem {
|
|
overflow-x: auto;
|
|
}
|
|
.conector {
|
|
display: grid;
|
|
grid-template-columns: minmax(6.5em, 1fr) auto auto minmax(6.5em, 1fr);
|
|
gap: 3px 6px;
|
|
align-items: center;
|
|
width: max-content;
|
|
min-width: 100%;
|
|
margin-top: var(--e-3);
|
|
}
|
|
.pino {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: var(--t-xs);
|
|
color: var(--texto-2);
|
|
transition: opacity var(--transicao);
|
|
}
|
|
.pino.esq {
|
|
justify-content: flex-end;
|
|
}
|
|
.pino.usado .nome {
|
|
color: var(--texto);
|
|
font-weight: 600;
|
|
}
|
|
.bola {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
background: color-mix(in srgb, var(--c) 22%, var(--superficie-2));
|
|
border: 2px solid var(--c);
|
|
color: var(--texto);
|
|
font: 600 10px var(--fonte-mono, monospace);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.bola.quadrada {
|
|
border-radius: 4px;
|
|
}
|
|
.fios {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
transition: opacity var(--transicao);
|
|
}
|
|
.fios.esq {
|
|
justify-content: flex-end;
|
|
}
|
|
.fio {
|
|
padding: 1px 7px;
|
|
border-radius: var(--raio-pill);
|
|
background: color-mix(in srgb, var(--c) 18%, transparent);
|
|
border: 1px solid var(--c);
|
|
color: var(--texto);
|
|
font-size: 11px;
|
|
white-space: nowrap;
|
|
}
|
|
.apagado {
|
|
opacity: 0.25;
|
|
}
|
|
.legenda {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px var(--e-3);
|
|
margin: var(--e-3) 0 0;
|
|
font-size: var(--t-xs);
|
|
color: var(--texto-3);
|
|
}
|
|
.legenda span {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
.legenda i {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
border: 2px solid var(--c);
|
|
}
|
|
.cabeca {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--e-3);
|
|
width: 100%;
|
|
padding: 0;
|
|
border: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
.cabeca .emoji {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 40px;
|
|
height: 40px;
|
|
flex: none;
|
|
border-radius: 50%;
|
|
background: var(--superficie-2);
|
|
font-size: 22px;
|
|
}
|
|
.cabeca.ativo .emoji {
|
|
outline: 2px solid var(--destaque);
|
|
}
|
|
.txt {
|
|
display: grid;
|
|
gap: 2px;
|
|
}
|
|
.st {
|
|
font-size: var(--t-xs);
|
|
color: var(--texto-3);
|
|
}
|
|
.st.ok {
|
|
color: var(--ok);
|
|
}
|
|
.st.aviso {
|
|
color: var(--aviso, #ff9500);
|
|
}
|
|
.desc {
|
|
margin: var(--e-3) 0 var(--e-2);
|
|
font-size: var(--t-sm);
|
|
color: var(--texto-2);
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: var(--t-sm);
|
|
}
|
|
th {
|
|
text-align: left;
|
|
font-weight: 500;
|
|
font-size: var(--t-xs);
|
|
color: var(--texto-3);
|
|
padding: 4px 6px;
|
|
border-bottom: 1px solid var(--borda);
|
|
}
|
|
td {
|
|
padding: 5px 6px;
|
|
border-bottom: 1px solid var(--borda);
|
|
}
|
|
td .ponto {
|
|
display: inline-block;
|
|
width: 9px;
|
|
height: 9px;
|
|
margin-right: 6px;
|
|
border-radius: 50%;
|
|
background: var(--c);
|
|
}
|
|
.avisos {
|
|
display: grid;
|
|
gap: 6px;
|
|
margin: var(--e-3) 0 0;
|
|
padding-left: 1.1em;
|
|
font-size: var(--t-sm);
|
|
color: var(--texto-2);
|
|
}
|
|
.nota {
|
|
margin: var(--e-2) 0 0;
|
|
font-size: var(--t-xs);
|
|
color: var(--texto-3);
|
|
}
|
|
</style>
|