Ajustes vira uma tela com submenu (#/ajustes/<seção>): cada grupo do celular é uma seção (Tela, Sensores, Sono, Vídeo, Sensibilidade), mais Robô (Inteligência artificial, Ligações da placa) e Painel (Segurança, Sistema). No PC o menu fica numa coluna fixa com descrição; no celular, numa fileira rolável que traz a seção aberta para a vista. A busca procura em todos os grupos do celular. Ligações da placa passa a ser uma seção dos Ajustes (#/ligacoes antigo redireciona) e segue o esquema que já tinha sido desenhado para o PIR: - HC-SR505 mini, marcação + · out · −; no cabo, + no fio preto → pino 4, out no verde → pino 11, − no branco → pino 14; - etiquetas do conector e tabela mostram a cor real do fio; - tela TFT reserva também o pino 7; pinos livres listados; - avisos do HC-SR505 (sinal de ~8 s, 30–60 s para estabilizar, teste com gpiomon). Rotas ganham nav.sub (seção da tela); atalhos vão direto à seção (câmera, IA, ligações). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
336 lines
10 KiB
Svelte
336 lines
10 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { pedir } from '../api';
|
|
import { estado } from '../estado.svelte';
|
|
import { dispositivos, NOME_TIPO, pinoDaLinha, PINOS, type Dispositivo, type Fio, type Pino, type TipoPino } from '../ligacoes';
|
|
import Card from './Card.svelte';
|
|
|
|
// 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, (Fio & { d: Dispositivo })[]>();
|
|
for (const d of lista) for (const f of d.fios) m.set(f.pino, [...(m.get(f.pino) ?? []), { ...f, d }]);
|
|
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.cabo ? ' · fio ' + f.cabo.nome : '')}>
|
|
{f.d.emoji} {f.terminal}{#if f.cabo}<i class="cabo" style="--k: {f.cabo.cor}" aria-label={'fio ' + f.cabo.nome}></i>{/if}
|
|
</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.cabo ? ' · fio ' + f.cabo.nome : '')}>
|
|
{#if f.cabo}<i class="cabo" style="--k: {f.cabo.cor}" aria-label={'fio ' + f.cabo.nome}></i>{/if}{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>
|
|
{@const temCabo = d.fios.some((f) => f.cabo)}
|
|
<table>
|
|
<thead><tr><th>No dispositivo</th>{#if temCabo}<th>Fio do cabo</th>{/if}<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>
|
|
{#if temCabo}
|
|
<td class="sem-quebra">{#if f.cabo}<i class="cabo" style="--k: {f.cabo.cor}"></i> {f.cabo.nome}{/if}</td>
|
|
{/if}
|
|
<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;
|
|
}
|
|
.sem-quebra {
|
|
white-space: nowrap;
|
|
}
|
|
.cabo {
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 10px;
|
|
margin: 0 4px;
|
|
border-radius: 50%;
|
|
background: var(--k);
|
|
border: 1.5px solid var(--texto-3);
|
|
vertical-align: -1px;
|
|
}
|
|
.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>
|