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
+16 -2
View File
@@ -74,6 +74,20 @@ export const TOPICOS: Record<string, TopicoGuia> = {
],
dica: 'Para ensinar rápido: quando ele fizer algo que você gostou, toque na tela do celular (vale como carinho) ou fale com ele logo em seguida.',
},
ligacoes: {
titulo: 'Ligações da placa',
emoji: '🔌',
onde: 'Ajustes › Ligações da placa (também em Ao vivo › Acontecimentos)',
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: '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: '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.',
},
cuidados: {
titulo: 'Cuidar do Carinha',
emoji: '❤️',
@@ -354,12 +368,12 @@ export const TOPICOS: Record<string, TopicoGuia> = {
/** Ordem do tutorial (página Guia). */
export const ORDEM_GUIA = [
'inicio', 'vida', 'aprendizado', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'criar', 'ideias', 'aparencia',
'personalidade', 'acontecimentos', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade',
'personalidade', 'acontecimentos', 'ligacoes', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade',
'reacoes', 'conversa', 'movimento', 'atalhos',
];
/** 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',
movimento: 'movimento', ajustes: 'tela', rosto: 'aparencia', guia: 'inicio', criar: 'criar', 'dia-a-dia': 'aprendizado', ligacoes: 'ligacoes',
};
+133
View File
@@ -0,0 +1,133 @@
/**
* Ligações da placa: o conector de 26 pinos do Orange Pi Zero 3 e o que está ligado em cada pino.
* O pino do PIR vem ao vivo da placa (CARINHA_PIR, padrão gpiochip1:70 = PC6 = pino 11).
*/
export type TipoPino = '5v' | '3v3' | 'gnd' | 'gpio' | 'i2c' | 'spi' | 'uart';
export interface Pino {
num: number;
nome: string;
tipo: TipoPino;
/** Linha no gpiochip1 (libgpiod), quando é um GPIO. */
linha?: number;
funcao?: string;
}
// Conector de 26 pinos (2 fileiras): ímpares de um lado, pares do outro.
export const PINOS: Pino[] = [
{ num: 1, nome: '3,3 V', tipo: '3v3' },
{ num: 2, nome: '5 V', tipo: '5v' },
{ num: 3, nome: 'PH5', tipo: 'i2c', linha: 229, funcao: 'I²C3 SDA' },
{ num: 4, nome: '5 V', tipo: '5v' },
{ num: 5, nome: 'PH4', tipo: 'i2c', linha: 228, funcao: 'I²C3 SCL' },
{ num: 6, nome: 'GND', tipo: 'gnd' },
{ num: 7, nome: 'PC9', tipo: 'gpio', linha: 73 },
{ num: 8, nome: 'PH2', tipo: 'uart', linha: 226, funcao: 'UART5 TX' },
{ num: 9, nome: 'GND', tipo: 'gnd' },
{ num: 10, nome: 'PH3', tipo: 'uart', linha: 227, funcao: 'UART5 RX' },
{ num: 11, nome: 'PC6', tipo: 'gpio', linha: 70 },
{ num: 12, nome: 'PC11', tipo: 'gpio', linha: 75 },
{ num: 13, nome: 'PC5', tipo: 'gpio', linha: 69 },
{ num: 14, nome: 'GND', tipo: 'gnd' },
{ num: 15, nome: 'PC8', tipo: 'gpio', linha: 72 },
{ num: 16, nome: 'PC15', tipo: 'gpio', linha: 79 },
{ num: 17, nome: '3,3 V', tipo: '3v3' },
{ num: 18, nome: 'PC14', tipo: 'gpio', linha: 78 },
{ num: 19, nome: 'PH7', tipo: 'spi', linha: 231, funcao: 'SPI1 MOSI' },
{ num: 20, nome: 'GND', tipo: 'gnd' },
{ num: 21, nome: 'PH8', tipo: 'spi', linha: 232, funcao: 'SPI1 MISO' },
{ num: 22, nome: 'PC7', tipo: 'gpio', linha: 71 },
{ num: 23, nome: 'PH6', tipo: 'spi', linha: 230, funcao: 'SPI1 CLK' },
{ num: 24, nome: 'PH9', tipo: 'spi', linha: 233, funcao: 'SPI1 CS0' },
{ num: 25, nome: 'GND', tipo: 'gnd' },
{ num: 26, nome: 'PC10', tipo: 'gpio', linha: 74 },
];
export const NOME_TIPO: Record<TipoPino, string> = {
'5v': '5 V', '3v3': '3,3 V', gnd: 'Terra (GND)', gpio: 'GPIO (3,3 V)', i2c: 'I²C', spi: 'SPI', uart: 'Serial',
};
export interface Fio {
pino: number;
/** Nome do terminal no dispositivo (VCC, OUT, GND, SDA...). */
terminal: string;
cor: string;
}
export interface Dispositivo {
id: string;
nome: string;
emoji: string;
cor: string;
descricao: string;
fios: Fio[];
avisos: string[];
/** 'em uso' = o servidor usa; 'reservado' = a placa reserva os pinos (overlay), mas o Carinha não usa. */
situacao: 'em uso' | 'reservado';
}
export function pinoDaLinha(linha: number) {
return PINOS.find((p) => p.linha === linha)?.num ?? null;
}
/** Dispositivos registrados. O pino do PIR é o que a placa informa (CARINHA_PIR). */
export function dispositivos(pinoPir: number | null): Dispositivo[] {
const saida = pinoPir ?? 11;
return [
{
id: 'pir',
nome: 'Sensor de presença (PIR)',
emoji: '🚶',
cor: '#34c759',
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.',
fios: [
{ pino: 2, terminal: 'VCC', cor: '#ff3b30' },
{ pino: saida, terminal: 'OUT', cor: '#ffcc00' },
{ pino: 9, terminal: 'GND', cor: '#8e8e93' },
],
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).',
],
},
{
id: 'pca9685',
nome: 'Placa dos servos (PCA9685)',
emoji: '🦾',
cor: '#0a84ff',
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: 3, terminal: 'SDA', cor: '#0a84ff' },
{ pino: 5, terminal: 'SCL', cor: '#5ac8fa' },
{ pino: 6, terminal: 'GND', cor: '#8e8e93' },
],
avisos: [
'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).',
],
},
{
id: 'tela',
nome: 'Tela SPI 3,5" (overlay tft35)',
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.',
fios: [
{ pino: 18, terminal: 'DC', cor: '#af52de' },
{ pino: 19, terminal: 'MOSI', cor: '#af52de' },
{ pino: 21, terminal: 'MISO', cor: '#af52de' },
{ pino: 22, terminal: 'RESET', cor: '#af52de' },
{ pino: 23, terminal: 'CLK', cor: '#af52de' },
{ pino: 24, terminal: 'CS', cor: '#af52de' },
],
avisos: [],
},
];
}
+1
View File
@@ -37,6 +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-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]) => ({
+2 -1
View File
@@ -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';
export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto' | 'guia' | 'criar' | 'dia-a-dia' | 'ligacoes';
export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [
{ id: 'ao-vivo', titulo: 'Ao vivo', atalho: 'g v' },
@@ -17,6 +17,7 @@ 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 ?? '';