feat(i18n): routing and localization infrastructure (phase 1)

Implement Phase 1 of i18n spec:
- Configure next-intl with 6 locales (pt, en, es, fr, de, zh-CN) and pathnames
- Move routes to app/[locale] structure with localized layouts and metadata
- Add middleware for root language negotiation (307), legacy redirects (308), and 404 for non-pt book
- Implement language selector dropdown in header with cookie persistence
- Add system typography rules for zh-CN and word-break/hyphenation for de/fr

Co-Authored-By: Antigravity <noreply@google.com>
This commit is contained in:
Lucas RayanandAntigravity committed 2026-11-01 09:31:22 -03:00
1 parent e92aea0c84
commit 83c0ba3e74
38 files changed
+1699 -324

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
export const metadata = paginaMeta({
titulo: "Catálogo de placas ESP32",
descricao: "Placas de desenvolvimento com ESP32 disponíveis no Brasil, com filtro por série, conector e conversor USB-serial.",
caminho: "/catalogo",
keywords: ["placas ESP32", "DevKit ESP32", "comprar ESP32", "ESP32 DevKitC"],
});
export default function Layout({ children }) {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: "Catálogo de Placas ESP32",
descricao: "Guia e catálogo interativo de placas de desenvolvimento e DevKits com microcontroladores ESP32.",
caminho: "/catalogo",
})
),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Catálogo de Placas", caminho: "/catalogo" },
])
),
}}
/>
{children}
</>
);
}
+325
View File
@@ -0,0 +1,325 @@
"use client";
import { useState, useEffect } from 'react';
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import BoardCard from "@/components/BoardCard";
import { AlertTriangle, RefreshCw, Search, ShoppingCart, SlidersHorizontal } from "lucide-react";
export default function PlacasPage() {
const [boards, setBoards] = useState([]);
const [filteredBoards, setFilteredBoards] = useState([]);
const [headers, setHeaders] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [activeFilters, setActiveFilters] = useState({});
const [filterOptions, setFilterOptions] = useState({});
const [filtrosAbertos, setFiltrosAbertos] = useState(false);
const excludedColumns = ['Nome', 'Descrição', 'Link', 'Imagem', 'Loja Oficial'];
useEffect(() => {
fetchBoards();
// No desktop os filtros ficam sempre à vista; no celular começam recolhidos.
setFiltrosAbertos(window.matchMedia('(min-width: 1024px)').matches);
}, []);
useEffect(() => {
applyFilters();
}, [boards, searchTerm, activeFilters]);
const fetchBoards = async () => {
try {
setLoading(true);
const response = await fetch('/api/placas');
if (!response.ok) {
throw new Error('Erro ao carregar dados das placas');
}
const result = await response.json();
setBoards(result.data);
setHeaders(result.headers);
setFilteredBoards(result.data);
generateFilterOptions(result.data, result.headers);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const generateFilterOptions = (data, headers) => {
const options = {};
const filterableColumns = headers.filter(h => !excludedColumns.includes(h));
filterableColumns.forEach(column => {
const uniqueValues = new Set();
data.forEach(item => {
const value = item[column];
if (value && value.toString().trim()) {
uniqueValues.add(value.toString().trim());
}
});
const values = Array.from(uniqueValues).sort();
// Verificar se é um filtro binário (apenas Sim/Não/Opcional)
const binaryValues = ['Sim', 'Não', 'Opcional'];
const isBinaryFilter = values.every(v => binaryValues.includes(v));
options[column] = {
values: values,
isBinary: isBinaryFilter
};
});
setFilterOptions(options);
};
const applyFilters = () => {
let filtered = [...boards];
if (searchTerm) {
filtered = filtered.filter(board => {
const searchLower = searchTerm.toLowerCase();
return (
board.Nome?.toLowerCase().includes(searchLower) ||
board.Descrição?.toLowerCase().includes(searchLower)
);
});
}
Object.keys(activeFilters).forEach(column => {
if (activeFilters[column] && activeFilters[column].length > 0) {
filtered = filtered.filter(board => {
const boardValue = board[column]?.toString().trim();
// Para filtros binários, o valor armazenado é 'binary-enabled'
if (activeFilters[column].includes('binary-enabled')) {
return boardValue === 'Sim' || boardValue === 'Opcional';
}
return activeFilters[column].includes(boardValue);
});
}
});
setFilteredBoards(filtered);
};
const handleFilterToggle = (column, value) => {
setActiveFilters(prev => {
const newFilters = { ...prev };
if (!newFilters[column]) {
newFilters[column] = [value];
} else {
const index = newFilters[column].indexOf(value);
if (index > -1) {
// Remove o valor
newFilters[column] = newFilters[column].filter(v => v !== value);
if (newFilters[column].length === 0) {
delete newFilters[column];
}
} else {
// Adiciona o valor
newFilters[column] = [...newFilters[column], value];
}
}
return newFilters;
});
};
const clearFilters = () => {
setActiveFilters({});
setSearchTerm('');
};
const activeFilterCount = Object.keys(activeFilters).length + (searchTerm ? 1 : 0);
const filterableColumns = headers.filter((h) => !excludedColumns.includes(h));
const hero = (
<PageHero
trail={[{ label: "Catálogo de placas" }]}
icon={ShoppingCart}
label="DevKits e módulos reais"
title="Catálogo de placas ESP32"
lede="Compare placas oficiais e alternativas para o seu projeto: interface USB, sensores integrados e link direto para a loja."
/>
);
if (loading) {
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
{hero}
<section className="shell section pt-10" aria-busy="true" aria-label="Carregando placas">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="card h-72 animate-pulse bg-surface-2" />
))}
</div>
<p role="status" className="mt-6 text-center text-sm text-muted">Consultando o banco de placas…</p>
</section>
</main>
<Footer />
</div>
);
}
if (error) {
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
{hero}
<section className="shell section pt-10">
<div role="alert" className="callout callout-danger flex items-start gap-3">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-danger" aria-hidden="true" />
<div>
<p className="font-semibold text-ink">Não foi possível carregar as placas</p>
<p className="mt-1">{error}. Verifique a conexão e tente de novo.</p>
<button type="button" onClick={fetchBoards} className="btn btn-primary btn-sm mt-4">
<RefreshCw className="h-4 w-4" aria-hidden="true" />
Tentar novamente
</button>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
}
const filtros = (
<div className="space-y-6">
<div>
<label htmlFor="busca-placa" className="text-sm font-semibold text-ink">
Buscar placa
</label>
<div className="relative mt-2">
<Search className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
id="busca-placa"
type="search"
placeholder="Nome ou descrição"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="field pl-10 text-sm"
/>
</div>
</div>
{filterableColumns.map((column) => {
const filterData = filterOptions[column];
if (!filterData) return null;
if (filterData.isBinary) {
return (
<label key={column} className="flex min-h-11 cursor-pointer items-center justify-between gap-3 border-t border-line pt-4 text-sm font-medium text-ink-soft">
{column}
<input
type="checkbox"
checked={activeFilters[column]?.includes("binary-enabled") || false}
onChange={() => handleFilterToggle(column, "binary-enabled")}
className="h-5 w-5 shrink-0 cursor-pointer accent-[var(--accent)]"
/>
</label>
);
}
return (
<fieldset key={column} className="border-t border-line pt-4">
<legend className="flex items-center gap-2 pr-2 text-sm font-semibold text-ink">
{column}
{activeFilters[column]?.length > 0 && <span className="chip chip-accent px-2 py-0">{activeFilters[column].length}</span>}
</legend>
<div className="mt-2 max-h-44 space-y-0.5 overflow-y-auto pr-1">
{filterData.values.map((option) => (
<label key={option} className="flex min-h-10 cursor-pointer items-center gap-3 rounded-lg px-2 text-sm text-muted transition-colors hover:bg-surface-2 hover:text-ink">
<input
type="checkbox"
checked={activeFilters[column]?.includes(option) || false}
onChange={() => handleFilterToggle(column, option)}
className="h-4 w-4 shrink-0 cursor-pointer accent-[var(--accent)]"
/>
{option}
</label>
))}
</div>
</fieldset>
);
})}
</div>
);
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
{hero}
<section className="shell section pt-10 sm:pt-12" aria-label="Placas">
<div className="grid gap-8 lg:grid-cols-[17rem_1fr] lg:gap-10">
<aside className="lg:sticky lg:top-28 lg:self-start">
<details className="panel" open={filtrosAbertos} onToggle={(e) => setFiltrosAbertos(e.currentTarget.open)}>
<summary className="flex min-h-12 cursor-pointer list-none items-center justify-between gap-3 px-5 py-3 lg:cursor-default">
<span className="flex items-center gap-2 text-sm font-semibold text-ink">
<SlidersHorizontal className="h-4 w-4 text-accent" aria-hidden="true" />
Filtros
{activeFilterCount > 0 && <span className="chip chip-accent px-2 py-0">{activeFilterCount}</span>}
</span>
{activeFilterCount > 0 && (
<button
type="button"
onClick={(e) => { e.preventDefault(); clearFilters(); }}
className="text-sm font-medium text-accent hover:text-accent-strong"
>
Limpar
</button>
)}
</summary>
<div className="border-t border-line p-5 lg:max-h-[calc(100vh-11rem)] lg:overflow-y-auto">{filtros}</div>
</details>
</aside>
<div>
<p role="status" aria-live="polite" className="text-sm text-muted">
<strong className="font-semibold text-ink">{filteredBoards.length}</strong>{" "}
{filteredBoards.length === 1 ? "placa encontrada" : "placas encontradas"}
</p>
{filteredBoards.length === 0 ? (
<div className="card mt-4 px-6 py-14 text-center">
<Search className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h2 className="mt-4 font-display text-xl font-semibold text-ink">Nenhuma placa com esses filtros</h2>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Remova alguns filtros ou busque por outro nome.</p>
<button type="button" onClick={clearFilters} className="btn btn-primary mt-6">
Limpar filtros
</button>
</div>
) : (
<div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{filteredBoards.map((board, index) => (
<BoardCard key={index} board={board} />
))}
</div>
)}
</div>
</div>
</section>
</main>
<Footer />
</div>
);
}
+60
View File
@@ -0,0 +1,60 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
export const metadata = paginaMeta({
titulo: "Comparar séries ESP32",
descricao: "Compare até quatro séries de ESP32 lado a lado: processador, Wi-Fi, Bluetooth, memória, GPIO, periféricos e segurança em hardware, com os dados dos datasheets oficiais.",
caminho: "/comparacao",
keywords: ["comparar ESP32", "ESP32 vs ESP32-S3", "tabela comparativa ESP32", "qual ESP32 é melhor"],
});
const faqs = [
{
pergunta: "Como comparar microcontroladores ESP32 lado a lado?",
resposta: "O comparador do ESPDocs permite selecionar até quatro séries simultaneamente para confrontar arquitetura, frequência de clock, memória SRAM/PSRAM, padrões de rádio (Wi-Fi 4/6, Bluetooth 5/6, 802.15.4), interfaces de periféricos e blocos de segurança em hardware.",
},
{
pergunta: "Qual a diferença entre o ESP32 clássico e o ESP32-S3?",
resposta: "O ESP32 clássico usa núcleos Xtensa LX6 e Bluetooth clássico/LE. O ESP32-S3 traz núcleos Xtensa LX7 com instruções vetoriais para inteligência artificial (TinyML), USB nativo OTG, mais GPIOs (45 contra 36) e Bluetooth 5.0 LE.",
},
{
pergunta: "Qual a diferença entre ESP32-C3 e ESP32-C6?",
resposta: "O ESP32-C3 é um microcontrolador RISC-V com Wi-Fi 4 e Bluetooth 5 LE. O ESP32-C6 evolui para Wi-Fi 6 (802.11ax), Bluetooth 5.4 LE, rádio IEEE 802.15.4 para Zigbee 3.0 e Thread 1.3, além de coprocessador de ultrabaixo consumo.",
},
];
export default function Layout({ children }) {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: "Comparador de Séries ESP32",
descricao: "Tabela comparativa técnica para analisar especificações, pinagens e recursos de múltiplas séries de ESP32.",
caminho: "/comparacao",
})
),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Comparar Séries", caminho: "/comparacao" },
])
),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd.perguntas(faqs)),
}}
/>
{children}
</>
);
}
+306
View File
@@ -0,0 +1,306 @@
"use client";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import { useEffect, useState, Fragment } from "react";
import Link from "next/link";
import seriesData from "@/public/series.json";
import { ArrowRight, ArrowUpRight, Check, ChevronDown, Scale } from "lucide-react";
const MIN_SERIES = 2;
const MAX_SERIES = 4;
/* Lê "seguranca.ecc" além de chaves simples. */
function valorDe(objeto, caminho) {
return caminho.split(".").reduce((atual, parte) => atual?.[parte], objeto);
}
export default function Comparacao() {
const [selectedSeries, setSelectedSeries] = useState(["ESP32", "ESP32-S3"]);
const [soDiferencas, setSoDiferencas] = useState(false);
const [collapsedCategories, setCollapsedCategories] = useState({
"Interfaces Especiais": true,
"Especificações Ambientais": true,
});
const series = Object.entries(seriesData);
// O seletor envia as séries finalistas em ?chips=ESP32-S3,ESP32-C6.
useEffect(() => {
const chips = new URLSearchParams(window.location.search).get("chips");
if (!chips) return;
const validas = [...new Set(chips.split(","))].filter((k) => seriesData[k]).slice(0, MAX_SERIES);
if (validas.length >= MIN_SERIES) setSelectedSeries(validas);
}, []);
const toggleSeries = (seriesKey) => {
if (selectedSeries.includes(seriesKey)) {
if (selectedSeries.length > MIN_SERIES) setSelectedSeries(selectedSeries.filter((s) => s !== seriesKey));
} else if (selectedSeries.length < MAX_SERIES) {
setSelectedSeries([...selectedSeries, seriesKey]);
}
};
const toggleCategory = (category) => {
setCollapsedCategories({ ...collapsedCategories, [category]: !collapsedCategories[category] });
};
const compareFields = [
{ category: "Geral", fields: [
{ key: "nome_completo", label: "Nome Completo" },
{ key: "descricao", label: "Descrição" },
]},
{ category: "Disponibilidade e Ferramentas", fields: [
{ key: "esp_idf_minimo", label: "ESP-IDF mínimo" },
{ key: "arduino_core", label: "Core Arduino" },
{ key: "depuracao", label: "Depuração / JTAG" },
{ key: "encapsulamento", label: "Encapsulamento" },
{ key: "modulos", label: "Módulos disponíveis" },
]},
{ category: "Processador", fields: [
{ key: "arquitetura", label: "Arquitetura" },
{ key: "nucleos", label: "Núcleos" },
{ key: "frequencia", label: "Frequência" },
{ key: "coprocessador_ulp", label: "Coprocessador ULP" },
{ key: "aceleradores_ia", label: "Aceleradores de IA" },
]},
{ category: "Conectividade", fields: [
{ key: "wifi", label: "Wi-Fi" },
{ key: "bluetooth", label: "Bluetooth" },
{ key: "zigbee_thread", label: "Zigbee/Thread" },
{ key: "matter", label: "Matter" },
{ key: "ethernet", label: "Ethernet" },
]},
{ category: "Memória", fields: [
{ key: "memoria_sram", label: "SRAM" },
{ key: "memoria_sram_rtc", label: "SRAM RTC" },
{ key: "memoria_rom", label: "ROM" },
{ key: "flash_externa", label: "Flash Externa" },
{ key: "psram_externa", label: "PSRAM Externa" },
]},
{ category: "Periféricos Digitais", fields: [
{ key: "gpio", label: "GPIO" },
{ key: "uart", label: "UART" },
{ key: "spi", label: "SPI" },
{ key: "i2c", label: "I²C" },
{ key: "i2s", label: "I²S" },
{ key: "pwm", label: "PWM" },
{ key: "can", label: "CAN" },
{ key: "usb", label: "USB" },
{ key: "depuracao", label: "Depuração / JTAG" },
{ key: "sdio", label: "SDIO" },
]},
{ category: "Periféricos Analógicos", fields: [
{ key: "adc", label: "ADC" },
{ key: "dac", label: "DAC" },
{ key: "touch", label: "Touch Capacitivo" },
{ key: "sensor_hall", label: "Sensor Hall" },
{ key: "sensor_temperatura", label: "Sensor de Temperatura" },
]},
{ category: "Segurança", fields: [
{ key: "seguranca.aes", label: "AES" },
{ key: "seguranca.sha", label: "SHA" },
{ key: "seguranca.rsa", label: "RSA / MPI" },
{ key: "seguranca.ecc", label: "ECC (curvas elípticas)" },
{ key: "seguranca.ecdsa", label: "ECDSA em hardware" },
{ key: "seguranca.hmac", label: "HMAC" },
{ key: "seguranca.assinatura_digital", label: "Assinatura Digital (DS)" },
{ key: "seguranca.key_manager", label: "Key Manager" },
{ key: "seguranca.rng", label: "Gerador aleatório (RNG)" },
{ key: "seguranca.criptografia_flash", label: "Criptografia de flash" },
{ key: "seguranca.secure_boot", label: "Secure Boot" },
{ key: "seguranca.protecao_dpa", label: "Proteção contra DPA" },
]},
{ category: "Interfaces Especiais", fields: [
{ key: "lcd", label: "LCD" },
{ key: "camera", label: "Câmera" },
{ key: "mipi_csi", label: "MIPI CSI" },
{ key: "mipi_dsi", label: "MIPI DSI" },
{ key: "ethernet_mac", label: "Ethernet MAC" },
]},
{ category: "Especificações Ambientais", fields: [
{ key: "temperatura_operacao", label: "Temp. Operação" },
{ key: "temperatura_armazenamento", label: "Temp. Armazenamento" },
]},
];
const renderValue = (value) => {
if (!value || value === "Não") return <span className="text-muted">Não</span>;
if (value === "Sim") {
return (
<span className="inline-flex items-center gap-1.5 font-medium text-ok">
<Check className="h-4 w-4" aria-hidden="true" />
Sim
</span>
);
}
return <span className="font-medium text-ink">{value}</span>;
};
const valores = (key) => selectedSeries.map((k) => String(valorDe(seriesData[k], key) ?? "Não"));
const diferem = (key) => new Set(valores(key)).size > 1;
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
<PageHero
trail={[{ label: "Comparador" }]}
icon={Scale}
label="Até quatro séries lado a lado"
title="Comparador de séries"
lede="Coloque núcleos, rádio, memória, periféricos e segurança na mesma tabela e veja o que realmente muda de um chip para outro."
/>
<section className="shell pt-10 sm:pt-12" aria-labelledby="escolha-titulo">
<div className="panel p-5 sm:p-7">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h2 id="escolha-titulo" className="text-base font-semibold text-ink">Escolha de {MIN_SERIES} a {MAX_SERIES} séries</h2>
<p role="status" aria-live="polite" className="mt-1 text-sm text-muted">
{selectedSeries.length} de {MAX_SERIES} selecionadas
{selectedSeries.length === MAX_SERIES && ". Remova uma para trocar."}
</p>
</div>
</div>
<div className="mt-4 flex flex-wrap gap-2">
{series.map(([key]) => {
const marcada = selectedSeries.includes(key);
const bloqueada = !marcada && selectedSeries.length >= MAX_SERIES;
return (
<button
key={key}
type="button"
onClick={() => toggleSeries(key)}
aria-pressed={marcada}
aria-disabled={bloqueada}
className={`chip-btn ${bloqueada ? "opacity-50" : ""}`}
>
{marcada && <Check className="h-4 w-4" aria-hidden="true" />}
{key}
</button>
);
})}
</div>
</div>
</section>
<section className="shell mt-8" aria-labelledby="tabela-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="tabela-titulo" className="text-base font-semibold text-ink">Especificações</h2>
<label className="flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-medium text-ink-soft">
<input
type="checkbox"
checked={soDiferencas}
onChange={(e) => setSoDiferencas(e.target.checked)}
className="h-5 w-5 cursor-pointer accent-[var(--accent)]"
/>
Mostrar só as diferenças
</label>
</div>
<div className="panel mt-3 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full min-w-[30rem] border-collapse text-sm">
<caption className="sr-only">Comparação das séries {selectedSeries.join(", ")}</caption>
<thead>
<tr className="border-b border-line bg-surface-2">
<th scope="col" className="sticky left-0 z-20 w-28 bg-surface-2 px-4 py-4 text-left font-semibold text-muted sm:w-52 sm:px-6">
Especificação
</th>
{selectedSeries.map((seriesKey) => (
<th key={seriesKey} scope="col" className="min-w-40 px-4 py-4 text-left sm:px-6">
<Link href={`/series/${seriesKey}`} className="inline-flex min-h-11 items-center gap-1 font-display text-lg font-semibold tracking-[-0.03em] text-ink hover:text-accent">
{seriesKey}
<ArrowUpRight className="h-4 w-4 text-muted" aria-hidden="true" />
</Link>
</th>
))}
</tr>
</thead>
<tbody>
{compareFields.map(({ category, fields }) => {
const visiveis = soDiferencas ? fields.filter(({ key }) => diferem(key)) : fields;
if (soDiferencas && visiveis.length === 0) return null;
const isCollapsed = collapsedCategories[category] && !soDiferencas;
return (
<Fragment key={category}>
<tr className="border-y border-line bg-surface-2/60">
<th scope="rowgroup" colSpan={selectedSeries.length + 1} className="p-0 text-left">
<button
type="button"
onClick={() => toggleCategory(category)}
aria-expanded={!isCollapsed}
className="flex min-h-12 w-full cursor-pointer items-center justify-between gap-3 px-4 py-3 text-left font-semibold text-ink sm:px-6"
>
<span className="sticky left-4 sm:left-6">{category}</span>
<ChevronDown className={`h-4 w-4 shrink-0 text-muted transition-transform ${isCollapsed ? "" : "rotate-180"}`} aria-hidden="true" />
</button>
</th>
</tr>
{!isCollapsed &&
visiveis.map(({ key, label }) => (
<tr key={key} className="border-b border-line last:border-b-0 hover:bg-surface-2/50">
<th scope="row" className="sticky left-0 z-10 bg-surface px-4 py-3.5 text-left align-top font-medium text-muted sm:px-6">
{label}
</th>
{selectedSeries.map((seriesKey) => (
<td key={seriesKey} className="px-4 py-3.5 align-top sm:px-6">
{renderValue(valorDe(seriesData[seriesKey], key))}
</td>
))}
</tr>
))}
</Fragment>
);
})}
</tbody>
</table>
</div>
</div>
</section>
<section className="shell section" aria-labelledby="docs-titulo">
<h2 id="docs-titulo" className="section-title">Documentação oficial</h2>
<div className="mt-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{selectedSeries.map((seriesKey) => {
const seriesItem = seriesData[seriesKey];
return (
<div key={seriesKey} className="card flex flex-col justify-between gap-5 p-5">
<Link href={`/series/${seriesKey}`} className="inline-flex min-h-11 items-center font-display text-lg font-semibold tracking-[-0.03em] text-ink hover:text-accent">
{seriesKey}
</Link>
{seriesItem.datasheet ? (
<a href={seriesItem.datasheet} target="_blank" rel="noopener noreferrer" className="btn btn-ghost btn-sm">
Datasheet oficial
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
</a>
) : (
<span className="text-sm text-muted">Datasheet ainda não publicado</span>
)}
</div>
);
})}
</div>
<div className="panel mt-10 flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<h2 className="section-title">Quer testar na bancada?</h2>
<p className="mt-3 text-base text-muted">Veja as placas de desenvolvimento com pinagem, periféricos e link para a loja.</p>
</div>
<Link href="/catalogo" className="btn btn-primary shrink-0">
Ver catálogo de placas
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</section>
</main>
<Footer />
</div>
);
}
+74
View File
@@ -0,0 +1,74 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
export const metadata = paginaMeta({
titulo: "ESP Component Registry & IDF Component Manager · ESPDocs",
descricao: "Catálogo oficial e interativo do ESP Component Registry: drivers de displays, LVGL, reconhecimento de voz ESP-SR, RainMaker, BSPs e gerador de idf_component.yml.",
caminho: "/componentes",
keywords: [
"ESP Component Registry",
"IDF Component Manager",
"idf_component.yml",
"idf.py add-dependency",
"componentes ESP32",
"espressif esp_lvgl_port",
"espressif esp_rainmaker",
"espressif esp-sr",
"bibliotecas ESP-IDF",
],
});
const faqs = [
{
pergunta: "O que é o ESP Component Registry e o IDF Component Manager?",
resposta: "O ESP Component Registry (components.espressif.com) é o repositório oficial de pacotes modulares da Espressif. O IDF Component Manager é a ferramenta integrada ao ESP-IDF (a partir do v4.4 e padrão no v5.x+) que baixa, compila e resolve versões de dependências automaticamente a partir do manifesto idf_component.yml.",
},
{
pergunta: "Como adicionar um componente ao meu projeto ESP-IDF?",
resposta: "Você pode executar o comando 'idf.py add-dependency \"namespace/componente^versao\"' no terminal do projeto (por exemplo: idf.py add-dependency \"espressif/esp_lvgl_port^2.4.0\") ou editar diretamente o arquivo main/idf_component.yml.",
},
{
pergunta: "Qual a diferença entre usar o Component Registry e git submodules?",
resposta: "O Component Registry elimina a complexidade de gerenciar submódulos Git pesados e versões manuais. Ele oferece versionamento semântico (^ e ~), resolução automática de dependências transitivas e compilação modular sob demanda.",
},
{
pergunta: "Posso criar e publicar meus próprios componentes?",
resposta: "Sim, qualquer desenvolvedor ou empresa pode publicar componentes públicos ou privados no ESP Component Registry utilizando a ferramenta oficial Compote CLI (pip install idf-component-manager).",
},
];
export default function ComponentesLayout({ children }) {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "ESP Component Registry", caminho: "/componentes" },
])
),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd.perguntas(faqs)),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: "Hub do ESP Component Registry & Construtor de Manifesto",
descricao: "Catálogo curado e gerador de manifesto idf_component.yml para componentes modulares do ESP-IDF.",
caminho: "/componentes",
})
),
}}
/>
{children}
</>
);
}
+419
View File
@@ -0,0 +1,419 @@
"use client";
import { useState, useMemo } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import SectionHead from "@/components/SectionHead";
import {
CATEGORIAS_COMPONENTES,
COMPONENTES_REGISTRY,
GUIA_COMPONENT_MANAGER
} from "@/lib/componentRegistryData";
import {
ArrowUpRight,
Box,
Brain,
Check,
CircuitBoard,
Cloud,
Copy,
Cpu,
Download,
HelpCircle,
Home,
Layers,
Monitor,
Plus,
Search,
ShieldCheck,
Terminal,
Trash2,
Wrench,
X,
} from "lucide-react";
const ICONES_CATEGORIA = {
ui_display: Monitor,
cloud_conectividade: Cloud,
audio_voz_ia: Brain,
smart_home_mesh: Home,
drivers_perifericos: Cpu,
bsp_placas: CircuitBoard,
sistema_utilitarios: Wrench,
};
export default function ComponentRegistryPage() {
const [busca, setBusca] = useState("");
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
const [chipFiltro, setChipFiltro] = useState("todos");
const [copiadoId, setCopiadoId] = useState(null);
const [manifestoCopiado, setManifestoCopiado] = useState(false);
// Componentes selecionados no construtor de idf_component.yml
const [componentesSelecionados, setComponentesSelecionados] = useState([
"espressif-esp_lvgl_port",
"espressif-led_indicator"
]);
// Lista de chips únicos para filtro
const chipsDisponiveis = ["todos", "ESP32", "ESP32-S3", "ESP32-C6", "ESP32-P4", "ESP32-H2", "ESP32-C3"];
// Filtragem dos componentes
const componentesFiltrados = useMemo(() => {
return COMPONENTES_REGISTRY.filter((comp) => {
const matchCategoria = categoriaAtiva === "todos" || comp.categoria === categoriaAtiva;
const matchChip = chipFiltro === "todos" || (comp.chips && comp.chips.includes(chipFiltro));
const matchBusca =
busca.trim() === "" ||
comp.name.toLowerCase().includes(busca.toLowerCase()) ||
comp.namespace.toLowerCase().includes(busca.toLowerCase()) ||
comp.titulo.toLowerCase().includes(busca.toLowerCase()) ||
comp.descricao.toLowerCase().includes(busca.toLowerCase()) ||
comp.tags.some((t) => t.toLowerCase().includes(busca.toLowerCase()));
return matchCategoria && matchChip && matchBusca;
});
}, [busca, categoriaAtiva, chipFiltro]);
// Manipuladores de cópia
const copiarComando = (id, comando) => {
navigator.clipboard.writeText(comando);
setCopiadoId(id);
setTimeout(() => setCopiadoId(null), 2000);
};
const toggleSelecaoManifesto = (id) => {
setComponentesSelecionados((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
);
};
// Geração dinâmica do manifesto YAML
const manifestoYaml = useMemo(() => {
const selecionados = COMPONENTES_REGISTRY.filter((c) =>
componentesSelecionados.includes(c.id)
);
if (selecionados.length === 0) {
return `# Nenhum componente selecionado.\n# Clique em "+ Adicionar ao Manifesto" nos cards abaixo.`;
}
let yaml = `# main/idf_component.yml\n# Gerado via ESPDocs Component Hub (https://espdocs.cienciaembarcada.com.br/componentes)\n\ndependencies:\n`;
selecionados.forEach((c) => {
yaml += ` ${c.namespace}/${c.name}: "${c.versao}"\n`;
});
return yaml;
}, [componentesSelecionados]);
const copiarManifesto = () => {
navigator.clipboard.writeText(manifestoYaml);
setManifestoCopiado(true);
setTimeout(() => setManifestoCopiado(false), 2000);
};
const baixarManifesto = () => {
const blob = new Blob([manifestoYaml], { type: "text/yaml;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "idf_component.yml";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const limparFiltros = () => {
setBusca("");
setCategoriaAtiva("todos");
setChipFiltro("todos");
};
const totalFiltros = (busca.trim() ? 1 : 0) + (categoriaAtiva !== "todos" ? 1 : 0) + (chipFiltro !== "todos" ? 1 : 0);
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
<PageHero
trail={[{ label: "Component Registry" }]}
icon={Box}
label="ESP-IDF Component Manager"
title="ESP Component Registry"
lede="O catálogo oficial de pacotes reutilizáveis da Espressif para o ESP-IDF. Escolha drivers, voz e pilhas industriais, copie o comando e gere o manifesto do projeto."
>
<a href="https://components.espressif.com" target="_blank" rel="noopener noreferrer" className="btn btn-primary">
Abrir o registro oficial
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
</a>
<a href="#manifesto-builder" className="btn btn-ghost">
Gerar idf_component.yml
</a>
</PageHero>
<section className="shell pt-10 sm:pt-12" aria-label="Busca e filtros">
<div className="panel p-5 sm:p-6">
<div className="relative">
<label htmlFor="busca-componente" className="sr-only">Buscar por nome, namespace ou tag</label>
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
id="busca-componente"
type="search"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder="Buscar por nome, namespace ou tag"
autoComplete="off"
className="field pl-12 pr-4 text-base"
/>
</div>
<div className="mt-5">
<h2 className="text-sm font-semibold text-ink">Categoria</h2>
<div className="mt-3 flex flex-wrap gap-2">
{CATEGORIAS_COMPONENTES.map((cat) => {
const Icone = ICONES_CATEGORIA[cat.id] ?? Layers;
const contagem = cat.id === "todos" ? COMPONENTES_REGISTRY.length : COMPONENTES_REGISTRY.filter((c) => c.categoria === cat.id).length;
return (
<button key={cat.id} type="button" onClick={() => setCategoriaAtiva(cat.id)} aria-pressed={categoriaAtiva === cat.id} className="chip-btn">
<Icone className="h-4 w-4" aria-hidden="true" />
{cat.id === "todos" ? "Todos" : cat.nome.replace(/\s*\(.*\)/, "").split(",")[0].split(" &")[0]}
<span className="tabular-nums opacity-80">{contagem}</span>
</button>
);
})}
</div>
</div>
<div className="mt-5 border-t border-line pt-5">
<h2 className="text-sm font-semibold text-ink">Chip compatível</h2>
<div className="mt-3 flex flex-wrap gap-2">
{chipsDisponiveis.map((chip) => (
<button key={chip} type="button" onClick={() => setChipFiltro(chip)} aria-pressed={chipFiltro === chip} className="chip-btn">
{chip === "todos" ? "Todos" : chip}
</button>
))}
</div>
</div>
</div>
</section>
<section className="shell section pt-8 sm:pt-10" aria-labelledby="lista-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="lista-titulo" role="status" aria-live="polite" className="text-base font-semibold text-ink">
{componentesFiltrados.length} {componentesFiltrados.length === 1 ? "componente" : "componentes"}
</h2>
<div className="flex flex-wrap items-center gap-x-5 gap-y-1">
{totalFiltros > 0 && (
<button type="button" onClick={limparFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
<X className="h-4 w-4" aria-hidden="true" />
Limpar filtros
</button>
)}
<a href="#manifesto-builder" className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
Manifesto: {componentesSelecionados.length} {componentesSelecionados.length === 1 ? "componente" : "componentes"}
</a>
</div>
</div>
{componentesFiltrados.length === 0 ? (
<div className="card mt-6 px-6 py-14 text-center">
<Box className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h3 className="mt-4 font-display text-xl font-semibold text-ink">Nenhum componente encontrado</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Busque por um termo mais geral ou volte para todas as categorias.</p>
<button type="button" onClick={limparFiltros} className="btn btn-primary mt-6">Limpar filtros</button>
</div>
) : (
<div className="mt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{componentesFiltrados.map((comp) => {
const selecionado = componentesSelecionados.includes(comp.id);
const cliCmd = `idf.py add-dependency "${comp.namespace}/${comp.name}${comp.versao}"`;
const foiCopiado = copiadoId === comp.id;
return (
<article key={comp.id} className={`card flex flex-col overflow-hidden ${selecionado ? "border-accent ring-2 ring-accent/20" : ""}`}>
<div className="flex-1 p-5 sm:p-6">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="flex flex-wrap items-center gap-2 text-xs text-muted">
{comp.namespace}/
{comp.oficial && (
<span className="chip chip-accent px-2 py-0">
<ShieldCheck className="h-3 w-3" aria-hidden="true" />
Oficial
</span>
)}
</p>
<h3 className="mt-1 break-words font-display text-xl font-semibold tracking-[-0.03em] text-ink">{comp.name}</h3>
<p className="mt-0.5 text-sm font-medium text-accent">{comp.titulo}</p>
</div>
<span className="shrink-0 rounded-md border border-line bg-surface-2 px-2 py-1 font-mono text-xs text-ink-soft">{comp.versao}</span>
</div>
<p className="mt-3 line-clamp-3 text-sm leading-relaxed text-muted">{comp.descricao}</p>
<ul className="mt-4 flex flex-wrap gap-1.5">
{comp.tags.map((tag) => (
<li key={tag} className="chip">{tag}</li>
))}
</ul>
</div>
<div className="border-t border-line bg-surface-2/60 p-5 sm:px-6">
<button
type="button"
onClick={() => copiarComando(comp.id, cliCmd)}
className="code-panel flex w-full min-h-11 items-center justify-between gap-3 rounded-lg px-3 py-2 text-left font-mono text-xs transition-colors hover:border-white/30"
aria-label={`Copiar comando para adicionar ${comp.name}`}
>
<span className="truncate text-[#e9eef4]">{cliCmd}</span>
{foiCopiado ? <Check className="h-4 w-4 shrink-0 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4 shrink-0 text-[#a6afbd]" aria-hidden="true" />}
</button>
<p role="status" className="sr-only">{foiCopiado ? "Comando copiado" : ""}</p>
<p className="mt-3 text-xs text-muted">
<span className="font-medium text-ink-soft">Chips: </span>
{comp.chips.join(", ")}
</p>
</div>
<div className="flex items-center justify-between gap-2 border-t border-line p-4 sm:px-6">
<button
type="button"
onClick={() => toggleSelecaoManifesto(comp.id)}
aria-pressed={selecionado}
className={`btn btn-sm ${selecionado ? "btn-primary" : "btn-ghost"}`}
>
{selecionado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Plus className="h-4 w-4" aria-hidden="true" />}
{selecionado ? "No manifesto" : "Adicionar ao manifesto"}
</button>
<a
href={comp.urlRegistry}
target="_blank"
rel="noopener noreferrer"
className="btn-icon inline-flex h-11 w-11 items-center justify-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-accent"
aria-label={`Abrir ${comp.name} no registro oficial (nova aba)`}
>
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
</div>
</article>
);
})}
</div>
)}
</section>
<section id="manifesto-builder" className="shell scroll-mt-28 pb-16" aria-labelledby="manifesto-titulo">
<div className="panel p-6 sm:p-9">
<div className="flex flex-col justify-between gap-5 md:flex-row md:items-end">
<div className="max-w-2xl">
<p className="eyebrow">Gerador de manifesto</p>
<h2 id="manifesto-titulo" className="section-title mt-3">Seu idf_component.yml</h2>
<p className="mt-3 text-sm leading-relaxed text-muted">
Salve o arquivo na pasta <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono">main/</code> do projeto e o
ESP-IDF baixa todas as bibliotecas na compilação.
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<button type="button" onClick={copiarManifesto} className="btn btn-primary btn-sm">
{manifestoCopiado ? <Check className="h-4 w-4" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
<span role="status">{manifestoCopiado ? "Copiado" : "Copiar YAML"}</span>
</button>
<button type="button" onClick={baixarManifesto} className="btn btn-ghost btn-sm">
<Download className="h-4 w-4" aria-hidden="true" />
Baixar .yml
</button>
{componentesSelecionados.length > 0 && (
<button type="button" onClick={() => setComponentesSelecionados([])} className="btn btn-ghost btn-sm">
<Trash2 className="h-4 w-4" aria-hidden="true" />
Limpar
</button>
)}
</div>
</div>
<div className="code-panel mt-6">
<pre tabIndex={0} aria-label="Conteúdo do idf_component.yml" className="!text-sm"><code className="text-[#e9eef4]">{manifestoYaml}</code></pre>
</div>
{componentesSelecionados.length > 0 && (
<ul className="mt-5 flex flex-wrap items-center gap-2" aria-label="Componentes incluídos">
{componentesSelecionados.map((id) => {
const c = COMPONENTES_REGISTRY.find((item) => item.id === id);
if (!c) return null;
return (
<li key={id} className="chip chip-accent gap-1 pr-1">
{c.name}
<button
type="button"
onClick={() => toggleSelecaoManifesto(id)}
className="-my-2 grid h-11 w-11 place-items-center rounded-full hover:bg-accent/15"
aria-label={`Remover ${c.name} do manifesto`}
>
<X className="h-3.5 w-3.5" aria-hidden="true" />
</button>
</li>
);
})}
</ul>
)}
</div>
</section>
<section className="shell section border-t border-line pt-14" aria-labelledby="guia-titulo">
<SectionHead id="guia-titulo" eyebrow="Guia rápido" title={GUIA_COMPONENT_MANAGER.titulo} lede={GUIA_COMPONENT_MANAGER.resumo} />
<ul className="mt-8 grid gap-3 md:grid-cols-2 lg:grid-cols-4">
{GUIA_COMPONENT_MANAGER.vantagens.map((v) => (
<li key={v.titulo} className="card p-5">
<h3 className="font-semibold text-ink">{v.titulo}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted">{v.descricao}</p>
</li>
))}
</ul>
<h3 className="mt-12 flex items-center gap-2 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
<Terminal className="h-5 w-5 text-accent" aria-hidden="true" />
Comandos essenciais
</h3>
<ul className="mt-4 space-y-3">
{GUIA_COMPONENT_MANAGER.comandosCli.map((cmd, i) => (
<li key={cmd.exemplo} className="code-panel flex flex-col justify-between gap-3 p-4 md:flex-row md:items-center">
<div className="min-w-0">
<code className="block break-all font-mono text-sm text-[#5eead4]">$ {cmd.exemplo}</code>
<p className="mt-1 text-sm text-[#a6afbd]">{cmd.explicacao}</p>
</div>
<button
type="button"
onClick={() => copiarComando(`cmd-${i}`, cmd.exemplo)}
className="inline-flex min-h-11 shrink-0 items-center gap-1.5 self-start rounded-full border border-white/15 px-4 text-sm font-medium text-[#e9eef4] transition-colors hover:bg-white/10 md:self-auto"
>
{copiadoId === `cmd-${i}` ? <Check className="h-4 w-4 text-[#5fe0a2]" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
{copiadoId === `cmd-${i}` ? "Copiado" : "Copiar"}
</button>
</li>
))}
</ul>
<div className="callout mt-8">
<h3 className="flex items-center gap-2 font-semibold text-ink">
<HelpCircle className="h-4 w-4 text-accent" aria-hidden="true" />
Versionamento semântico no ESP-IDF
</h3>
<ul className="mt-2 list-disc space-y-1.5 pl-5">
<li><code>^1.2.0</code>: aceita atualizações menores e correções (1.2.1, 1.3.0), mas bloqueia mudanças grandes de API (2.0.0). <strong>Recomendado.</strong></li>
<li><code>~1.2.0</code>: aceita só correções de bugs (1.2.1, 1.2.5).</li>
<li><code>1.2.0</code>: trava na versão exata.</li>
<li><code>*</code>: aceita qualquer versão. Evite em produção.</li>
</ul>
</div>
</section>
</main>
<Footer />
</div>
);
}
+75
View File
@@ -0,0 +1,75 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
export const metadata = paginaMeta({
titulo: "Diagnóstico de Erros Comuns no ESP32: Guia Profissional",
descricao: "Guia técnico definitivo para identificar e resolver Brownout, Guru Meditation, Task Watchdog, Stack Overflow, vazamento de memória e falhas de boot no ESP32 (Arduino e ESP-IDF).",
caminho: "/diagnostico",
keywords: [
"erros comuns ESP32",
"brownout detector was triggered",
"guru meditation error",
"task watchdog got triggered",
"stack canary watchpoint triggered",
"como debugar ESP32",
"ESP32 reiniciando em loop",
"failed to connect to esp32",
],
});
const faqs = [
{
pergunta: "O que causa o erro 'Brownout detector was triggered' no ESP32?",
resposta: "Ocorre quando a tensão no pino 3V3 cai abaixo de 2.8V ~ 2.43V devido a picos rápidos de corrente do rádio Wi-Fi/BLE (superando 500mA), cabos USB finos ou regulador LDO inadequado. A solução de engenharia envolve adicionar capacitores Low-ESR de 10µF a 100µF próximos ao VCC e usar LDOs robustos de 600mA+.",
},
{
pergunta: "Como resolver o 'Guru Meditation Error (LoadProhibited)'?",
resposta: "LoadProhibited indica tentativa de acessar um ponteiro nulo (NULL) ou inválido na memória. A solução consiste em usar o Espressif Exception Decoder ou addr2line para localizar a linha exata no código e implementar validações defensivas como if (ponteiro != NULL).",
},
{
pergunta: "Por que o Task Watchdog Timer (TWDT) dispara no FreeRTOS?",
resposta: "O TWDT dispara quando uma tarefa de prioridade maior monopoliza a CPU em um loop bloqueante (ex: while(1) sem vTaskDelay), impedindo a tarefa IDLE de alimentar o watchdog do sistema operacional.",
},
{
pergunta: "Por que conexões HTTPS e TLS falham com código -0x2700 no ESP32?",
resposta: "Porque ao ligar, o ESP32 inicia o relógio interno no ano de 1970. O validador de certificados TLS considera o certificado do servidor inválido por estar no 'futuro'. Sincronizar o relógio via SNTP (configTime) antes da conexão resolve o problema.",
},
];
export default function DiagnosticoLayout({ children }) {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Diagnóstico & Erros Comuns", caminho: "/diagnostico" },
])
),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.perguntas(faqs)
),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: "Guia de Diagnóstico e Erros no ESP32",
descricao: "Ferramenta e guia técnico de análise de falhas, pânicos de CPU e decodificação de logs no ESP32.",
caminho: "/diagnostico",
})
),
}}
/>
{children}
</>
);
}
+469
View File
@@ -0,0 +1,469 @@
"use client";
import { useState, useMemo, useRef } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import {
categoriasDiagnostico,
sintomasRapidos,
errosCatalogo,
regrasDecodificadorLog,
} from "@/lib/diagnosticoData";
import {
Activity,
AlertTriangle,
Check,
CheckCircle2,
ChevronDown,
Copy,
Cpu,
ExternalLink,
HelpCircle,
Layers,
LayoutGrid,
RotateCcw,
Search,
ShieldAlert,
Terminal,
Wifi,
Wrench,
X,
XCircle,
Zap,
} from "lucide-react";
/** Os rótulos dos sintomas vêm com emoji nos dados; a interface usa só texto e ícones. */
const semEmoji = (texto) => texto.replace(/^[\p{Extended_Pictographic}\uFE0F\s]+/u, "");
const ICONES = { Zap, Cpu, Layers, Activity, Terminal, Wifi };
const EXEMPLOS_LOG = [
["Brownout", "Brownout detector was triggered\n\nrst:0xc (SW_CPU_RESET),boot:0x13"],
["LoadProhibited", "Guru Meditation Error: Core 0 panic'ed (LoadProhibited).\nEXCVADDR: 0x00000000"],
["Task watchdog", "Task watchdog got triggered. The following tasks/users did not reset the watchdog: - IDLE0"],
["Handshake TLS", "mbedtls_ssl_handshake returned -0x2700\nThe certificate is not yet valid"],
];
const GRAVIDADE = {
"Crítica": "text-danger",
Alta: "text-gold",
};
const CHECKLISTS = [
{
icon: Zap,
titulo: "Hardware e alimentação",
itens: [
"Capacitor de 10 µF a 100 µF de baixo ESR no VCC, mais um cerâmico de 100 nF junto ao chip.",
"LDO dimensionado para picos de 600 mA a 1 A, com folga térmica.",
"Nenhum periférico forçando nível lógico nos pinos de strapping durante o boot.",
"Capacitor de 10 µF ou 1 µF no pino EN (RST) para auto-reset estável na gravação.",
],
},
{
icon: Layers,
titulo: "Firmware e FreeRTOS",
itens: [
"Toda tarefa FreeRTOS cede a CPU com vTaskDelay, filas ou semáforos.",
"Toda ISR marcada com IRAM_ATTR e sem chamadas bloqueantes.",
"Sem uso contínuo de String do Arduino em laços, para evitar fragmentação do heap.",
"Horário sincronizado por SNTP antes de abrir conexões TLS/HTTPS.",
],
},
];
export default function DiagnosticoPage() {
const [termoBusca, setTermoBusca] = useState("");
const [categoriaAtiva, setCategoriaAtiva] = useState("todos");
const [sintomaAtivo, setSintomaAtivo] = useState(null);
const [cardAberto, setCardAberto] = useState(null);
const [linguagemCodigo, setLinguagemCodigo] = useState({}); // { [id]: 'arduino' | 'espidf' }
const [copiadoId, setCopiadoId] = useState(null);
const searchInputRef = useRef(null);
// Estado do Analisador de Logs
const [logInput, setLogInput] = useState("");
const [resultadoLog, setResultadoLog] = useState(null);
// Função de Análise do Log
const analisarLog = (texto) => {
setLogInput(texto);
if (!texto || texto.trim() === "") {
setResultadoLog(null);
return;
}
for (const regra of regrasDecodificadorLog) {
if (regra.padrao.test(texto)) {
setResultadoLog(regra);
return;
}
}
setResultadoLog({
titulo: "Padrão de erro não categorizado automaticamente",
tipo: "Desconhecido",
explicacao:
"Não identificamos uma assinatura clássica de Guru Meditation ou Brownout neste trecho. Verifique se a taxa de baud rate (ex: 115200) está correta e se o cabeçalho 'rst:' ou 'panic' está presente.",
acaoRecomendada:
"Procure por palavras-chave como 'Guru Meditation', 'rst:', 'LoadProhibited', 'watchdog' ou 'panic' no console serial.",
});
};
const copiarParaClipboard = (texto, id) => {
navigator.clipboard.writeText(texto);
setCopiadoId(id);
setTimeout(() => setCopiadoId(null), 2000);
};
// Filtragem dos erros
const errosFiltrados = useMemo(() => {
const termo = termoBusca.toLowerCase().trim();
return errosCatalogo.filter((erro) => {
const matchCategoria =
categoriaAtiva === "todos" || erro.categoria === categoriaAtiva;
const matchSintoma =
!sintomaAtivo || erro.sintomas.includes(sintomaAtivo);
const matchBusca =
!termo ||
erro.titulo.toLowerCase().includes(termo) ||
erro.resumo.toLowerCase().includes(termo) ||
erro.causaRaiz.toLowerCase().includes(termo) ||
erro.logSerial.toLowerCase().includes(termo);
return matchCategoria && matchSintoma && matchBusca;
});
}, [categoriaAtiva, sintomaAtivo, termoBusca]);
const temFiltroAtivo =
termoBusca.trim() !== "" ||
categoriaAtiva !== "todos" ||
sintomaAtivo !== null;
const limparTodosFiltros = () => {
setTermoBusca("");
setCategoriaAtiva("todos");
setSintomaAtivo(null);
searchInputRef.current?.focus();
};
const toggleCard = (id) => {
setCardAberto((prev) => (prev === id ? null : id));
};
const setLang = (id, lang) => {
setLinguagemCodigo((prev) => ({ ...prev, [id]: lang }));
};
const totalFiltros = (termoBusca.trim() ? 1 : 0) + (categoriaAtiva !== "todos" ? 1 : 0) + (sintomaAtivo ? 1 : 0);
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
<PageHero
trail={[{ label: "Diagnóstico" }]}
icon={AlertTriangle}
label="Depuração de firmware"
title="Diagnóstico de falhas do ESP32"
lede="Entenda a causa de Brownout, Guru Meditation, Watchdog e Stack Overflow, e aplique a correção de engenharia, não o remendo."
>
<a
href="https://cienciaembarcada.com.br/publicacoes/esp32-5-erros-comuns-e-como-resolve-los-profissionalmente/"
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong"
>
Baseado no artigo do Ciência Embarcada
<ExternalLink className="h-4 w-4" aria-hidden="true" />
<span className="sr-only"> (abre em nova aba)</span>
</a>
</PageHero>
<section className="shell pt-10 sm:pt-12" aria-labelledby="decodificador-titulo">
<div className="panel p-5 sm:p-8">
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-end">
<div className="max-w-xl">
<h2 id="decodificador-titulo" className="section-title">Cole o log e veja a causa</h2>
<p className="mt-2 text-sm text-muted">Copie o trecho do monitor serial. O diagnóstico aparece na hora.</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted">Testar com</span>
{EXEMPLOS_LOG.map(([rotulo, texto]) => (
<button key={rotulo} type="button" onClick={() => analisarLog(texto)} className="chip-btn">
{rotulo}
</button>
))}
</div>
</div>
<div className="relative mt-5">
<label htmlFor="log-serial" className="sr-only">Log do monitor serial</label>
<textarea
id="log-serial"
rows={4}
value={logInput}
onChange={(e) => analisarLog(e.target.value)}
placeholder="Cole o log do monitor serial aqui"
spellCheck={false}
className="code-panel field !min-h-32 w-full resize-y !rounded-xl !border-white/10 !bg-[var(--code-bg)] p-4 font-mono text-sm !text-[#e9eef4] placeholder:!text-[#a6afbd]"
/>
{logInput && (
<button type="button" onClick={() => analisarLog("")} className="absolute right-3 top-3 inline-flex min-h-11 items-center gap-1 rounded-full border border-white/15 px-3 text-xs font-medium text-[#e9eef4] hover:bg-white/10">
<X className="h-3.5 w-3.5" aria-hidden="true" />
Limpar
</button>
)}
</div>
<div role="status" aria-live="polite">
{resultadoLog && (
<div className="callout callout-warn mt-4 flex items-start gap-3">
<ShieldAlert className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
<div className="min-w-0 space-y-2">
<div className="flex flex-wrap items-center gap-2">
<h3 className="font-semibold text-ink">{resultadoLog.titulo}</h3>
<span className="chip">{resultadoLog.tipo}</span>
</div>
<p><strong className="font-semibold text-ink">Causa provável:</strong> {resultadoLog.explicacao}</p>
<p><strong className="font-semibold text-ink">O que fazer:</strong> {resultadoLog.acaoRecomendada}</p>
</div>
</div>
)}
</div>
</div>
</section>
<section className="shell section pt-12 sm:pt-14" aria-labelledby="catalogo-titulo">
<h2 id="catalogo-titulo" className="section-title">Catálogo de falhas</h2>
<div className="panel mt-6 p-5 sm:p-6">
<div className="relative">
<label htmlFor="busca-erro" className="sr-only">Buscar por erro, sintoma ou mensagem serial</label>
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
id="busca-erro"
ref={searchInputRef}
type="search"
value={termoBusca}
onChange={(e) => setTermoBusca(e.target.value)}
placeholder="Buscar erro, sintoma ou mensagem"
autoComplete="off"
className="field pl-12 pr-4 text-base"
/>
</div>
<div className="mt-5">
<h3 className="text-sm font-semibold text-ink">Sintoma</h3>
<div className="mt-3 flex flex-wrap gap-2">
{sintomasRapidos.map((sintoma) => (
<button key={sintoma.id} type="button" onClick={() => setSintomaAtivo(sintomaAtivo === sintoma.id ? null : sintoma.id)} aria-pressed={sintomaAtivo === sintoma.id} className="chip-btn">
{semEmoji(sintoma.rotulo)}
</button>
))}
</div>
</div>
<div className="mt-5 border-t border-line pt-5">
<h3 className="text-sm font-semibold text-ink">Área</h3>
<div className="mt-3 flex flex-wrap gap-2">
<button type="button" onClick={() => setCategoriaAtiva("todos")} aria-pressed={categoriaAtiva === "todos"} className="chip-btn">
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
Todas
<span className="tabular-nums opacity-80">{errosCatalogo.length}</span>
</button>
{categoriasDiagnostico.map((cat) => {
const Icone = ICONES[cat.icone] ?? AlertTriangle;
const total = errosCatalogo.filter((e) => e.categoria === cat.id).length;
return (
<button key={cat.id} type="button" onClick={() => setCategoriaAtiva(cat.id)} aria-pressed={categoriaAtiva === cat.id} className="chip-btn">
<Icone className="h-4 w-4" aria-hidden="true" />
{cat.nome.split(/ &|,/)[0]}
<span className="tabular-nums opacity-80">{total}</span>
</button>
);
})}
</div>
</div>
</div>
<div className="mt-6 flex flex-wrap items-center justify-between gap-3">
<p role="status" aria-live="polite" className="text-sm font-semibold text-ink">
{errosFiltrados.length} {errosFiltrados.length === 1 ? "falha" : "falhas"}
{totalFiltros > 0 && <span className="font-normal text-muted"> com os filtros atuais</span>}
</p>
{temFiltroAtivo && (
<button type="button" onClick={limparTodosFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
Redefinir filtros
</button>
)}
</div>
<div className="mt-3 space-y-3">
{errosFiltrados.length === 0 ? (
<div className="card px-6 py-14 text-center">
<HelpCircle className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h3 className="mt-4 font-display text-xl font-semibold text-ink">Nenhuma falha com esses filtros</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">Busque por um termo mais geral ou remova o sintoma e a área selecionados.</p>
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">Limpar filtros</button>
</div>
) : (
errosFiltrados.map((erro) => {
const estaAberto = cardAberto === erro.id;
const lang = linguagemCodigo[erro.id] || "arduino";
const codigoAtual =
lang === "arduino" ? erro.codigoArduino : lang === "espidf" ? erro.codigoEspIdf : erro.codigoMicroPython || "# Exemplo não aplicável";
const catInfo = categoriasDiagnostico.find((c) => c.id === erro.categoria);
const IconeCat = ICONES[catInfo?.icone] ?? AlertTriangle;
return (
<article key={erro.id} id={erro.id} className={`card scroll-mt-28 overflow-hidden ${estaAberto ? "border-line-strong" : ""}`}>
<h3>
<button
type="button"
onClick={() => toggleCard(erro.id)}
aria-expanded={estaAberto}
aria-controls={`detalhe-${erro.id}`}
className="flex w-full cursor-pointer items-start justify-between gap-4 p-5 text-left sm:p-6"
>
<span className="min-w-0">
<span className="flex flex-wrap items-center gap-2">
<span className="chip">
<IconeCat className="h-3.5 w-3.5 text-accent" aria-hidden="true" />
{catInfo?.nome}
</span>
<span className={`chip ${GRAVIDADE[erro.gravidade] ?? "text-accent"}`}>Gravidade {erro.gravidade.toLowerCase()}</span>
</span>
<span className="mt-3 block font-display text-xl font-semibold tracking-[-0.03em] text-ink">{erro.titulo}</span>
<span className="mt-1.5 block text-sm font-normal leading-relaxed text-muted">{erro.resumo}</span>
</span>
<span className="mt-1 grid h-9 w-9 shrink-0 place-items-center rounded-full bg-surface-2 text-muted">
<ChevronDown className={`h-4 w-4 transition-transform ${estaAberto ? "rotate-180" : ""}`} aria-hidden="true" />
</span>
</button>
</h3>
{estaAberto && (
<div id={`detalhe-${erro.id}`} className="space-y-6 border-t border-line p-5 sm:p-6">
<div>
<div className="flex items-center justify-between gap-3">
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
<Terminal className="h-4 w-4 text-accent" aria-hidden="true" />
Como aparece no monitor serial
</h4>
<button type="button" onClick={() => copiarParaClipboard(erro.logSerial, `log-${erro.id}`)} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
{copiadoId === `log-${erro.id}` ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
{copiadoId === `log-${erro.id}` ? "Copiado" : "Copiar log"}
</button>
</div>
<div className="code-panel mt-1">
<pre tabIndex={0}><code className="text-[#e9eef4]">{erro.logSerial}</code></pre>
</div>
</div>
<div>
<h4 className="flex items-center gap-2 text-sm font-semibold text-ink">
<Cpu className="h-4 w-4 text-accent" aria-hidden="true" />
Causa raiz
</h4>
<p className="callout mt-2">{erro.causaRaiz}</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="callout callout-danger">
<p className="flex items-center gap-2 font-semibold text-ink">
<XCircle className="h-4 w-4 shrink-0 text-danger" aria-hidden="true" />
O remendo comum
</p>
<p className="mt-2 font-medium text-ink">{erro.gambiarra.titulo}</p>
<p className="mt-1">{erro.gambiarra.descricao}</p>
<p className="mt-2"><strong className="font-semibold text-danger">Consequência:</strong> {erro.gambiarra.consequencia}</p>
</div>
<div className="callout callout-ok">
<p className="flex items-center gap-2 font-semibold text-ink">
<CheckCircle2 className="h-4 w-4 shrink-0 text-ok" aria-hidden="true" />
A solução de engenharia
</p>
<p className="mt-2 font-medium text-ink">{erro.solucaoEngenharia.titulo}</p>
<ul className="mt-2 space-y-1.5">
{erro.solucaoEngenharia.passos.map((passo) => (
<li key={passo} className="flex items-start gap-2">
<Check className="mt-1 h-3.5 w-3.5 shrink-0 text-ok" aria-hidden="true" />
{passo}
</li>
))}
</ul>
</div>
</div>
<div>
<div className="flex flex-wrap items-center justify-between gap-3">
<div role="group" aria-label="Linguagem do exemplo" className="flex flex-wrap gap-2">
{[
["arduino", "Arduino C++"],
["espidf", "ESP-IDF (C)"],
["micropython", "MicroPython"],
].map(([id, rotulo]) => (
<button key={id} type="button" onClick={() => setLang(erro.id, id)} aria-pressed={lang === id} className="chip-btn">
{rotulo}
</button>
))}
</div>
<button type="button" onClick={() => copiarParaClipboard(codigoAtual, `code-${erro.id}`)} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
{copiadoId === `code-${erro.id}` ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Copy className="h-4 w-4" aria-hidden="true" />}
{copiadoId === `code-${erro.id}` ? "Copiado" : "Copiar código"}
</button>
</div>
<div className="code-panel mt-3">
<pre tabIndex={0}><code className="text-[#e9eef4]">{codigoAtual}</code></pre>
</div>
</div>
<div className="callout callout-warn flex items-start gap-3">
<Wrench className="mt-0.5 h-4 w-4 shrink-0 text-gold" aria-hidden="true" />
<p><strong className="font-semibold text-ink">Dica de engenharia:</strong> {erro.dicaPro}</p>
</div>
</div>
)}
</article>
);
})
)}
</div>
</section>
<section className="shell pb-20 md:pb-28" aria-labelledby="checklist-titulo">
<h2 id="checklist-titulo" className="section-title">Checklist antes de ir para campo</h2>
<p className="mt-3 max-w-2xl text-base text-muted">Itens essenciais para validar no hardware e no firmware antes de entregar o produto.</p>
<div className="mt-8 grid gap-4 md:grid-cols-2">
{CHECKLISTS.map(({ icon: Icon, titulo, itens }) => (
<div key={titulo} className="card p-5 sm:p-7">
<h3 className="flex items-center gap-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
<span className="grid h-9 w-9 place-items-center rounded-full bg-accent-soft text-accent">
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
{titulo}
</h3>
<ul className="mt-5 space-y-3">
{itens.map((item) => (
<li key={item} className="flex items-start gap-3 text-sm leading-relaxed text-ink-soft">
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-ok" aria-hidden="true" />
{item}
</li>
))}
</ul>
</div>
))}
</div>
</section>
</main>
<Footer />
</div>
);
}
+180
View File
@@ -0,0 +1,180 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
import Link from "next/link";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import { notFound } from "next/navigation";
import CodeExample from "@/components/CodeExample";
import frameworksData from "@/public/frameworks.json";
import { ArrowRight, ArrowUpRight, BookOpen, Box, Check, Code, Layers } from "lucide-react";
export default async function FrameworkDetail({ params }) {
const { key } = await params;
const framework = frameworksData[key];
if (!framework) {
notFound();
}
const features = framework.caracteristicas || [];
const useCases = framework.casos_uso || [];
const isEspIdf = key === "ESP-IDF";
return (
<div className="site-canvas min-h-screen">
{/* Ficha técnica do framework e trilha de navegação para o Google */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.framework(key, framework)) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Frameworks", caminho: "/frameworks" },
{ nome: framework.nome, caminho: `/frameworks/${encodeURIComponent(key)}` },
])),
}}
/>
<Header />
<main id="conteudo">
<PageHero
trail={[{ label: "Frameworks e SDKs", href: "/frameworks" }, { label: framework.nome }]}
icon={Layers}
label={framework.tipo}
title={framework.nome}
lede={framework.descricao}
>
{framework.documentacao && (
<a href={framework.documentacao} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
<BookOpen className="h-4 w-4" aria-hidden="true" />
Documentação oficial
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
{framework.repositorio && (
<a href={framework.repositorio} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
<Code className="h-4 w-4" aria-hidden="true" />
Repositório no GitHub
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
</PageHero>
<section className="shell mt-10" aria-label="Resumo">
<dl className="grid gap-3 md:grid-cols-3">
<div className="tile">
<dt className="text-xs font-medium text-muted">Linguagem</dt>
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{framework.linguagem}</dd>
<dd className="mt-0.5 text-xs text-muted">{framework.nome_completo}</dd>
</div>
<div className="tile">
<dt className="text-xs font-medium text-muted">Categoria</dt>
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{framework.tipo}</dd>
</div>
<div className="tile">
<dt className="text-xs font-medium text-muted">Objetivo principal</dt>
<dd className="mt-1 text-sm font-medium leading-relaxed text-ink">{framework.funcao_principal}</dd>
</div>
</dl>
</section>
<section className="shell mt-12 grid gap-5 lg:grid-cols-2" aria-label="Recursos e aplicações">
<div className="card p-5 sm:p-7">
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Recursos de destaque</h2>
<ul className="mt-5 space-y-3">
{features.map((feature) => (
<li key={feature} className="flex items-start gap-3 text-sm leading-relaxed text-ink-soft">
<Check className="mt-0.5 h-4 w-4 shrink-0 text-ok" aria-hidden="true" />
{feature}
</li>
))}
</ul>
</div>
<div className="card p-5 sm:p-7">
<h2 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">Quando usar</h2>
<ul className="mt-5 space-y-2.5">
{useCases.map((useCase) => (
<li key={useCase} className="tile text-sm font-medium text-ink-soft">
{useCase}
</li>
))}
</ul>
</div>
</section>
{isEspIdf && (
<section className="shell mt-8">
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<span className="chip chip-accent">
<Box className="h-3.5 w-3.5" aria-hidden="true" />
ESP-IDF 4.4 ou superior
</span>
<h2 className="section-title mt-4">Módulos com o ESP Component Registry</h2>
<p className="mt-3 text-base leading-relaxed text-muted">
Nos projetos ESP-IDF v4.4+ e v5.x, as bibliotecas não precisam mais entrar como submódulos Git. Use o{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code> e o comando{" "}
<code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf.py add-dependency</code> para baixar drivers de display (LVGL), pilhas industriais (Modbus), RainMaker e mais.
</p>
</div>
<Link href="/componentes" className="btn btn-primary shrink-0">
Explorar componentes
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</section>
)}
{framework.exemplo && (
<section className="shell mt-8">
<CodeExample framework={framework} />
</section>
)}
<div className="shell section pt-10">
<Link href="/frameworks" className="inline-flex min-h-11 items-center gap-2 text-sm font-medium text-accent hover:text-accent-strong">
<ArrowRight className="h-4 w-4 rotate-180" aria-hidden="true" />
Ver todos os frameworks
</Link>
</div>
</main>
<Footer />
</div>
);
}
export async function generateMetadata({ params }) {
const { key } = await params;
const framework = frameworksData[key];
if (!framework) {
return { title: "Framework não encontrado", robots: { index: false, follow: false } };
}
return {
...paginaMeta({
titulo: `${framework.nome} para ESP32 · Guia Técnico`,
descricao: `${framework.nome_completo}: ${framework.descricao} Guia com exemplos práticos de código e casos de uso.`,
caminho: `/frameworks/${encodeURIComponent(key)}`,
tipo: "article",
keywords: [
framework.nome, framework.nome_completo,
`${framework.nome} ESP32`, `programar ESP32 com ${framework.nome}`,
"ESP32", "framework ESP32",
],
}),
other: { "article:section": "Frameworks" },
};
}
export async function generateStaticParams() {
return Object.keys(frameworksData).map((key) => ({
key: key,
}));
}
+39
View File
@@ -0,0 +1,39 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
export const metadata = paginaMeta({
titulo: "Frameworks & SDKs para ESP32: Guia Completo e Comparativo",
descricao: "Guia completo de desenvolvimento: ESP-IDF, ESP-RainMaker, ESP-Matter, Rust, ESP-ADF, ESP-SR, ESP-WHO, Arduino e MicroPython com exemplos de código.",
caminho: "/frameworks",
keywords: [
"ESP-IDF",
"ESP-RainMaker",
"ESP-Matter",
"Rust ESP32",
"ESP-SR",
"ESP-ADF",
"ESP-IoT-Solution",
"Arduino ESP32",
"MicroPython ESP32",
"frameworks ESP32",
"programar ESP32",
],
});
export default function FrameworksLayout({ children }) {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Frameworks", caminho: "/frameworks" },
])
),
}}
/>
{children}
</>
);
}
+136
View File
@@ -0,0 +1,136 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import SectionHead from "@/components/SectionHead";
import Link from "next/link";
import frameworksData from "@/public/frameworks.json";
import { ArrowRight, ArrowUpRight, Box, Brain, Cloud, GraduationCap, Layers, Mic, Music, Sparkles } from "lucide-react";
const GUIAS = [
{
icon: GraduationCap,
titulo: "Estou começando",
texto: "Arduino ou MicroPython são os caminhos mais rápidos para testar sensores e APIs. Para produto, vá de ESP-IDF ou Rust.",
links: [
["Arduino", "/frameworks/Arduino"],
["MicroPython", "/frameworks/MicroPython"],
["Rust", "/frameworks/Rust-ESP"],
],
},
{
icon: Cloud,
titulo: "Nuvem e casa conectada",
texto: "ESP-Matter para interoperabilidade, ESP-RainMaker para nuvem pronta e Zigbee para redes locais de baixo consumo.",
links: [
["RainMaker", "/frameworks/ESP-RainMaker"],
["Matter", "/frameworks/ESP-Matter"],
["Zigbee", "/frameworks/ESP-Zigbee-SDK"],
],
},
{
icon: Sparkles,
titulo: "Áudio, voz e IA",
texto: "ESP-ADF para áudio, ESP-SR para reconhecimento de fala offline e ESP-NN para acelerar TinyML.",
links: [
["Áudio", "/frameworks/ESP-ADF", Music],
["Voz", "/frameworks/ESP-SR", Mic],
["IA", "/frameworks/ESP-NN", Brain],
],
},
];
export default function Frameworks() {
const frameworks = Object.entries(frameworksData);
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
<PageHero
trail={[{ label: "Frameworks e SDKs" }]}
icon={Layers}
label={`${frameworks.length} frameworks e SDKs`}
title="Frameworks e SDKs do ESP32"
lede="Do C/C++ nativo com FreeRTOS ao Rust, passando por nuvem IoT, reconhecimento de voz, áudio e Matter. Escolha pelo que você quer construir."
/>
<section className="shell pt-10 sm:pt-12" aria-label="Por onde começar">
<div className="grid gap-4 md:grid-cols-3">
{GUIAS.map(({ icon: Icon, titulo, texto, links }) => (
<div key={titulo} className="card flex flex-col p-5 sm:p-6">
<span className="grid h-10 w-10 place-items-center rounded-full bg-accent-soft text-accent">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<h2 className="mt-4 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{titulo}</h2>
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted">{texto}</p>
<ul className="mt-5 flex flex-wrap gap-2 border-t border-line pt-4">
{links.map(([rotulo, href, Extra]) => (
<li key={rotulo}>
<Link href={href} className="chip-btn">
{Extra && <Extra className="h-4 w-4" aria-hidden="true" />}
{rotulo}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
</section>
<section className="shell mt-10">
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<span className="chip chip-accent">
<Box className="h-3.5 w-3.5" aria-hidden="true" />
Arquitetura modular
</span>
<h2 className="section-title mt-4">ESP Component Registry</h2>
<p className="mt-3 text-base leading-relaxed text-muted">
Instale drivers de display (LVGL), pilhas industriais (Modbus), recursos de nuvem e pacotes de placa
(BSPs) com uma linha no manifesto <code className="rounded bg-surface-2 px-1.5 py-0.5 font-mono text-sm">idf_component.yml</code>.
</p>
</div>
<Link href="/componentes" className="btn btn-primary shrink-0">
Explorar componentes
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</section>
<section className="shell section" aria-labelledby="todos-titulo">
<SectionHead id="todos-titulo" eyebrow="Todos" title="Todos os frameworks" lede="Cada ficha traz recursos, aplicações ideais, exemplo de código e links oficiais." />
<div className="mt-8 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{frameworks.map(([key, framework]) => (
<Link key={key} href={`/frameworks/${encodeURIComponent(key)}`} className="card card-interactive group flex flex-col p-5 sm:p-6">
<div className="flex flex-wrap items-start justify-between gap-2">
<h3 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">{framework.nome}</h3>
<span className="chip max-w-full">{framework.linguagem}</span>
</div>
<p className="mt-1 text-sm text-muted">{framework.tipo}</p>
<p className="mt-3 line-clamp-3 text-sm leading-relaxed text-muted">{framework.descricao}</p>
<ul className="mt-5 flex flex-1 flex-wrap content-start gap-1.5">
{framework.casos_uso.slice(0, 3).map((caso) => (
<li key={caso} className="chip max-w-full">
<span className="truncate">{caso}</span>
</li>
))}
</ul>
<span className="mt-5 inline-flex items-center gap-1 border-t border-line pt-4 text-sm font-medium text-accent">
Ver detalhes e exemplo
<ArrowUpRight className="h-4 w-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</Link>
))}
</div>
</section>
</main>
<Footer />
</div>
);
}
+54
View File
@@ -0,0 +1,54 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
import { categoriasGlossario } from "@/lib/glossarioData";
export const metadata = paginaMeta({
titulo: "Glossário Técnico do ESP32: Termos e Conceitos",
descricao: "Mais de 50 termos e conceitos essenciais do ecossistema ESP32 explicados em português: GPIO, Strapping Pins, ULP, PSRAM, FreeRTOS, eFuse, Deep Sleep, OTA, Secure Boot, ECDSA, Matter e Thread.",
caminho: "/glossario",
keywords: [
"glossário ESP32",
"o que é GPIO",
"o que é strapping pins",
"o que é ULP ESP32",
"o que é PSRAM ESP32",
"termos técnicos microcontrolador",
"conceitos ESP32 português",
],
});
export default function GlossarioLayout({ children }) {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd.glossario(categoriasGlossario)),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Glossário Técnico", caminho: "/glossario" },
])
),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: "Glossário Técnico do ESP32",
descricao: "Enciclopédia de termos técnicos e conceitos de hardware e software do ecossistema ESP32.",
caminho: "/glossario",
})
),
}}
/>
{children}
</>
);
}
+334
View File
@@ -0,0 +1,334 @@
"use client";
import { useState, useMemo, useRef } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import Link from "next/link";
import { categoriasGlossario } from "@/lib/glossarioData";
import {
ArrowRight,
BookOpen,
Check,
Code2,
Cpu,
Database,
HelpCircle,
LayoutGrid,
Lightbulb,
Link2,
BatteryCharging,
RotateCcw,
Search,
ShieldCheck,
Sliders,
Sparkles,
Wifi,
X,
} from "lucide-react";
const BUSCAS_POPULARES = ["GPIO", "ULP", "PSRAM", "Strapping Pins", "Deep Sleep", "OTA", "Matter", "Secure Boot", "Wi-Fi 6", "MIPI"];
const ICONES = { Cpu, Database, Wifi, Sliders, Sparkles, BatteryCharging, Code2, ShieldCheck };
/** "Processamento, CPU & Silício" vira "Processamento": cabe no filtro sem truncar. */
const nomeCurto = (nome) => nome.split(/,| &/)[0].trim();
export default function GlossarioPage() {
const [busca, setBusca] = useState("");
const [categoriaSelecionada, setCategoriaSelecionada] = useState("todas");
const [letraFiltro, setLetraFiltro] = useState(null);
const [copiadoSlug, setCopiadoSlug] = useState(null);
const searchInputRef = useRef(null);
const todosOsTermos = useMemo(
() =>
categoriasGlossario.flatMap((cat) =>
cat.termos.map((t) => ({
...t,
categoriaId: cat.id,
categoriaNome: cat.nome,
categoriaIcone: cat.icone,
slug: t.termo.toLowerCase().replace(/[\s()/²]+/g, "-"),
primeiraLetra: t.termo.charAt(0).toUpperCase(),
}))
),
[]
);
const alfabeto = useMemo(() => {
const disponiveis = new Set(todosOsTermos.map((t) => t.primeiraLetra));
return "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((letra) => ({ letra, temTermos: disponiveis.has(letra) }));
}, [todosOsTermos]);
const termosFiltrados = useMemo(() => {
const termoBusca = busca.toLowerCase().trim();
return todosOsTermos.filter((item) => {
const matchCategoria = categoriaSelecionada === "todas" || item.categoriaId === categoriaSelecionada;
const matchLetra = !letraFiltro || item.primeiraLetra === letraFiltro;
const matchBusca =
!termoBusca ||
item.termo.toLowerCase().includes(termoBusca) ||
item.definicao.toLowerCase().includes(termoBusca) ||
item.exemplo.toLowerCase().includes(termoBusca) ||
item.categoriaNome.toLowerCase().includes(termoBusca);
return matchCategoria && matchLetra && matchBusca;
});
}, [todosOsTermos, categoriaSelecionada, letraFiltro, busca]);
const temFiltroAtivo = busca.trim() !== "" || categoriaSelecionada !== "todas" || letraFiltro !== null;
const limparTodosFiltros = () => {
setBusca("");
setCategoriaSelecionada("todas");
setLetraFiltro(null);
searchInputRef.current?.focus();
};
const copiarLinkTermo = (slug) => {
navigator.clipboard.writeText(`${window.location.origin}/glossario#${slug}`);
setCopiadoSlug(slug);
setTimeout(() => setCopiadoSlug(null), 2000);
};
const total = termosFiltrados.length;
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
<PageHero
trail={[{ label: "Glossário" }]}
icon={BookOpen}
label={`${todosOsTermos.length} termos explicados`}
title="Glossário técnico do ESP32"
lede="Definições diretas, parâmetros de hardware e um exemplo prático para cada termo do ecossistema, de ADC a Secure Boot."
/>
<section className="shell pt-10 sm:pt-12" aria-label="Busca e filtros">
<div className="panel p-5 sm:p-7">
<div className="relative">
<label htmlFor="busca-glossario" className="sr-only">
Buscar termo, sigla ou conceito
</label>
<Search className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted" aria-hidden="true" />
<input
id="busca-glossario"
ref={searchInputRef}
type="search"
value={busca}
onChange={(e) => setBusca(e.target.value)}
placeholder="Buscar termo, sigla ou conceito"
autoComplete="off"
className={`field pl-12 text-base ${busca ? "pr-28" : "pr-4"}`}
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2">
{busca && (
<button type="button" onClick={() => { setBusca(""); searchInputRef.current?.focus(); }} className="btn btn-ghost btn-sm">
<X className="h-3.5 w-3.5" aria-hidden="true" />
Limpar
</button>
)}
</div>
</div>
<div className="mt-4 flex flex-wrap items-center gap-2">
<span className="mr-1 text-sm text-muted">Mais buscados</span>
{BUSCAS_POPULARES.map((rotulo) => (
<button
key={rotulo}
type="button"
onClick={() => setBusca(rotulo)}
aria-pressed={busca.toLowerCase() === rotulo.toLowerCase()}
className="chip-btn"
>
{rotulo}
</button>
))}
</div>
<div className="mt-7 border-t border-line pt-6">
<h2 className="text-sm font-semibold text-ink">Categoria</h2>
<div className="mt-3 flex flex-wrap gap-2">
<button type="button" onClick={() => setCategoriaSelecionada("todas")} aria-pressed={categoriaSelecionada === "todas"} className="chip-btn">
<LayoutGrid className="h-4 w-4" aria-hidden="true" />
Todas
<span className="tabular-nums opacity-80">{todosOsTermos.length}</span>
</button>
{categoriasGlossario.map((cat) => {
const Icone = ICONES[cat.icone] ?? Cpu;
return (
<button
key={cat.id}
type="button"
onClick={() => setCategoriaSelecionada(cat.id)}
aria-pressed={categoriaSelecionada === cat.id}
className="chip-btn"
>
<Icone className="h-4 w-4" aria-hidden="true" />
{nomeCurto(cat.nome)}
<span className="tabular-nums opacity-80">{cat.termos.length}</span>
</button>
);
})}
</div>
</div>
<div className="mt-6 border-t border-line pt-6">
<h2 className="text-sm font-semibold text-ink">Ordem alfabética</h2>
<div className="mt-3 flex flex-wrap gap-1.5" role="group" aria-label="Filtrar pela letra inicial">
<button type="button" onClick={() => setLetraFiltro(null)} aria-pressed={letraFiltro === null} className="chip-btn">
Todas
</button>
{alfabeto.map(({ letra, temTermos }) => (
<button
key={letra}
type="button"
disabled={!temTermos}
onClick={() => setLetraFiltro(letraFiltro === letra ? null : letra)}
aria-pressed={letraFiltro === letra}
className="chip-btn min-w-11 justify-center px-0 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-surface"
>
{letra}
</button>
))}
</div>
</div>
</div>
</section>
<section className="shell section pt-8 sm:pt-10" aria-labelledby="resultado-titulo">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 id="resultado-titulo" className="text-base font-semibold text-ink" role="status" aria-live="polite">
{total} {total === 1 ? "termo" : "termos"}
{temFiltroAtivo && <span className="font-normal text-muted"> com os filtros atuais</span>}
</h2>
{temFiltroAtivo && (
<button type="button" onClick={limparTodosFiltros} className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent hover:text-accent-strong">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
Redefinir filtros
</button>
)}
</div>
{total === 0 ? (
<div className="card mt-6 px-6 py-14 text-center">
<HelpCircle className="mx-auto h-10 w-10 text-muted" aria-hidden="true" />
<h3 className="mt-4 font-display text-xl font-semibold text-ink">Nenhum termo encontrado</h3>
<p className="mx-auto mt-2 max-w-md text-sm text-muted">
Nada combina com a busca e os filtros atuais. Tente uma sigla mais curta ou volte para todas as categorias.
</p>
<button type="button" onClick={limparTodosFiltros} className="btn btn-primary mt-6">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
Limpar filtros
</button>
</div>
) : (
<div className="mt-6 grid gap-4 md:grid-cols-2">
{termosFiltrados.map((item) => {
const Icone = ICONES[item.categoriaIcone] ?? Cpu;
return (
<article key={item.slug} id={item.slug} className="card flex scroll-mt-28 flex-col p-5 sm:p-6">
<div className="flex items-center justify-between gap-3">
<span className="chip">
<Icone className="h-3.5 w-3.5 text-accent" aria-hidden="true" />
{nomeCurto(item.categoriaNome)}
</span>
<button
type="button"
onClick={() => copiarLinkTermo(item.slug)}
aria-label={`Copiar link para ${item.termo}`}
className="btn-icon inline-flex h-11 w-11 items-center justify-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-accent"
>
{copiadoSlug === item.slug ? <Check className="h-4 w-4 text-ok" aria-hidden="true" /> : <Link2 className="h-4 w-4" aria-hidden="true" />}
</button>
</div>
<h3 className="mt-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{item.termo}</h3>
<p className="mt-2 text-[0.9375rem] leading-relaxed text-muted">
<TextoComLinks texto={item.definicao} />
</p>
<div className="callout mt-5">
<p className="flex items-center gap-1.5 text-sm font-semibold text-ink">
<Lightbulb className="h-4 w-4 text-accent" aria-hidden="true" />
Na prática
</p>
<p className="mt-1">
<TextoComLinks texto={item.exemplo} />
</p>
</div>
</article>
);
})}
</div>
)}
</section>
<section className="shell pb-20 md:pb-28">
<div className="panel p-8 text-center sm:p-12">
<h2 className="section-title mx-auto max-w-2xl">Do conceito ao chip certo.</h2>
<p className="lede mx-auto mt-4 max-w-2xl">
Com os termos em mãos, compare as séries lado a lado ou responda ao seletor para chegar ao modelo que cabe no seu projeto.
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href="/seletor" className="btn btn-primary w-full sm:w-auto">
<Sliders className="h-4 w-4" aria-hidden="true" />
Escolher meu chip
</Link>
<Link href="/series" className="btn btn-ghost w-full sm:w-auto">
Explorar as séries
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
}
/**
* Função utilitária que detecta nomes de séries do ESP32 dentro do texto e os converte em links clicáveis.
*/
function TextoComLinks({ texto }) {
if (!texto) return null;
const seriesKeys = [
"ESP32-S31",
"ESP32-C61",
"ESP32-S2",
"ESP32-S3",
"ESP32-C2",
"ESP32-C3",
"ESP32-C5",
"ESP32-C6",
"ESP32-P4",
"ESP32-H2",
"ESP32-H4",
"ESP32",
];
const regex = new RegExp(`\\b(${seriesKeys.join("|")})\\b`, "g");
const partes = texto.split(regex);
return (
<>
{partes.map((parte, i) => {
if (seriesKeys.includes(parte)) {
return (
<Link
key={i}
href={`/series/${parte}`}
className="font-semibold text-accent underline decoration-accent/30 underline-offset-2 hover:decoration-accent"
>
{parte}
</Link>
);
}
return parte;
})}
</>
);
}
+162
View File
@@ -0,0 +1,162 @@
import { notFound } from "next/navigation";
import { JetBrains_Mono, Manrope, Space_Grotesk } from "next/font/google";
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { routing } from "@/i18n/routing";
import "../globals.css";
import Providers from "@/components/Providers";
import { SITE, jsonLd } from "@/lib/seo";
const spaceGrotesk = Space_Grotesk({
subsets: ["latin"],
variable: "--font-space-grotesk",
display: "swap",
weight: ["500", "600", "700"],
});
const manrope = Manrope({
subsets: ["latin"],
variable: "--font-manrope",
display: "swap",
weight: ["400", "500", "600", "700"],
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-jetbrains",
display: "swap",
weight: ["400", "500", "600"],
});
const LANG_MAP = {
pt: "pt-BR",
en: "en",
es: "es-419",
fr: "fr",
de: "de",
"zh-CN": "zh-CN",
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export async function generateMetadata({ params }) {
const { locale } = await params;
const ogLocaleMap = {
pt: "pt_BR",
en: "en_US",
es: "es_419",
fr: "fr_FR",
de: "de_DE",
"zh-CN": "zh_CN",
};
return {
metadataBase: new URL(SITE.url),
title: {
default: "ESPDocs · Documentação do ESP32 em português",
template: "%s · ESPDocs",
},
description:
"Especificações, pinagem, comparação e seleção das 12 séries de microcontroladores ESP32, em português brasileiro. Datasheets, periféricos, segurança em hardware e diagramas de conexão.",
applicationName: "ESPDocs",
authors: [{ name: "Lucas Rayan Guerra", url: "https://lucasrguerra.dev.br" }],
creator: "Lucas Rayan Guerra",
publisher: "Ciência Embarcada",
category: "technology",
keywords: [
"ESP32", "ESP32 português", "documentação ESP32", "pinout ESP32",
"ESP32-S3", "ESP32-C3", "ESP32-C6", "ESP32-P4", "ESP32-H2",
"comparar ESP32", "qual ESP32 escolher", "datasheet ESP32",
"microcontrolador", "Espressif", "ESP-IDF", "IoT", "sistemas embarcados",
],
alternates: { canonical: `/${locale}` },
openGraph: {
type: "website",
locale: ogLocaleMap[locale] || "pt_BR",
url: `${SITE.url}/${locale}`,
siteName: SITE.nome,
title: "ESPDocs · Documentação do ESP32 em português",
description:
"Especificações, pinagem, comparação e seleção das 12 séries de microcontroladores ESP32, em português brasileiro.",
},
twitter: {
card: "summary_large_image",
title: "ESPDocs · Documentação do ESP32 em português",
description:
"Especificações, pinagem, comparação e seleção das 12 séries de ESP32, em português.",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
"max-video-preview": -1,
},
},
icons: {
icon: [
{ url: "/favicon.ico", sizes: "48x48", type: "image/x-icon" },
{ url: "/favicon-48x48.png", sizes: "48x48", type: "image/png" },
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
{ url: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ url: "/icon-512.png", sizes: "512x512", type: "image/png" },
{ url: "/icon.svg", type: "image/svg+xml" },
],
apple: [
{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
],
shortcut: "/favicon.ico",
},
manifest: "/site.webmanifest",
formatDetection: { telephone: false, address: false, email: false },
};
}
export const viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#f5f6f8" },
{ media: "(prefers-color-scheme: dark)", color: "#07090d" },
],
};
export default async function RootLayout({ children, params }) {
const { locale } = await params;
if (!routing.locales.includes(locale)) {
notFound();
}
const lang = LANG_MAP[locale] || locale;
const messages = await getMessages();
return (
<html lang={lang} className={`${spaceGrotesk.variable} ${manrope.variable} ${jetbrainsMono.variable}`} suppressHydrationWarning>
<head>
{/* Dados estruturados do site e da organização. É o que permite ao
Google montar o box de busca interna e entender a autoria. */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.siteESociedade()) }}
/>
</head>
<body className="font-sans antialiased bg-canvas text-ink overflow-x-clip" suppressHydrationWarning>
<a
href="#conteudo"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[200] focus:rounded-full focus:bg-ink focus:px-5 focus:py-3 focus:text-sm focus:font-semibold focus:text-canvas"
>
Pular para o conteúdo
</a>
<NextIntlClientProvider locale={locale} messages={messages}>
<Providers>
{children}
</Providers>
</NextIntlClientProvider>
</body>
</html>
);
}
+374
View File
@@ -0,0 +1,374 @@
import { notFound } from "next/navigation";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import BookCover from "@/components/BookCover";
import CodeSample from "@/components/CodeSample";
import Link from "next/link";
import { jsonLd, paginaMeta, SITE } from "@/lib/seo";
import {
ArrowRight,
ArrowUpRight,
BookOpen,
Cpu,
Gauge,
Radio,
ShieldCheck,
Terminal,
Wifi,
} from "lucide-react";
export function generateStaticParams() {
return [{ locale: "pt" }];
}
export async function generateMetadata({ params }) {
const { locale } = await params;
if (locale !== "pt") {
return {};
}
return paginaMeta({
titulo: "Microcontroladores ESP32, o livro",
descricao:
"Conheça o livro Microcontroladores ESP32: conectividade, eficiência e desempenho. 12 partes e 78 capítulos em português sobre arquitetura, energia, rádio, periféricos e firmware.",
caminho: "/pt/livro",
keywords: [
"livro ESP32",
"Microcontroladores ESP32",
"livro de ESP32 em português",
"Lucas Rayan Guerra",
"Ciência Embarcada",
],
});
}
const CHAPTERS = [
{
title: "O ecossistema ESP32",
desc: "Como as famílias se organizam, o que muda entre Xtensa e RISC-V e por que a escolha do chip começa pelo problema.",
topics: ["Famílias e sufixos", "Critérios de escolha", "Ferramentas de apoio"],
icon: Cpu,
},
{
title: "Arquitetura e memória",
desc: "Núcleos, caches, SRAM, PSRAM e o caminho entre o código em flash e a execução em alta velocidade.",
topics: ["Mapa de memória", "PSRAM na prática", "Caches e latência"],
icon: Gauge,
},
{
title: "Energia e autonomia",
desc: "Os modos de sono, o custo real do rádio e as decisões de hardware que definem a vida útil da bateria.",
topics: ["Deep sleep", "Domínios de energia", "Orçamento de consumo"],
icon: Gauge,
},
{
title: "Conectividade",
desc: "Wi-Fi, BLE, Thread, Zigbee e Matter: o que cada rádio resolve e onde a arquitetura começa a pesar.",
topics: ["Wi-Fi 6 e TWT", "BLE e malhas", "Matter e 802.15.4"],
icon: Radio,
},
{
title: "Periféricos e pinagem",
desc: "GPIO, ADC, I2S, USB, MIPI e as restrições que só aparecem quando o hardware já está na placa.",
topics: ["Pinos de strapping", "ADC e rádio", "USB nativo"],
icon: Wifi,
},
{
title: "Do protótipo ao produto",
desc: "Firmware, partições, OTA, segurança e os cuidados de engenharia que sobrevivem ao primeiro deploy.",
topics: ["ESP-IDF e Arduino", "OTA e rollback", "Secure boot"],
icon: ShieldCheck,
},
];
const ESP_IDF = `#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define LED GPIO_NUM_2
void app_main(void)
{
gpio_reset_pin(LED);
gpio_set_direction(LED, GPIO_MODE_OUTPUT);
while (1) {
gpio_set_level(LED, 1);
vTaskDelay(pdMS_TO_TICKS(500));
gpio_set_level(LED, 0);
vTaskDelay(pdMS_TO_TICKS(500));
}
}`;
const ARDUINO = `#include <WiFi.h>
const char *ssid = "SUA_REDE";
const char *senha = "SUA_SENHA";
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, senha);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(WiFi.localIP());
}
void loop() {}`;
const DEEP_SLEEP = `#include "esp_sleep.h"
#include "esp_log.h"
void app_main(void)
{
uint64_t intervalo_us = 30ULL * 1000000ULL;
esp_sleep_enable_timer_wakeup(intervalo_us);
ESP_LOGI("energia", "dormindo por 30 segundos");
esp_deep_sleep_start();
}`;
const FACTS = [
["Idioma", "Português"],
["Partes", "12"],
["Capítulos", "78"],
["Digital", "Amazon Kindle"],
["Impresso", "UICLAP"],
["ISBN", SITE.livroIsbn],
];
export default async function LivroPage({ params }) {
const { locale } = await params;
if (locale !== "pt") {
notFound();
}
return (
<div className="site-canvas min-h-screen">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.livro()) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Livro", caminho: "/livro" },
])
),
}}
/>
<Header />
<main id="conteudo">
<section className="shell pt-8 sm:pt-12">
<nav aria-label="Trilha de navegação" className="flex items-center gap-2 text-sm text-muted">
<Link href="/" className="transition-colors hover:text-accent">
Início
</Link>
<span aria-hidden="true" className="text-line-strong">/</span>
<span aria-current="page" className="text-ink">Livro</span>
</nav>
<div className="mt-7 grid items-center gap-10 lg:grid-cols-[0.84fr_1.16fr] lg:gap-16">
<div className="order-1 lg:order-2">
<span className="chip chip-accent">
<BookOpen className="h-3.5 w-3.5" aria-hidden="true" />
Livro novo · 1ª edição
</span>
<h1 className="page-title mt-5 text-[clamp(1.85rem,8vw,2.5rem)] sm:text-[clamp(2.5rem,5.4vw,4.4rem)]">
Microcontroladores ESP32
</h1>
<p className="mt-3 text-lg font-medium text-copper">{SITE.livroSubtitulo}</p>
<p className="lede mt-6 max-w-2xl">
Um percurso completo pelo ecossistema ESP32 — da arquitetura do chip à implantação
de produtos conectados. São 12 partes e 78 capítulos que cobrem Arduino e ESP-IDF,
FreeRTOS, Wi-Fi, BLE, Thread e Matter, periféricos, depuração, baixo consumo e segurança.
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<a
href={SITE.livroAmazon}
target="_blank"
rel="noopener noreferrer"
className="btn btn-primary w-full sm:w-auto"
>
<BookOpen className="h-4 w-4" aria-hidden="true" />
Comprar digital
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
<a
href={SITE.livroUiclap}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost w-full sm:w-auto"
>
Comprar impresso
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
<Link href="/series" className="inline-flex min-h-11 items-center justify-center gap-1.5 px-2 text-sm font-medium text-accent transition-colors hover:text-accent-strong">
Explorar a documentação
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
<dl className="mt-9 grid grid-cols-2 gap-2.5 sm:grid-cols-3">
{FACTS.map(([label, value]) => (
<div key={label} className="rounded-2xl bg-surface-2 px-4 py-3">
<dt className="text-[0.6875rem] font-semibold text-muted">{label}</dt>
<dd className="mt-0.5 text-sm font-semibold text-ink">{value}</dd>
</div>
))}
</dl>
</div>
<div className="relative order-2 overflow-hidden rounded-[2rem] bg-[#0b0d10] p-8 ring-1 ring-white/10 sm:rounded-[2.5rem] sm:p-12 lg:order-1">
<div
className="pointer-events-none absolute left-1/2 top-1/2 -z-0 h-72 w-72 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#2dd4bf]/15 blur-3xl"
aria-hidden="true"
/>
<div className="relative mx-auto w-[min(16rem,76%)]">
<BookCover />
</div>
<div className="relative mt-9 flex items-end justify-between gap-4 border-t border-white/10 pt-4 text-xs text-[#a6afbd]">
<span>Lucas Rayan Guerra</span>
<span>Ciência Embarcada</span>
</div>
</div>
</div>
</section>
<section className="shell section" aria-labelledby="sumario-titulo">
<div className="grid gap-8 lg:grid-cols-[0.72fr_1.28fr] lg:gap-16">
<div>
<p className="eyebrow">Sumário</p>
<h2 id="sumario-titulo" className="display mt-4 text-[clamp(2rem,4.6vw,3.4rem)]">
Uma linha de raciocínio, do chip ao produto.
</h2>
</div>
<div className="lg:pt-2">
<p className="lede">
A ordem acompanha o projeto real: entender o chip, medir energia, conectar, expor
periféricos e sustentar o produto. Os seis eixos abaixo mostram como as 12 partes
se organizam.
</p>
<p className="mt-4 text-sm text-muted">
78 capítulos, com exercícios e projetos ao longo do percurso.
</p>
</div>
</div>
<div className="mt-10 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{CHAPTERS.map((chapter) => {
const Icon = chapter.icon;
return (
<article key={chapter.title} className="card flex flex-col p-5 sm:p-6">
<span className="grid h-11 w-11 place-items-center rounded-2xl bg-accent-soft text-accent">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<h3 className="mt-5 font-display text-xl font-semibold tracking-[-0.03em] text-ink">{chapter.title}</h3>
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted">{chapter.desc}</p>
<ul className="mt-5 flex flex-wrap gap-1.5">
{chapter.topics.map((topic) => (
<li key={topic} className="chip">
{topic}
</li>
))}
</ul>
</article>
);
})}
</div>
</section>
<section className="border-y border-line bg-surface/70" aria-labelledby="codigo-titulo">
<div className="shell section">
<div className="grid gap-10 lg:grid-cols-[0.7fr_1.3fr] lg:gap-16">
<div className="min-w-0 lg:sticky lg:top-28 lg:self-start">
<p className="eyebrow">Código</p>
<h2 id="codigo-titulo" className="display mt-4 text-[clamp(2rem,4.6vw,3.4rem)]">
Exemplos curtos, prontos para a bancada.
</h2>
<p className="lede mt-4">
Os trechos abaixo são originais e servem como ponto de partida. Ajuste o pino, a rede
e a frequência ao seu hardware antes de gravar.
</p>
<div className="mt-6 rounded-2xl border border-line bg-surface p-4">
<p className="flex items-center gap-2 text-sm font-semibold text-ink">
<Terminal className="h-4 w-4 text-accent" aria-hidden="true" />
Sem conteúdo protegido
</p>
<p className="mt-2 text-xs leading-relaxed text-muted">
Esta página apresenta apenas a ficha do livro e exemplos autorais. O texto integral,
PDF, EPUB e arquivos intermediários não são distribuídos pelo ESPDocs.
</p>
</div>
</div>
<div className="min-w-0 space-y-7">
<CodeSample
label="Ponto de partida · ESP-IDF"
language="C"
code={ESP_IDF}
note="Pisca o GPIO 2 usando a API atual do ESP-IDF e uma task do FreeRTOS."
/>
<CodeSample
label="Conectividade · Arduino"
language="C++"
code={ARDUINO}
note="Conexão Wi-Fi mínima, com espera ativa e impressão do IP obtido."
/>
<CodeSample
label="Energia · deep sleep"
language="C · ESP-IDF"
code={DEEP_SLEEP}
note="Timer de 30 segundos e entrada no modo de menor consumo disponível."
/>
</div>
</div>
</div>
</section>
<section className="shell section pb-20 md:pb-28">
<div className="mx-auto max-w-3xl text-center">
<p className="eyebrow">Para quem é</p>
<h2 className="display mt-4 text-[clamp(2rem,4.5vw,3.2rem)]">
Para quem já cansou de juntar pedaços de datasheet.
</h2>
<p className="lede mx-auto mt-4 max-w-2xl">
Estudantes, makers e engenheiros de firmware que querem uma visão coerente do
ecossistema — sem abandonar a fonte oficial.
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<a
href={SITE.livroAmazon}
target="_blank"
rel="noopener noreferrer"
className="btn btn-primary w-full sm:w-auto"
>
<BookOpen className="h-4 w-4" aria-hidden="true" />
Comprar digital
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
<a
href={SITE.livroUiclap}
target="_blank"
rel="noopener noreferrer"
className="btn btn-ghost w-full sm:w-auto"
>
Comprar impresso
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
}
+58
View File
@@ -0,0 +1,58 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import Link from "next/link";
import { ArrowRight, BookOpen, Compass, Scale, SlidersHorizontal } from "lucide-react";
export const metadata = {
title: "Página não encontrada",
robots: { index: false, follow: false },
};
const CAMINHOS = [
{ href: "/series", icon: Compass, titulo: "Séries ESP32", texto: "As 12 famílias, com pinagem e especificações." },
{ href: "/seletor", icon: SlidersHorizontal, titulo: "Seletor de chip", texto: "Responda a oito perguntas e receba os modelos indicados." },
{ href: "/comparacao", icon: Scale, titulo: "Comparador", texto: "Até quatro séries lado a lado." },
{ href: "/glossario", icon: BookOpen, titulo: "Glossário", texto: "Termos de hardware explicados em português." },
];
export default function NotFound() {
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo" className="shell section">
<div className="mx-auto max-w-2xl text-center">
<p className="eyebrow justify-center">Erro 404</p>
<h1 className="page-title mt-4">Essa página não existe.</h1>
<p className="lede mx-auto mt-5 max-w-xl">
O endereço pode ter mudado ou estar digitado errado. Escolha um destes caminhos para continuar.
</p>
<div className="mt-8 flex justify-center">
<Link href="/" className="btn btn-primary">
Voltar ao início
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
<ul className="mx-auto mt-14 grid max-w-4xl gap-3 sm:grid-cols-2">
{CAMINHOS.map(({ href, icon: Icon, titulo, texto }) => (
<li key={href}>
<Link href={href} className="card card-interactive group flex h-full items-start gap-4 p-5">
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-accent-soft text-accent">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<span>
<span className="block font-semibold text-ink">{titulo}</span>
<span className="mt-0.5 block text-sm text-muted">{texto}</span>
</span>
</Link>
</li>
))}
</ul>
</main>
<Footer />
</div>
);
}
+352
View File
@@ -0,0 +1,352 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import BookCover from "@/components/BookCover";
import Link from "next/link";
import seriesData from "@/public/series.json";
import frameworksData from "@/public/frameworks.json";
import { SITE } from "@/lib/seo";
import { getLocalizedHref } from "@/components/LanguageSelector";
import {
AlertTriangle,
ArrowRight,
ArrowUpRight,
BookOpen,
Box,
Cpu,
Scale,
SlidersHorizontal,
Terminal,
} from "lucide-react";
const FEATURED = ["ESP32", "ESP32-S3", "ESP32-C6", "ESP32-C3", "ESP32-P4", "ESP32-H2"];
const TOOLS = [
{
href: "/seletor",
title: "Seletor inteligente",
desc: "Responda oito perguntas sobre o projeto e receba os chips mais compatíveis, com justificativa técnica.",
icon: SlidersHorizontal,
action: "Encontrar meu chip",
},
{
href: "/comparacao",
title: "Comparador de séries",
desc: "Coloque até quatro modelos lado a lado e compare núcleos, rádio, memória, periféricos e segurança.",
icon: Scale,
action: "Comparar agora",
},
{
href: "/diagnostico",
title: "Diagnóstico de falhas",
desc: "Causas e correções para brownout, bootloop, Guru Meditation, watchdog e falhas de flash.",
icon: AlertTriangle,
action: "Abrir diagnóstico",
},
{
href: "/componentes",
title: "Component Registry",
desc: "Drivers, bibliotecas e BSPs do registro oficial da Espressif, com manifesto pronto para o projeto.",
icon: Box,
action: "Explorar componentes",
},
{
href: "/frameworks",
title: "Frameworks e SDKs",
desc: "ESP-IDF, Arduino, Rust, Matter, MicroPython e as ferramentas que ficam em volta do chip.",
icon: Terminal,
action: "Ver frameworks",
},
{
href: "/catalogo",
title: "Catálogo de placas",
desc: "DevKits e módulos reais, com interface USB, sensores integrados e link para a loja oficial.",
icon: Cpu,
action: "Ver placas",
},
];
const ERRORS = [
{
code: "Brownout detector was triggered",
cause: "Queda de tensão na inicialização do rádio.",
fix: "Capacitor de 10–100 µF junto ao chip e fonte de 5 V com corrente suficiente.",
},
{
code: "Guru Meditation Error",
cause: "Ponteiro inválido, stack overflow ou uso incorreto de interrupção.",
fix: "Leia o backtrace com addr2line e dimensione a stack da task no FreeRTOS.",
},
{
code: "ADC2 indisponível com Wi-Fi",
cause: "O controlador de rádio reserva o ADC2 enquanto está ativo.",
fix: "Use os pinos do ADC1 para sensores analógicos que convivem com o Wi-Fi.",
},
{
code: "Flash Read err / bootloader",
cause: "Tensão ou modo de flash incorretos, ou pino de strapping preso no boot.",
fix: "Confira 3,3 V, pull-ups de boot e a configuração de flash no esptool.",
},
];
export default async function Home({ params }) {
const { locale } = await params;
const isPt = locale === "pt";
const getLinkHref = (href) => getLocalizedHref(`/${locale}${href}`, locale);
const seriesCount = Object.keys(seriesData).length;
const frameworkCount = Object.keys(frameworksData).length;
const featured = FEATURED.map((key) => [key, seriesData[key]]).filter(([, serie]) => Boolean(serie));
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
<section className="shell pb-10 pt-14 sm:pt-20 lg:pb-14 lg:pt-24">
<div className="mx-auto max-w-4xl text-center">
<span className="chip chip-accent">
<Cpu className="h-3.5 w-3.5" aria-hidden="true" />
Documentação aberta do ESP32
</span>
<h1 className="display mt-6 text-[clamp(2.9rem,7.4vw,5.4rem)]">
O ESP32,
<br className="sm:hidden" /> sem ruído.
</h1>
<p className="lede mx-auto mt-6 max-w-2xl">
Especificações, pinagem, comparações e diagnóstico em uma leitura só.
Menos tradução de datasheet, mais tempo com o protótipo na bancada.
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href={getLinkHref("/seletor")} className="btn btn-primary w-full sm:w-auto">
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
Escolher meu chip
</Link>
<Link href={getLinkHref("/series")} className="btn btn-ghost w-full sm:w-auto">
Explorar as {seriesCount} séries
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
<p className="mt-7 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-sm text-muted">
<span><strong className="font-semibold text-ink">{seriesCount}</strong> séries</span>
<span aria-hidden="true" className="text-line-strong">·</span>
<span><strong className="font-semibold text-ink">{frameworkCount}</strong> frameworks</span>
{isPt && (
<>
<span aria-hidden="true" className="text-line-strong">·</span>
<span><strong className="font-semibold text-ink">78</strong> capítulos no livro</span>
<span aria-hidden="true" className="text-line-strong">·</span>
<span>100% em português</span>
</>
)}
</p>
</div>
<div className="panel mt-12 overflow-hidden sm:mt-16">
<div className="flex flex-wrap items-center justify-between gap-3 px-5 py-4 sm:px-7">
<div>
<h2 className="text-sm font-semibold text-ink">Séries em destaque</h2>
<p className="mt-0.5 text-xs text-muted">O essencial de cada família, antes de abrir o datasheet.</p>
</div>
<Link href="/series" className="inline-flex min-h-11 items-center gap-1.5 text-sm font-medium text-accent transition-colors hover:text-accent-strong">
Índice completo
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
<div className="grid gap-px border-t border-line bg-line sm:grid-cols-2 lg:grid-cols-3">
{featured.map(([key, serie]) => (
<Link
key={key}
href={`/series/${key}`}
className="group flex min-w-0 flex-col bg-surface p-5 transition-colors duration-300 hover:bg-surface-2 sm:p-6"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<h3 className="font-display text-xl font-semibold tracking-[-0.035em] text-ink">{key}</h3>
<p className="mt-1 truncate text-xs text-muted">{serie.arquitetura}</p>
</div>
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-accent-soft text-accent transition-transform duration-300 group-hover:scale-105">
<Cpu className="h-4 w-4" aria-hidden="true" />
</span>
</div>
<p className="mt-5 line-clamp-2 text-sm leading-relaxed text-muted">{serie.descricao}</p>
<div className="mt-5 flex items-center justify-between gap-3 border-t border-line pt-3.5">
<span className="truncate font-mono text-[0.6875rem] text-ink-soft">{serie.frequencia}</span>
<span className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-accent">
Ficha
<ArrowUpRight className="h-3.5 w-3.5 transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</div>
</Link>
))}
</div>
</div>
</section>
{isPt && (
<section className="shell section pt-8 sm:pt-12" aria-labelledby="livro-titulo">
<div className="relative isolate overflow-hidden rounded-[2rem] bg-[#0b0d10] ring-1 ring-white/10 dark:bg-[#05070a] dark:ring-white/15 sm:rounded-[2.5rem]">
<div
className="pointer-events-none absolute -left-24 top-1/2 -z-10 h-80 w-80 -translate-y-1/2 rounded-full bg-[#2dd4bf]/15 blur-3xl"
aria-hidden="true"
/>
<div className="grid lg:grid-cols-[0.82fr_1.18fr]">
<div className="order-1 flex flex-col justify-center p-7 sm:p-10 lg:order-2 lg:p-14">
<span className="inline-flex w-fit items-center gap-2 rounded-full border border-white/15 bg-white/5 px-3 py-1 text-xs font-semibold text-[#75cdc1]">
<BookOpen className="h-3.5 w-3.5" aria-hidden="true" />
Livro novo
</span>
<h2 id="livro-titulo" className="mt-5 font-display text-[clamp(2rem,4.2vw,3.2rem)] font-semibold leading-[1.05] tracking-[-0.04em] text-[#f4f6f8]">
{SITE.livroTitulo}
</h2>
<p className="mt-2 text-base text-[#a6afbd]">{SITE.livroSubtitulo}</p>
<p className="mt-5 max-w-xl text-sm leading-relaxed text-[#c4cbd5]">
Um percurso completo pelo hardware e pelo firmware do ESP32: da arquitetura do chip
à implantação de produtos conectados. São 12 partes e 78 capítulos sobre Arduino,
ESP-IDF, FreeRTOS, Wi-Fi, BLE, Thread e Matter, periféricos, depuração, baixo consumo
e segurança.
</p>
<p className="mt-6 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-xs text-[#a6afbd]">
<span>12 partes</span>
<span aria-hidden="true" className="text-white/20">·</span>
<span>78 capítulos</span>
<span aria-hidden="true" className="text-white/20">·</span>
<span>ISBN {SITE.livroIsbn}</span>
</p>
<div className="mt-7 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<a
href={SITE.livroAmazon}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center justify-center gap-2 rounded-full bg-[#2dd4bf] px-5 text-sm font-semibold text-[#06201d] transition-colors duration-300 hover:bg-[#5eead4]"
>
Comprar digital
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
<a
href={SITE.livroUiclap}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center justify-center gap-2 rounded-full border border-white/20 px-5 text-sm font-semibold text-[#f4f6f8] transition-colors duration-300 hover:border-white/40 hover:bg-white/5"
>
Comprar impresso
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
<Link href="/pt/livro" className="inline-flex min-h-11 items-center justify-center gap-1.5 rounded-full px-3 text-sm font-medium text-[#a6afbd] transition-colors duration-300 hover:text-[#f4f6f8]">
Ficha completa
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
<div className="order-2 flex items-center justify-center border-t border-white/10 p-8 sm:p-12 lg:order-1 lg:border-r lg:border-t-0 lg:p-14">
<div className="relative w-[min(15.5rem,68%)] lg:w-[min(17rem,100%)]">
<div className="absolute -inset-10 -z-10 rounded-full bg-[#2dd4bf]/10 blur-3xl" aria-hidden="true" />
<BookCover />
</div>
</div>
</div>
</div>
</section>
)}
<section className="shell section" aria-labelledby="ferramentas-titulo">
<div className="mx-auto max-w-2xl text-center">
<p className="eyebrow">Ferramentas</p>
<h2 id="ferramentas-titulo" className="display mt-4 text-[clamp(2rem,4.8vw,3.4rem)]">
Comece pelo problema.
</h2>
<p className="lede mt-4">
Cada ferramenta resolve uma etapa concreta do projeto, sem cadastro e sem rodeio.
</p>
</div>
<div className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{TOOLS.map((tool) => {
const Icon = tool.icon;
return (
<Link key={tool.href} href={tool.href} className="card card-interactive group flex flex-col p-5 sm:p-6">
<span className="grid h-11 w-11 place-items-center rounded-2xl bg-accent-soft text-accent">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<h3 className="mt-5 font-display text-lg font-semibold tracking-[-0.025em] text-ink">{tool.title}</h3>
<p className="mt-2 flex-1 text-sm leading-relaxed text-muted">{tool.desc}</p>
<span className="mt-5 inline-flex items-center gap-1.5 text-sm font-medium text-accent">
{tool.action}
<ArrowRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</Link>
);
})}
</div>
</section>
<section className="shell section pt-0" aria-labelledby="diagnostico-titulo">
<div className="grid items-start gap-10 lg:grid-cols-[0.78fr_1.22fr] lg:gap-16">
<div className="lg:sticky lg:top-28">
<p className="eyebrow">Diagnóstico</p>
<h2 id="diagnostico-titulo" className="display mt-4 text-[clamp(2rem,4.8vw,3.4rem)]">
O primeiro boot não precisa virar noite.
</h2>
<p className="lede mt-4">
Os erros mais comuns têm causa conhecida. O guia parte da mensagem no terminal e chega ao ajuste de hardware ou firmware.
</p>
<Link href="/diagnostico" className="btn btn-accent mt-7">
<AlertTriangle className="h-4 w-4" aria-hidden="true" />
Abrir os 30+ diagnósticos
</Link>
</div>
<div className="panel-dark min-w-0 overflow-hidden rounded-[1.75rem]">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-white/10 px-5 py-4 sm:px-6">
<span className="inline-flex items-center gap-2 font-mono text-xs text-[#75cdc1]">
<Terminal className="h-4 w-4" aria-hidden="true" />
boot.log
</span>
<span className="text-xs text-[#a6afbd]">4 exemplos de 30+</span>
</div>
<div className="divide-y divide-white/10">
{ERRORS.map((item) => (
<Link key={item.code} href="/diagnostico" className="group grid gap-2 px-5 py-5 transition-colors duration-300 hover:bg-white/[0.035] sm:px-6">
<span className="break-words font-mono text-[0.75rem] font-medium leading-relaxed text-[#ffb39b]">{item.code}</span>
<span className="text-sm font-medium text-[#f4f6f8]">{item.cause}</span>
<span className="text-xs leading-relaxed text-[#a6afbd]">{item.fix}</span>
</Link>
))}
</div>
</div>
</div>
</section>
<section className="shell pb-20 pt-4 md:pb-28">
<div className="mx-auto max-w-3xl text-center">
<p className="eyebrow">Curadoria independente</p>
<h2 className="display mt-4 text-[clamp(2rem,4.5vw,3.2rem)]">
Feito para consultar no meio do projeto.
</h2>
<p className="lede mx-auto mt-4 max-w-2xl">
O ESPDocs organiza dados públicos da Espressif e aponta sempre para a fonte oficial.
Para decisões críticas, o datasheet e o Technical Reference Manual continuam sendo a palavra final.
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
<Link href="/glossario" className="btn btn-ghost w-full sm:w-auto">
<BookOpen className="h-4 w-4" aria-hidden="true" />
Glossário
</Link>
<Link href="/sobre" className="btn btn-primary w-full sm:w-auto">
Sobre o projeto
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</div>
</section>
</main>
<Footer />
</div>
);
}
+64
View File
@@ -0,0 +1,64 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
export const metadata = paginaMeta({
titulo: "Qual ESP32 escolher",
descricao: "Responda a perguntas simples sobre conectividade (Wi-Fi 6, 5GHz, Matter), energia, periféricos e segurança ou use o filtro paramétrico para encontrar o ESP32 perfeito.",
caminho: "/seletor",
keywords: ["qual ESP32 escolher", "qual ESP32 usar", "escolher microcontrolador", "ESP32 para meu projeto", "comparar ESP32"],
});
const faqs = [
{
pergunta: "Qual ESP32 escolher para iniciantes?",
resposta: "Para quem está começando no mundo Maker e IoT, o ESP32 clássico (NodeMCU/DevKitC) ou o ESP32-C3 são as melhores opções devido à vasta quantidade de bibliotecas, exemplos prontos no Arduino IDE e farta documentação em português.",
},
{
pergunta: "Qual ESP32 tem suporte a Wi-Fi 6, Bluetooth 6 e Thread/Zigbee/Matter?",
resposta: "O ESP32-C6 e o ESP32-C5 são as melhores séries para redes modernas. O ESP32-C6 oferece Wi-Fi 6 (2.4 GHz) e rádio 802.15.4 (Zigbee 3.0 / Thread / Matter). Já o ESP32-C5 é o primeiro a oferecer suporte a Wi-Fi 6 Dual-Band operando em 2.4 GHz e 5 GHz simultaneamente.",
},
{
pergunta: "Qual o ESP32 mais potente para Inteligência Artificial (TinyML) e Displays?",
resposta: "O ESP32-P4 (dual-core RISC-V a 400 MHz com extensões vetoriais de IA, interfaces MIPI-CSI/DSI para câmeras/telas e até 64 MB de PSRAM) e o ESP32-S3 (dual-core Xtensa a 240 MHz com aceleradores vetoriais para redes neurais e telas LCD RGB) são os mais velozes para IA na borda e interfaces gráficas ricas.",
},
{
pergunta: "Qual ESP32 consome menos energia para projetos a bateria?",
resposta: "Microcontroladores com arquitetura RISC-V compacta e coprocessador ULP, como o ESP32-C2, ESP32-C3 e ESP32-H2 (focado em 802.15.4 sem rádio Wi-Fi), oferecem correntes de deep sleep extremamente baixas (na faixa de 5 µA a 10 µA).",
},
];
export default function Layout({ children }) {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.aplicacaoWeb({
nome: "Seletor Inteligente de ESP32",
descricao: "Ferramenta interativa que analisa conectividade, consumo, periféricos e segurança para indicar a série ideal de ESP32.",
caminho: "/seletor",
})
),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Seletor de ESP32", caminho: "/seletor" },
])
),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd.perguntas(faqs)),
}}
/>
{children}
</>
);
}
File diff suppressed because it is too large. Load diff
+488
View File
@@ -0,0 +1,488 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
import Link from "next/link";
import Image from "next/image";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import { notFound } from "next/navigation";
import seriesData from "@/public/series.json";
import ConnectionsDiagram from "@/components/ConnectionsDiagram";
import SeriesTabMenu from "@/components/SeriesTabMenu";
import PinRestrictions from "@/components/PinRestrictions";
import {
AlertTriangle,
ArrowLeft,
ArrowRight,
ArrowUpRight,
Battery,
BookOpen,
Code,
Cpu,
Database,
ExternalLink,
FileText,
Lightbulb,
Package,
Scale,
ShieldCheck,
Sliders,
Thermometer,
Wifi,
} from "lucide-react";
export default async function SerieDetail({ params }) {
const { key } = await params;
const serie = seriesData[key];
if (!serie) {
notFound();
}
let conexoes = [];
try {
conexoes = require(`@/public/conexoes/${key}.json`);
} catch (error) {
console.log(`Arquivo de conexões não encontrado para ${key}`);
}
const seriesKeys = Object.keys(seriesData);
const currentIndex = seriesKeys.indexOf(key);
const previousKey = currentIndex > 0 ? seriesKeys[currentIndex - 1] : null;
const nextKey = currentIndex < seriesKeys.length - 1 ? seriesKeys[currentIndex + 1] : null;
const processadorSpecs = [
{ label: "Arquitetura", value: serie.arquitetura },
{ label: "Núcleos", value: serie.nucleos },
{ label: "Frequência", value: serie.frequencia },
{ label: "Coprocessador ULP", value: serie.coprocessador_ulp || "Não" },
{ label: "Aceleradores IA", value: serie.aceleradores_ia || "Não" },
];
const seg = serie.seguranca || {};
const segurancaSpecs = [
{ label: "AES", value: seg.aes || "Não" },
{ label: "SHA", value: seg.sha || "Não" },
{ label: "RSA / MPI", value: seg.rsa || "Não" },
{ label: "ECC (curvas elípticas)", value: seg.ecc || "Não" },
{ label: "ECDSA em hardware", value: seg.ecdsa || "Não" },
{ label: "HMAC", value: seg.hmac || "Não" },
{ label: "Assinatura Digital (DS)", value: seg.assinatura_digital || "Não" },
{ label: "Key Manager", value: seg.key_manager || "Não" },
{ label: "Gerador aleatório (RNG)", value: seg.rng || "Não" },
{ label: "Criptografia de flash", value: seg.criptografia_flash || "Não" },
{ label: "Secure Boot", value: seg.secure_boot || "Não" },
{ label: "Proteção contra DPA", value: seg.protecao_dpa || "Não" },
];
const ferramentasSpecs = [
{ label: "ESP-IDF mínimo", value: serie.esp_idf_minimo || "Não informado" },
{ label: "Core Arduino", value: serie.arduino_core || "Não informado" },
{ label: "Depuração", value: serie.depuracao || "Não informado" },
{ label: "Encapsulamento", value: serie.encapsulamento || "Não informado" },
{ label: "Módulos disponíveis", value: serie.modulos || "Não informado" },
];
const conectividadeSpecs = [
{ label: "Wi-Fi", value: serie.wifi || "Não" },
{ label: "Bluetooth", value: serie.bluetooth || "Não" },
{ label: "Zigbee/Thread", value: serie.zigbee_thread || "Não" },
{ label: "Matter", value: serie.matter || "Não" },
{ label: "Ethernet", value: serie.ethernet || serie.ethernet_mac || "Não" },
{ label: "CAN", value: serie.can || "Não" },
];
const memoriaSpecs = [
{ label: "SRAM", value: serie.memoria_sram },
{ label: "SRAM RTC", value: serie.memoria_sram_rtc },
{ label: "ROM", value: serie.memoria_rom },
{ label: "Flash Externa", value: serie.flash_externa },
{ label: "PSRAM Externa", value: serie.psram_externa || "Não" },
];
const perifericos = [
{ label: "GPIO", value: serie.gpio },
{ label: "ADC", value: serie.adc },
{ label: "DAC", value: serie.dac || "Não" },
{ label: "Touch", value: serie.touch || "Não" },
{ label: "UART", value: serie.uart },
{ label: "SPI", value: serie.spi },
{ label: "I2C", value: serie.i2c },
{ label: "I2S", value: serie.i2s },
{ label: "PWM", value: serie.pwm },
{ label: "USB", value: serie.usb || "Não" },
];
const interfacesEspeciais = [];
if (serie.lcd) interfacesEspeciais.push({ label: "LCD", value: serie.lcd });
if (serie.camera) interfacesEspeciais.push({ label: "Câmera", value: serie.camera });
if (serie.mipi_csi) interfacesEspeciais.push({ label: "MIPI CSI", value: serie.mipi_csi });
if (serie.mipi_dsi) interfacesEspeciais.push({ label: "MIPI DSI", value: serie.mipi_dsi });
if (serie.sdio) interfacesEspeciais.push({ label: "SDIO", value: serie.sdio });
if (serie.sensor_hall) interfacesEspeciais.push({ label: "Sensor Hall", value: serie.sensor_hall });
if (serie.sensor_temperatura) interfacesEspeciais.push({ label: "Sensor Temperatura", value: serie.sensor_temperatura });
// Consumo de Energia
const consumoEnergia = [];
if (serie.consumo_energia) {
Object.entries(serie.consumo_energia).forEach(([modo, consumo]) => {
const modoLabels = {
'active': 'Modo Ativo',
'active_cpu_240mhz': 'Ativo (CPU 240MHz)',
'active_cpu_160mhz': 'Ativo (CPU 160MHz)',
'active_wifi_tx': 'Ativo (Wi-Fi TX)',
'active_wifi_tx_peak': 'Ativo (Wi-Fi TX Pico)',
'active_wifi': 'Ativo (Wi-Fi)',
'active_wifi6': 'Ativo (Wi-Fi 6)',
'modem_sleep': 'Modem Sleep',
'modem_sleep_cpu_160mhz': 'Modem Sleep (CPU 160MHz)',
'light_sleep': 'Light Sleep',
'deep_sleep': 'Deep Sleep',
'hibernation': 'Hibernação'
};
consumoEnergia.push({
label: modoLabels[modo] || modo.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
value: consumo
});
});
}
const tabs = [
{
id: "devboard",
label: "Placa de desenvolvimento",
available: serie.placa,
content: (
<div className="card p-5 sm:p-7">
<h2 className="text-lg font-semibold text-ink">Kit de desenvolvimento oficial</h2>
<div className="mt-5 overflow-hidden rounded-xl border border-line bg-surface-2 p-4">
<Image
src={serie.placa}
alt={`Placa de desenvolvimento ${key}`}
width={1200}
height={800}
className="mx-auto h-auto max-h-[28rem] w-full rounded-lg object-contain"
priority
/>
</div>
<p className="mt-3 text-sm text-muted">Placa padrão da Espressif para a série {key}.</p>
</div>
),
},
{
id: "connections",
label: "Diagrama de conexões",
available: conexoes.length > 0,
content: (
<div className="card p-5 sm:p-7">
<div className="callout callout-warn flex items-start gap-3">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
<div>
<p className="font-semibold text-ink">Este é o diagrama do chip, não da placa</p>
<p className="mt-1">
O mapa mostra as conexões do silício do <strong>{key}</strong>. Cada placa de desenvolvimento
expõe só parte delas, ou as roteia de outra forma.
</p>
</div>
</div>
<div className="mt-6">
<ConnectionsDiagram connections={conexoes} serie={serie} />
</div>
<div className="callout mt-8">
<p className="flex items-center gap-2 font-semibold text-ink">
<Lightbulb className="h-4 w-4 text-accent" aria-hidden="true" />
Quase todo pino é reconfigurável
</p>
<p className="mt-1">
A matriz de IO do {key} permite mover SPI, I2C, UART, PWM e barramentos de câmera ou display
para a maioria dos pinos digitais. Para as restrições de strapping e dos pinos analógicos,
consulte o{" "}
<a
href={`https://docs.espressif.com/projects/esp-idf/en/latest/${String(key).toLowerCase().replace("-", "")}/api-reference/peripherals/gpio.html`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-accent underline decoration-accent/30 underline-offset-2 hover:decoration-accent"
>
guia oficial de GPIO
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
</a>
.
</p>
</div>
</div>
),
},
];
const secoes = [
{ id: "processador", titulo: "Processador", icon: Cpu, specs: processadorSpecs },
{ id: "conectividade", titulo: "Conectividade", icon: Wifi, specs: conectividadeSpecs },
{ id: "ferramentas", titulo: "Ferramentas e disponibilidade", icon: Package, specs: ferramentasSpecs },
{ id: "seguranca", titulo: "Segurança", icon: ShieldCheck, specs: segurancaSpecs },
{ id: "memoria", titulo: "Memória", icon: Database, specs: memoriaSpecs },
...(consumoEnergia.length > 0
? [{
id: "energia",
titulo: "Consumo de energia",
icon: Battery,
specs: consumoEnergia,
nota: "Valores típicos a 3,3 V e 25 °C. O consumo real depende do ciclo de trabalho do rádio e da frequência de clock configurada.",
}]
: []),
{ id: "perifericos", titulo: "Periféricos", icon: Sliders, specs: perifericos },
...(interfacesEspeciais.length > 0
? [{ id: "interfaces", titulo: "Interfaces especiais", icon: Sliders, specs: interfacesEspeciais }]
: []),
{
id: "temperatura",
titulo: "Limites de temperatura",
icon: Thermometer,
specs: [
{ label: "Operação", value: serie.temperatura_operacao },
{ label: "Armazenamento", value: serie.temperatura_armazenamento },
],
},
];
const fatos = [
{ label: "Processador", value: String(serie.nucleos).includes("2") ? "Dual-core" : "Single-core", detail: serie.frequencia },
{ label: "SRAM", value: serie.memoria_sram, detail: `+ ${serie.memoria_sram_rtc} de SRAM RTC` },
{ label: "GPIO", value: serie.gpio, detail: "pinos programáveis" },
{ label: "Arquitetura", value: String(serie.arquitetura).includes("RISC-V") ? "RISC-V" : "Xtensa", detail: serie.arquitetura },
];
return (
<div className="site-canvas min-h-screen">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.serie(key, serie)) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Séries", caminho: "/series" },
{ nome: serie.nome_completo, caminho: `/series/${key}` },
])),
}}
/>
<Header />
<main id="conteudo">
<PageHero
trail={[{ label: "Séries", href: "/series" }, { label: key }]}
icon={Cpu}
label={serie.arquitetura}
title={key}
lede={serie.descricao}
>
{serie.datasheet ? (
<a href={serie.datasheet} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
<FileText className="h-4 w-4" aria-hidden="true" />
Datasheet oficial
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
) : (
<span className="btn btn-ghost cursor-not-allowed opacity-70" aria-disabled="true">
<FileText className="h-4 w-4" aria-hidden="true" />
Datasheet ainda não publicado
</span>
)}
{serie.guia_de_programacao && (
<a href={serie.guia_de_programacao} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
<Code className="h-4 w-4" aria-hidden="true" />
Guia de programação
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
{serie.manual_tecnico && (
<a href={serie.manual_tecnico} target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
<BookOpen className="h-4 w-4" aria-hidden="true" />
Manual técnico
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
)}
</PageHero>
<section className="shell mt-10" aria-label="Resumo">
<dl className="grid grid-cols-2 gap-3 lg:grid-cols-4">
{fatos.map((fato) => (
<div key={fato.label} className="tile">
<dt className="text-xs font-medium text-muted">{fato.label}</dt>
<dd className="mt-1 font-display text-2xl font-semibold tracking-[-0.03em] text-ink">{fato.value}</dd>
<dd className="mt-0.5 text-xs text-muted">{fato.detail}</dd>
</div>
))}
</dl>
{serie.status_documentacao && (
<div className="callout callout-warn mt-4 flex items-start gap-3">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-gold" aria-hidden="true" />
<div>
<p className="font-semibold text-ink">Documentação preliminar</p>
<p className="mt-1">{serie.status_documentacao}</p>
</div>
</div>
)}
</section>
<div className="shell mt-10">
<SeriesTabMenu tabs={tabs} />
<PinRestrictions conexoes={conexoes} serieKey={key} />
</div>
<div className="shell mt-12">
<nav
aria-label="Seções da ficha"
className="nav-pill no-scrollbar sticky top-20 z-30 flex gap-1 overflow-x-auto rounded-full p-1.5"
>
{secoes.map((secao) => (
<a key={secao.id} href={`#${secao.id}`} className="nav-link shrink-0">
{secao.titulo}
</a>
))}
</nav>
<div className="mt-8 space-y-5">
{secoes.map((secao) => (
<SpecSection key={secao.id} {...secao} />
))}
</div>
</div>
<section className="shell section">
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<h2 className="section-title">Pronto para prototipar o {key}?</h2>
<p className="mt-3 text-base text-muted">
Veja as placas de desenvolvimento disponíveis para a série, com USB, sensores integrados e link para a loja oficial.
</p>
</div>
<Link href="/catalogo" className="btn btn-primary shrink-0">
Ver placas
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
<nav aria-label="Outras séries" className="mt-6 grid gap-3 sm:grid-cols-[1fr_auto_1fr] sm:items-center">
{previousKey ? (
<Link href={`/series/${previousKey}`} className="card card-interactive group flex items-center gap-3 px-5 py-4">
<ArrowLeft className="h-4 w-4 text-muted transition-transform group-hover:-translate-x-1" aria-hidden="true" />
<span>
<span className="block text-xs text-muted">Série anterior</span>
<span className="block font-display text-lg font-semibold text-ink">{previousKey}</span>
</span>
</Link>
) : (
<span className="hidden sm:block" />
)}
<Link href="/comparacao" className="btn btn-ghost justify-self-center">
<Scale className="h-4 w-4" aria-hidden="true" />
Comparar séries
</Link>
{nextKey ? (
<Link href={`/series/${nextKey}`} className="card card-interactive group flex items-center justify-end gap-3 px-5 py-4 text-right">
<span>
<span className="block text-xs text-muted">Próxima série</span>
<span className="block font-display text-lg font-semibold text-ink">{nextKey}</span>
</span>
<ArrowRight className="h-4 w-4 text-muted transition-transform group-hover:translate-x-1" aria-hidden="true" />
</Link>
) : (
<span className="hidden sm:block" />
)}
</nav>
</section>
</main>
<Footer />
</div>
);
}
function SpecSection({ id, titulo, icon: Icon, specs, nota }) {
return (
<section id={id} aria-labelledby={`${id}-titulo`} className="card scroll-mt-36 p-5 sm:p-7">
<h2 id={`${id}-titulo`} className="flex items-center gap-3 font-display text-xl font-semibold tracking-[-0.03em] text-ink">
<span className="grid h-9 w-9 place-items-center rounded-full bg-accent-soft text-accent">
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
{titulo}
</h2>
{nota && <p className="mt-3 text-sm text-muted">{nota}</p>}
<dl className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-3">
{specs.map((spec) => {
const negativo = /^n[ãa]o\b/i.test(String(spec.value));
return (
<div key={spec.label} className="tile">
<dt className="text-xs font-medium text-muted">{spec.label}</dt>
<dd className={`mt-1 text-sm font-semibold leading-snug ${negativo ? "text-muted" : "text-ink"}`}>{spec.value}</dd>
</div>
);
})}
</dl>
</section>
);
}
export async function generateMetadata({ params }) {
const { key } = await params;
const serie = seriesData[key];
if (!serie) {
return { title: "Série não encontrada", robots: { index: false, follow: false } };
}
// A descrição usa as especificações reais em vez de texto genérico: é o que
// aparece no resultado de busca e o que faz alguém clicar.
const radio = [
String(serie.wifi).startsWith("Não") ? null : serie.wifi.split(" (")[0],
String(serie.bluetooth).startsWith("Não") ? null : serie.bluetooth.split(" (")[0],
serie.zigbee_thread && !String(serie.zigbee_thread).startsWith("Não") ? "802.15.4" : null,
].filter(Boolean).join(", ");
const descricao =
`${serie.nome_completo}: ${serie.arquitetura}, ${serie.frequencia.replace("Até ", "até ")}, ` +
`${serie.memoria_sram} de SRAM e ${serie.gpio} GPIOs` +
(radio ? `. Rádio: ${radio}` : "") +
`. Pinagem completa, periféricos, segurança em hardware e pinos com restrição, em português.`;
// A pinagem da placa é a melhor prévia possível para uma série: quem
// compartilha o link já mostra o diagrama. Séries sem placa publicada caem
// na imagem padrão do site.
// Definir openGraph explicitamente impede a herança automática da imagem do
// site, então séries sem placa publicada (o H4) precisam do fallback aqui.
const imagem = serie.placa
? [{ url: serie.placa, width: 1600, height: 1091, alt: `Pinagem do ${serie.nome_completo}` }]
: [{ url: "/opengraph-image.png", width: 1200, height: 630, alt: "ESPDocs" }];
const meta = {
...paginaMeta({
titulo: `${serie.nome_completo}: especificações e pinagem`,
descricao,
caminho: `/series/${key}`,
tipo: "article",
keywords: [
serie.nome_completo, `${serie.nome_completo} pinout`,
`${serie.nome_completo} datasheet`, `${serie.nome_completo} especificações`,
`${serie.nome_completo} português`, "ESP32", "Espressif", "microcontrolador",
],
}),
other: { "article:section": "Séries ESP32" },
};
meta.openGraph = { ...meta.openGraph, images: imagem };
meta.twitter = { ...meta.twitter, images: imagem };
return meta;
}
export async function generateStaticParams() {
return Object.keys(seriesData).map((key) => ({
key: key,
}));
}
+46
View File
@@ -0,0 +1,46 @@
import { paginaMeta, jsonLd } from "@/lib/seo";
export const metadata = paginaMeta({
titulo: "Séries ESP32",
descricao: "As 12 séries de ESP32 lado a lado: ESP32, S2, S3, S31, C2, C3, C5, C6, C61, P4, H2 e H4. Arquitetura, memória, rádio, periféricos e pinagem de cada uma, em português.",
caminho: "/series",
keywords: ["séries ESP32", "modelos de ESP32", "ESP32 S3", "ESP32 C6", "ESP32 P4", "diferença entre ESP32"],
});
metadata.title = { default: "Séries ESP32", template: "%s · ESPDocs" };
const faqs = [
{
pergunta: "Quantas séries de ESP32 existem atualmente?",
resposta: "Atualmente o ecossistema Espressif conta com 12 séries principais: ESP32 clássico, ESP32-S2, ESP32-S3, ESP32-S31, ESP32-C2, ESP32-C3, ESP32-C5, ESP32-C6, ESP32-C61, ESP32-P4, ESP32-H2 e ESP32-H4.",
},
{
pergunta: "Quais são as séries mais recentes da Espressif?",
resposta: "As séries mais recentes incluem o ESP32-P4 (focado em alto desempenho com dual-core RISC-V a 400 MHz), o ESP32-C5 (primeiro chip com Wi-Fi dual-band 2.4/5 GHz da Espressif) e o ESP32-C61 (Wi-Fi 6 e Bluetooth 6 de baixo custo).",
},
];
export default function Layout({ children }) {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Séries ESP32", caminho: "/series" },
])
),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd.perguntas(faqs)),
}}
/>
{children}
</>
);
}
+154
View File
@@ -0,0 +1,154 @@
"use client";
import { useMemo, useState } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import Link from "next/link";
import seriesData from "@/public/series.json";
import {
AlertTriangle,
ArrowRight,
ArrowUpRight,
Compass,
Home,
Scale,
ShieldCheck,
Sparkles,
} from "lucide-react";
const FILTROS = [
{ id: "todas", rotulo: "Todas", testa: () => true },
{ id: "riscv", rotulo: "RISC-V", testa: (s) => s.arquitetura.includes("RISC-V") },
{ id: "xtensa", rotulo: "Xtensa", testa: (s) => !s.arquitetura.includes("RISC-V") },
{ id: "matter", rotulo: "Matter", testa: (s) => s.matter === "Sim" },
{ id: "ia", rotulo: "Aceleração de IA", testa: (s) => Boolean(s.aceleradores_ia) },
];
/** Marcador de segurança: o recurso que diferencia a série, não o que todas têm. */
function nivelSeguranca(serie) {
const seg = serie.seguranca || {};
if (seg.key_manager === "Sim") return "Key Manager";
if (String(seg.ecdsa).startsWith("Sim")) return "ECDSA em hardware";
if (String(seg.ecc).startsWith("Sim")) return "ECC em hardware";
if (String(seg.aes).startsWith("AES")) return "AES + SHA";
return "Só SHA";
}
const sim = (v) => !String(v).includes("Não");
export default function Series() {
const [filtro, setFiltro] = useState("todas");
const todas = Object.entries(seriesData);
const ativo = FILTROS.find((f) => f.id === filtro);
const series = useMemo(() => todas.filter(([, s]) => ativo.testa(s)), [todas, ativo]);
return (
<div className="site-canvas min-h-screen">
<Header />
<main id="conteudo">
<PageHero
trail={[{ label: "Séries" }]}
icon={Compass}
label={`${todas.length} famílias da Espressif`}
title="Séries ESP32"
lede="Cada série foi desenhada para um tipo de projeto, do sensor a bateria ao acelerador de IA. Abra a ficha para ver pinagem, periféricos e segurança."
/>
<section className="shell pt-10 sm:pt-12" aria-label="Lista de séries">
<div role="group" aria-label="Filtrar séries" className="flex flex-wrap gap-2">
{FILTROS.map((f) => (
<button key={f.id} type="button" aria-pressed={filtro === f.id} onClick={() => setFiltro(f.id)} className="chip-btn">
{f.rotulo}
</button>
))}
</div>
<p role="status" aria-live="polite" className="mt-4 text-sm text-muted">
{series.length} {series.length === 1 ? "série" : "séries"}
</p>
<div className="mt-4 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{series.map(([key, serie]) => {
const riscv = serie.arquitetura.includes("RISC-V");
const fatos = [
["Núcleos", serie.nucleos],
["Frequência", serie.frequencia],
["SRAM", serie.memoria_sram],
["Wi-Fi", sim(serie.wifi) ? "Sim" : "Não"],
["Bluetooth", serie.bluetooth === "Não" ? "Não" : "Sim"],
serie.consumo_energia?.deep_sleep ? ["Deep sleep", serie.consumo_energia.deep_sleep] : null,
].filter(Boolean);
return (
<Link key={key} href={`/series/${key}`} className="card card-interactive group flex flex-col p-5 sm:p-6">
<div className="flex items-start justify-between gap-3">
<h2 className="font-display text-2xl font-semibold tracking-[-0.035em] text-ink">{key}</h2>
<span className="chip">{riscv ? "RISC-V" : "Xtensa"}</span>
</div>
<p className="mt-2 line-clamp-2 min-h-[2.9rem] text-sm leading-relaxed text-muted">{serie.descricao}</p>
<dl className="mt-5 divide-y divide-line border-y border-line text-sm">
{fatos.map(([rotulo, valor]) => (
<div key={rotulo} className="flex items-baseline justify-between gap-4 py-2">
<dt className="text-muted">{rotulo}</dt>
<dd className={`text-right font-medium ${valor === "Não" ? "text-muted" : "text-ink"}`}>{valor}</dd>
</div>
))}
</dl>
<ul className="mt-4 flex flex-1 flex-wrap content-start gap-1.5">
{serie.aceleradores_ia && (
<li className="chip chip-accent">
<Sparkles className="h-3 w-3" aria-hidden="true" />
Aceleradores de IA
</li>
)}
<li className="chip">
<ShieldCheck className="h-3 w-3 text-accent" aria-hidden="true" />
{nivelSeguranca(serie)}
</li>
{serie.matter === "Sim" && (
<li className="chip">
<Home className="h-3 w-3 text-accent" aria-hidden="true" />
Matter
</li>
)}
{serie.status_documentacao && (
<li className="chip">
<AlertTriangle className="h-3 w-3 text-gold" aria-hidden="true" />
Documentação preliminar
</li>
)}
</ul>
<span className="mt-5 inline-flex items-center gap-1 text-sm font-medium text-accent">
Abrir ficha
<ArrowUpRight className="h-4 w-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true" />
</span>
</Link>
);
})}
</div>
</section>
<section className="shell section">
<div className="panel flex flex-col items-start justify-between gap-6 p-7 sm:p-10 md:flex-row md:items-center">
<div className="max-w-2xl">
<h2 className="section-title">Ficou em dúvida entre duas séries?</h2>
<p className="mt-3 text-base text-muted">
O comparador coloca até quatro modelos lado a lado: núcleos, rádio, memória, periféricos e segurança.
</p>
</div>
<Link href="/comparacao" className="btn btn-primary shrink-0">
<Scale className="h-4 w-4" aria-hidden="true" />
Comparar séries
<ArrowRight className="h-4 w-4" aria-hidden="true" />
</Link>
</div>
</section>
</main>
<Footer />
</div>
);
}
+168
View File
@@ -0,0 +1,168 @@
import { paginaMeta, jsonLd, SITE } from "@/lib/seo";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
import Image from "next/image";
import GiteaMark from "@/components/GiteaMark";
import { ArrowUpRight, Code, Globe, GitCompare, Info, Languages } from "lucide-react";
const REPOSITORIO = SITE.repositorio;
export const metadata = paginaMeta({
titulo: "Sobre o projeto",
descricao: "O ESPDocs é uma plataforma independente que reúne a documentação do ecossistema ESP32 em português brasileiro. Saiba como o projeto nasceu e como contribuir.",
caminho: "/sobre",
keywords: ["ESPDocs", "documentação ESP32 português", "projeto open source ESP32"],
});
export default function Sobre() {
const features = [
{ icon: Languages, title: "Em português", desc: "Dados técnicos organizados e apresentados em português do Brasil." },
{ icon: GitCompare, title: "Comparações visuais", desc: "Tabelas e diagramas para escolher a série certa sem abrir dez datasheets." },
{ icon: Globe, title: "Acesso centralizado", desc: "Links diretos para a documentação oficial, repositórios e exemplos." },
{ icon: Code, title: "Código aberto", desc: "Contribuições são bem-vindas no Gitea." },
];
return (
<div className="site-canvas min-h-screen">
{/* Metadados E-E-A-T de autoria e organização para o Google */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.paginaSobre()) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
jsonLd.trilha([
{ nome: "Início", caminho: "/" },
{ nome: "Sobre o Projeto", caminho: "/sobre" },
])
),
}}
/>
<Header />
<main id="conteudo">
<PageHero
trail={[{ label: "Sobre" }]}
icon={Info}
label="Projeto independente"
title="Sobre o ESPDocs"
lede="Uma plataforma não oficial, em português, que organiza as informações técnicas do ESP32 e leva você à documentação oficial da Espressif. Feita para a comunidade de hardware livre."
>
<a href={REPOSITORIO} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
<GiteaMark className="h-4 w-4" />
Ver o repositório
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
</PageHero>
<div className="shell section grid grid-cols-1 gap-10 pt-12 sm:pt-16 lg:grid-cols-[1fr_22rem] lg:gap-14">
<div className="min-w-0 max-w-3xl space-y-12">
<section aria-labelledby="missao">
<h2 id="missao" className="section-title">Missão</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
Facilitar o acesso e a compreensão do ecossistema ESP32 com uma interface em português que organiza
especificações, compara séries, apresenta frameworks e centraliza os links da documentação oficial.
O objetivo é economizar o seu tempo no desenvolvimento de firmware e IoT.
</p>
</section>
<section aria-labelledby="porque">
<h2 id="porque" className="section-title">Por que o ESPDocs existe</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
As informações sobre os ESP32 costumam estar espalhadas entre datasheets densos, manuais de referência
(TRM) e repositórios secundários, tudo em inglês. O ESPDocs condensa e estrutura esses dados para a
triagem rápida, antes de você aprofundar na engenharia oficial.
</p>
<ul className="mt-6 grid gap-3 sm:grid-cols-2">
{features.map(({ icon: Icon, title, desc }) => (
<li key={title} className="card flex items-start gap-4 p-5">
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-accent-soft text-accent">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<div>
<h3 className="font-semibold text-ink">{title}</h3>
<p className="mt-1 text-sm leading-relaxed text-muted">{desc}</p>
</div>
</li>
))}
</ul>
</section>
<section aria-labelledby="contribuir">
<h2 id="contribuir" className="section-title">Como contribuir</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
Correções de tradução, ajustes em tabelas e novos exemplos de código são muito bem-vindos. O projeto
cresce com a comunidade.
</p>
<ol className="mt-6 space-y-3">
{[
"Faça o fork do repositório no Gitea.",
"Implemente as melhorias e confira as referências técnicas oficiais.",
"Abra um Pull Request descrevendo os ajustes feitos.",
].map((passo, i) => (
<li key={passo} className="card flex items-center gap-4 px-5 py-4">
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-ink text-sm font-semibold text-canvas" aria-hidden="true">
{i + 1}
</span>
<span className="text-sm text-ink-soft sm:text-base">{passo}</span>
</li>
))}
</ol>
</section>
<section aria-labelledby="acessibilidade">
<h2 id="acessibilidade" className="section-title">Acessibilidade</h2>
<p className="mt-4 text-base leading-relaxed text-muted">
O ESPDocs prioriza a legibilidade, o contraste alto (nível AAA da WCAG) e a navegação completa por
teclado. Se encontrar alguma barreira de leitura ou navegação, abra uma issue no repositório.
</p>
</section>
</div>
<aside className="space-y-5 lg:sticky lg:top-28 lg:self-start" aria-label="Autor e aviso legal">
<div className="panel p-6 sm:p-7">
<div className="flex items-center gap-4">
<span className="grid h-16 w-16 shrink-0 place-items-center rounded-2xl border border-line bg-white">
<Image src="/logo.png" alt="" aria-hidden="true" width={48} height={48} className="h-12 w-12 object-contain" />
</span>
<div>
<h2 className="font-display text-lg font-semibold text-ink">Lucas Rayan Guerra</h2>
<p className="text-sm text-muted">Criador do Ciência Embarcada</p>
</div>
</div>
<div className="mt-5 space-y-3 text-sm leading-relaxed text-muted">
<p>
Técnico em Desenvolvimento de Sistemas, estudante de Ciência da Computação na UFRPE e pesquisador de
Blockchain e Criptografia.
</p>
<p>
Diretor de Software na Semine AgriTech e palestrante em eventos como o REC&apos;n&apos;Play, compartilha
conteúdo sobre hardware, IoT e cibersegurança.
</p>
</div>
<a href="https://cienciaembarcada.com.br" target="_blank" rel="noopener noreferrer" className="btn btn-ghost mt-6 w-full">
Visitar o Ciência Embarcada
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
</a>
</div>
<div className="callout callout-warn">
<p className="font-semibold text-ink">Aviso legal</p>
<p className="mt-1">
O ESPDocs é um agregador e organizador. Os manuais e repositórios referenciados pertencem à Espressif
Systems. Em projetos críticos ou comerciais, consulte sempre a documentação oficial.
</p>
</div>
</aside>
</div>
</main>
<Footer />
</div>
);
}