feat(painel): Ajustes com submenu e ligações do PIR pelo esquema real
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>
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
<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>
|
||||
+4
-4
@@ -81,12 +81,12 @@ export const TOPICOS: Record<string, TopicoGuia> = {
|
||||
resumo:
|
||||
'Mapa do conector de 26 pinos do Orange Pi Zero 3 com o que está ligado em cada pino: o sensor de presença (PIR), a placa dos servos (PCA9685) e os pinos reservados para a tela SPI. O pino do PIR vem da própria placa, então o desenho acompanha a configuração.',
|
||||
itens: [
|
||||
{ emoji: '🚶', nome: 'PIR', texto: 'VCC no 5 V (pino 2 ou 4), OUT no pino 11 (PC6) e GND num terra (pino 9, por exemplo). Nunca ligue o VCC no pino 11: os GPIOs são de 3,3 V. A ordem dos terminais muda entre módulos; leia o que está escrito no sensor.' },
|
||||
{ emoji: '🚶', nome: 'PIR (HC-SR505)', texto: 'Plaquinha marcada + · out · −. No seu cabo: + no fio PRETO → pino 4 (5 V); out no fio VERDE → pino 11 (PC6); − no fio BRANCO → pino 14 (terra; o 9 também serve). Siga a marcação, não a cor. Nunca ligue o + no pino 11: os GPIOs são de 3,3 V.' },
|
||||
{ emoji: '🦾', nome: 'PCA9685', texto: 'VCC no 3,3 V (pino 1), SDA no 3, SCL no 5 e GND no 6. Os servos usam a fonte externa no borne V+, com terra comum com a placa.' },
|
||||
{ emoji: '🖥️', nome: 'Tela SPI (tft35)', texto: 'O overlay tft35 reserva os pinos 18, 19, 21, 22, 23 e 24. Não ligue sensores neles.' },
|
||||
{ emoji: '🖥️', nome: 'Tela SPI (tft35)', texto: 'O overlay tft35 reserva os pinos 7, 18, 19, 21, 22, 23 e 24. Não ligue sensores neles. Livres para sensores novos: 12, 13, 15, 16 e 26.' },
|
||||
{ emoji: '🟢', nome: 'Estado ao vivo', texto: 'Cada dispositivo mostra se a placa está lendo: o PIR diz se o sinal está alto agora, e a PCA9685 se responde no I²C.' },
|
||||
],
|
||||
dica: 'Sinal do PIR alto o tempo todo, por muito tempo, costuma ser fio trocado (VCC e OUT invertidos), sensor apontado para uma fonte de calor ou o ajuste de tempo do HC-SR501 no máximo.',
|
||||
dica: 'Sinal do PIR alto o tempo todo, por muito tempo, não é normal no HC-SR505 (ele segura só uns 8 s): confira se o + e o out não estão trocados, ou se ele aponta para uma fonte de calor.',
|
||||
},
|
||||
cuidados: {
|
||||
titulo: 'Cuidar do Carinha',
|
||||
@@ -375,5 +375,5 @@ export const ORDEM_GUIA = [
|
||||
/** Tópico de cada tela, para o ⓘ do topo. */
|
||||
export const TOPICO_DA_TELA: Record<string, string> = {
|
||||
'ao-vivo': 'inicio', eventos: 'eventos', conversa: 'conversa', reacoes: 'reacoes',
|
||||
movimento: 'movimento', ajustes: 'tela', rosto: 'aparencia', guia: 'inicio', criar: 'criar', 'dia-a-dia': 'aprendizado', ligacoes: 'ligacoes',
|
||||
movimento: 'movimento', ajustes: 'tela', rosto: 'aparencia', guia: 'inicio', criar: 'criar', 'dia-a-dia': 'aprendizado',
|
||||
};
|
||||
|
||||
+18
-14
@@ -53,6 +53,8 @@ export interface Fio {
|
||||
/** Nome do terminal no dispositivo (VCC, OUT, GND, SDA...). */
|
||||
terminal: string;
|
||||
cor: string;
|
||||
/** Cor real do fio no cabo, quando a gente sabe (ex.: o PIR tem o fio preto no +). */
|
||||
cabo?: { nome: string; cor: string };
|
||||
}
|
||||
|
||||
export interface Dispositivo {
|
||||
@@ -79,19 +81,20 @@ export function dispositivos(pinoPir: number | null): Dispositivo[] {
|
||||
id: 'pir',
|
||||
nome: 'Sensor de presença (PIR)',
|
||||
emoji: '🚶',
|
||||
cor: '#34c759',
|
||||
cor: '#1c9b4f',
|
||||
situacao: 'em uso',
|
||||
descricao: 'Módulo PIR (tipo HC-SR501 ou AM312). Avisa quando alguém se mexe na frente: acorda o rosto e liga a câmera no modo presença.',
|
||||
descricao: 'HC-SR505 mini. Avisa quando alguém se mexe na frente (até uns 3 m, cone de 100°): acorda o rosto e liga a câmera no modo presença.',
|
||||
fios: [
|
||||
{ pino: 2, terminal: 'VCC', cor: '#ff3b30' },
|
||||
{ pino: saida, terminal: 'OUT', cor: '#ffcc00' },
|
||||
{ pino: 9, terminal: 'GND', cor: '#8e8e93' },
|
||||
{ pino: 4, terminal: '+', cor: '#1c9b4f', cabo: { nome: 'preto', cor: '#111111' } },
|
||||
{ pino: saida, terminal: 'out', cor: '#1c9b4f', cabo: { nome: 'verde', cor: '#1c9b4f' } },
|
||||
{ pino: 14, terminal: '−', cor: '#1c9b4f', cabo: { nome: 'branco', cor: '#f4f4f4' } },
|
||||
],
|
||||
avisos: [
|
||||
'A ordem dos 3 terminais muda de um módulo para outro: leia o que está escrito na plaquinha do sensor (no HC-SR501, tire a cúpula branca para ver VCC, OUT e GND).',
|
||||
`Nunca ligue o VCC (5 V) no pino ${saida}: os GPIOs da placa são de 3,3 V e queimam com 5 V.`,
|
||||
'A saída do HC-SR501 e do AM312 é de 3,3 V, segura para o GPIO. Se o seu sensor soltar 5 V na saída, use um divisor de tensão.',
|
||||
'O VCC pode ir em qualquer 5 V (pino 2 ou 4) e o GND em qualquer terra (6, 9, 14, 20 ou 25).',
|
||||
'No seu cabo o fio PRETO está no +. Siga a marcação da plaquinha (+ · out · −, da esquerda para a direita), não a cor do fio. Inverter + e − pode queimar o sensor.',
|
||||
`Nunca ligue o + (5 V) no pino ${saida}: os GPIOs da placa são de 3,3 V e queimam com 5 V. O out do HC-SR505 já sai em 3,3 V, seguro para o pino ${saida}.`,
|
||||
'O HC-SR505 não tem ajuste e segura o sinal ligado por uns 8 s depois de cada movimento. Sinal ligado direto por minutos seguidos não é normal: confira os fios.',
|
||||
'Depois de ligado, ele leva de 30 a 60 s para estabilizar; disparos falsos nesse tempo são normais. Aponte para onde as pessoas passam, longe de sol e de saída de ar quente.',
|
||||
'O − também pode ir no pino 9 (é terra). Teste na placa: gpiomon -c gpiochip1 -e rising ' + (saida === 11 ? '70' : 'LINHA') + ' imprime uma linha a cada movimento.',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -102,13 +105,13 @@ export function dispositivos(pinoPir: number | null): Dispositivo[] {
|
||||
situacao: 'em uso',
|
||||
descricao: 'Controla os servos do braço por I²C (barramento /dev/i2c-2, endereço 0x40).',
|
||||
fios: [
|
||||
{ pino: 1, terminal: 'VCC', cor: '#ff9500' },
|
||||
{ pino: 1, terminal: 'VCC', cor: '#0a84ff' },
|
||||
{ pino: 3, terminal: 'SDA', cor: '#0a84ff' },
|
||||
{ pino: 5, terminal: 'SCL', cor: '#5ac8fa' },
|
||||
{ pino: 6, terminal: 'GND', cor: '#8e8e93' },
|
||||
{ pino: 5, terminal: 'SCL', cor: '#0a84ff' },
|
||||
{ pino: 6, terminal: 'GND', cor: '#0a84ff' },
|
||||
],
|
||||
avisos: [
|
||||
'O VCC da PCA9685 vai no 3,3 V (pino 1): é só a lógica.',
|
||||
'Já ligada e funcionando. O VCC da PCA9685 vai no 3,3 V (pino 1): é só a lógica.',
|
||||
'Os servos NÃO são alimentados pela placa: o borne V+ da PCA9685 recebe a fonte externa (6 V), com o GND da fonte ligado ao GND da placa (terra comum).',
|
||||
],
|
||||
},
|
||||
@@ -118,8 +121,9 @@ export function dispositivos(pinoPir: number | null): Dispositivo[] {
|
||||
emoji: '🖥️',
|
||||
cor: '#af52de',
|
||||
situacao: 'reservado',
|
||||
descricao: 'O overlay tft35 está ativo no boot e reserva estes pinos para uma tela SPI. O Carinha não usa essa tela: não ligue outros sensores neles.',
|
||||
descricao: 'O overlay tft35 está ativo no boot e reserva estes pinos para uma tela SPI. O Carinha não usa essa tela: não ligue outros sensores neles. Livres para sensores novos: 12, 13, 15, 16 e 26.',
|
||||
fios: [
|
||||
{ pino: 7, terminal: 'TFT', cor: '#af52de' },
|
||||
{ pino: 18, terminal: 'DC', cor: '#af52de' },
|
||||
{ pino: 19, terminal: 'MOSI', cor: '#af52de' },
|
||||
{ pino: 21, terminal: 'MISO', cor: '#af52de' },
|
||||
|
||||
@@ -37,7 +37,7 @@ export function itensDaPaleta(): ItemPaleta[] {
|
||||
registrarNaPaleta(() => [
|
||||
...ROTAS.map((r) => ({ id: 'ir-' + r.id, grupo: 'Ir para', titulo: r.titulo, fazer: () => nav.ir(r.id) })),
|
||||
{ id: 'ir-rosto', grupo: 'Ir para', titulo: 'Rosto (estilo, formato e cor)', fazer: () => nav.ir('rosto') },
|
||||
{ id: 'ir-ligacoes', grupo: 'Ir para', titulo: 'Ligações da placa (pinos do PIR, servos e tela)', fazer: () => nav.ir('ligacoes') },
|
||||
{ id: 'ir-ligacoes', grupo: 'Ir para', titulo: 'Ligações da placa (pinos do PIR, servos e tela)', fazer: () => nav.ir('ajustes', 'ligacoes') },
|
||||
{ id: 'ir-dia-a-dia', grupo: 'Ir para', titulo: 'Dia a dia (rotina e decisões do Carinha)', fazer: () => nav.ir('dia-a-dia') },
|
||||
{ id: 'ir-criar', grupo: 'Ir para', titulo: 'Criar com IA (expressões, atividades, figurinhas)', fazer: () => nav.ir('criar') },
|
||||
...Object.entries(ATIVIDADES_INFO).map(([id, a]) => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Rotas por hash (#/conversa): o servidor só precisa entregar index.html em "/". */
|
||||
|
||||
export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto' | 'guia' | 'criar' | 'dia-a-dia' | 'ligacoes';
|
||||
export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto' | 'guia' | 'criar' | 'dia-a-dia';
|
||||
|
||||
export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [
|
||||
{ id: 'ao-vivo', titulo: 'Ao vivo', atalho: 'g v' },
|
||||
@@ -17,23 +17,34 @@ export const SECUNDARIAS: { id: Rota; titulo: string; volta: Rota }[] = [
|
||||
{ id: 'guia', titulo: 'Guia', volta: 'ajustes' },
|
||||
{ id: 'criar', titulo: 'Criar com IA', volta: 'reacoes' },
|
||||
{ id: 'dia-a-dia', titulo: 'Dia a dia', volta: 'ao-vivo' },
|
||||
{ id: 'ligacoes', titulo: 'Ligações da placa', volta: 'ajustes' },
|
||||
];
|
||||
|
||||
export const tituloDa = (id: Rota) => [...ROTAS, ...SECUNDARIAS].find((r) => r.id === id)?.titulo ?? '';
|
||||
|
||||
/** #/ajustes/ia -> ['ajustes', 'ia']: a segunda parte é a seção (submenu) da tela. */
|
||||
function partes(): [string, string] {
|
||||
const [id, sub = ''] = location.hash.replace(/^#\/?/, '').split('?')[0].split('/');
|
||||
if (id === 'historico') return ['eventos', sub]; // endereço antigo
|
||||
if (id === 'ligacoes') return ['ajustes', 'ligacoes']; // virou seção dos Ajustes
|
||||
return [id, sub];
|
||||
}
|
||||
|
||||
function lerHash(): Rota {
|
||||
let id = location.hash.replace(/^#\/?/, '').split('?')[0] as Rota;
|
||||
if ((id as string) === 'historico') id = 'eventos'; // endereço antigo
|
||||
const id = partes()[0] as Rota;
|
||||
return ROTAS.some((r) => r.id === id) || SECUNDARIAS.some((r) => r.id === id) ? id : 'ao-vivo';
|
||||
}
|
||||
|
||||
class Navegacao {
|
||||
atual = $state<Rota>(lerHash());
|
||||
ir(rota: Rota) {
|
||||
location.hash = '/' + rota;
|
||||
/** Seção dentro da tela (ex.: a parte dos Ajustes aberta). */
|
||||
sub = $state(partes()[1]);
|
||||
ir(rota: Rota, sub = '') {
|
||||
location.hash = '/' + rota + (sub ? '/' + sub : '');
|
||||
}
|
||||
}
|
||||
|
||||
export const nav = new Navegacao();
|
||||
addEventListener('hashchange', () => (nav.atual = lerHash()));
|
||||
addEventListener('hashchange', () => {
|
||||
nav.atual = lerHash();
|
||||
nav.sub = partes()[1];
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user