Compare commits
No files matched your search
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Uso: .gitea/scripts/digest-latest.sh <registro> <repositório>
|
||||||
|
# Imprime o digest do manifesto de <repositório>:latest, ou nada se a tag ainda
|
||||||
|
# não existe. Fica em .gitea/ para não entrar no hash da imagem (plano-imagem.sh).
|
||||||
|
# O release.yml compara o valor antes e depois de publicar para
|
||||||
|
# decidir se há o que implantar. Com o pacote privado, passe REGISTRY_USER e
|
||||||
|
# REGISTRY_PASSWORD (token com read:package).
|
||||||
|
set -euo pipefail
|
||||||
|
registry=$1; ns=${2,,}
|
||||||
|
|
||||||
|
auth=(); [ -n "${REGISTRY_PASSWORD:-}" ] && auth=(-u "${REGISTRY_USER:?}:$REGISTRY_PASSWORD")
|
||||||
|
token=$(curl -fsS "${auth[@]}" "https://$registry/v2/token?service=container_registry&scope=repository:$ns:pull" | jq -r .token)
|
||||||
|
curl -sSI \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H 'Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json' \
|
||||||
|
"https://$registry/v2/$ns/manifests/latest" \
|
||||||
|
| tr -d '\r' | awk -F': ' 'tolower($1) == "docker-content-digest" { print $2 }'
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
# Publica a imagem no registro de containers do Gitea depois de um merge em main,
|
# Publica a imagem no registro de containers do Gitea depois de um merge em main,
|
||||||
# e de novo com tag semver quando uma tag v* é enviada. O servidor só faz pull.
|
# e de novo com tag semver quando uma tag v* é enviada. Quando o latest passa a
|
||||||
|
# apontar para outra imagem, o job deploy pede ao Coolify para implantá-la.
|
||||||
|
# Precisa também dos secrets COOLIFY_URL e COOLIFY_TOKEN no repositório.
|
||||||
# Precisa do secret de usuário REGISTRY_TOKEN (token do Gitea com write:package).
|
# Precisa do secret de usuário REGISTRY_TOKEN (token do Gitea com write:package).
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -52,6 +54,8 @@ jobs:
|
|||||||
# O único runner é arm64 e o servidor de destino (Oracle) também, então a
|
# O único runner é arm64 e o servidor de destino (Oracle) também, então a
|
||||||
# imagem é construída nativamente, sem emulação.
|
# imagem é construída nativamente, sem emulação.
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
|
outputs:
|
||||||
|
mudou: ${{ steps.depois.outputs.mudou }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -69,6 +73,13 @@ jobs:
|
|||||||
username: ${{ gitea.actor }}
|
username: ${{ gitea.actor }}
|
||||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
# O deploy só acontece se o latest passar a apontar para outra imagem.
|
||||||
|
# Um merge só de docs ou uma tag reaponta o latest para o mesmo manifesto,
|
||||||
|
# e reiniciar o container nesse caso seria trabalho repetido.
|
||||||
|
- name: Digest do latest antes
|
||||||
|
id: antes
|
||||||
|
run: echo "digest=$(.gitea/scripts/digest-latest.sh "$REGISTRY" "${{ gitea.repository }}")" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Metadados
|
- name: Metadados
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v6
|
uses: docker/metadata-action@v6
|
||||||
@@ -101,6 +112,15 @@ jobs:
|
|||||||
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}')
|
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}')
|
||||||
docker buildx imagetools create $tags "${{ needs.plano.outputs.image }}:inputs-${{ needs.plano.outputs.inputs }}"
|
docker buildx imagetools create $tags "${{ needs.plano.outputs.image }}:inputs-${{ needs.plano.outputs.inputs }}"
|
||||||
|
|
||||||
|
- name: Digest do latest depois
|
||||||
|
id: depois
|
||||||
|
run: |
|
||||||
|
antes="${{ steps.antes.outputs.digest }}"
|
||||||
|
depois=$(.gitea/scripts/digest-latest.sh "$REGISTRY" "${{ gitea.repository }}")
|
||||||
|
echo "latest: ${antes:-nenhum} -> $depois"
|
||||||
|
[ -n "$depois" ] && [ "$depois" != "$antes" ] && mudou=true || mudou=false
|
||||||
|
echo "mudou=$mudou" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# Cria a entrada em "Releases" quando a execução é de uma tag v*. Roda depois
|
# Cria a entrada em "Releases" quando a execução é de uma tag v*. Roda depois
|
||||||
# de publicar a imagem, com as notas da seção do CHANGELOG.md.
|
# de publicar a imagem, com as notas da seção do CHANGELOG.md.
|
||||||
release:
|
release:
|
||||||
@@ -127,3 +147,38 @@ jobs:
|
|||||||
'{tag_name:$tag, name:$name, body:$body, prerelease:$pre}' > release.json
|
'{tag_name:$tag, name:$name, body:$body, prerelease:$pre}' > release.json
|
||||||
curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \
|
curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \
|
||||||
-H 'Content-Type: application/json' -d @release.json
|
-H 'Content-Type: application/json' -d @release.json
|
||||||
|
|
||||||
|
# Avisa o Coolify para puxar o latest novo e espera o deploy terminar, para
|
||||||
|
# que uma falha apareça aqui, e não só no painel do Coolify. Roda só quando o
|
||||||
|
# latest mudou, ou num disparo manual (que força o deploy).
|
||||||
|
# Precisa dos secrets COOLIFY_URL e COOLIFY_TOKEN no repositório.
|
||||||
|
deploy:
|
||||||
|
name: Deploy no Coolify
|
||||||
|
needs: publicar
|
||||||
|
if: ${{ !cancelled() && needs.publicar.result == 'success' && (needs.publicar.outputs.mudou == 'true' || gitea.event_name == 'workflow_dispatch') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy de ESPDocs
|
||||||
|
env:
|
||||||
|
COOLIFY_URL: ${{ secrets.COOLIFY_URL }}
|
||||||
|
COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }}
|
||||||
|
APP: 62wmq51cgbcz9nyfwizmytku
|
||||||
|
run: |
|
||||||
|
if [ -z "$COOLIFY_URL" ] || [ -z "$COOLIFY_TOKEN" ]; then
|
||||||
|
echo "faltam os secrets COOLIFY_URL e COOLIFY_TOKEN no repositório"; exit 1
|
||||||
|
fi
|
||||||
|
auth="Authorization: Bearer $COOLIFY_TOKEN"
|
||||||
|
dep=$(curl -fsS -X POST -H "$auth" "$COOLIFY_URL/api/v1/deploy?uuid=$APP&force=false" | jq -r '.deployments[0].deployment_uuid')
|
||||||
|
[ -n "$dep" ] && [ "$dep" != null ] || { echo "o Coolify não devolveu um deployment"; exit 1; }
|
||||||
|
echo "deployment $dep"
|
||||||
|
# Até 15 minutos: o pull da imagem arm64 e o healthcheck cabem com folga.
|
||||||
|
for _ in $(seq 1 90); do
|
||||||
|
st=$(curl -fsS -H "$auth" "$COOLIFY_URL/api/v1/deployments/$dep" | jq -r '.status')
|
||||||
|
echo "status: $st"
|
||||||
|
case "$st" in
|
||||||
|
finished) exit 0 ;;
|
||||||
|
failed|cancelled*) exit 1 ;;
|
||||||
|
esac
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
echo "o deploy não terminou em 15 minutos"; exit 1
|
||||||
@@ -49,3 +49,6 @@ next-env.d.ts
|
|||||||
|
|
||||||
# Configuração local do Superset
|
# Configuração local do Superset
|
||||||
.superset/
|
.superset/
|
||||||
|
|
||||||
|
# mockups do brainstorming
|
||||||
|
.superpowers/
|
||||||
@@ -6,6 +6,78 @@ Tudo o que muda de relevante no ESPDocs está aqui. O formato segue o
|
|||||||
|
|
||||||
## [Não lançado]
|
## [Não lançado]
|
||||||
|
|
||||||
|
## [1.8.0] - 2026-11-03
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- Faixa discreta de placas sugeridas nas páginas de série, na comparação e nos resultados do
|
||||||
|
seletor (`PlacasSugeridas`). Sorteia uma placa dos chips da tela, com preferência (peso 3)
|
||||||
|
pelas de loja oficial, e fica parada até o leitor usar as setas, com transição suave (respeita
|
||||||
|
"reduzir movimento"). Cada placa traz o chip, uma linha de motivo traduzida nos 6 idiomas e o
|
||||||
|
link de afiliado do idioma da página, marcado como tal. Não aparece quando o chip não tem
|
||||||
|
placa. Sem rastreamento nem cookies. Na série, fica entre a foto/diagrama de pinos e as
|
||||||
|
especificações; no celular, o botão ocupa a largura do cartão.
|
||||||
|
- Catálogo de placas com link de afiliado por país: a planilha ganha as colunas `Link_en`,
|
||||||
|
`Link_es`, `Link_fr`, `Link_de` e `Link_zh-CN`, e o botão do card usa a do idioma da página,
|
||||||
|
caindo no `Link` (Brasil) quando a coluna está vazia. As colunas de link nunca viram filtro.
|
||||||
|
- Deploy automático no Coolify: depois de publicar a imagem, o `release.yml` pede o deploy e
|
||||||
|
espera o resultado. Só implanta quando o `latest` muda, então merge só de documentação ou
|
||||||
|
tag não reiniciam o container. Um disparo manual do workflow força o deploy.
|
||||||
|
- Formação 2 da LASER em `/formacoes/2`: estação de temperatura e umidade com DHT22, LCD 16x2
|
||||||
|
I2C e ESP32-S3, simulada no Wokwi com o framework Arduino. Slides, apostila, projeto do
|
||||||
|
Wokwi e programa de scanner I2C, tudo validado no simulador.
|
||||||
|
- Página de entrada para cada formação (`/formacoes/1`, `/formacoes/2`) e lista geral em
|
||||||
|
`/formacoes`, geradas por `npm run formacoes-viewer` a partir do `formacao.json` de cada pasta.
|
||||||
|
- PDFs prontos dos slides e das apostilas das formações 1 e 2. `npm run slides-pdf -- N`
|
||||||
|
agora gera também o PDF da apostila.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
|
||||||
|
- Página Sobre reescrita nos seis idiomas. Lista o que o ESPDocs tem hoje (séries, comparador,
|
||||||
|
seletor, catálogo, diagnóstico, componentes, frameworks, glossário, busca, os seis idiomas e,
|
||||||
|
só em português, o livro), com um cartão com link para cada ferramenta. Os dados do autor
|
||||||
|
foram atualizados a partir do site pessoal (cargo na RNP, formação, reconhecimentos, foto e
|
||||||
|
links), e o JSON-LD do autor ganhou cargo e empregador.
|
||||||
|
- O botão avulso "Sobre" saiu da barra do desktop em todos os idiomas, porque a página já está
|
||||||
|
no menu Biblioteca. Ela continua no menu móvel e no rodapé.
|
||||||
|
- A apostila de cada formação passou de `index.html` para `apostila.html`; o `index.html`
|
||||||
|
agora é a página de entrada.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Barra do desktop em português: com o link "Livro" a mais, os rótulos quebravam com hífen
|
||||||
|
("Hard-ware"). O link saiu da barra (o livro segue no menu móvel, no rodapé e nas chamadas
|
||||||
|
do site) e `.nav-link` ganhou `white-space: nowrap`.
|
||||||
|
- Menu no celular: o botão do menu ultrapassava a borda do cabeçalho em 360 e 375 px. Abaixo
|
||||||
|
de 390 px o botão de tema passa para dentro do menu. O menu aberto rola inteiro (cartão do
|
||||||
|
livro e links juntos), então todos os itens ficam alcançáveis em telas baixas, e o botão de
|
||||||
|
fechar e o logo ganharam área de toque de 44 px.
|
||||||
|
- O build da imagem Docker repete até 3 vezes quando falha ao baixar as Google Fonts, falha
|
||||||
|
intermitente que derrubava o CI com "next/font/google queries have exactly one entry".
|
||||||
|
- Card do catálogo: o botão "Ver no AliExpress" fica sempre no rodapé, alinhado entre os cards.
|
||||||
|
- A API do catálogo lê `A1:AZ1000`, para comportar as colunas novas da planilha.
|
||||||
|
- O link do arquivo do Fritzing na apostila da formação 1 apontava para `/apostilas/1/`,
|
||||||
|
caminho que não existe.
|
||||||
|
|
||||||
|
## [1.7.0] - 2026-11-02
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
|
||||||
|
- A busca inteligente entende pinos específicos, nos seis idiomas: `pino 32 esp32-s3`,
|
||||||
|
`pin 32 esp32-s3`, `broche 32 esp32-s3`, `esp32-s3 32号引脚`. O resultado é um cartão
|
||||||
|
com tudo o que o diagrama diz sobre o pino (nome, funções, categorias e avisos) e o
|
||||||
|
link para o diagrama filtrado, como já acontecia com memória, energia e outros assuntos.
|
||||||
|
- Como "pino 32" pode ser o pino físico ou o GPIO32, os dois aparecem quando são
|
||||||
|
pinos diferentes.
|
||||||
|
- Sem o chip (`pino 32`), vem um quadro com o que há nesse pino em cada série; com um
|
||||||
|
número que o chip não tem, a busca avisa até onde vão os pinos.
|
||||||
|
- O pino reconhecido aparece junto do chip na faixa "Entendi" e nos exemplos da busca.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- Linhas repetidas num cartão da busca (por exemplo, vários avisos de um pino) saíam
|
||||||
|
duplicadas e fora de ordem.
|
||||||
|
|
||||||
## [1.6.1] - 2026-11-02
|
## [1.6.1] - 2026-11-02
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
@@ -255,7 +327,9 @@ entrada resume tudo desde o primeiro commit, em 2025-10-21, até a imagem Docker
|
|||||||
otimização de imagens (GHSA-2464-fhr4-w9xw), e `postcss` fixado acima da cópia
|
otimização de imagens (GHSA-2464-fhr4-w9xw), e `postcss` fixado acima da cópia
|
||||||
embutida no `next`.
|
embutida no `next`.
|
||||||
|
|
||||||
[Não lançado]: https://git.lucasrguerra.dev.br/lucasrguerra/ESPDocs/compare/v1.6.1...HEAD
|
[Não lançado]: https://git.lucasrguerra.dev.br/lucasrguerra/ESPDocs/compare/v1.8.0...HEAD
|
||||||
|
[1.8.0]: https://git.lucasrguerra.dev.br/lucasrguerra/ESPDocs/compare/v1.7.0...v1.8.0
|
||||||
|
[1.7.0]: https://git.lucasrguerra.dev.br/lucasrguerra/ESPDocs/compare/v1.6.1...v1.7.0
|
||||||
[1.6.1]: https://git.lucasrguerra.dev.br/lucasrguerra/ESPDocs/compare/v1.6.0...v1.6.1
|
[1.6.1]: https://git.lucasrguerra.dev.br/lucasrguerra/ESPDocs/compare/v1.6.0...v1.6.1
|
||||||
[1.6.0]: https://git.lucasrguerra.dev.br/lucasrguerra/ESPDocs/compare/v1.5.0...v1.6.0
|
[1.6.0]: https://git.lucasrguerra.dev.br/lucasrguerra/ESPDocs/compare/v1.5.0...v1.6.0
|
||||||
[1.5.0]: https://git.lucasrguerra.dev.br/lucasrguerra/ESPDocs/compare/v1.4.2...v1.5.0
|
[1.5.0]: https://git.lucasrguerra.dev.br/lucasrguerra/ESPDocs/compare/v1.4.2...v1.5.0
|
||||||
|
|||||||
@@ -15,7 +15,13 @@ ENV NEXT_TELEMETRY_DISABLED=1 NEXT_OUTPUT=standalone
|
|||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
# Cache de compilação do Next: só as páginas que mudaram são recompiladas.
|
# Cache de compilação do Next: só as páginas que mudaram são recompiladas.
|
||||||
RUN --mount=type=cache,target=/app/.next/cache npm run build
|
# O build baixa as Google Fonts (next/font/google); quando a rede do runner falha, o Turbopack
|
||||||
|
# aborta com "next/font/google queries have exactly one entry". Repete até 3 vezes.
|
||||||
|
RUN --mount=type=cache,target=/app/.next/cache \
|
||||||
|
for tentativa in 1 2 3; do \
|
||||||
|
npm run build && exit 0; \
|
||||||
|
echo "build falhou (tentativa $tentativa de 3)"; sleep 15; \
|
||||||
|
done; exit 1
|
||||||
|
|
||||||
FROM node:22-alpine
|
FROM node:22-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Header from "@/components/Header";
|
|||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import PageHero from "@/components/PageHero";
|
import PageHero from "@/components/PageHero";
|
||||||
import BoardCard from "@/components/BoardCard";
|
import BoardCard from "@/components/BoardCard";
|
||||||
|
import { isLinkColumn } from "@/lib/boardLinks";
|
||||||
import { AlertTriangle, RefreshCw, Search, ShoppingCart, SlidersHorizontal } from "lucide-react";
|
import { AlertTriangle, RefreshCw, Search, ShoppingCart, SlidersHorizontal } from "lucide-react";
|
||||||
|
|
||||||
const HEADER_MAP_EN = {
|
const HEADER_MAP_EN = {
|
||||||
@@ -184,7 +185,7 @@ export default function PlacasPage() {
|
|||||||
|
|
||||||
const generateFilterOptions = (data, headers) => {
|
const generateFilterOptions = (data, headers) => {
|
||||||
const options = {};
|
const options = {};
|
||||||
const filterableColumns = headers.filter((h) => !excludedColumns.includes(h));
|
const filterableColumns = headers.filter((h) => !excludedColumns.includes(h) && !isLinkColumn(h));
|
||||||
|
|
||||||
filterableColumns.forEach((column) => {
|
filterableColumns.forEach((column) => {
|
||||||
const uniqueValues = new Set();
|
const uniqueValues = new Set();
|
||||||
@@ -265,7 +266,7 @@ export default function PlacasPage() {
|
|||||||
|
|
||||||
const activeFilterCount = Object.keys(activeFilters).length + (searchTerm ? 1 : 0);
|
const activeFilterCount = Object.keys(activeFilters).length + (searchTerm ? 1 : 0);
|
||||||
|
|
||||||
const filterableColumns = headers.filter((h) => !excludedColumns.includes(h));
|
const filterableColumns = headers.filter((h) => !excludedColumns.includes(h) && !isLinkColumn(h));
|
||||||
|
|
||||||
const hero = (
|
const hero = (
|
||||||
<PageHero
|
<PageHero
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
|
import PlacasSugeridas from "@/components/PlacasSugeridas";
|
||||||
import PageHero from "@/components/PageHero";
|
import PageHero from "@/components/PageHero";
|
||||||
import { useEffect, useState, useMemo, Fragment } from "react";
|
import { useEffect, useState, useMemo, Fragment } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
@@ -740,6 +741,10 @@ export default function Comparacao() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div className="shell mt-8">
|
||||||
|
<PlacasSugeridas chips={selectedSeries} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<section className="shell section" aria-labelledby="docs-titulo">
|
<section className="shell section" aria-labelledby="docs-titulo">
|
||||||
<h2 id="docs-titulo" className="section-title">
|
<h2 id="docs-titulo" className="section-title">
|
||||||
{isEs ? "Documentación oficial" : isEn ? "Official documentation" : isFr ? "Documentation officielle" : isZh ? "官方文档" : isDe ? "Offizielle Dokumentation" : "Documentação oficial"}
|
{isEs ? "Documentación oficial" : isEn ? "Official documentation" : isFr ? "Documentation officielle" : isZh ? "官方文档" : isDe ? "Offizielle Dokumentation" : "Documentação oficial"}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState, useMemo } from "react";
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
|
import PlacasSugeridas from "@/components/PlacasSugeridas";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getLocalizedHref } from "@/i18n/links";
|
import { getLocalizedHref } from "@/i18n/links";
|
||||||
import seriesDataPt from "@/public/series.json";
|
import seriesDataPt from "@/public/series.json";
|
||||||
@@ -2554,6 +2555,8 @@ export default function Seletor() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PlacasSugeridas chips={topThree.map((rec) => rec.key)} className="mt-10" />
|
||||||
|
|
||||||
{otherRecommendations.length > 0 && (
|
{otherRecommendations.length > 0 && (
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<h3 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
<h3 className="font-display text-xl font-semibold tracking-[-0.03em] text-ink">
|
||||||
@@ -2714,6 +2717,7 @@ export default function Seletor() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{filteredChips.length > 0 && <PlacasSugeridas chips={filteredChips.map(([chave]) => chave)} className="mt-6" />}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import seriesDataDe from "@/data/de/series.json";
|
|||||||
import seriesDataZh from "@/data/zh-CN/series.json";
|
import seriesDataZh from "@/data/zh-CN/series.json";
|
||||||
import ConnectionsDiagram from "@/components/ConnectionsDiagram";
|
import ConnectionsDiagram from "@/components/ConnectionsDiagram";
|
||||||
import SeriesTabMenu from "@/components/SeriesTabMenu";
|
import SeriesTabMenu from "@/components/SeriesTabMenu";
|
||||||
|
import PlacasSugeridas from "@/components/PlacasSugeridas";
|
||||||
import PinRestrictions from "@/components/PinRestrictions";
|
import PinRestrictions from "@/components/PinRestrictions";
|
||||||
import { getLocalizedHref } from "@/i18n/links";
|
import { getLocalizedHref } from "@/i18n/links";
|
||||||
import {
|
import {
|
||||||
@@ -843,6 +844,7 @@ export default async function SerieDetail({ params }) {
|
|||||||
|
|
||||||
<div className="shell mt-10">
|
<div className="shell mt-10">
|
||||||
<SeriesTabMenu tabs={tabs} isEn={isEn} isEs={isEs} isFr={isFr} isDe={isDe} isZh={isZh} />
|
<SeriesTabMenu tabs={tabs} isEn={isEn} isEs={isEs} isFr={isFr} isDe={isDe} isZh={isZh} />
|
||||||
|
<PlacasSugeridas chips={[key]} className="mt-5" />
|
||||||
<PinRestrictions conexoes={conexoes} serieKey={key} isEn={isEn} isEs={isEs} isFr={isFr} isDe={isDe} isZh={isZh} />
|
<PinRestrictions conexoes={conexoes} serieKey={key} isEn={isEn} isEs={isEs} isFr={isFr} isDe={isDe} isZh={isZh} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,416 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import { paginaMeta, jsonLd, SITE } from "@/lib/seo";
|
import { paginaMeta, jsonLd, SITE } from "@/lib/seo";
|
||||||
|
import { getLocalizedHref } from "@/i18n/links";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import PageHero from "@/components/PageHero";
|
import PageHero from "@/components/PageHero";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import GiteaMark from "@/components/GiteaMark";
|
import GiteaMark from "@/components/GiteaMark";
|
||||||
import { ArrowUpRight, Code, Globe, GitCompare, Info, Languages } from "lucide-react";
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
ArrowUpRight,
|
||||||
|
BookA,
|
||||||
|
BookMarked,
|
||||||
|
BookOpen,
|
||||||
|
Box,
|
||||||
|
Cpu,
|
||||||
|
Info,
|
||||||
|
Scale,
|
||||||
|
Search,
|
||||||
|
SlidersHorizontal,
|
||||||
|
Sparkles,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
const REPOSITORIO = SITE.repositorio;
|
const REPOSITORIO = SITE.repositorio;
|
||||||
|
|
||||||
|
const LANGS = ["pt", "en", "es", "fr", "de", "zh-CN"];
|
||||||
|
|
||||||
|
/** Ferramentas e conteúdos do site: ícone e rota fixos, texto por idioma (ordem = ordem de FERRAMENTAS). */
|
||||||
|
const FERRAMENTAS = [
|
||||||
|
{ icon: Cpu, href: "/series" },
|
||||||
|
{ icon: Scale, href: "/comparacao" },
|
||||||
|
{ icon: SlidersHorizontal, href: "/seletor" },
|
||||||
|
{ icon: Box, href: "/catalogo" },
|
||||||
|
{ icon: AlertTriangle, href: "/diagnostico" },
|
||||||
|
{ icon: Search, href: "/componentes" },
|
||||||
|
{ icon: BookOpen, href: "/frameworks" },
|
||||||
|
{ icon: BookA, href: "/glossario" },
|
||||||
|
{ icon: Sparkles, href: null },
|
||||||
|
{ icon: BookMarked, href: "/livro", soPt: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
pt: {
|
||||||
|
metaTitulo: "Sobre o projeto",
|
||||||
|
metaDesc:
|
||||||
|
"O ESPDocs é uma plataforma independente, em seis idiomas, que reúne a documentação do ecossistema ESP32, com séries, comparador, seletor de chip, catálogo de placas, diagnóstico e glossário. Conheça o projeto e o autor.",
|
||||||
|
keywords: ["ESPDocs", "documentação ESP32 português", "projeto open source ESP32"],
|
||||||
|
inicio: "Início",
|
||||||
|
trilha: "Sobre",
|
||||||
|
tituloHero: "Sobre o ESPDocs",
|
||||||
|
label: "Projeto independente",
|
||||||
|
lede: "Uma plataforma não oficial, em seis idiomas, que organiza as informações técnicas do ESP32, ajuda a escolher o chip certo e leva você à documentação oficial da Espressif. Feita para a comunidade de hardware livre.",
|
||||||
|
repo: "Ver o repositório",
|
||||||
|
missaoTitulo: "Missão",
|
||||||
|
missao:
|
||||||
|
"Facilitar o acesso e a compreensão do ecossistema ESP32: organizar especificações, comparar séries, ajudar na escolha do chip e das placas, explicar erros comuns e centralizar os links da documentação oficial. O objetivo é economizar o seu tempo no desenvolvimento de firmware e IoT.",
|
||||||
|
porqueTitulo: "Por que o ESPDocs existe",
|
||||||
|
porque:
|
||||||
|
"As informações sobre os ESP32 costumam estar espalhadas entre datasheets densos, manuais de referência (TRM) e repositórios secundários, quase sempre só em inglês. O ESPDocs condensa e estrutura esses dados para a triagem rápida, antes de você aprofundar na engenharia oficial.",
|
||||||
|
temTitulo: "O que você encontra aqui",
|
||||||
|
temLede: "O projeto começou como uma tabela de séries e hoje reúne:",
|
||||||
|
ferramentas: [
|
||||||
|
["12 séries ESP32", "Especificações, diagramas de pinos interativos e pinos com restrição de uso, do ESP32 clássico ao ESP32-P4."],
|
||||||
|
["Comparador", "Até quatro séries lado a lado, com as diferenças em destaque."],
|
||||||
|
["Seletor inteligente", "Diga o que o projeto precisa e veja quais chips atendem."],
|
||||||
|
["Catálogo de placas", "DevKits com pinos, filtros e links de compra para o seu país."],
|
||||||
|
["Diagnóstico", "Brownout, bootloop, watchdog e mais: sintomas, causas e um decodificador de logs."],
|
||||||
|
["Component Registry", "42 componentes e drivers do registro oficial, com o manifest idf_component.yml pronto."],
|
||||||
|
["Frameworks e SDKs", "14 frameworks, do ESP-IDF e Arduino a Rust e Matter, com exemplos de código."],
|
||||||
|
["Glossário técnico", "Os termos que aparecem nos datasheets, explicados por categoria."],
|
||||||
|
["Busca inteligente", "Pergunte “pino 32 esp32-s3” ou “qual ESP32 tem PSRAM” e receba a resposta direta, com link para a fonte."],
|
||||||
|
["O livro", "Microcontroladores ESP32: hardware e firmware em 12 partes e 78 capítulos."],
|
||||||
|
],
|
||||||
|
idiomasTitulo: "Seis idiomas",
|
||||||
|
idiomas:
|
||||||
|
"O site inteiro está em português, inglês, espanhol, francês, alemão e chinês simplificado: interface, dados, busca e exemplos de código com comentários traduzidos. O livro existe só em português.",
|
||||||
|
contribuirTitulo: "Como contribuir",
|
||||||
|
contribuir: "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.",
|
||||||
|
passos: [
|
||||||
|
"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.",
|
||||||
|
],
|
||||||
|
acessTitulo: "Acessibilidade",
|
||||||
|
acess:
|
||||||
|
"O ESPDocs prioriza a legibilidade, o contraste alto (nível AAA da WCAG), o tema claro e escuro e a navegação completa por teclado. Se encontrar alguma barreira de leitura ou navegação, abra uma issue no repositório.",
|
||||||
|
asideLabel: "Autor e aviso legal",
|
||||||
|
autorCargo: "Técnico de Operações do PoP-PE na RNP",
|
||||||
|
autorBio: [
|
||||||
|
"Desenvolvedor full-stack JavaScript com especialização em resposta a incidentes e forense, entre código, circuitos e investigação digital.",
|
||||||
|
"Cursa Ciência da Computação na UFRPE e Blockchain e Criptografia Digital na FMU. Criador do Ciência Embarcada, autor de livros técnicos e de ferramentas livres de segurança.",
|
||||||
|
],
|
||||||
|
destaquesTitulo: "Reconhecimentos",
|
||||||
|
destaques: [
|
||||||
|
"Prêmio de Inovação RNP 2026",
|
||||||
|
"1º lugar no CyberGame do Hackers do Bem (CSBC 2025)",
|
||||||
|
"HackerOne, nível Signal Specialist",
|
||||||
|
"12º lugar civil na Operação Curupira 3.0",
|
||||||
|
],
|
||||||
|
siteAutor: "Site pessoal",
|
||||||
|
blog: "Visitar o Ciência Embarcada",
|
||||||
|
avisoTitulo: "Aviso legal",
|
||||||
|
aviso:
|
||||||
|
"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.",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
metaTitulo: "About the Project",
|
||||||
|
metaDesc:
|
||||||
|
"ESPDocs is an independent platform in six languages that brings together ESP32 documentation: series, comparator, chip selector, board catalog, diagnostics and glossary. Learn about the project and its author.",
|
||||||
|
keywords: ["ESPDocs", "ESP32 documentation", "open source ESP32 project"],
|
||||||
|
inicio: "Home",
|
||||||
|
trilha: "About",
|
||||||
|
tituloHero: "About ESPDocs",
|
||||||
|
label: "Independent project",
|
||||||
|
lede: "An unofficial platform in six languages that organizes technical information about the ESP32, helps you pick the right chip and links you to the official Espressif documentation. Built for the open hardware community.",
|
||||||
|
repo: "View repository",
|
||||||
|
missaoTitulo: "Mission",
|
||||||
|
missao:
|
||||||
|
"Make the ESP32 ecosystem easier to reach and understand: organize specifications, compare series, help choose the chip and the board, explain common errors and centralize links to the official documentation. The goal is to save you time in firmware and IoT development.",
|
||||||
|
porqueTitulo: "Why ESPDocs exists",
|
||||||
|
porque:
|
||||||
|
"Information about the ESP32 is often scattered across dense datasheets, Technical Reference Manuals (TRM) and secondary repositories. ESPDocs condenses and structures this data for rapid evaluation before you dive into the official engineering documentation.",
|
||||||
|
temTitulo: "What you will find here",
|
||||||
|
temLede: "The project started as a table of series and now brings together:",
|
||||||
|
ferramentas: [
|
||||||
|
["12 ESP32 series", "Specifications, interactive pinout diagrams and restricted-use pins, from the classic ESP32 to the ESP32-P4."],
|
||||||
|
["Comparator", "Up to four series side by side, with the differences highlighted."],
|
||||||
|
["Smart selector", "Say what the project needs and see which chips fit."],
|
||||||
|
["Board catalog", "DevKits with pins, filters and purchase links for your country."],
|
||||||
|
["Diagnostics", "Brownout, bootloop, watchdog and more: symptoms, causes and a log decoder."],
|
||||||
|
["Component Registry", "42 components and drivers from the official registry, with the idf_component.yml manifest ready."],
|
||||||
|
["Frameworks and SDKs", "14 frameworks, from ESP-IDF and Arduino to Rust and Matter, with code examples."],
|
||||||
|
["Technical glossary", "The terms that show up in datasheets, explained by category."],
|
||||||
|
["Smart search", "Ask “pin 32 esp32-s3” or “which ESP32 has PSRAM” and get a direct answer with a link to the source."],
|
||||||
|
],
|
||||||
|
idiomasTitulo: "Six languages",
|
||||||
|
idiomas:
|
||||||
|
"The whole site is available in Portuguese, English, Spanish, French, German and Simplified Chinese: interface, data, search and code examples with translated comments.",
|
||||||
|
contribuirTitulo: "How to contribute",
|
||||||
|
contribuir: "Translation fixes, table refinements and new code examples are welcome. The project grows with the community.",
|
||||||
|
passos: ["Fork the repository on Gitea.", "Implement improvements and verify official technical references.", "Open a Pull Request describing the changes."],
|
||||||
|
acessTitulo: "Accessibility",
|
||||||
|
acess:
|
||||||
|
"ESPDocs prioritizes readability, high contrast (WCAG AAA level), light and dark themes, and full keyboard navigation. If you encounter any accessibility or navigation hurdles, please open an issue in the repository.",
|
||||||
|
asideLabel: "Author and legal notice",
|
||||||
|
autorCargo: "Operations Technician at PoP-PE, RNP",
|
||||||
|
autorBio: [
|
||||||
|
"Full-stack JavaScript developer specialized in incident response and forensics, working between code, circuits and digital investigation.",
|
||||||
|
"Studying Computer Science at UFRPE and Blockchain and Digital Cryptography at FMU. Creator of Ciência Embarcada and author of free security tools.",
|
||||||
|
],
|
||||||
|
destaquesTitulo: "Recognition",
|
||||||
|
destaques: [
|
||||||
|
"RNP Innovation Award 2026",
|
||||||
|
"1st place at the Hackers do Bem CyberGame (CSBC 2025)",
|
||||||
|
"HackerOne, Signal Specialist level",
|
||||||
|
"12th place (civil) at Operação Curupira 3.0",
|
||||||
|
],
|
||||||
|
siteAutor: "Personal website",
|
||||||
|
blog: "Visit Ciência Embarcada",
|
||||||
|
avisoTitulo: "Legal notice",
|
||||||
|
aviso:
|
||||||
|
"ESPDocs is an aggregator and curation platform. Referenced manuals and repositories are property of Espressif Systems. In mission-critical or commercial designs, always consult official documentation.",
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
metaTitulo: "Acerca del proyecto",
|
||||||
|
metaDesc:
|
||||||
|
"ESPDocs es una plataforma independiente, en seis idiomas, que reúne la documentación del ecosistema ESP32: series, comparador, selector de chip, catálogo de placas, diagnóstico y glosario. Conozca el proyecto y a su autor.",
|
||||||
|
keywords: ["ESPDocs", "documentación ESP32", "proyecto open source ESP32", "código abierto"],
|
||||||
|
inicio: "Inicio",
|
||||||
|
trilha: "Acerca",
|
||||||
|
tituloHero: "Acerca de ESPDocs",
|
||||||
|
label: "Proyecto independiente",
|
||||||
|
lede: "Una plataforma no oficial, en seis idiomas, que organiza la información técnica del ESP32, le ayuda a elegir el chip adecuado y le conecta con la documentación oficial de Espressif. Creada para la comunidad de hardware abierto.",
|
||||||
|
repo: "Ver el repositorio",
|
||||||
|
missaoTitulo: "Misión",
|
||||||
|
missao:
|
||||||
|
"Facilitar el acceso y la comprensión del ecosistema ESP32: organizar especificaciones, comparar series, ayudar a elegir el chip y la placa, explicar errores comunes y centralizar los enlaces a la documentación oficial. El objetivo es ahorrar tiempo en el desarrollo de firmware e IoT.",
|
||||||
|
porqueTitulo: "¿Por qué existe ESPDocs?",
|
||||||
|
porque:
|
||||||
|
"La información sobre el ESP32 suele estar dispersa entre hojas de datos densas, manuales de referencia técnica (TRM) y repositorios secundarios. ESPDocs condensa y estructura estos datos para una evaluación rápida antes de profundizar en la ingeniería oficial.",
|
||||||
|
temTitulo: "Qué encontrará aquí",
|
||||||
|
temLede: "El proyecto empezó como una tabla de series y hoy reúne:",
|
||||||
|
ferramentas: [
|
||||||
|
["12 series ESP32", "Especificaciones, diagramas de pines interactivos y pines con restricciones de uso, del ESP32 clásico al ESP32-P4."],
|
||||||
|
["Comparador", "Hasta cuatro series lado a lado, con las diferencias resaltadas."],
|
||||||
|
["Selector inteligente", "Indique lo que necesita el proyecto y vea qué chips lo cumplen."],
|
||||||
|
["Catálogo de placas", "DevKits con pines, filtros y enlaces de compra para su país."],
|
||||||
|
["Diagnóstico", "Brownout, bootloop, watchdog y más: síntomas, causas y un decodificador de logs."],
|
||||||
|
["Component Registry", "42 componentes y drivers del registro oficial, con el manifest idf_component.yml listo."],
|
||||||
|
["Frameworks y SDKs", "14 frameworks, de ESP-IDF y Arduino a Rust y Matter, con ejemplos de código."],
|
||||||
|
["Glosario técnico", "Los términos que aparecen en las hojas de datos, explicados por categoría."],
|
||||||
|
["Búsqueda inteligente", "Pregunte “pin 32 esp32-s3” o “qué ESP32 tiene PSRAM” y reciba la respuesta directa, con enlace a la fuente."],
|
||||||
|
],
|
||||||
|
idiomasTitulo: "Seis idiomas",
|
||||||
|
idiomas:
|
||||||
|
"Todo el sitio está en portugués, inglés, español, francés, alemán y chino simplificado: interfaz, datos, búsqueda y ejemplos de código con comentarios traducidos.",
|
||||||
|
contribuirTitulo: "Cómo contribuir",
|
||||||
|
contribuir: "Las correcciones de traducción, ajustes en tablas y nuevos ejemplos de código son muy bienvenidos. El proyecto crece junto con la comunidad.",
|
||||||
|
passos: [
|
||||||
|
"Haga un fork del repositorio en Gitea.",
|
||||||
|
"Implemente las mejoras y verifique las referencias técnicas oficiales.",
|
||||||
|
"Abra una solicitud de extracción (Pull Request) describiendo los cambios realizados.",
|
||||||
|
],
|
||||||
|
acessTitulo: "Accesibilidad",
|
||||||
|
acess:
|
||||||
|
"ESPDocs prioriza la legibilidad, el alto contraste (nivel AAA de WCAG), los temas claro y oscuro y la navegación completa por teclado. Si encuentra alguna barrera de lectura o navegación, abra un issue en el repositorio.",
|
||||||
|
asideLabel: "Autor y aviso legal",
|
||||||
|
autorCargo: "Técnico de Operaciones del PoP-PE en la RNP",
|
||||||
|
autorBio: [
|
||||||
|
"Desarrollador full-stack JavaScript especializado en respuesta a incidentes y forense, entre código, circuitos e investigación digital.",
|
||||||
|
"Cursa Ciencias de la Computación en la UFRPE y Blockchain y Criptografía Digital en la FMU. Creador de Ciência Embarcada y autor de herramientas libres de seguridad.",
|
||||||
|
],
|
||||||
|
destaquesTitulo: "Reconocimientos",
|
||||||
|
destaques: [
|
||||||
|
"Premio de Innovación RNP 2026",
|
||||||
|
"1.er lugar en el CyberGame de Hackers do Bem (CSBC 2025)",
|
||||||
|
"HackerOne, nivel Signal Specialist",
|
||||||
|
"12.º lugar civil en la Operação Curupira 3.0",
|
||||||
|
],
|
||||||
|
siteAutor: "Sitio personal",
|
||||||
|
blog: "Visitar Ciência Embarcada",
|
||||||
|
avisoTitulo: "Aviso legal",
|
||||||
|
aviso:
|
||||||
|
"ESPDocs es un agregador y organizador. Los manuales y repositorios referenciados pertenecen a Espressif Systems. En proyectos críticos o comerciales, consulte siempre la documentación oficial.",
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
metaTitulo: "À propos du projet",
|
||||||
|
metaDesc:
|
||||||
|
"ESPDocs est une plateforme indépendante, en six langues, qui rassemble la documentation de l'écosystème ESP32 : séries, comparateur, sélecteur de puce, catalogue de cartes, diagnostic et glossaire. Découvrez le projet et son auteur.",
|
||||||
|
keywords: ["ESPDocs", "documentation ESP32", "projet open source ESP32", "open source"],
|
||||||
|
inicio: "Accueil",
|
||||||
|
trilha: "À propos",
|
||||||
|
tituloHero: "À propos d'ESPDocs",
|
||||||
|
label: "Projet indépendant",
|
||||||
|
lede: "Une plateforme non officielle, en six langues, qui organise les informations techniques de l'ESP32, vous aide à choisir la bonne puce et vous oriente vers la documentation officielle d'Espressif. Conçue pour la communauté du matériel libre.",
|
||||||
|
repo: "Voir le dépôt",
|
||||||
|
missaoTitulo: "Mission",
|
||||||
|
missao:
|
||||||
|
"Faciliter l'accès à l'écosystème ESP32 et sa compréhension : organiser les spécifications, comparer les séries, aider à choisir la puce et la carte, expliquer les erreurs courantes et centraliser les liens vers la documentation officielle. L'objectif est de vous faire gagner du temps dans le développement de firmware et d'IoT.",
|
||||||
|
porqueTitulo: "Pourquoi ESPDocs existe",
|
||||||
|
porque:
|
||||||
|
"Les informations sur l'ESP32 sont souvent dispersées entre des fiches techniques denses, des manuels de référence technique (TRM) et des dépôts secondaires. ESPDocs condense et structure ces données pour une évaluation rapide avant d'approfondir l'ingénierie officielle.",
|
||||||
|
temTitulo: "Ce que vous trouverez ici",
|
||||||
|
temLede: "Le projet a commencé comme un tableau de séries et rassemble aujourd'hui :",
|
||||||
|
ferramentas: [
|
||||||
|
["12 séries ESP32", "Spécifications, schémas de brochage interactifs et broches à usage restreint, de l'ESP32 classique à l'ESP32-P4."],
|
||||||
|
["Comparateur", "Jusqu'à quatre séries côte à côte, avec les différences mises en évidence."],
|
||||||
|
["Sélecteur intelligent", "Dites ce dont le projet a besoin et voyez quelles puces conviennent."],
|
||||||
|
["Catalogue de cartes", "DevKits avec broches, filtres et liens d'achat pour votre pays."],
|
||||||
|
["Diagnostic", "Brownout, bootloop, watchdog et plus : symptômes, causes et décodeur de logs."],
|
||||||
|
["Component Registry", "42 composants et pilotes du registre officiel, avec le manifest idf_component.yml prêt."],
|
||||||
|
["Frameworks et SDK", "14 frameworks, d'ESP-IDF et Arduino à Rust et Matter, avec des exemples de code."],
|
||||||
|
["Glossaire technique", "Les termes des fiches techniques, expliqués par catégorie."],
|
||||||
|
["Recherche intelligente", "Demandez « broche 32 esp32-s3 » ou « quel ESP32 a de la PSRAM » et obtenez la réponse directe, avec le lien vers la source."],
|
||||||
|
],
|
||||||
|
idiomasTitulo: "Six langues",
|
||||||
|
idiomas:
|
||||||
|
"Tout le site est disponible en portugais, anglais, espagnol, français, allemand et chinois simplifié : interface, données, recherche et exemples de code avec commentaires traduits.",
|
||||||
|
contribuirTitulo: "Comment contribuer",
|
||||||
|
contribuir: "Les corrections de traduction, les ajustements de tableaux et les nouveaux exemples de code sont très appréciés. Le projet grandit avec la communauté.",
|
||||||
|
passos: [
|
||||||
|
"Forkez le dépôt sur Gitea.",
|
||||||
|
"Apportez les améliorations et vérifiez les références techniques officielles.",
|
||||||
|
"Ouvrez une Pull Request décrivant les modifications apportées.",
|
||||||
|
],
|
||||||
|
acessTitulo: "Accessibilité",
|
||||||
|
acess:
|
||||||
|
"ESPDocs privilégie la lisibilité, un contraste élevé (niveau AAA du WCAG), les thèmes clair et sombre et une navigation complète au clavier. Si vous rencontrez un obstacle de lecture ou de navigation, ouvrez un ticket dans le dépôt.",
|
||||||
|
asideLabel: "Auteur et mentions légales",
|
||||||
|
autorCargo: "Technicien d'opérations du PoP-PE au RNP",
|
||||||
|
autorBio: [
|
||||||
|
"Développeur full-stack JavaScript spécialisé en réponse aux incidents et en forensique, entre code, circuits et investigation numérique.",
|
||||||
|
"Étudie l'informatique à l'UFRPE et la blockchain et la cryptographie numérique à la FMU. Créateur de Ciência Embarcada et auteur d'outils libres de sécurité.",
|
||||||
|
],
|
||||||
|
destaquesTitulo: "Distinctions",
|
||||||
|
destaques: [
|
||||||
|
"Prix de l'innovation RNP 2026",
|
||||||
|
"1re place au CyberGame de Hackers do Bem (CSBC 2025)",
|
||||||
|
"HackerOne, niveau Signal Specialist",
|
||||||
|
"12e place civile à l'Operação Curupira 3.0",
|
||||||
|
],
|
||||||
|
siteAutor: "Site personnel",
|
||||||
|
blog: "Visiter Ciência Embarcada",
|
||||||
|
avisoTitulo: "Mentions légales",
|
||||||
|
aviso:
|
||||||
|
"ESPDocs est une plateforme d'agrégation et de synthèse. Les manuels et dépôts référencés appartiennent à Espressif Systems. Pour les projets critiques ou commerciaux, consultez systématiquement la documentation officielle.",
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
metaTitulo: "Über das Projekt",
|
||||||
|
metaDesc:
|
||||||
|
"ESPDocs ist eine unabhängige Plattform in sechs Sprachen, die die Dokumentation des ESP32-Ökosystems bündelt: Serien, Vergleich, Chip-Auswahl, Board-Katalog, Diagnose und Glossar. Lernen Sie das Projekt und den Autor kennen.",
|
||||||
|
keywords: ["ESPDocs", "ESP32 Dokumentation", "ESP32 Open-Source-Projekt", "Open Source"],
|
||||||
|
inicio: "Startseite",
|
||||||
|
trilha: "Über uns",
|
||||||
|
tituloHero: "Über ESPDocs",
|
||||||
|
label: "Unabhängiges Projekt",
|
||||||
|
lede: "Eine inoffizielle Plattform in sechs Sprachen, die technische Informationen zum ESP32 strukturiert, bei der Wahl des passenden Chips hilft und Sie mit der offiziellen Dokumentation von Espressif verbindet. Entwickelt für die Open-Hardware-Community.",
|
||||||
|
repo: "Repository ansehen",
|
||||||
|
missaoTitulo: "Mission",
|
||||||
|
missao:
|
||||||
|
"Den Zugang zum ESP32-Ökosystem und das Verständnis dafür erleichtern: Spezifikationen bündeln, Serien vergleichen, bei der Wahl von Chip und Board helfen, häufige Fehler erklären und Links zur offiziellen Dokumentation zentralisieren. Ziel ist es, Entwicklungszeit bei Firmware und IoT-Projekten zu sparen.",
|
||||||
|
porqueTitulo: "Warum es ESPDocs gibt",
|
||||||
|
porque:
|
||||||
|
"Informationen über den ESP32 sind oft über dichte Datenblätter, technische Referenzhandbücher (TRM) und sekundäre Repositories verstreut. ESPDocs verdichtet und strukturiert diese Daten für eine schnelle Vorabauswahl, bevor Sie tiefer in die offizielle Technik einsteigen.",
|
||||||
|
temTitulo: "Was Sie hier finden",
|
||||||
|
temLede: "Das Projekt begann als Tabelle von Serien und umfasst heute:",
|
||||||
|
ferramentas: [
|
||||||
|
["12 ESP32-Serien", "Spezifikationen, interaktive Pinbelegungen und Pins mit Nutzungseinschränkungen, vom klassischen ESP32 bis zum ESP32-P4."],
|
||||||
|
["Vergleich", "Bis zu vier Serien nebeneinander, Unterschiede hervorgehoben."],
|
||||||
|
["Intelligente Auswahl", "Sagen Sie, was das Projekt braucht, und sehen Sie, welche Chips passen."],
|
||||||
|
["Board-Katalog", "DevKits mit Pins, Filtern und Kauflinks für Ihr Land."],
|
||||||
|
["Diagnose", "Brownout, Bootloop, Watchdog und mehr: Symptome, Ursachen und ein Log-Decoder."],
|
||||||
|
["Component Registry", "42 Komponenten und Treiber aus der offiziellen Registry, mit fertigem idf_component.yml-Manifest."],
|
||||||
|
["Frameworks und SDKs", "14 Frameworks, von ESP-IDF und Arduino bis Rust und Matter, mit Codebeispielen."],
|
||||||
|
["Technisches Glossar", "Die Begriffe aus den Datenblättern, nach Kategorie erklärt."],
|
||||||
|
["Intelligente Suche", "Fragen Sie „Pin 32 esp32-s3“ oder „welcher ESP32 hat PSRAM“ und erhalten Sie die direkte Antwort mit Link zur Quelle."],
|
||||||
|
],
|
||||||
|
idiomasTitulo: "Sechs Sprachen",
|
||||||
|
idiomas:
|
||||||
|
"Die gesamte Website gibt es auf Portugiesisch, Englisch, Spanisch, Französisch, Deutsch und vereinfachtem Chinesisch: Oberfläche, Daten, Suche und Codebeispiele mit übersetzten Kommentaren.",
|
||||||
|
contribuirTitulo: "Wie Sie beitragen können",
|
||||||
|
contribuir: "Übersetzungskorrekturen, Tabellenanpassungen und neue Codebeispiele sind herzlich willkommen. Das Projekt wächst gemeinsam mit der Community.",
|
||||||
|
passos: [
|
||||||
|
"Erstellen Sie einen Fork des Repositories auf Gitea.",
|
||||||
|
"Implementieren Sie die Verbesserungen und prüfen Sie die offiziellen technischen Referenzen.",
|
||||||
|
"Eröffnen Sie einen Pull Request mit einer Beschreibung der vorgenommenen Änderungen.",
|
||||||
|
],
|
||||||
|
acessTitulo: "Barrierefreiheit",
|
||||||
|
acess:
|
||||||
|
"ESPDocs legt großen Wert auf Lesbarkeit, hohen Kontrast (WCAG-Stufe AAA), helles und dunkles Design und vollständige Tastaturnavigation. Wenn Sie auf Lese- oder Navigationsbarrieren stoßen, eröffnen Sie bitte ein Issue im Repository.",
|
||||||
|
asideLabel: "Autor und rechtlicher Hinweis",
|
||||||
|
autorCargo: "Betriebstechniker des PoP-PE beim RNP",
|
||||||
|
autorBio: [
|
||||||
|
"Full-Stack-JavaScript-Entwickler mit Spezialisierung auf Incident Response und Forensik, zwischen Code, Schaltungen und digitaler Ermittlung.",
|
||||||
|
"Studiert Informatik an der UFRPE sowie Blockchain und digitale Kryptographie an der FMU. Gründer von Ciência Embarcada und Autor freier Sicherheitswerkzeuge.",
|
||||||
|
],
|
||||||
|
destaquesTitulo: "Auszeichnungen",
|
||||||
|
destaques: [
|
||||||
|
"RNP-Innovationspreis 2026",
|
||||||
|
"1. Platz beim CyberGame von Hackers do Bem (CSBC 2025)",
|
||||||
|
"HackerOne, Stufe Signal Specialist",
|
||||||
|
"12. Platz (zivil) bei der Operação Curupira 3.0",
|
||||||
|
],
|
||||||
|
siteAutor: "Persönliche Website",
|
||||||
|
blog: "Ciência Embarcada besuchen",
|
||||||
|
avisoTitulo: "Rechtlicher Hinweis",
|
||||||
|
aviso:
|
||||||
|
"ESPDocs ist ein Aggregator und Kurator. Die referenzierten Handbücher und Repositories sind Eigentum von Espressif Systems. Konsultieren Sie bei geschäftskritischen oder kommerziellen Projekten stets die offizielle Dokumentation.",
|
||||||
|
},
|
||||||
|
"zh-CN": {
|
||||||
|
metaTitulo: "关于本项目",
|
||||||
|
metaDesc: "ESPDocs 是一个独立平台,提供六种语言,汇集 ESP32 生态的文档:系列、对比、芯片选择、开发板目录、诊断与术语表。了解本项目及其作者。",
|
||||||
|
keywords: ["ESPDocs", "ESP32 文档", "开源 ESP32 项目"],
|
||||||
|
inicio: "首页",
|
||||||
|
trilha: "关于",
|
||||||
|
tituloHero: "关于 ESPDocs",
|
||||||
|
label: "独立项目",
|
||||||
|
lede: "一个非官方平台,提供六种语言,整理 ESP32 的技术信息,帮助你选出合适的芯片,并直接链接到乐鑫官方文档。为开源硬件社区而做。",
|
||||||
|
repo: "查看代码仓库",
|
||||||
|
missaoTitulo: "使命",
|
||||||
|
missao: "让 ESP32 生态更易获取、更易理解:整理规格、对比系列、帮助选择芯片和开发板、解释常见错误,并集中官方文档链接。目标是节省固件与物联网项目的开发时间。",
|
||||||
|
porqueTitulo: "ESPDocs 为什么存在",
|
||||||
|
porque: "ESP32 的信息常常分散在冗长的数据手册、技术参考手册(TRM)和零散的代码仓库中。ESPDocs 把这些数据浓缩并结构化,便于在深入工程文档之前快速评估。",
|
||||||
|
temTitulo: "你能在这里找到什么",
|
||||||
|
temLede: "项目最初只是一张系列对照表,如今汇集了:",
|
||||||
|
ferramentas: [
|
||||||
|
["12 个 ESP32 系列", "规格、交互式引脚图,以及有使用限制的引脚,从经典 ESP32 到 ESP32-P4。"],
|
||||||
|
["系列对比", "最多四个系列并排对比,差异一目了然。"],
|
||||||
|
["智能选型", "说出项目的需求,看看哪些芯片合适。"],
|
||||||
|
["开发板目录", "DevKit 的引脚、筛选条件,以及适用于你所在国家的购买链接。"],
|
||||||
|
["故障诊断", "Brownout、bootloop、看门狗等:症状、原因和日志解码器。"],
|
||||||
|
["Component Registry", "来自官方注册表的 42 个组件和驱动,附现成的 idf_component.yml。"],
|
||||||
|
["框架与 SDK", "14 个框架,从 ESP-IDF、Arduino 到 Rust 和 Matter,并附代码示例。"],
|
||||||
|
["技术术语表", "数据手册里的术语,按类别解释。"],
|
||||||
|
["智能搜索", "问“esp32-s3 32号引脚”或“哪款 ESP32 有 PSRAM”,直接得到答案,并附来源链接。"],
|
||||||
|
],
|
||||||
|
idiomasTitulo: "六种语言",
|
||||||
|
idiomas: "整个网站提供葡萄牙语、英语、西班牙语、法语、德语和简体中文:界面、数据、搜索,以及注释已翻译的代码示例。",
|
||||||
|
contribuirTitulo: "如何参与贡献",
|
||||||
|
contribuir: "欢迎提交翻译修正、表格改进和新的代码示例。项目与社区一同成长。",
|
||||||
|
passos: ["在 Gitea 上 Fork 本仓库。", "实现改进,并核对官方技术资料。", "发起 Pull Request 并说明改动。"],
|
||||||
|
acessTitulo: "无障碍",
|
||||||
|
acess: "ESPDocs 重视可读性、高对比度(WCAG AAA 级)、浅色与深色主题和完整的键盘导航。如果你遇到任何无障碍或导航上的障碍,请在代码仓库中提交 issue。",
|
||||||
|
asideLabel: "作者与法律声明",
|
||||||
|
autorCargo: "RNP PoP-PE 运维技术员",
|
||||||
|
autorBio: [
|
||||||
|
"全栈 JavaScript 开发者,专注于事件响应与取证,穿梭于代码、电路与数字调查之间。",
|
||||||
|
"在 UFRPE 就读计算机科学,在 FMU 学习区块链与数字密码学。Ciência Embarcada 创始人,开源安全工具作者。",
|
||||||
|
],
|
||||||
|
destaquesTitulo: "荣誉",
|
||||||
|
destaques: ["2026 年 RNP 创新奖", "Hackers do Bem CyberGame 第 1 名(CSBC 2025)", "HackerOne Signal Specialist 等级", "Operação Curupira 3.0 平民组第 12 名"],
|
||||||
|
siteAutor: "个人网站",
|
||||||
|
blog: "访问 Ciência Embarcada",
|
||||||
|
avisoTitulo: "法律声明",
|
||||||
|
aviso: "ESPDocs 是一个聚合与整理平台。所引用的手册和代码仓库归乐鑫系统(Espressif Systems)所有。在关键任务或商业设计中,请始终查阅官方文档。",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export async function generateMetadata({ params }) {
|
export async function generateMetadata({ params }) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const isEn = locale === "en";
|
const t = T[locale] ?? T.pt;
|
||||||
const isEs = locale === "es";
|
|
||||||
const isFr = locale === "fr";
|
|
||||||
const isDe = locale === "de";
|
|
||||||
const isZh = locale === "zh-CN";
|
|
||||||
return paginaMeta({
|
return paginaMeta({
|
||||||
titulo: isZh ? "关于本项目" : isDe
|
titulo: t.metaTitulo,
|
||||||
? "Über das Projekt"
|
descricao: t.metaDesc,
|
||||||
: isFr
|
|
||||||
? "À propos du projet"
|
|
||||||
: isEs
|
|
||||||
? "Acerca del proyecto"
|
|
||||||
: isEn
|
|
||||||
? "About the Project"
|
|
||||||
: "Sobre o projeto",
|
|
||||||
descricao: isZh ? "ESPDocs 是一个独立平台,整理 ESP32 生态的技术文档。了解本项目以及如何参与贡献。" : isDe
|
|
||||||
? "ESPDocs ist eine unabhängige Plattform, die die technische Dokumentation des ESP32-Ökosystems strukturiert. Erfahren Sie mehr über das Projekt und wie Sie beitragen können."
|
|
||||||
: isFr
|
|
||||||
? "ESPDocs est une plateforme indépendante qui organise la documentation technique de l'écosystème ESP32. Découvrez le projet et comment contribuer."
|
|
||||||
: isEs
|
|
||||||
? "ESPDocs es una plataforma independiente que organiza la documentación técnica del ecosistema ESP32. Conozca el proyecto y cómo contribuir."
|
|
||||||
: isEn
|
|
||||||
? "ESPDocs is an independent platform organizing technical documentation of the ESP32 ecosystem. Learn about the project and how to contribute."
|
|
||||||
: "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",
|
caminho: "/sobre",
|
||||||
keywords: isZh ? ["ESPDocs", "ESP32 文档", "开源 ESP32 项目"] : isDe
|
keywords: t.keywords,
|
||||||
? ["ESPDocs", "ESP32 Dokumentation", "ESP32 Open-Source-Projekt", "Open Source"]
|
|
||||||
: isFr
|
|
||||||
? ["ESPDocs", "documentation ESP32", "projet open source ESP32", "open source"]
|
|
||||||
: isEs
|
|
||||||
? ["ESPDocs", "documentación ESP32", "proyecto open source ESP32", "código abierto"]
|
|
||||||
: isEn
|
|
||||||
? ["ESPDocs", "ESP32 documentation", "open source ESP32 project"]
|
|
||||||
: ["ESPDocs", "documentação ESP32 português", "projeto open source ESP32"],
|
|
||||||
locale,
|
locale,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Sobre({ params }) {
|
export default async function Sobre({ params }) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
|
const t = T[locale] ?? T.pt;
|
||||||
const isPt = locale === "pt";
|
const isPt = locale === "pt";
|
||||||
const isEn = locale === "en";
|
const isEn = locale === "en";
|
||||||
const isEs = locale === "es";
|
const isEs = locale === "es";
|
||||||
@@ -57,63 +418,26 @@ export default async function Sobre({ params }) {
|
|||||||
const isDe = locale === "de";
|
const isDe = locale === "de";
|
||||||
const isZh = locale === "zh-CN";
|
const isZh = locale === "zh-CN";
|
||||||
|
|
||||||
const featuresPt = [
|
const getLinkHref = (href) => getLocalizedHref(`/${locale}${href}`, locale);
|
||||||
{ icon: Languages, title: "Em português", desc: "Dados técnicos organizados e apresentados em português do Brasil." },
|
const homePath = getLinkHref("/");
|
||||||
{ icon: GitCompare, title: "Comparações visuais", desc: "Tabelas e diagramas para escolher a série certa sem abrir dez datasheets." },
|
const sobrePath = getLinkHref("/sobre");
|
||||||
{ 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." },
|
|
||||||
];
|
|
||||||
|
|
||||||
const featuresEn = [
|
// A lista de textos do inglês ao chinês não traz "O livro": ele só existe em pt.
|
||||||
{ icon: Languages, title: "Curated Content", desc: "Structured technical data presented clearly without datasheet noise." },
|
const ferramentas = FERRAMENTAS.filter((f) => isPt || !f.soPt).map((f, i) => ({ ...f, titulo: t.ferramentas[i][0], desc: t.ferramentas[i][1] }));
|
||||||
{ icon: GitCompare, title: "Visual comparisons", desc: "Tables and diagrams to select the right series without opening ten datasheets." },
|
|
||||||
{ icon: Globe, title: "Centralized access", desc: "Direct links to official documentation, repositories and examples." },
|
|
||||||
{ icon: Code, title: "Open source", desc: "Contributions are welcome on Gitea." },
|
|
||||||
];
|
|
||||||
|
|
||||||
const featuresEs = [
|
const nomesIdiomas = { pt: "Português", en: "English", es: "Español", fr: "Français", de: "Deutsch", "zh-CN": "简体中文" };
|
||||||
{ icon: Languages, title: "En español", desc: "Datos técnicos organizados y presentados de forma clara en español neutro." },
|
|
||||||
{ icon: GitCompare, title: "Comparaciones visuales", desc: "Tablas y diagramas para elegir la serie adecuada sin abrir diez hojas de datos." },
|
|
||||||
{ icon: Globe, title: "Acceso centralizado", desc: "Enlaces directos a la documentación oficial, repositorios y ejemplos." },
|
|
||||||
{ icon: Code, title: "Código abierto", desc: "Las contribuciones son bienvenidas en Gitea." },
|
|
||||||
];
|
|
||||||
|
|
||||||
const featuresFr = [
|
|
||||||
{ icon: Languages, title: "En français", desc: "Données techniques organisées et présentées clairement en français." },
|
|
||||||
{ icon: GitCompare, title: "Comparaisons visuelles", desc: "Tableaux et schémas pour choisir la bonne série sans ouvrir dix datasheets." },
|
|
||||||
{ icon: Globe, title: "Accès centralisé", desc: "Liens directs vers la documentation officielle, les dépôts et les exemples." },
|
|
||||||
{ icon: Code, title: "Open source", desc: "Les contributions sont les bienvenues sur Gitea." },
|
|
||||||
];
|
|
||||||
|
|
||||||
const featuresDe = [
|
|
||||||
{ icon: Languages, title: "Auf Deutsch", desc: "Technische Daten übersichtlich und strukturiert auf Deutsch aufbereitet." },
|
|
||||||
{ icon: GitCompare, title: "Visuelle Vergleiche", desc: "Tabellen und Diagramme zur Auswahl der richtigen Serie, ohne zehn Datenblätter zu öffnen." },
|
|
||||||
{ icon: Globe, title: "Zentraler Zugang", desc: "Direkte Links zu offizieller Dokumentation, Repositories und Beispielen." },
|
|
||||||
{ icon: Code, title: "Open Source", desc: "Beiträge sind auf Gitea herzlich willkommen." },
|
|
||||||
];
|
|
||||||
const featuresZh = [
|
|
||||||
{ icon: Languages, title: "精心整理的内容", desc: "结构化的技术数据,清晰呈现,没有数据手册的冗余。" },
|
|
||||||
{ icon: GitCompare, title: "可视化对比", desc: "用表格和图示选出合适的系列,无需翻阅十份数据手册。" },
|
|
||||||
{ icon: Globe, title: "集中入口", desc: "直达官方文档、代码仓库与示例的链接。" },
|
|
||||||
{ icon: Code, title: "开源", desc: "欢迎在 Gitea 上贡献。" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const features = isZh ? featuresZh : isDe ? featuresDe : isFr ? featuresFr : isEs ? featuresEs : isEn ? featuresEn : featuresPt;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="site-canvas min-h-screen">
|
<div className="site-canvas min-h-screen">
|
||||||
{/* Metadados E-E-A-T de autoria e organização para o Google */}
|
{/* Metadados E-E-A-T de autoria e organização para o Google */}
|
||||||
<script
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.paginaSobre(locale)) }} />
|
||||||
type="application/ld+json"
|
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd.paginaSobre(locale)) }}
|
|
||||||
/>
|
|
||||||
<script
|
<script
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: JSON.stringify(
|
__html: JSON.stringify(
|
||||||
jsonLd.trilha([
|
jsonLd.trilha([
|
||||||
{ nome: isZh ? "首页" : isDe ? "Startseite" : isFr ? "Accueil" : isEs ? "Inicio" : isPt ? "Início" : "Home", caminho: isZh ? "/zh-CN" : isDe ? "/de" : isFr ? "/fr" : isEn ? "/en" : isEs ? "/es" : "/" },
|
{ nome: t.inicio, caminho: homePath },
|
||||||
{ nome: isZh ? "关于本项目" : isDe ? "Über das Projekt" : isFr ? "À propos du projet" : isEs ? "Acerca del proyecto" : isPt ? "Sobre o Projeto" : "About the Project", caminho: isZh ? "/zh-CN/about" : isDe ? "/de/ueber-uns" : isFr ? "/fr/a-propos" : isEn ? "/en/about" : isEs ? "/es/acerca" : "/sobre" },
|
{ nome: t.metaTitulo, caminho: sobrePath },
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
@@ -126,26 +450,17 @@ export default async function Sobre({ params }) {
|
|||||||
isEn={isEn}
|
isEn={isEn}
|
||||||
isEs={isEs}
|
isEs={isEs}
|
||||||
isFr={isFr}
|
isFr={isFr}
|
||||||
isDe={isDe} isZh={isZh}
|
isDe={isDe}
|
||||||
trail={[{ label: isZh ? "关于" : isDe ? "Über uns" : isFr ? "À propos" : isEs ? "Acerca" : isPt ? "Sobre" : "About" }]}
|
isZh={isZh}
|
||||||
|
trail={[{ label: t.trilha }]}
|
||||||
icon={Info}
|
icon={Info}
|
||||||
label={isZh ? "独立项目" : isDe ? "Unabhängiges Projekt" : isFr ? "Projet indépendant" : isEs ? "Proyecto independiente" : isPt ? "Projeto independente" : "Independent project"}
|
label={t.label}
|
||||||
title={isZh ? "关于 ESPDocs" : isDe ? "Über ESPDocs" : isFr ? "À propos d'ESPDocs" : isEs ? "Acerca de ESPDocs" : isPt ? "Sobre o ESPDocs" : "About ESPDocs"}
|
title={t.tituloHero}
|
||||||
lede={
|
lede={t.lede}
|
||||||
isZh ? "一个非官方平台,整理 ESP32 的技术信息,并直接链接到乐鑫官方文档。为开源硬件社区而做。" : isDe
|
|
||||||
? "Eine inoffizielle deutschsprachige Plattform, die technische Informationen zum ESP32 strukturiert und Sie mit der offiziellen Dokumentation von Espressif verbindet. Entwickelt für die Open-Hardware-Community."
|
|
||||||
: isFr
|
|
||||||
? "Une plateforme non officielle en français qui organise les informations techniques de l'ESP32 et vous oriente vers la documentation officielle d'Espressif. Conçue pour la communauté du matériel libre."
|
|
||||||
: isEs
|
|
||||||
? "Una plataforma no oficial, en español, que organiza la información técnica del ESP32 y le conecta con la documentación oficial de Espressif. Creada para la comunidad de hardware abierto."
|
|
||||||
: isPt
|
|
||||||
? "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."
|
|
||||||
: "An unofficial platform organizing technical information about the ESP32 and linking directly to official Espressif documentation. Built for the open hardware community."
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<a href={REPOSITORIO} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
|
<a href={REPOSITORIO} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
|
||||||
<GiteaMark className="h-4 w-4" />
|
<GiteaMark className="h-4 w-4" />
|
||||||
{isZh ? "查看代码仓库" : isDe ? "Repository ansehen" : isFr ? "Voir le dépôt" : isEs ? "Ver el repositorio" : isPt ? "Ver o repositório" : "View repository"}
|
{t.repo}
|
||||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||||
</a>
|
</a>
|
||||||
</PageHero>
|
</PageHero>
|
||||||
@@ -153,92 +468,71 @@ export default async function Sobre({ params }) {
|
|||||||
<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="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">
|
<div className="min-w-0 max-w-3xl space-y-12">
|
||||||
<section aria-labelledby="missao">
|
<section aria-labelledby="missao">
|
||||||
<h2 id="missao" className="section-title">{isZh ? "使命" : isDe ? "Mission" : isFr ? "Mission" : isEs ? "Misión" : isPt ? "Missão" : "Mission"}</h2>
|
<h2 id="missao" className="section-title">{t.missaoTitulo}</h2>
|
||||||
<p className="mt-4 text-base leading-relaxed text-muted">
|
<p className="mt-4 text-base leading-relaxed text-muted">{t.missao}</p>
|
||||||
{isZh ? "通过一个整理规格、对比系列、介绍框架并集中官方文档链接的界面,让 ESP32 生态更易获取、更易理解。目标是节省固件与物联网项目的开发时间。" : isDe
|
|
||||||
? "Den Zugang zum und das Verständnis für das ESP32-Ökosystem durch eine deutsche Oberfläche erleichtern, die Spezifikationen bündelt, Serien vergleicht, Frameworks vorstellt und offizielle Dokumentationslinks zentralisiert. Ziel ist es, Entwicklungszeit bei Firmware und IoT-Projekten zu sparen."
|
|
||||||
: isFr
|
|
||||||
? "Faciliter l'accès et la compréhension de l'écosystème ESP32 avec une interface en français qui organise les spécifications, compare les séries, présente les frameworks et centralise les liens vers la documentation officielle. L'objectif est de vous faire gagner du temps dans le développement de firmware et d'IoT."
|
|
||||||
: isEs
|
|
||||||
? "Facilitar el acceso y la comprensión del ecosistema ESP32 con una interfaz en español que organiza especificaciones, compara series, presenta frameworks y centraliza los enlaces a la documentación oficial. El objetivo es ahorrar tiempo en el desarrollo de firmware e IoT."
|
|
||||||
: isPt
|
|
||||||
? "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."
|
|
||||||
: "Facilitate access and understanding of the ESP32 ecosystem with an interface organizing specifications, comparing series, presenting frameworks and centralizing links to official documentation. The goal is to save development time in firmware and IoT projects."}
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section aria-labelledby="porque">
|
<section aria-labelledby="porque">
|
||||||
<h2 id="porque" className="section-title">{isZh ? "ESPDocs 为什么存在" : isDe ? "Warum es ESPDocs gibt" : isFr ? "Pourquoi ESPDocs existe" : isEs ? "¿Por qué existe ESPDocs?" : isPt ? "Por que o ESPDocs existe" : "Why ESPDocs exists"}</h2>
|
<h2 id="porque" className="section-title">{t.porqueTitulo}</h2>
|
||||||
<p className="mt-4 text-base leading-relaxed text-muted">
|
<p className="mt-4 text-base leading-relaxed text-muted">{t.porque}</p>
|
||||||
{isZh ? "ESP32 的信息常常分散在冗长的数据手册、技术参考手册(TRM)和零散的代码仓库中。ESPDocs 把这些数据浓缩并结构化,便于在深入工程文档之前快速评估。" : isDe
|
</section>
|
||||||
? "Informationen über den ESP32 sind oft über dichte Datenblätter, technische Referenzhandbücher (TRM) und sekundäre Repositories verstreut. ESPDocs verdichtet und strukturiert diese Daten für eine schnelle Vorabauswahl, bevor Sie tiefer in die offizielle Technik einsteigen."
|
|
||||||
: isFr
|
<section aria-labelledby="tem">
|
||||||
? "Les informations sur l'ESP32 sont souvent dispersées entre des fiches techniques denses, des manuels de référence technique (TRM) et des dépôts secondaires. ESPDocs condense et structure ces données pour une évaluation rapide avant d'approfondir l'ingénierie officielle."
|
<h2 id="tem" className="section-title">{t.temTitulo}</h2>
|
||||||
: isEs
|
<p className="mt-4 text-base leading-relaxed text-muted">{t.temLede}</p>
|
||||||
? "La información sobre el ESP32 suele estar dispersa entre hojas de datos densas, manuales de referencia técnica (TRM) y repositorios secundarios. ESPDocs condensa y estructura estos datos para una evaluación rápida antes de profundizar en la ingeniería oficial."
|
|
||||||
: isPt
|
|
||||||
? "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."
|
|
||||||
: "Information regarding the ESP32 is often scattered across dense datasheets, Technical Reference Manuals (TRM), and secondary repositories. ESPDocs condenses and structures this data for rapid evaluation before diving into in-depth engineering documentation."}
|
|
||||||
</p>
|
|
||||||
<ul className="mt-6 grid gap-3 sm:grid-cols-2">
|
<ul className="mt-6 grid gap-3 sm:grid-cols-2">
|
||||||
{features.map(({ icon: Icon, title, desc }) => (
|
{ferramentas.map(({ icon: Icon, href, titulo, desc }) => {
|
||||||
<li key={title} className="card flex items-start gap-4 p-5">
|
const conteudo = (
|
||||||
<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 className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-accent-soft text-accent">
|
||||||
</span>
|
<Icon className="h-5 w-5" aria-hidden="true" />
|
||||||
<div>
|
</span>
|
||||||
<h3 className="font-semibold text-ink">{title}</h3>
|
<div>
|
||||||
<p className="mt-1 text-sm leading-relaxed text-muted">{desc}</p>
|
<h3 className="font-semibold text-ink">{titulo}</h3>
|
||||||
</div>
|
<p className="mt-1 text-sm leading-relaxed text-muted">{desc}</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<li key={titulo}>
|
||||||
|
{href ? (
|
||||||
|
<Link href={getLinkHref(href)} className="card flex h-full items-start gap-4 p-5">
|
||||||
|
{conteudo}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div className="card flex h-full items-start gap-4 p-5">{conteudo}</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section aria-labelledby="idiomas">
|
||||||
|
<h2 id="idiomas" className="section-title">{t.idiomasTitulo}</h2>
|
||||||
|
<p className="mt-4 text-base leading-relaxed text-muted">{t.idiomas}</p>
|
||||||
|
<ul className="mt-5 flex flex-wrap gap-2">
|
||||||
|
{LANGS.map((l) => (
|
||||||
|
<li key={l}>
|
||||||
|
<Link
|
||||||
|
href={getLocalizedHref(`/${locale}/sobre`, l)}
|
||||||
|
hrefLang={l}
|
||||||
|
lang={l}
|
||||||
|
className={`btn btn-sm ${l === locale ? "btn-primary" : "btn-ghost"}`}
|
||||||
|
aria-current={l === locale ? "true" : undefined}
|
||||||
|
>
|
||||||
|
{nomesIdiomas[l]}
|
||||||
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section aria-labelledby="contribuir">
|
<section aria-labelledby="contribuir">
|
||||||
<h2 id="contribuir" className="section-title">{isZh ? "如何参与贡献" : isDe ? "Wie Sie beitragen können" : isFr ? "Comment contribuer" : isEs ? "Cómo contribuir" : isPt ? "Como contribuir" : "How to contribute"}</h2>
|
<h2 id="contribuir" className="section-title">{t.contribuirTitulo}</h2>
|
||||||
<p className="mt-4 text-base leading-relaxed text-muted">
|
<p className="mt-4 text-base leading-relaxed text-muted">{t.contribuir}</p>
|
||||||
{isZh ? "欢迎提交翻译修正、表格改进和新的代码示例。项目与社区一同成长。" : isDe
|
|
||||||
? "Übersetzungskorrekturen, Tabellenanpassungen und neue Codebeispiele sind herzlich willkommen. Das Projekt wächst gemeinsam mit der Community."
|
|
||||||
: isFr
|
|
||||||
? "Les corrections de traduction, les ajustements de tableaux et les nouveaux exemples de code sont très appréciés. Le projet grandit avec la communauté."
|
|
||||||
: isEs
|
|
||||||
? "Las correcciones de traducción, ajustes en tablas y nuevos ejemplos de código son muy bienvenidos. El proyecto crece junto con la comunidad."
|
|
||||||
: isPt
|
|
||||||
? "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."
|
|
||||||
: "Translation fixes, table refinements and new code examples are welcome. The project grows with the community."}
|
|
||||||
</p>
|
|
||||||
<ol className="mt-6 space-y-3">
|
<ol className="mt-6 space-y-3">
|
||||||
{(isZh ? ["在 Gitea 上 Fork 本仓库。", "实现改进,并核对官方技术资料。", "发起 Pull Request 并说明改动。"] : isDe
|
{t.passos.map((passo, i) => (
|
||||||
? [
|
|
||||||
"Erstellen Sie einen Fork des Repositories auf Gitea.",
|
|
||||||
"Implementieren Sie die Verbesserungen und prüfen Sie die offiziellen technischen Referenzen.",
|
|
||||||
"Eröffnen Sie einen Pull Request mit einer Beschreibung der vorgenommenen Änderungen.",
|
|
||||||
]
|
|
||||||
: isFr
|
|
||||||
? [
|
|
||||||
"Forkez le dépôt sur Gitea.",
|
|
||||||
"Apportez les améliorations et vérifiez les références techniques officielles.",
|
|
||||||
"Ouvrez une Pull Request décrivant les modifications apportées.",
|
|
||||||
]
|
|
||||||
: isEs
|
|
||||||
? [
|
|
||||||
"Haga un fork del repositorio en Gitea.",
|
|
||||||
"Implemente las mejoras y verifique las referencias técnicas oficiales.",
|
|
||||||
"Abra una solicitud de extracción (Pull Request) describiendo los cambios realizados.",
|
|
||||||
]
|
|
||||||
: isPt
|
|
||||||
? [
|
|
||||||
"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.",
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
"Fork the repository on Gitea.",
|
|
||||||
"Implement improvements and verify official technical references.",
|
|
||||||
"Open a Pull Request describing the changes.",
|
|
||||||
]
|
|
||||||
).map((passo, i) => (
|
|
||||||
<li key={passo} className="card flex items-center gap-4 px-5 py-4">
|
<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">
|
<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}
|
{i + 1}
|
||||||
@@ -250,110 +544,46 @@ export default async function Sobre({ params }) {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section aria-labelledby="acessibilidade">
|
<section aria-labelledby="acessibilidade">
|
||||||
<h2 id="acessibilidade" className="section-title">{isZh ? "无障碍" : isDe ? "Barrierefreiheit" : isFr ? "Accessibilité" : isEs ? "Accesibilidad" : isPt ? "Acessibilidade" : "Accessibility"}</h2>
|
<h2 id="acessibilidade" className="section-title">{t.acessTitulo}</h2>
|
||||||
<p className="mt-4 text-base leading-relaxed text-muted">
|
<p className="mt-4 text-base leading-relaxed text-muted">{t.acess}</p>
|
||||||
{isZh ? "ESPDocs 重视可读性、高对比度(WCAG AAA 级)和完整的键盘导航。如果你遇到任何无障碍或导航上的障碍,请在代码仓库中提交 issue。" : isDe
|
|
||||||
? "ESPDocs legt großen Wert auf Lesbarkeit, hohen Kontrast (WCAG-Stufe AAA) und vollständige Tastaturnavigation. Wenn Sie auf Lese- oder Navigationsbarrieren stoßen, eröffnen Sie bitte ein Issue im Repository."
|
|
||||||
: isFr
|
|
||||||
? "ESPDocs privilégie la lisibilité, un contraste élevé (niveau AAA du WCAG) et une navigation complète au clavier. Si vous rencontrez un obstacle de lecture ou de navigation, ouvrez un ticket dans le dépôt."
|
|
||||||
: isEs
|
|
||||||
? "ESPDocs prioriza la legibilidad, el alto contraste (nivel AAA de WCAG) y la navegación completa por teclado. Si encuentra alguna barrera de lectura o navegación, abra un issue en el repositorio."
|
|
||||||
: isPt
|
|
||||||
? "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."
|
|
||||||
: "ESPDocs prioritizes readability, high contrast (WCAG AAA level), and full keyboard navigation. If you encounter any accessibility or navigation hurdles, please open an issue in the repository."}
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside className="space-y-5 lg:sticky lg:top-28 lg:self-start" aria-label={isZh ? "作者与法律声明" : isDe ? "Autor und rechtlicher Hinweis" : isFr ? "Auteur et mentions légales" : isEs ? "Autor y aviso legal" : isPt ? "Autor e aviso legal" : "Author and legal notice"}>
|
<aside className="space-y-5 lg:sticky lg:top-28 lg:self-start" aria-label={t.asideLabel}>
|
||||||
<div className="panel p-6 sm:p-7">
|
<div className="panel p-6 sm:p-7">
|
||||||
<div className="flex items-center gap-4">
|
<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="/autor.jpg" alt="" aria-hidden="true" width={64} height={64} className="h-16 w-16 shrink-0 rounded-2xl border border-line object-cover" />
|
||||||
<Image src="/logo.png" alt="" aria-hidden="true" width={48} height={48} className="h-12 w-12 object-contain" />
|
|
||||||
</span>
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-display text-lg font-semibold text-ink">Lucas Rayan Guerra</h2>
|
<h2 className="font-display text-lg font-semibold text-ink">{SITE.autor}</h2>
|
||||||
<p className="text-sm text-muted">{isZh ? "Ciência Embarcada 创始人" : isDe ? "Gründer von Ciência Embarcada" : isFr ? "Créateur de Ciência Embarcada" : isEs ? "Creador de Ciência Embarcada" : isPt ? "Criador do Ciência Embarcada" : "Creator of Ciência Embarcada"}</p>
|
<p className="text-sm text-muted">{t.autorCargo}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 space-y-3 text-sm leading-relaxed text-muted">
|
<div className="mt-5 space-y-3 text-sm leading-relaxed text-muted">
|
||||||
{isZh ? (
|
{t.autorBio.map((p) => (
|
||||||
<>
|
<p key={p}>{p}</p>
|
||||||
<p>
|
))}
|
||||||
系统开发技术员,UFRPE 计算机科学在读,区块链与密码学研究者。
|
</div>
|
||||||
</p>
|
<h3 className="mt-5 text-sm font-semibold text-ink">{t.destaquesTitulo}</h3>
|
||||||
<p>
|
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm leading-relaxed text-muted">
|
||||||
Semine AgriTech 软件总监,并在 REC'n'Play 等活动中作演讲,分享硬件、物联网和网络安全方面的见解。
|
{t.destaques.map((d) => (
|
||||||
</p>
|
<li key={d}>{d}</li>
|
||||||
</>
|
))}
|
||||||
) : isDe ? (
|
</ul>
|
||||||
<>
|
<div className="mt-6 grid gap-2">
|
||||||
<p>
|
<a href={SITE.autorUrl} target="_blank" rel="noopener noreferrer" className="btn btn-primary w-full">
|
||||||
Systementwicklungstechniker, Informatikstudent an der UFRPE und Forscher im Bereich Blockchain und Kryptographie.
|
{t.siteAutor}
|
||||||
</p>
|
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||||
<p>
|
</a>
|
||||||
Software-Direktor bei Semine AgriTech und Referent bei Veranstaltungen wie REC'n'Play, der Inhalte zu Hardware, IoT und Cybersicherheit vermittelt.
|
<a href={SITE.blog} target="_blank" rel="noopener noreferrer" className="btn btn-ghost w-full">
|
||||||
</p>
|
{t.blog}
|
||||||
</>
|
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||||
) : isFr ? (
|
</a>
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
Technicien en développement de systèmes, étudiant en informatique à l'UFRPE et chercheur en blockchain et cryptographie.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Directeur logiciel chez Semine AgriTech et conférencier lors d'événements comme REC'n'Play, partageant des connaissances sur le matériel, l'IoT et la cybersécurité.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : isEs ? (
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
Técnico en Desarrollo de Sistemas, estudiante de Ciencias de la Computación en la UFRPE e investigador en Blockchain y Criptografía.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Director de Software en Semine AgriTech y ponente en eventos como REC'n'Play, compartiendo contenido sobre hardware, IoT y ciberseguridad.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : isPt ? (
|
|
||||||
<>
|
|
||||||
<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'n'Play, compartilha
|
|
||||||
conteúdo sobre hardware, IoT e cibersegurança.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
Systems development technician, Computer Science student at UFRPE, and researcher in Blockchain and Cryptography.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Software Director at Semine AgriTech and speaker at events like REC'n'Play, sharing insights on hardware, IoT, and cybersecurity.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<a href="https://cienciaembarcada.com.br" target="_blank" rel="noopener noreferrer" className="btn btn-ghost mt-6 w-full">
|
|
||||||
{isZh ? "访问 Ciência Embarcada" : isDe ? "Ciência Embarcada besuchen" : isFr ? "Visiter Ciência Embarcada" : isEs ? "Visitar Ciência Embarcada" : isPt ? "Visitar o Ciência Embarcada" : "Visit Ciência Embarcada"}
|
|
||||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="callout callout-warn">
|
<div className="callout callout-warn">
|
||||||
<p className="font-semibold text-ink">{isZh ? "法律声明" : isDe ? "Rechtlicher Hinweis" : isFr ? "Mentions légales" : isEs ? "Aviso legal" : isPt ? "Aviso legal" : "Legal notice"}</p>
|
<p className="font-semibold text-ink">{t.avisoTitulo}</p>
|
||||||
<p className="mt-1">
|
<p className="mt-1">{t.aviso}</p>
|
||||||
{isZh ? "ESPDocs 是一个聚合与整理平台。所引用的手册和代码仓库归乐鑫系统(Espressif Systems)所有。在关键任务或商业设计中,请始终查阅官方文档。" : isDe
|
|
||||||
? "ESPDocs ist ein Aggregator und Kurator. Die referenzierten Handbücher und Repositories sind Eigentum von Espressif Systems. Konsultieren Sie bei geschäftskritischen oder kommerziellen Projekten stets die offizielle Dokumentation."
|
|
||||||
: isFr
|
|
||||||
? "ESPDocs est une plateforme d'agrégation et de synthèse. Les manuels et dépôts référencés appartiennent à Espressif Systems. Pour les projets critiques ou commerciaux, consultez systématiquement la documentation officielle."
|
|
||||||
: isEs
|
|
||||||
? "ESPDocs es un agregador y organizador. Los manuales y repositorios referenciados pertenecen a Espressif Systems. En proyectos críticos o comerciales, consulte siempre la documentación oficial."
|
|
||||||
: isPt
|
|
||||||
? "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."
|
|
||||||
: "ESPDocs is an aggregator and curation platform. Referenced manuals and repositories are property of Espressif Systems. In mission-critical or commercial designs, always consult official documentation."}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function GET() {
|
|||||||
|
|
||||||
const response = await sheets.spreadsheets.values.get({
|
const response = await sheets.spreadsheets.values.get({
|
||||||
spreadsheetId,
|
spreadsheetId,
|
||||||
range: 'A1:Z1000',
|
range: 'A1:AZ1000',
|
||||||
key: process.env.GOOGLE_SHEETS_API_KEY,
|
key: process.env.GOOGLE_SHEETS_API_KEY,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -644,6 +644,7 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
|
white-space: nowrap;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.2s, background-color 0.2s;
|
transition: color 0.2s, background-color 0.2s;
|
||||||
@@ -752,6 +753,32 @@
|
|||||||
from { opacity: 0; transform: translateY(16px); }
|
from { opacity: 0; transform: translateY(16px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Faixa de placas sugeridas: entra suave e desliza na direção da seta usada.
|
||||||
|
"Reduzir movimento" já zera as durações na regra global acima. */
|
||||||
|
.sugestao-faixa {
|
||||||
|
animation: sugestao-entrada 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sugestao-proxima {
|
||||||
|
animation: sugestao-proxima 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sugestao-anterior {
|
||||||
|
animation: sugestao-anterior 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sugestao-entrada {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sugestao-proxima {
|
||||||
|
from { opacity: 0; transform: translateX(20px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sugestao-anterior {
|
||||||
|
from { opacity: 0; transform: translateX(-20px); }
|
||||||
|
}
|
||||||
|
|
||||||
.no-scrollbar {
|
.no-scrollbar {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { ArrowUpRight, Award, Check, CircleHelp } from "lucide-react";
|
import { ArrowUpRight, Award, Check, CircleHelp } from "lucide-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import { isLinkColumn, linkForLocale } from "@/lib/boardLinks";
|
||||||
|
|
||||||
const HEADER_MAP_EN = {
|
const HEADER_MAP_EN = {
|
||||||
"Nome": "Name",
|
"Nome": "Name",
|
||||||
@@ -105,9 +106,13 @@ export default function BoardCard({ board }) {
|
|||||||
const isDe = pathname === "/de" || pathname.startsWith("/de/");
|
const isDe = pathname === "/de" || pathname.startsWith("/de/");
|
||||||
const isZh = pathname === "/zh-CN" || pathname.startsWith("/zh-CN/");
|
const isZh = pathname === "/zh-CN" || pathname.startsWith("/zh-CN/");
|
||||||
|
|
||||||
const mainFields = ["Nome", "Link", "Imagem", "Loja Oficial"];
|
const locale = isEn ? "en" : isEs ? "es" : isFr ? "fr" : isDe ? "de" : isZh ? "zh-CN" : "pt";
|
||||||
|
const link = linkForLocale(board, locale);
|
||||||
|
const imagem = board.Imagem && link ? board.Imagem.replace(/href="[^"]*"/, `href="${link}"`) : board.Imagem;
|
||||||
|
|
||||||
const technicalFields = Object.keys(board).filter((key) => !mainFields.includes(key) && board[key]);
|
const mainFields = ["Nome", "Imagem", "Loja Oficial"];
|
||||||
|
|
||||||
|
const technicalFields = Object.keys(board).filter((key) => !isLinkColumn(key) && !mainFields.includes(key) && board[key]);
|
||||||
|
|
||||||
const valor = (campo) => board[campo]?.toString().toLowerCase().trim();
|
const valor = (campo) => board[campo]?.toString().toLowerCase().trim();
|
||||||
const recursos = technicalFields.filter((campo) => ["sim", "opcional"].includes(valor(campo)));
|
const recursos = technicalFields.filter((campo) => ["sim", "opcional"].includes(valor(campo)));
|
||||||
@@ -118,7 +123,7 @@ export default function BoardCard({ board }) {
|
|||||||
{board.Imagem && (
|
{board.Imagem && (
|
||||||
<div
|
<div
|
||||||
className="flex shrink-0 justify-center border-b border-line bg-surface-2 p-4 [&_img]:h-44 [&_img]:w-auto [&_img]:max-w-full [&_img]:object-contain"
|
className="flex shrink-0 justify-center border-b border-line bg-surface-2 p-4 [&_img]:h-44 [&_img]:w-auto [&_img]:max-w-full [&_img]:object-contain"
|
||||||
dangerouslySetInnerHTML={{ __html: board.Imagem }}
|
dangerouslySetInnerHTML={{ __html: imagem }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -167,14 +172,16 @@ export default function BoardCard({ board }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{board.Link && (
|
{link && (
|
||||||
<a href={board.Link} target="_blank" rel="noopener noreferrer" className="btn btn-primary mt-6 w-full">
|
<div className="mt-auto pt-6">
|
||||||
{isEn ? "View on AliExpress" : isEs ? "Ver en AliExpress" : isFr ? "Voir sur AliExpress" : isZh ? "在 AliExpress 查看" : isDe ? "Auf AliExpress ansehen" : "Ver no AliExpress"}
|
<a href={link} target="_blank" rel="noopener noreferrer" className="btn btn-primary w-full">
|
||||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
{isEn ? "View on AliExpress" : isEs ? "Ver en AliExpress" : isFr ? "Voir sur AliExpress" : isZh ? "在 AliExpress 查看" : isDe ? "Auf AliExpress ansehen" : "Ver no AliExpress"}
|
||||||
<span className="sr-only">
|
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||||
{isEn ? " (opens in new tab)" : isEs ? " (abre en una pestaña nueva)" : isFr ? " (ouvre un nouvel onglet)" : isZh ? "(在新标签页打开)" : isDe ? " (öffnet in neuem Tab)" : " (abre em nova aba)"}
|
<span className="sr-only">
|
||||||
</span>
|
{isEn ? " (opens in new tab)" : isEs ? " (abre en una pestaña nueva)" : isFr ? " (ouvre un nouvel onglet)" : isZh ? "(在新标签页打开)" : isDe ? " (öffnet in neuem Tab)" : " (abre em nova aba)"}
|
||||||
</a>
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ export default function Header() {
|
|||||||
<div className="nav-pill flex h-16 items-center justify-between gap-1.5 rounded-full px-2.5 sm:gap-3 sm:px-4">
|
<div className="nav-pill flex h-16 items-center justify-between gap-1.5 rounded-full px-2.5 sm:gap-3 sm:px-4">
|
||||||
<Link
|
<Link
|
||||||
href={getLinkHref("/")}
|
href={getLinkHref("/")}
|
||||||
className="flex min-h-11 shrink-0 items-center gap-2.5 rounded-full pl-1 pr-2"
|
className="flex min-h-11 min-w-11 shrink-0 items-center gap-2.5 rounded-full pl-1 pr-2"
|
||||||
aria-label={
|
aria-label={
|
||||||
isPt
|
isPt
|
||||||
? "ESPDocs, página inicial"
|
? "ESPDocs, página inicial"
|
||||||
@@ -437,15 +437,6 @@ export default function Header() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{isPt && (
|
|
||||||
<Link href={getLinkHref("/livro")} className={`nav-link ${isActivePath(pathname, "/livro", currentLocale) ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/livro", currentLocale) ? "page" : undefined}>
|
|
||||||
Livro
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<Link href={getLinkHref("/sobre")} className={`nav-link ${isActivePath(pathname, "/sobre", currentLocale) ? "is-active" : ""}`} aria-current={isActivePath(pathname, "/sobre", currentLocale) ? "page" : undefined}>
|
|
||||||
{isPt ? "Sobre" : isEs ? "Acerca de" : isFr ? "À propos" : isZh ? "关于" : isDe ? "Über uns" : "About"}
|
|
||||||
</Link>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex items-center gap-1 sm:gap-1.5">
|
<div className="flex items-center gap-1 sm:gap-1.5">
|
||||||
@@ -455,7 +446,9 @@ export default function Header() {
|
|||||||
</Link>
|
</Link>
|
||||||
<SearchDialog />
|
<SearchDialog />
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
<ThemeToggle isPt={isPt} isEs={isEs} isFr={isFr} isDe={isDe} isZh={isZh} />
|
<div className="max-[389px]:hidden">
|
||||||
|
<ThemeToggle isPt={isPt} isEs={isEs} isFr={isFr} isDe={isDe} isZh={isZh} />
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
ref={menuButtonRef}
|
ref={menuButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -523,13 +516,14 @@ export default function Header() {
|
|||||||
ref={closeRef}
|
ref={closeRef}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDrawerOpen(false)}
|
onClick={() => setDrawerOpen(false)}
|
||||||
className="grid h-10 w-10 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2"
|
className="grid h-11 w-11 place-items-center rounded-full border border-line text-ink transition-colors hover:bg-surface-2"
|
||||||
aria-label={isPt ? "Fechar menu" : isEs ? "Cerrar menú" : isFr ? "Fermer le menu" : isZh ? "关闭菜单" : isDe ? "Menü schließen" : "Close menu"}
|
aria-label={isPt ? "Fechar menu" : isEs ? "Cerrar menú" : isFr ? "Fermer le menu" : isZh ? "关闭菜单" : isDe ? "Menü schließen" : "Close menu"}
|
||||||
>
|
>
|
||||||
<X className="h-5 w-5" aria-hidden="true" />
|
<X className="h-5 w-5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
||||||
{isPt && (
|
{isPt && (
|
||||||
<div className="px-4 pb-2">
|
<div className="px-4 pb-2">
|
||||||
<Link
|
<Link
|
||||||
@@ -557,7 +551,7 @@ export default function Header() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
className="flex-1 overflow-y-auto px-3 py-2"
|
className="px-3 py-2"
|
||||||
aria-label={isPt ? "Navegação móvel" : isEs ? "Navegación móvil" : isFr ? "Navigation mobile" : isZh ? "移动端导航" : isDe ? "Mobile Navigation" : "Mobile navigation"}
|
aria-label={isPt ? "Navegação móvel" : isEs ? "Navegación móvil" : isFr ? "Navigation mobile" : isZh ? "移动端导航" : isDe ? "Mobile Navigation" : "Mobile navigation"}
|
||||||
>
|
>
|
||||||
{visibleMobileLinks.map((link) => (
|
{visibleMobileLinks.map((link) => (
|
||||||
@@ -574,6 +568,14 @@ export default function Header() {
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-3 px-6 py-2 min-[390px]:hidden">
|
||||||
|
<span className="text-sm font-medium text-ink-soft">
|
||||||
|
{isPt ? "Tema" : isEs ? "Tema" : isFr ? "Thème" : isZh ? "主题" : isDe ? "Design" : "Theme"}
|
||||||
|
</span>
|
||||||
|
<ThemeToggle isPt={isPt} isEs={isEs} isFr={isFr} isDe={isDe} isZh={isZh} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="p-4 pt-2">
|
<div className="p-4 pt-2">
|
||||||
<Link href={getLinkHref("/seletor")} onClick={() => setDrawerOpen(false)} className="btn btn-primary w-full">
|
<Link href={getLinkHref("/seletor")} onClick={() => setDrawerOpen(false)} className="btn btn-primary w-full">
|
||||||
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
|
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { ArrowUpRight, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { boardsForChips, imageOf, reasonFor, weightedOrder } from "@/lib/boardSuggestions";
|
||||||
|
import { linkForLocale } from "@/lib/boardLinks";
|
||||||
|
|
||||||
|
const TEXTOS = {
|
||||||
|
pt: { titulo: "Placa sugerida", afiliado: "Link de afiliado", ver: "Ver no AliExpress", anterior: "Placa anterior", proxima: "Próxima placa", de: "de", aba: "(abre em nova aba)", regiao: "Placas sugeridas com estes chips" },
|
||||||
|
en: { titulo: "Suggested board", afiliado: "Affiliate link", ver: "View on AliExpress", anterior: "Previous board", proxima: "Next board", de: "of", aba: "(opens in new tab)", regiao: "Suggested boards with these chips" },
|
||||||
|
es: { titulo: "Placa sugerida", afiliado: "Enlace de afiliado", ver: "Ver en AliExpress", anterior: "Placa anterior", proxima: "Siguiente placa", de: "de", aba: "(abre en una pestaña nueva)", regiao: "Placas sugeridas con estos chips" },
|
||||||
|
fr: { titulo: "Carte suggérée", afiliado: "Lien d'affiliation", ver: "Voir sur AliExpress", anterior: "Carte précédente", proxima: "Carte suivante", de: "sur", aba: "(ouvre un nouvel onglet)", regiao: "Cartes suggérées avec ces puces" },
|
||||||
|
de: { titulo: "Board-Vorschlag", afiliado: "Affiliate-Link", ver: "Auf AliExpress ansehen", anterior: "Vorheriges Board", proxima: "Nächstes Board", de: "von", aba: "(öffnet in neuem Tab)", regiao: "Board-Vorschläge mit diesen Chips" },
|
||||||
|
"zh-CN": { titulo: "推荐开发板", afiliado: "推广链接", ver: "在 AliExpress 查看", anterior: "上一块开发板", proxima: "下一块开发板", de: "/", aba: "(在新标签页打开)", regiao: "搭载这些芯片的推荐开发板" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function localeFrom(pathname) {
|
||||||
|
for (const loc of ["en", "es", "fr", "de", "zh-CN"]) {
|
||||||
|
if (pathname === `/${loc}` || pathname.startsWith(`/${loc}/`)) return loc;
|
||||||
|
}
|
||||||
|
return "pt";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uma só busca por visita, compartilhada entre as instâncias; falha vira "sem placas".
|
||||||
|
let promessaDePlacas;
|
||||||
|
function carregarPlacas() {
|
||||||
|
if (!promessaDePlacas) {
|
||||||
|
promessaDePlacas = fetch("/api/placas")
|
||||||
|
.then((resposta) => (resposta.ok ? resposta.json() : Promise.reject(new Error(String(resposta.status)))))
|
||||||
|
.then((json) => json.data || [])
|
||||||
|
.catch(() => {
|
||||||
|
promessaDePlacas = undefined;
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return promessaDePlacas;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Faixa discreta com uma placa sorteada dos chips da página. Complementa a documentação:
|
||||||
|
// fica parada, só troca quando o leitor usa as setas, e some se não houver placa.
|
||||||
|
export default function PlacasSugeridas({ chips, className = "" }) {
|
||||||
|
const pathname = usePathname() || "";
|
||||||
|
const locale = localeFrom(pathname);
|
||||||
|
const t = TEXTOS[locale];
|
||||||
|
const chipsKey = [...chips].sort().join("|");
|
||||||
|
|
||||||
|
const [placas, setPlacas] = useState(null);
|
||||||
|
const [indice, setIndice] = useState(0);
|
||||||
|
const [sentido, setSentido] = useState("entrada");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let ativo = true;
|
||||||
|
carregarPlacas().then((dados) => ativo && setPlacas(dados));
|
||||||
|
return () => {
|
||||||
|
ativo = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// O sorteio refaz só quando os dados ou o conjunto de chips mudam, não a cada render.
|
||||||
|
const ordem = useMemo(() => {
|
||||||
|
if (!placas) return null;
|
||||||
|
return weightedOrder(boardsForChips(placas, chipsKey ? chipsKey.split("|") : [], locale));
|
||||||
|
}, [placas, chipsKey, locale]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIndice(0);
|
||||||
|
setSentido("entrada");
|
||||||
|
}, [chipsKey]);
|
||||||
|
|
||||||
|
// Enquanto carrega, reserva a altura da faixa para a página não pular quando ela aparece.
|
||||||
|
if (ordem === null) return <div aria-hidden="true" className={`min-h-[5.5rem] ${className}`} />;
|
||||||
|
if (ordem.length === 0) return null;
|
||||||
|
|
||||||
|
const placa = ordem[Math.min(indice, ordem.length - 1)];
|
||||||
|
const total = ordem.length;
|
||||||
|
const imagem = imageOf(placa);
|
||||||
|
const motivo = reasonFor(placa, locale);
|
||||||
|
const mostrarChip = chips.length > 1;
|
||||||
|
|
||||||
|
const ir = (passo) => {
|
||||||
|
setSentido(passo > 0 ? "proxima" : "anterior");
|
||||||
|
setIndice((atual) => (atual + passo + total) % total);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section aria-label={t.regiao} className={`sugestao-faixa card-quiet flex flex-col gap-4 p-4 md:flex-row md:items-center md:gap-5 ${className}`}>
|
||||||
|
<div
|
||||||
|
key={`${placa.Nome}-${indice}`}
|
||||||
|
className={`flex min-w-0 flex-1 items-center gap-4 ${sentido === "proxima" ? "sugestao-proxima" : sentido === "anterior" ? "sugestao-anterior" : ""}`}
|
||||||
|
>
|
||||||
|
{imagem && (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img src={imagem} alt="" width={56} height={56} loading="lazy" className="h-14 w-14 shrink-0 rounded-lg bg-white object-contain" />
|
||||||
|
)}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="flex flex-wrap items-center gap-x-2 text-xs font-semibold text-accent">
|
||||||
|
<span>{mostrarChip ? placa["Série"] : t.titulo}</span>
|
||||||
|
<span className="font-medium text-muted">· {t.afiliado}</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-0.5 truncate font-display text-base font-semibold tracking-[-0.02em] text-ink">{placa.Nome}</p>
|
||||||
|
<p className="truncate text-sm text-muted">{motivo}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex shrink-0 flex-col gap-3 sm:flex-row sm:items-center sm:gap-2">
|
||||||
|
{total > 1 && (
|
||||||
|
<div className="flex items-center justify-between gap-1 sm:justify-start">
|
||||||
|
<button type="button" onClick={() => ir(-1)} className="btn btn-ghost btn-icon" aria-label={t.anterior}>
|
||||||
|
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<span className="min-w-[3.25rem] text-center text-xs tabular-nums text-muted">
|
||||||
|
{Math.min(indice, total - 1) + 1} {t.de} {total}
|
||||||
|
</span>
|
||||||
|
<button type="button" onClick={() => ir(1)} className="btn btn-ghost btn-icon" aria-label={t.proxima}>
|
||||||
|
<ChevronRight className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<a href={linkForLocale(placa, locale)} target="_blank" rel="sponsored noopener noreferrer" className="btn btn-primary btn-sm w-full sm:w-auto">
|
||||||
|
{t.ver}
|
||||||
|
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||||
|
<span className="sr-only"> {t.aba}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="sr-only" aria-live="polite">
|
||||||
|
{`${placa.Nome}, ${Math.min(indice, total - 1) + 1} ${t.de} ${total}`}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -88,11 +88,11 @@ const TIPOS_ZH = {
|
|||||||
|
|
||||||
const INTELIGENTES = new Set(["resposta", "pino"]);
|
const INTELIGENTES = new Set(["resposta", "pino"]);
|
||||||
|
|
||||||
const EXEMPLOS_EN = ["flash s3", "usb pins c6", "gpio0 esp32", "brownout", "LVGL", "FreeRTOS"];
|
const EXEMPLOS_EN = ["flash s3", "usb pins c6", "pin 32 esp32-s3", "gpio0 esp32", "brownout", "LVGL"];
|
||||||
const EXEMPLOS_ES = ["flash s3", "pines usb c6", "gpio0 esp32", "brownout", "LVGL", "FreeRTOS"];
|
const EXEMPLOS_ES = ["flash s3", "pines usb c6", "pin 32 esp32-s3", "gpio0 esp32", "brownout", "LVGL"];
|
||||||
const EXEMPLOS_FR = ["flash s3", "broches usb c6", "gpio0 esp32", "brownout", "LVGL", "FreeRTOS"];
|
const EXEMPLOS_FR = ["flash s3", "broches usb c6", "broche 32 esp32-s3", "gpio0 esp32", "brownout", "LVGL"];
|
||||||
const EXEMPLOS_DE = ["flash s3", "usb pins c6", "gpio0 esp32", "brownout", "LVGL", "FreeRTOS"];
|
const EXEMPLOS_DE = ["flash s3", "usb pins c6", "pin 32 esp32-s3", "gpio0 esp32", "brownout", "LVGL"];
|
||||||
const EXEMPLOS_ZH = ["s3 闪存", "c6 usb 引脚", "esp32 gpio0", "brownout", "LVGL", "FreeRTOS"];
|
const EXEMPLOS_ZH = ["s3 闪存", "c6 usb 引脚", "esp32-s3 32号引脚", "esp32 gpio0", "brownout", "LVGL"];
|
||||||
|
|
||||||
function Destaque({ texto, termos }) {
|
function Destaque({ texto, termos }) {
|
||||||
if (!termos.length) return texto;
|
if (!termos.length) return texto;
|
||||||
@@ -298,7 +298,7 @@ export default function SearchDialog() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const entendeu = interpretacao.chips.length > 0 || interpretacao.topicos.length > 0;
|
const entendeu = interpretacao.chips.length > 0 || interpretacao.topicos.length > 0 || (interpretacao.pinos?.length ?? 0) > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -361,6 +361,9 @@ export default function SearchDialog() {
|
|||||||
{interpretacao.chips.map((c) => (
|
{interpretacao.chips.map((c) => (
|
||||||
<Pilula key={c.chave} forte>{c.nome}</Pilula>
|
<Pilula key={c.chave} forte>{c.nome}</Pilula>
|
||||||
))}
|
))}
|
||||||
|
{interpretacao.pinos?.map((p) => (
|
||||||
|
<Pilula key={p}>{p}</Pilula>
|
||||||
|
))}
|
||||||
{interpretacao.topicos.map((t) => (
|
{interpretacao.topicos.map((t) => (
|
||||||
<Pilula key={t.id}>{t.nome}</Pilula>
|
<Pilula key={t.id}>{t.nome}</Pilula>
|
||||||
))}
|
))}
|
||||||
@@ -489,8 +492,8 @@ export default function SearchDialog() {
|
|||||||
|
|
||||||
{item.linhas && (
|
{item.linhas && (
|
||||||
<dl className="mt-2 grid gap-x-4 gap-y-1 text-xs sm:grid-cols-[auto_1fr]">
|
<dl className="mt-2 grid gap-x-4 gap-y-1 text-xs sm:grid-cols-[auto_1fr]">
|
||||||
{item.linhas.map((l) => (
|
{item.linhas.map((l, i) => (
|
||||||
<div key={l.k} className="contents">
|
<div key={`${l.k}-${i}`} className="contents">
|
||||||
<dt className="font-semibold text-ink-soft">{l.k}</dt>
|
<dt className="font-semibold text-ink-soft">{l.k}</dt>
|
||||||
<dd className="text-ink">{l.v}</dd>
|
<dd className="text-ink">{l.v}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Colunas de link da planilha: "Link" é o padrão (Brasil) e "Link_<locale>" o link
|
||||||
|
// de afiliado gerado para o país de envio daquele idioma.
|
||||||
|
export const LINK_COLUMN_PREFIX = "Link_";
|
||||||
|
|
||||||
|
export function isLinkColumn(coluna) {
|
||||||
|
return coluna === "Link" || coluna.startsWith(LINK_COLUMN_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function linkForLocale(board, locale) {
|
||||||
|
const especifico = locale && locale !== "pt" ? board[`${LINK_COLUMN_PREFIX}${locale}`] : "";
|
||||||
|
return (especifico || board.Link || "").trim();
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// Lógica da faixa de placas sugeridas (components/PlacasSugeridas.jsx): quais placas entram,
|
||||||
|
// em que ordem sorteada e o texto curto de motivo. Sem React, para ficar fácil de testar.
|
||||||
|
import { linkForLocale } from "@/lib/boardLinks";
|
||||||
|
|
||||||
|
// Placa de loja oficial entra no sorteio com este peso; as demais valem 1.
|
||||||
|
export const PESO_OFICIAL = 3;
|
||||||
|
|
||||||
|
export function boardsForChips(boards, chips, locale) {
|
||||||
|
const alvo = new Set(chips);
|
||||||
|
return boards.filter((board) => alvo.has(board["Série"]) && linkForLocale(board, locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorteio ponderado sem reposição (Efraimidis-Spirakis): quanto maior o peso, maior a chance
|
||||||
|
// de ficar no começo, e todas as placas aparecem uma vez na ordem final.
|
||||||
|
export function weightedOrder(boards, random = Math.random) {
|
||||||
|
return boards
|
||||||
|
.map((board) => {
|
||||||
|
const peso = board["Loja Oficial"]?.trim() === "Sim" ? PESO_OFICIAL : 1;
|
||||||
|
return { board, chave: Math.pow(random(), 1 / peso) };
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.chave - a.chave)
|
||||||
|
.map(({ board }) => board);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function imageOf(board) {
|
||||||
|
const src = board.Imagem?.match(/<img[^>]*\ssrc="([^"]+)"/i)?.[1];
|
||||||
|
if (!src) return null;
|
||||||
|
return src.startsWith("//") ? `https:${src}` : src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursos que viram o texto de motivo, do mais característico para o mais comum.
|
||||||
|
const FEATURES = [
|
||||||
|
{ columns: ["Display Touch"], key: "touch" },
|
||||||
|
{ columns: ["Display"], key: "display" },
|
||||||
|
{ columns: ["Câmera"], key: "camera" },
|
||||||
|
{ columns: ["LoRa"], key: "lora" },
|
||||||
|
{ columns: ["GPS"], key: "gps" },
|
||||||
|
{ columns: ["GSM (3G/4G)"], key: "gsm" },
|
||||||
|
{ columns: ["Conector Ethernet"], key: "ethernet" },
|
||||||
|
{ columns: ["RS485"], key: "rs485" },
|
||||||
|
{ columns: ["Radiofrequência"], key: "rf" },
|
||||||
|
{ columns: ["Infravermelho"], key: "ir" },
|
||||||
|
{ columns: ["Bateria Interna"], key: "battery" },
|
||||||
|
{ columns: ["Conector p/ Bateria"], key: "batteryConnector" },
|
||||||
|
{ columns: ["Conector p/ Painel Solar"], key: "solar" },
|
||||||
|
{ columns: ["Wi-Fi 5GHz"], key: "wifi5" },
|
||||||
|
{ columns: ["Suporte a Matter"], key: "matter" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LABELS = {
|
||||||
|
pt: { touch: "tela touch", display: "tela", camera: "câmera", lora: "LoRa", gps: "GPS", gsm: "4G/GSM", ethernet: "Ethernet", rs485: "RS485", rf: "rádio sub-GHz", ir: "infravermelho", battery: "bateria", batteryConnector: "conector de bateria", solar: "entrada solar", wifi5: "Wi-Fi 5 GHz", matter: "Matter", optional: "opcional", basic: "placa de desenvolvimento essencial" },
|
||||||
|
en: { touch: "touchscreen", display: "display", camera: "camera", lora: "LoRa", gps: "GPS", gsm: "4G/GSM", ethernet: "Ethernet", rs485: "RS485", rf: "sub-GHz radio", ir: "infrared", battery: "battery", batteryConnector: "battery connector", solar: "solar input", wifi5: "5 GHz Wi-Fi", matter: "Matter", optional: "optional", basic: "essential development board" },
|
||||||
|
es: { touch: "pantalla táctil", display: "pantalla", camera: "cámara", lora: "LoRa", gps: "GPS", gsm: "4G/GSM", ethernet: "Ethernet", rs485: "RS485", rf: "radio sub-GHz", ir: "infrarrojos", battery: "batería", batteryConnector: "conector de batería", solar: "entrada solar", wifi5: "Wi-Fi de 5 GHz", matter: "Matter", optional: "opcional", basic: "placa de desarrollo esencial" },
|
||||||
|
fr: { touch: "écran tactile", display: "écran", camera: "caméra", lora: "LoRa", gps: "GPS", gsm: "4G/GSM", ethernet: "Ethernet", rs485: "RS485", rf: "radio sub-GHz", ir: "infrarouge", battery: "batterie", batteryConnector: "connecteur de batterie", solar: "entrée solaire", wifi5: "Wi-Fi 5 GHz", matter: "Matter", optional: "optionnel", basic: "carte de développement essentielle" },
|
||||||
|
de: { touch: "Touchscreen", display: "Display", camera: "Kamera", lora: "LoRa", gps: "GPS", gsm: "4G/GSM", ethernet: "Ethernet", rs485: "RS485", rf: "Sub-GHz-Funk", ir: "Infrarot", battery: "Akku", batteryConnector: "Akkuanschluss", solar: "Solareingang", wifi5: "5-GHz-WLAN", matter: "Matter", optional: "optional", basic: "Entwicklungsboard für den Einstieg" },
|
||||||
|
"zh-CN": { touch: "触摸屏", display: "显示屏", camera: "摄像头", lora: "LoRa", gps: "GPS", gsm: "4G/GSM", ethernet: "以太网", rs485: "RS485", rf: "Sub-GHz 无线电", ir: "红外", battery: "内置电池", batteryConnector: "电池接口", solar: "太阳能输入", wifi5: "5 GHz Wi-Fi", matter: "Matter", optional: "可选", basic: "基础开发板" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// "tela touch, câmera e LoRa": até três recursos, do mais característico para o mais comum.
|
||||||
|
// Quando há touch, a tela comum não entra à parte (já está implícita).
|
||||||
|
export function reasonFor(board, locale) {
|
||||||
|
const rotulos = LABELS[locale] || LABELS.pt;
|
||||||
|
const temTouch = ["Sim", "Opcional"].includes(board["Display Touch"]?.trim());
|
||||||
|
const itens = [];
|
||||||
|
for (const { columns, key } of FEATURES) {
|
||||||
|
if (key === "display" && temTouch) continue;
|
||||||
|
const valor = board[columns[0]]?.trim();
|
||||||
|
if (valor !== "Sim" && valor !== "Opcional") continue;
|
||||||
|
const texto = valor === "Opcional" ? `${rotulos[key]} (${rotulos.optional})` : rotulos[key];
|
||||||
|
itens.push(texto);
|
||||||
|
if (itens.length === 3) break;
|
||||||
|
}
|
||||||
|
if (itens.length === 0) return rotulos.basic;
|
||||||
|
if (locale === "zh-CN") return itens.join("、");
|
||||||
|
if (itens.length === 1) return itens[0];
|
||||||
|
const conector = { pt: "e", en: "and", es: "y", fr: "et", de: "und" }[locale] || "e";
|
||||||
|
return `${itens.slice(0, -1).join(", ")} ${conector} ${itens[itens.length - 1]}`;
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ const STOP = new Set([
|
|||||||
"para", "pra", "pro", "com", "sem", "que", "qual", "quais", "como", "e", "ou", "ver", "sobre", "eh",
|
"para", "pra", "pro", "com", "sem", "que", "qual", "quais", "como", "e", "ou", "ver", "sobre", "eh",
|
||||||
"preciso", "quero", "onde", "fica", "ficam", "esta", "estao", "meu", "minha", "usar", "uso",
|
"preciso", "quero", "onde", "fica", "ficam", "esta", "estao", "meu", "minha", "usar", "uso",
|
||||||
]);
|
]);
|
||||||
const INTENCAO_PINOS = new Set(["pino", "pinos", "pinagem", "pinout", "pin", "pins", "gpios"]);
|
const INTENCAO_PINOS = new Set(["pino", "pinos", "pinagem", "pinout", "pin", "pins", "gpios", "pines", "broche", "broches", "brochage", "pinbelegung"]);
|
||||||
const INTENCAO_DADOS = new Set([
|
const INTENCAO_DADOS = new Set([
|
||||||
"dados", "especificacao", "especificacoes", "spec", "specs", "informacao", "informacoes", "info",
|
"dados", "especificacao", "especificacoes", "spec", "specs", "informacao", "informacoes", "info",
|
||||||
"quanto", "quantos", "quantas", "capacidade", "limite", "limites", "tem", "possui", "suporta",
|
"quanto", "quantos", "quantas", "capacidade", "limite", "limites", "tem", "possui", "suporta",
|
||||||
@@ -137,9 +137,17 @@ function casarTermo(termo, livres) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "pino 32", "pin 32", "broche 32", "引脚 32", "32号引脚": número do pino físico do diagrama.
|
||||||
|
const PINO_FISICO = /\b(?:pino|pinos|pin|pins|pines|broche|broches)\s*(?:n[ºo°.]?\s*|numero\s*|number\s*|nummer\s*)?(\d{1,3})\b(?![.,]\d)/g;
|
||||||
|
const PINO_FISICO_ZH = /第?(\d{1,3})\s*(?:号|脚)?\s*(?:引脚|管脚|针脚)|(?:引脚|管脚|针脚)\s*第?(\d{1,3})(?![.,\d])/g;
|
||||||
|
|
||||||
export function interpretar(indice, consulta) {
|
export function interpretar(indice, consulta) {
|
||||||
const bruto = normalizar(consulta)
|
const pinNums = [];
|
||||||
|
const semPinos = normalizar(consulta)
|
||||||
.replace(/gpio\s*-?\s*(\d+)/g, "gpio$1")
|
.replace(/gpio\s*-?\s*(\d+)/g, "gpio$1")
|
||||||
|
.replace(PINO_FISICO, (_, n) => (pinNums.push(n), " "))
|
||||||
|
.replace(PINO_FISICO_ZH, (_, a, b) => (pinNums.push(a ?? b), " "));
|
||||||
|
const bruto = semPinos
|
||||||
.replace(/([\u3400-\u9fff])([a-z0-9])/g, "$1 $2")
|
.replace(/([\u3400-\u9fff])([a-z0-9])/g, "$1 $2")
|
||||||
.replace(/([a-z0-9])([\u3400-\u9fff])/g, "$1 $2")
|
.replace(/([a-z0-9])([\u3400-\u9fff])/g, "$1 $2")
|
||||||
.replace(/[^a-z0-9.+\-_ \u3400-\u9fff]/g, " ");
|
.replace(/[^a-z0-9.+\-_ \u3400-\u9fff]/g, " ");
|
||||||
@@ -231,7 +239,7 @@ export function interpretar(indice, consulta) {
|
|||||||
|
|
||||||
cjkUsados.forEach((i) => usados.add(i));
|
cjkUsados.forEach((i) => usados.add(i));
|
||||||
const restantes = tokens.filter((_, i) => !usados.has(i)).flatMap((t) => (CJK.test(t) ? segmentar(t) : [t]));
|
const restantes = tokens.filter((_, i) => !usados.has(i)).flatMap((t) => (CJK.test(t) ? segmentar(t) : [t]));
|
||||||
return { tokens, chips, topicos, gpios, funcoes, restantes, querPinos, querDados };
|
return { tokens, chips, topicos, gpios, funcoes, restantes, querPinos, querDados, pinNums: [...new Set(pinNums)].slice(0, 3) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const rotuloPino = (p) => {
|
const rotuloPino = (p) => {
|
||||||
@@ -253,6 +261,27 @@ function pinosDoTopico(serie, topico) {
|
|||||||
return { forte, fraco };
|
return { forte, fraco };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const corta = (s, max = 110) => (s.length > max ? `${s.slice(0, max - 1).trimEnd()}…` : s);
|
||||||
|
|
||||||
|
/** Cartão com tudo o que o diagrama diz sobre um pino (nome, funções, categorias, avisos). */
|
||||||
|
function cartaoPino(I, chave, serie, p, titulo) {
|
||||||
|
const T = I.t;
|
||||||
|
const linhas = [
|
||||||
|
{ k: T.kPino, v: String(p.n) },
|
||||||
|
{ k: T.kNome, v: rotuloPino(p) },
|
||||||
|
];
|
||||||
|
if (p.f?.length) linhas.push({ k: T.kFuncoes, v: corta(p.f.join(" · ")) });
|
||||||
|
if (p.c?.length) linhas.push({ k: T.kCategorias, v: p.c.join(", ") });
|
||||||
|
(p.a ?? []).slice(0, 3).forEach((a) => linhas.push({ k: T.kAviso, v: corta(a) }));
|
||||||
|
return {
|
||||||
|
tipo: "resposta",
|
||||||
|
titulo,
|
||||||
|
descricao: T.pinoDesc,
|
||||||
|
linhas,
|
||||||
|
href: hrefPinos(I, chave, [p], fmt(T.motivoPino, { pino: rotuloPino(p) })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const hrefPinos = (I, chave, pinos, motivo) =>
|
const hrefPinos = (I, chave, pinos, motivo) =>
|
||||||
`${I.rotas.series}/${chave}?pinos=${pinos.map((p) => p.n).join(",")}&motivo=${encodeURIComponent(motivo)}#connections`;
|
`${I.rotas.series}/${chave}?pinos=${pinos.map((p) => p.n).join(",")}&motivo=${encodeURIComponent(motivo)}#connections`;
|
||||||
|
|
||||||
@@ -307,7 +336,19 @@ function respostas(indice, it) {
|
|||||||
(it.querPinos ? [cartaoPinos, cartaoSpec] : [cartaoSpec, cartaoPinos]).filter(Boolean).forEach((c) => saida.push(c));
|
(it.querPinos ? [cartaoPinos, cartaoSpec] : [cartaoSpec, cartaoPinos]).filter(Boolean).forEach((c) => saida.push(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!it.topicos.length && !it.gpios.length && !it.funcoes.length) {
|
// "pino 32 s3": o pino físico 32 do diagrama e, se for outro, o GPIO32.
|
||||||
|
for (const n of it.pinNums) {
|
||||||
|
const fisico = serie.pinos.find((p) => String(p.n) === n);
|
||||||
|
const gpio = (indice.porFuncao.get(`gpio${n}`) ?? []).find((a) => a.chave === chave)?.pino;
|
||||||
|
if (fisico) saida.push(cartaoPino(I, chave, serie, fisico, fmt(T.pinoFisico, { n, serie: serie.nome })));
|
||||||
|
if (gpio && gpio !== fisico) saida.push(cartaoPino(I, chave, serie, gpio, fmt(T.pinoNo, { pino: rotuloPino(gpio), serie: serie.nome })));
|
||||||
|
if (!fisico && !gpio) {
|
||||||
|
const max = Math.max(0, ...serie.pinos.map((p) => Number(p.n) || 0));
|
||||||
|
saida.push({ tipo: "pino", titulo: fmt(T.pinoInexistente, { n, serie: serie.nome }), descricao: fmt(T.pinoInexistenteDesc, { max }), href: `${I.rotas.series}/${chave}#connections` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!it.topicos.length && !it.gpios.length && !it.funcoes.length && !it.pinNums.length) {
|
||||||
if (it.querPinos && serie.pinos.length) {
|
if (it.querPinos && serie.pinos.length) {
|
||||||
saida.push({
|
saida.push({
|
||||||
tipo: "pino",
|
tipo: "pino",
|
||||||
@@ -337,6 +378,18 @@ function respostas(indice, it) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pino físico sem chip: o que há nele em cada série.
|
||||||
|
if (!alvos.length) {
|
||||||
|
for (const n of it.pinNums) {
|
||||||
|
const linhas = [];
|
||||||
|
for (const chave of indice.chaves) {
|
||||||
|
const p = indice.series[chave].pinos.find((x) => String(x.n) === n);
|
||||||
|
if (p) linhas.push({ k: indice.series[chave].nome, v: corta(rotuloPino(p), 46) });
|
||||||
|
}
|
||||||
|
if (linhas.length) saida.push({ tipo: "resposta", titulo: fmt(T.pinoEmCada, { n }), descricao: fmt(T.pinoEmCadaDesc, { n }), linhas, href: I.rotas.series });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GPIO26 e nomes de função: "gpio26 s3", "mtck"
|
// GPIO26 e nomes de função: "gpio26 s3", "mtck"
|
||||||
const buscas = [...it.gpios, ...it.funcoes];
|
const buscas = [...it.gpios, ...it.funcoes];
|
||||||
for (const termo of buscas) {
|
for (const termo of buscas) {
|
||||||
@@ -387,6 +440,7 @@ export function buscar(indice, consulta) {
|
|||||||
const interpretacao = {
|
const interpretacao = {
|
||||||
chips: it.chips.map((c) => ({ chave: c, nome: indice.series[c]?.nome ?? c })),
|
chips: it.chips.map((c) => ({ chave: c, nome: indice.series[c]?.nome ?? c })),
|
||||||
topicos: it.topicos.map((t) => ({ id: t.id, nome: indice.topicosLoc?.[t.id]?.nome ?? t.nome })),
|
topicos: it.topicos.map((t) => ({ id: t.id, nome: indice.topicosLoc?.[t.id]?.nome ?? t.nome })),
|
||||||
|
pinos: it.pinNums.map((n) => fmt((BUSCA_I18N[indice.locale] ?? BUSCA_I18N.pt).t.chipPino, { n })),
|
||||||
};
|
};
|
||||||
|
|
||||||
const smart = respostas(indice, it).map((r, i) => ({ id: `r${i}`, keywords: "", ...r }));
|
const smart = respostas(indice, it).map((r, i) => ({ id: `r${i}`, keywords: "", ...r }));
|
||||||
@@ -436,11 +490,11 @@ export function buscar(indice, consulta) {
|
|||||||
const sugestoes = [];
|
const sugestoes = [];
|
||||||
if (it.topicos.length && !it.chips.length) {
|
if (it.topicos.length && !it.chips.length) {
|
||||||
for (const c of indice.chaves.slice(0, 12)) sugestoes.push({ rotulo: indice.series[c].nome, anexar: c });
|
for (const c of indice.chaves.slice(0, 12)) sugestoes.push({ rotulo: indice.series[c].nome, anexar: c });
|
||||||
} else if (it.chips.length && !it.topicos.length && !it.gpios.length) {
|
} else if (it.chips.length && !it.topicos.length && !it.gpios.length && !it.pinNums.length) {
|
||||||
for (const nome of EXEMPLOS_TOPICOS) sugestoes.push({ rotulo: nome, anexar: nome });
|
for (const nome of EXEMPLOS_TOPICOS) sugestoes.push({ rotulo: nome, anexar: nome });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { resultados: [...smart, ...texto], interpretacao, sugestoes, totalRespostas: smart.length };
|
return { resultados: [...smart, ...texto], interpretacao, sugestoes, totalRespostas: smart.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EXEMPLOS_DE_BUSCA = ["flash s3", "pinos usb c6", "deep sleep p4", "gpio0 esp32", "secure boot c3", "psram"];
|
export const EXEMPLOS_DE_BUSCA = ["flash s3", "pinos usb c6", "pino 32 esp32-s3", "gpio0 esp32", "secure boot c3", "psram"];
|
||||||
@@ -29,6 +29,18 @@ export const BUSCA_I18N = {
|
|||||||
compDesc: "Comparativo rápido · adicione o chip na busca para detalhar",
|
compDesc: "Comparativo rápido · adicione o chip na busca para detalhar",
|
||||||
pinoNo: "{pino} no {serie}",
|
pinoNo: "{pino} no {serie}",
|
||||||
motivoPino: "Pino {pino}",
|
motivoPino: "Pino {pino}",
|
||||||
|
pinoFisico: "Pino físico {n} do {serie}",
|
||||||
|
pinoDesc: "Dados do pino no diagrama",
|
||||||
|
kPino: "Pino",
|
||||||
|
kNome: "Nome",
|
||||||
|
kFuncoes: "Funções",
|
||||||
|
kCategorias: "Categorias",
|
||||||
|
kAviso: "Aviso",
|
||||||
|
pinoInexistente: "O {serie} não tem o pino {n}",
|
||||||
|
pinoInexistenteDesc: "Os pinos vão de 1 a {max} · abre o diagrama",
|
||||||
|
pinoEmCada: "Pino {n} em cada série",
|
||||||
|
pinoEmCadaDesc: "O que há no pino físico {n} de cada chip",
|
||||||
|
chipPino: "Pino {n}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
@@ -51,6 +63,18 @@ export const BUSCA_I18N = {
|
|||||||
compDesc: "Quick comparison · add the chip to your search for details",
|
compDesc: "Quick comparison · add the chip to your search for details",
|
||||||
pinoNo: "{pino} on the {serie}",
|
pinoNo: "{pino} on the {serie}",
|
||||||
motivoPino: "Pin {pino}",
|
motivoPino: "Pin {pino}",
|
||||||
|
pinoFisico: "Physical pin {n} on the {serie}",
|
||||||
|
pinoDesc: "Pin data from the diagram",
|
||||||
|
kPino: "Pin",
|
||||||
|
kNome: "Name",
|
||||||
|
kFuncoes: "Functions",
|
||||||
|
kCategorias: "Categories",
|
||||||
|
kAviso: "Warning",
|
||||||
|
pinoInexistente: "The {serie} has no pin {n}",
|
||||||
|
pinoInexistenteDesc: "Pins run from 1 to {max} · opens the diagram",
|
||||||
|
pinoEmCada: "Pin {n} across all series",
|
||||||
|
pinoEmCadaDesc: "What sits on physical pin {n} of each chip",
|
||||||
|
chipPino: "Pin {n}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
es: {
|
es: {
|
||||||
@@ -73,6 +97,18 @@ export const BUSCA_I18N = {
|
|||||||
compDesc: "Comparativa rápida · añade el chip a la búsqueda para ver el detalle",
|
compDesc: "Comparativa rápida · añade el chip a la búsqueda para ver el detalle",
|
||||||
pinoNo: "{pino} en el {serie}",
|
pinoNo: "{pino} en el {serie}",
|
||||||
motivoPino: "Pin {pino}",
|
motivoPino: "Pin {pino}",
|
||||||
|
pinoFisico: "Pin físico {n} del {serie}",
|
||||||
|
pinoDesc: "Datos del pin en el diagrama",
|
||||||
|
kPino: "Pin",
|
||||||
|
kNome: "Nombre",
|
||||||
|
kFuncoes: "Funciones",
|
||||||
|
kCategorias: "Categorías",
|
||||||
|
kAviso: "Aviso",
|
||||||
|
pinoInexistente: "El {serie} no tiene el pin {n}",
|
||||||
|
pinoInexistenteDesc: "Los pines van del 1 al {max} · abre el diagrama",
|
||||||
|
pinoEmCada: "Pin {n} en cada serie",
|
||||||
|
pinoEmCadaDesc: "Qué hay en el pin físico {n} de cada chip",
|
||||||
|
chipPino: "Pin {n}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fr: {
|
fr: {
|
||||||
@@ -95,6 +131,18 @@ export const BUSCA_I18N = {
|
|||||||
compDesc: "Comparatif rapide · ajoutez la puce à la recherche pour détailler",
|
compDesc: "Comparatif rapide · ajoutez la puce à la recherche pour détailler",
|
||||||
pinoNo: "{pino} sur le {serie}",
|
pinoNo: "{pino} sur le {serie}",
|
||||||
motivoPino: "Broche {pino}",
|
motivoPino: "Broche {pino}",
|
||||||
|
pinoFisico: "Broche physique {n} du {serie}",
|
||||||
|
pinoDesc: "Données de la broche dans le schéma",
|
||||||
|
kPino: "Broche",
|
||||||
|
kNome: "Nom",
|
||||||
|
kFuncoes: "Fonctions",
|
||||||
|
kCategorias: "Catégories",
|
||||||
|
kAviso: "Avertissement",
|
||||||
|
pinoInexistente: "Le {serie} n'a pas de broche {n}",
|
||||||
|
pinoInexistenteDesc: "Les broches vont de 1 à {max} · ouvre le schéma",
|
||||||
|
pinoEmCada: "Broche {n} dans chaque série",
|
||||||
|
pinoEmCadaDesc: "Ce qui se trouve sur la broche physique {n} de chaque puce",
|
||||||
|
chipPino: "Broche {n}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
@@ -117,6 +165,18 @@ export const BUSCA_I18N = {
|
|||||||
compDesc: "Schnellvergleich · fügen Sie den Chip zur Suche hinzu, um Details zu sehen",
|
compDesc: "Schnellvergleich · fügen Sie den Chip zur Suche hinzu, um Details zu sehen",
|
||||||
pinoNo: "{pino} beim {serie}",
|
pinoNo: "{pino} beim {serie}",
|
||||||
motivoPino: "Pin {pino}",
|
motivoPino: "Pin {pino}",
|
||||||
|
pinoFisico: "Physischer Pin {n} des {serie}",
|
||||||
|
pinoDesc: "Pin-Daten aus dem Diagramm",
|
||||||
|
kPino: "Pin",
|
||||||
|
kNome: "Name",
|
||||||
|
kFuncoes: "Funktionen",
|
||||||
|
kCategorias: "Kategorien",
|
||||||
|
kAviso: "Hinweis",
|
||||||
|
pinoInexistente: "Der {serie} hat keinen Pin {n}",
|
||||||
|
pinoInexistenteDesc: "Die Pins reichen von 1 bis {max} · öffnet das Diagramm",
|
||||||
|
pinoEmCada: "Pin {n} in jeder Serie",
|
||||||
|
pinoEmCadaDesc: "Was an physischem Pin {n} jedes Chips liegt",
|
||||||
|
chipPino: "Pin {n}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"zh-CN": {
|
"zh-CN": {
|
||||||
@@ -139,6 +199,18 @@ export const BUSCA_I18N = {
|
|||||||
compDesc: "快速对比 · 在搜索中加上芯片可查看详情",
|
compDesc: "快速对比 · 在搜索中加上芯片可查看详情",
|
||||||
pinoNo: "{serie} 的 {pino}",
|
pinoNo: "{serie} 的 {pino}",
|
||||||
motivoPino: "引脚 {pino}",
|
motivoPino: "引脚 {pino}",
|
||||||
|
pinoFisico: "{serie} 的物理引脚 {n}",
|
||||||
|
pinoDesc: "图中的引脚数据",
|
||||||
|
kPino: "引脚",
|
||||||
|
kNome: "名称",
|
||||||
|
kFuncoes: "功能",
|
||||||
|
kCategorias: "类别",
|
||||||
|
kAviso: "警告",
|
||||||
|
pinoInexistente: "{serie} 没有引脚 {n}",
|
||||||
|
pinoInexistenteDesc: "引脚编号为 1 到 {max} · 打开引脚图",
|
||||||
|
pinoEmCada: "各系列的引脚 {n}",
|
||||||
|
pinoEmCadaDesc: "各芯片物理引脚 {n} 上的内容",
|
||||||
|
chipPino: "引脚 {n}",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -482,6 +482,8 @@ export const jsonLd = {
|
|||||||
name: SITE.autor,
|
name: SITE.autor,
|
||||||
url: SITE.autorUrl,
|
url: SITE.autorUrl,
|
||||||
email: SITE.autorEmail,
|
email: SITE.autorEmail,
|
||||||
|
jobTitle: "Operations Technician",
|
||||||
|
worksFor: { "@type": "Organization", name: "RNP (Rede Nacional de Ensino e Pesquisa)", url: "https://www.rnp.br" },
|
||||||
sameAs: [
|
sameAs: [
|
||||||
SITE.autorLinkedin,
|
SITE.autorLinkedin,
|
||||||
SITE.repositorio,
|
SITE.repositorio,
|
||||||
|
|||||||
@@ -65,6 +65,15 @@ export default function middleware(request) {
|
|||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Material das formações: /formacoes e /formacoes/N mostram a página de
|
||||||
|
// entrada gerada em public/formacoes. Sem isto a rota cairia na regra de
|
||||||
|
// URL antiga e iria para /pt/formacoes, que não existe.
|
||||||
|
if (/^\/formacoes(\/\d+)?\/?$/.test(pathname)) {
|
||||||
|
const url = request.nextUrl.clone();
|
||||||
|
url.pathname = `${pathname.replace(/\/$/, "")}/index.html`;
|
||||||
|
return NextResponse.rewrite(url);
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Raiz: negociação de idioma (307) considerando apenas idiomas ATIVOS
|
// 1. Raiz: negociação de idioma (307) considerando apenas idiomas ATIVOS
|
||||||
if (pathname === "/") {
|
if (pathname === "/") {
|
||||||
const targetLocale = resolveRootLocale(request);
|
const targetLocale = resolveRootLocale(request);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"favicons": "node scripts/generate-favicons.mjs",
|
"favicons": "node scripts/generate-favicons.mjs",
|
||||||
"auditar-temas": "node scripts/auditar-temas.mjs",
|
"auditar-temas": "node scripts/auditar-temas.mjs",
|
||||||
"slides-pdf": "node scripts/slides-pdf.mjs",
|
"slides-pdf": "node scripts/slides-pdf.mjs",
|
||||||
|
"formacoes-viewer": "node scripts/formacoes-viewer.mjs",
|
||||||
"check:pinos": "NODE_NO_WARNINGS=1 node scripts/verifica-pinos-restricoes.mjs && NODE_NO_WARNINGS=1 node scripts/verifica-pinos-busca.mjs"
|
"check:pinos": "NODE_NO_WARNINGS=1 node scripts/verifica-pinos-restricoes.mjs && NODE_NO_WARNINGS=1 node scripts/verifica-pinos-busca.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"modulo": 1,
|
||||||
|
"titulo": ["Conhecendo a", "família ESP32"],
|
||||||
|
"subtitulo": "Do silício à bancada: como esses microcontroladores funcionam por dentro, o que cada uma das doze séries faz e não faz, e como escolher o chip certo. Termina com um DHT11 lendo temperatura e umidade num ESP32-S3.",
|
||||||
|
"descricao": "Slides, apostila e arquivos do módulo 1 da formação em ESP32 da LASER: arquitetura, as doze séries da família e uma prática com DHT11 e ESP32-S3.",
|
||||||
|
"fichas": [["Duração", "3 horas"], ["Formato", "presencial"], ["Placa", "ESP32-S3-DevKitC-1"], ["Prática", "DHT11 + monitor serial"]],
|
||||||
|
"pratica": {
|
||||||
|
"titulo": "Prática na bancada",
|
||||||
|
"texto": "Esquema de ligação do DHT11 ao ESP32-S3, com o arquivo editável do Fritzing para montar ou adaptar.",
|
||||||
|
"imagem": "pratica.png",
|
||||||
|
"meta": "Fritzing · capítulo 9 da apostila",
|
||||||
|
"acoes": [["Ver esquema", "pratica.png", false], ["Baixar .fzz", "pratica.fzz", true]]
|
||||||
|
},
|
||||||
|
"grupos": [
|
||||||
|
{ "titulo": "Material de aula", "arquivos": [
|
||||||
|
{ "arquivo": "slide.html", "tipo": "slides", "descricao": "Os slides da formação. Abrem no navegador, em tela cheia, prontos para projetar.", "abrir": true },
|
||||||
|
{ "arquivo": "apostila.html", "tipo": "apostila", "descricao": "A apostila completa, paginada em A4 no próprio navegador, com glossário e capítulos extras.", "abrir": true }
|
||||||
|
]},
|
||||||
|
{ "titulo": "Para baixar e imprimir", "arquivos": [
|
||||||
|
{ "arquivo": "LASER-ESP32-Modulo-1-Slides.pdf", "tipo": "pdf", "descricao": "Os slides em PDF 16:9, um slide por página.", "abrir": true, "baixar": true },
|
||||||
|
{ "arquivo": "LASER-ESP32-Modulo-1-Apostila.pdf", "tipo": "pdf", "descricao": "A apostila em PDF A4, pronta para imprimir.", "abrir": true, "baixar": true }
|
||||||
|
]},
|
||||||
|
{ "titulo": "Prática", "arquivos": [
|
||||||
|
{ "arquivo": "pratica.fzz", "tipo": "fritzing", "descricao": "Montagem do DHT11 com o ESP32-S3, editável no Fritzing.", "baixar": true },
|
||||||
|
{ "arquivo": "pratica.png", "tipo": "imagem", "descricao": "O mesmo esquema em imagem, para consultar sem instalar nada.", "abrir": true }
|
||||||
|
]},
|
||||||
|
{ "titulo": "Imagens", "arquivos": [
|
||||||
|
{ "arquivo": "esp32-s3-blocos.png", "tipo": "imagem", "descricao": "Diagrama de blocos do ESP32-S3, usado no capítulo 2.", "abrir": true }
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
"guias": [
|
||||||
|
{ "titulo": "Montando a prática", "itens": [
|
||||||
|
"Ligue o <b>VCC</b> do DHT11 ao <b>3V3</b>, o <b>DATA</b> ao <b>GPIO12</b> e o <b>GND</b> ao <b>GND</b>.",
|
||||||
|
"Coloque o resistor de <b>10 kΩ</b> entre DATA e 3V3: é o pull-up da linha de dados.",
|
||||||
|
"Instale a <b>DHT sensor library</b>, da Adafruit, na IDE do Arduino.",
|
||||||
|
"Na porta <b>USB</b> da placa, ative <i>USB CDC On Boot</i>; na porta <b>UART</b>, desative."
|
||||||
|
]},
|
||||||
|
{ "titulo": "Abrindo o arquivo do Fritzing", "texto": "O <code>pratica.fzz</code> abre no <a href=\"https://fritzing.org\">Fritzing</a>, onde dá para trocar o pino, mover os fios e exportar o esquema em imagem ou PDF." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -2073,7 +2073,7 @@ Temperatura: 26.1 C | Umidade: 62.0 %</code></pre>
|
|||||||
<h1>Obrigado.<strong>Perguntas?</strong></h1>
|
<h1>Obrigado.<strong>Perguntas?</strong></h1>
|
||||||
<p class="capa-sub">
|
<p class="capa-sub">
|
||||||
A apostila completa deste módulo, com o glossário, os capítulos que não couberam no tempo
|
A apostila completa deste módulo, com o glossário, os capítulos que não couberam no tempo
|
||||||
e o arquivo do Fritzing, está em <strong>index.html</strong>, na mesma pasta.
|
e o arquivo do Fritzing, está em <strong>espdocs.cienciaembarcada.com.br/formacoes/1</strong>.
|
||||||
</p>
|
</p>
|
||||||
<div class="capa-autor">
|
<div class="capa-autor">
|
||||||
<strong>Lucas Rayan Guerra</strong>
|
<strong>Lucas Rayan Guerra</strong>
|
||||||
|
|||||||
@@ -0,0 +1,800 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>LASER · Formação em ESP32 · Módulo 2: Estação de Temperatura e Umidade</title>
|
||||||
|
<meta name="author" content="Lucas Rayan Guerra">
|
||||||
|
<meta name="description" content="Apostila do módulo 2 da formação em ESP32 da LASER: DHT22, LCD 16x2 I2C e ESP32-S3, simulados no Wokwi com o framework Arduino.">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="../../marca/espdocs-marca.svg">
|
||||||
|
<link rel="stylesheet" href="../assets/apostila.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<p class="aviso-carregando">Paginando a apostila…</p>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════════ CAPA ══════════════════════════════ -->
|
||||||
|
<section class="pagina-capa">
|
||||||
|
<div class="capa">
|
||||||
|
<img class="capa-curvas" src="../assets/capa-curvas.svg" alt="">
|
||||||
|
|
||||||
|
<div class="capa-marca">
|
||||||
|
<img src="../assets/laser-escuro.svg" alt="LASER">
|
||||||
|
<span>Liga Acadêmica de Sistemas Embarcados<br>Centro de Informática · UFPE</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="capa-selo">Formação em ESP32 · Módulo 2</div>
|
||||||
|
|
||||||
|
<h1 class="capa-titulo">Estação de<br><strong>temperatura e umidade</strong></h1>
|
||||||
|
|
||||||
|
<p class="capa-sub">
|
||||||
|
Um DHT22, um display LCD 16x2 e um ESP32-S3, primeiro no simulador Wokwi e depois na
|
||||||
|
bancada. Dois protocolos de comunicação, um programa curto e os cuidados elétricos que
|
||||||
|
fazem o circuito funcionar fora da tela também.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="capa-rodape">
|
||||||
|
<div>
|
||||||
|
<strong>Lucas Rayan Guerra</strong>
|
||||||
|
Ciência Embarcada · ESPDocs
|
||||||
|
linkedin.com/in/lucasrguerra
|
||||||
|
</div>
|
||||||
|
<div style="text-align:right">
|
||||||
|
<strong>Material de apoio</strong>
|
||||||
|
Formação presencial · uso livre para fins educacionais
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ══════════════════════════ APRESENTAÇÃO ══════════════════════════ -->
|
||||||
|
<section class="capitulo" id="apresentacao">
|
||||||
|
<div class="capitulo-abertura">
|
||||||
|
<div>
|
||||||
|
<h2>Apresentação</h2>
|
||||||
|
<p class="subtitulo">O que vamos construir, o que você precisa e como usar esta apostila.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
No módulo 1 lemos um sensor e imprimimos o resultado no monitor serial, que só existe
|
||||||
|
enquanto há um computador ligado à placa. Neste módulo o projeto ganha <strong>uma tela
|
||||||
|
própria</strong>: um display LCD de 16 colunas por 2 linhas mostra a temperatura e a
|
||||||
|
umidade medidas por um <strong>DHT22</strong>, e o conjunto passa a funcionar sozinho,
|
||||||
|
ligado em qualquer carregador USB.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Toda a formação acontece no <strong>Wokwi</strong>, um simulador de microcontroladores que
|
||||||
|
roda no navegador. Ele compila o mesmo código que você gravaria na placa, simula o ESP32-S3,
|
||||||
|
o sensor e o display, e deixa você mudar a temperatura com um controle deslizante. Assim
|
||||||
|
cada pessoa monta e testa o circuito sem precisar de componentes, e quem quiser repetir em
|
||||||
|
casa com peças de verdade encontra no capítulo 7 o que muda fora do simulador.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>O que você vai levar deste módulo</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Montar e simular um projeto completo de ESP32 no Wokwi, do zero.</li>
|
||||||
|
<li>Entender como o DHT22 transmite uma medição por um único fio.</li>
|
||||||
|
<li>Entender o barramento I2C: dois fios, endereços e resistores de pull-up.</li>
|
||||||
|
<li>Escrever no LCD 16x2, inclusive com um caractere que você mesmo desenha.</li>
|
||||||
|
<li>Saber o que precisa mudar para o mesmo circuito funcionar na bancada.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Pré-requisitos</h3>
|
||||||
|
<p>
|
||||||
|
Ter acompanhado o módulo 1 ajuda, mas não é obrigatório. Basta saber o que são
|
||||||
|
<code>setup()</code> e <code>loop()</code> num programa Arduino. Para a simulação você
|
||||||
|
precisa só de um navegador atualizado; não é preciso instalar nada nem criar conta.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Os arquivos deste módulo</h3>
|
||||||
|
<p>
|
||||||
|
Tudo o que aparece nesta apostila foi compilado e executado no Wokwi, e os arquivos estão
|
||||||
|
na mesma pasta da formação:
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th style="width:34%">Arquivo</th><th>Conteúdo</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="destaque"><a href="wokwi/sketch.ino">sketch.ino</a></td><td>O programa da estação</td></tr>
|
||||||
|
<tr><td class="destaque"><a href="wokwi/diagram.json">diagram.json</a></td><td>O circuito: peças, posições e fios</td></tr>
|
||||||
|
<tr><td class="destaque"><a href="wokwi/libraries.txt">libraries.txt</a></td><td>As bibliotecas que o Wokwi deve instalar</td></tr>
|
||||||
|
<tr><td class="destaque"><a href="wokwi/scanner-i2c.ino">scanner-i2c.ino</a></td><td>Programa de diagnóstico do barramento I2C</td></tr>
|
||||||
|
<tr><td class="destaque"><a href="modulo-2-wokwi.zip">modulo-2-wokwi.zip</a></td><td>Os quatro arquivos acima num só pacote</td></tr>
|
||||||
|
<tr><td class="destaque"><a href="LASER-ESP32-Modulo-2-Apostila.pdf">Apostila em PDF</a></td><td>Esta apostila, pronta para imprimir</td></tr>
|
||||||
|
<tr><td class="destaque"><a href="LASER-ESP32-Modulo-2-Slides.pdf">Slides em PDF</a></td><td>Os slides da formação, em 16:9</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="grupo">
|
||||||
|
<p class="junto">Ao longo do texto você encontra quatro tipos de caixa:</p>
|
||||||
|
<div class="caixa dica"><div class="caixa-titulo">Dica</div><p>Atalho, boa prática ou recomendação que economiza tempo.</p></div>
|
||||||
|
<div class="caixa atencao"><div class="caixa-titulo">Atenção</div><p>Ponto que costuma causar problema se passar despercebido.</p></div>
|
||||||
|
<div class="caixa erro"><div class="caixa-titulo">Erro comum</div><p>Armadilha concreta, com o sintoma que ela produz e como escapar.</p></div>
|
||||||
|
<div class="caixa curiosidade"><div class="caixa-titulo">Curiosidade</div><p>Contexto ou detalhe técnico que ajuda a fixar o conceito.</p></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═════════════════════════════ SUMÁRIO ═════════════════════════════ -->
|
||||||
|
<section class="capitulo">
|
||||||
|
<div class="capitulo-abertura">
|
||||||
|
<div>
|
||||||
|
<h2>Sumário</h2>
|
||||||
|
<p class="subtitulo">Oito capítulos, da peça solta ao circuito funcionando.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="sumario">
|
||||||
|
<a href="#cap1"><span class="num">1</span><span class="txt">O projeto</span><span class="pontos"></span></a>
|
||||||
|
<a href="#cap2"><span class="num">2</span><span class="txt">O sensor DHT22</span><span class="pontos"></span></a>
|
||||||
|
<a href="#cap3"><span class="num">3</span><span class="txt">O display LCD 16x2 e o barramento I2C</span><span class="pontos"></span></a>
|
||||||
|
<a href="#cap4"><span class="num">4</span><span class="txt">O simulador Wokwi</span><span class="pontos"></span></a>
|
||||||
|
<a href="#cap5"><span class="num">5</span><span class="txt">Montagem</span><span class="pontos"></span></a>
|
||||||
|
<a href="#cap6"><span class="num">6</span><span class="txt">O programa</span><span class="pontos"></span></a>
|
||||||
|
<a href="#cap7"><span class="num">7</span><span class="txt">Da simulação para a bancada</span><span class="pontos"></span></a>
|
||||||
|
<a href="#cap8"><span class="num">8</span><span class="txt">Problemas comuns e desafios</span><span class="pontos"></span></a>
|
||||||
|
</nav>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════ CAPÍTULO 1 ═══════════════════════════ -->
|
||||||
|
<section class="capitulo" id="cap1">
|
||||||
|
<div class="capitulo-abertura">
|
||||||
|
<div class="capitulo-numero">1</div>
|
||||||
|
<div>
|
||||||
|
<h2>O projeto</h2>
|
||||||
|
<p class="subtitulo">Três peças, dois protocolos e uma tela que mostra o ambiente.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
A estação é composta por três peças. O <strong>DHT22</strong> mede temperatura e umidade
|
||||||
|
relativa do ar. O <strong>ESP32-S3</strong> pede uma medição a cada dois segundos, formata
|
||||||
|
os números e os envia ao <strong>LCD 16x2</strong>, que os mantém na tela até a próxima
|
||||||
|
atualização.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="circuito-wokwi.png" alt="Circuito completo no simulador Wokwi: o DHT22 à esquerda, a placa ESP32-S3-DevKitC-1 ao centro, três resistores no alto ligados ao 3V3 e o LCD 16x2 embaixo mostrando Temp: 26.5°C e Umid: 62.0 %." style="max-height:120mm">
|
||||||
|
<figcaption>
|
||||||
|
O circuito deste módulo rodando no Wokwi. O display mostra a leitura que o DHT22 simulado
|
||||||
|
está informando: 26,5 °C e 62 % de umidade.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Cada peça conversa com o ESP32 de um jeito diferente, e é isso que torna o projeto bom para
|
||||||
|
aprender. O DHT22 usa um <strong>protocolo próprio de um fio</strong>, em que o valor de
|
||||||
|
cada bit está na duração de um pulso. O LCD usa o <strong>I2C</strong>, um barramento
|
||||||
|
padronizado de dois fios em que cada dispositivo tem um endereço. Quem entende esses dois
|
||||||
|
protocolos já entende a maior parte dos sensores e módulos baratos do mercado.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Lista de material</h3>
|
||||||
|
<p>No Wokwi as peças já existem. Para montar em bancada você vai precisar de:</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th style="width:44%">Item</th><th style="width:10%">Qtd.</th><th>Observação</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="destaque">ESP32-S3-DevKitC-1</td><td>1</td><td>Qualquer variante de memória serve</td></tr>
|
||||||
|
<tr><td class="destaque">Sensor DHT22 (AM2302)</td><td>1</td><td>O componente branco de 4 pinos</td></tr>
|
||||||
|
<tr><td class="destaque">Display LCD 16x2 com módulo I2C</td><td>1</td><td>A plaquinha PCF8574 vem soldada atrás</td></tr>
|
||||||
|
<tr><td class="destaque">Conversor de nível I2C (BSS138)</td><td>1</td><td>Explicado no capítulo 7</td></tr>
|
||||||
|
<tr><td class="destaque">Resistor de 10 kΩ</td><td>1</td><td>Pull-up do DHT22 (marrom, preto, laranja)</td></tr>
|
||||||
|
<tr><td class="destaque">Resistor de 4,7 kΩ</td><td>2</td><td>Pull-ups do I2C (amarelo, violeta, vermelho)</td></tr>
|
||||||
|
<tr><td class="destaque">Protoboard, jumpers e cabo USB de dados</td><td>—</td><td>Cabo só de carga não grava a placa</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════ CAPÍTULO 2 ═══════════════════════════ -->
|
||||||
|
<section class="capitulo" id="cap2">
|
||||||
|
<div class="capitulo-abertura">
|
||||||
|
<div class="capitulo-numero">2</div>
|
||||||
|
<div>
|
||||||
|
<h2>O sensor DHT22</h2>
|
||||||
|
<p class="subtitulo">Dois sensores numa caixinha e um protocolo de um fio só.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Dentro do DHT22 existem dois elementos de medição e um pequeno microcontrolador. A umidade
|
||||||
|
é medida por um <strong>sensor capacitivo</strong>: um material que absorve vapor d'água
|
||||||
|
e, com isso, muda a capacitância. A temperatura vem de um <strong>termistor</strong>, um
|
||||||
|
resistor cujo valor varia com o calor. O microcontrolador interno lê os dois, aplica a
|
||||||
|
calibração gravada de fábrica e entrega os números já prontos, em formato digital.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>DHT22 ou DHT11</h3>
|
||||||
|
<p>No módulo 1 usamos o DHT11. O DHT22 tem a mesma pinagem e o mesmo protocolo, mas mede melhor:</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th style="width:34%">Característica</th><th style="width:33%">DHT11</th><th>DHT22</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="destaque">Faixa de temperatura</td><td>0 a 50 °C</td><td class="sim">−40 a 80 °C</td></tr>
|
||||||
|
<tr><td class="destaque">Precisão de temperatura</td><td>±2 °C</td><td class="sim">±0,5 °C</td></tr>
|
||||||
|
<tr><td class="destaque">Faixa de umidade</td><td>20 a 90 %</td><td class="sim">0 a 100 %</td></tr>
|
||||||
|
<tr><td class="destaque">Precisão de umidade</td><td>±5 %</td><td class="sim">±2 a 5 %</td></tr>
|
||||||
|
<tr><td class="destaque">Resolução</td><td>1 °C e 1 %</td><td class="sim">0,1 °C e 0,1 %</td></tr>
|
||||||
|
<tr><td class="destaque">Intervalo mínimo entre leituras</td><td class="sim">1 s</td><td>2 s</td></tr>
|
||||||
|
<tr><td class="destaque">Alimentação</td><td>3,3 a 5 V</td><td>3,3 a 6 V</td></tr>
|
||||||
|
</tbody>
|
||||||
|
<caption>Valores típicos dos datasheets da Aosong. Confira sempre o do seu fornecedor.</caption>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O preço é a única desvantagem real do DHT22, e o intervalo de dois segundos entre leituras
|
||||||
|
raramente importa: temperatura e umidade de um ambiente não mudam mais rápido do que isso.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Os quatro pinos</h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th style="width:16%">Pino</th><th style="width:22%">Nome</th><th>Ligação</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="destaque">1</td><td>VCC</td><td>3V3 do ESP32</td></tr>
|
||||||
|
<tr><td class="destaque">2</td><td>DATA</td><td>GPIO12, com pull-up de 10 kΩ para o 3V3</td></tr>
|
||||||
|
<tr><td class="destaque">3</td><td>NC</td><td>Não conectado</td></tr>
|
||||||
|
<tr><td class="destaque">4</td><td>GND</td><td>GND do ESP32</td></tr>
|
||||||
|
</tbody>
|
||||||
|
<caption>Pinos contados da esquerda para a direita, com a grade do sensor virada para você.</caption>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Como uma medição atravessa um único fio</h3>
|
||||||
|
<p>
|
||||||
|
O ESP32 e o sensor dividem o mesmo fio de dados, e por isso nenhum dos dois "força" nível
|
||||||
|
alto: os dois apenas puxam a linha para o terra quando precisam de um zero e a soltam no
|
||||||
|
resto do tempo. Quem traz a linha de volta para o nível alto é o resistor de
|
||||||
|
<strong>pull-up</strong>. Uma leitura acontece assim:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="passo">
|
||||||
|
<div class="passo-num">1</div>
|
||||||
|
<div><h3>O ESP32 pede a medição</h3><p>Puxa a linha para baixo por cerca de 1 ms e depois a solta.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="passo">
|
||||||
|
<div class="passo-num">2</div>
|
||||||
|
<div><h3>O sensor confirma</h3><p>Responde com 80 µs em nível baixo seguidos de 80 µs em nível alto.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="passo">
|
||||||
|
<div class="passo-num">3</div>
|
||||||
|
<div>
|
||||||
|
<h3>Chegam 40 bits</h3>
|
||||||
|
<p>
|
||||||
|
Cada bit começa com 50 µs em baixo. Depois vem um pulso alto: se ele dura cerca de
|
||||||
|
26 µs é um <strong>0</strong>; se dura cerca de 70 µs é um <strong>1</strong>.
|
||||||
|
São 16 bits de umidade, 16 de temperatura e 8 de verificação.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<svg class="diagrama" viewBox="0 0 640 150" role="img" aria-label="Diagrama de tempo: pedido do ESP32, resposta do sensor, um bit 0 com pulso alto curto e um bit 1 com pulso alto longo.">
|
||||||
|
<g stroke="#9aa9b3" stroke-width="0.8" stroke-dasharray="2 3" fill="none">
|
||||||
|
<line x1="50" y1="40" x2="630" y2="40"/><line x1="50" y1="100" x2="630" y2="100"/>
|
||||||
|
</g>
|
||||||
|
<text x="44" y="43" font-size="9" text-anchor="end" fill="#5b6b75" font-family="JetBrains Mono, monospace">alto</text>
|
||||||
|
<text x="44" y="103" font-size="9" text-anchor="end" fill="#5b6b75" font-family="JetBrains Mono, monospace">baixo</text>
|
||||||
|
<polyline fill="none" stroke="#003049" stroke-width="2.2" stroke-linejoin="round"
|
||||||
|
points="50,40 70,40 70,100 170,100 170,40 205,40 205,100 255,100 255,40 305,40 305,100 345,100 345,40 375,40 375,100 415,100 415,40 500,40 500,100 540,100 540,40 630,40"/>
|
||||||
|
<g font-family="JetBrains Mono, monospace" font-size="9.5" fill="#D62828" text-anchor="middle">
|
||||||
|
<text x="120" y="30">~1 ms</text><text x="280" y="30">80 µs</text><text x="360" y="30">26 µs</text><text x="457" y="30">70 µs</text>
|
||||||
|
</g>
|
||||||
|
<g font-family="Inter, sans-serif" font-size="10" fill="#1d2b33" text-anchor="middle">
|
||||||
|
<text x="120" y="125">ESP32 pede</text><text x="255" y="125">sensor confirma</text><text x="360" y="125">bit 0</text><text x="477" y="125">bit 1</text><text x="595" y="125" fill="#5b6b75">…40 bits</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<figcaption>
|
||||||
|
O valor de cada bit está na <strong>largura</strong> do pulso alto. Sem o pull-up a
|
||||||
|
linha sobe devagar demais, os tempos saem errados e a leitura falha.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<div class="caixa atencao">
|
||||||
|
<div class="caixa-titulo">Sensor solto ou módulo em plaquinha</div>
|
||||||
|
<p>
|
||||||
|
O DHT22 também é vendido soldado numa plaquinha de três pinos, que já traz o resistor de
|
||||||
|
pull-up. Se o seu for assim, o resistor externo de 10 kΩ é dispensável. O sensor solto,
|
||||||
|
de quatro pinos, <strong>precisa</strong> dele.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════ CAPÍTULO 3 ═══════════════════════════ -->
|
||||||
|
<section class="capitulo" id="cap3">
|
||||||
|
<div class="capitulo-abertura">
|
||||||
|
<div class="capitulo-numero">3</div>
|
||||||
|
<div>
|
||||||
|
<h2>O display LCD 16x2 e o barramento I2C</h2>
|
||||||
|
<p class="subtitulo">Trinta e dois caracteres, quatro fios e um endereço.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O LCD 16x2 mostra duas linhas de dezesseis caracteres. Cada caractere é uma matriz de
|
||||||
|
<strong>5 por 8 pontos</strong>, e o controlador do display, o HD44780 ou um compatível,
|
||||||
|
já sabe desenhar letras, números e símbolos comuns. Ele também guarda até
|
||||||
|
<strong>oito caracteres personalizados</strong>, que você desenha ponto a ponto. É assim
|
||||||
|
que este projeto mostra o símbolo de grau.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Sozinho, o display precisa de pelo menos seis fios de dados e controle. Por isso ele quase
|
||||||
|
sempre vem com uma plaquinha soldada atrás, baseada no chip <strong>PCF8574</strong>, que
|
||||||
|
converte esses sinais para o barramento <strong>I2C</strong>: com ela, bastam quatro fios
|
||||||
|
para alimentar e controlar o display. A plaquinha também tem um <strong>trimpot</strong>
|
||||||
|
para ajustar o contraste.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Como o I2C funciona</h3>
|
||||||
|
<p>
|
||||||
|
O I2C usa dois fios: <strong>SDA</strong>, por onde passam os dados, e
|
||||||
|
<strong>SCL</strong>, o relógio que marca o ritmo da conversa. O ESP32 é o
|
||||||
|
<em>controlador</em>: é sempre ele quem inicia a comunicação. Os outros dispositivos são
|
||||||
|
<em>alvos</em>, e cada um tem um endereço de 7 bits. Vários dispositivos podem dividir os
|
||||||
|
mesmos dois fios, porque só responde quem tiver o endereço chamado.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th style="width:30%">Plaquinha</th><th style="width:22%">Endereço</th><th>Como saber</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="destaque">PCF8574 ou PCF8574T</td><td class="mono">0x27</td><td>A mais comum, e a usada no Wokwi</td></tr>
|
||||||
|
<tr><td class="destaque">PCF8574A</td><td class="mono">0x3F</td><td>Mesma placa, outro chip</td></tr>
|
||||||
|
</tbody>
|
||||||
|
<caption>Na dúvida, o programa de scanner do capítulo 7 descobre o endereço por você.</caption>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Assim como no DHT22, as linhas do I2C são de <strong>dreno aberto</strong>: os
|
||||||
|
dispositivos só puxam para baixo, e são resistores de pull-up que trazem SDA e SCL de volta
|
||||||
|
ao nível alto. Valores entre 2,2 kΩ e 10 kΩ funcionam na velocidade padrão; aqui usamos
|
||||||
|
4,7 kΩ, o valor mais comum.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="caixa erro">
|
||||||
|
<div class="caixa-titulo">O display liga mas não mostra nada</div>
|
||||||
|
<p>
|
||||||
|
Com o contraste no mínimo, a tela acende e parece vazia; com ele no máximo, aparecem
|
||||||
|
só retângulos cheios. Antes de desconfiar do código, gire o trimpot azul atrás do
|
||||||
|
display com uma chave de fenda até os caracteres aparecerem. No Wokwi isso não
|
||||||
|
existe, porque o contraste simulado já vem ajustado.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════ CAPÍTULO 4 ═══════════════════════════ -->
|
||||||
|
<section class="capitulo" id="cap4">
|
||||||
|
<div class="capitulo-abertura">
|
||||||
|
<div class="capitulo-numero">4</div>
|
||||||
|
<div>
|
||||||
|
<h2>O simulador Wokwi</h2>
|
||||||
|
<p class="subtitulo">Um laboratório de eletrônica que abre no navegador.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O Wokwi simula o processador do ESP32-S3 instrução por instrução e simula também cada peça
|
||||||
|
ligada a ele. O programa é compilado com o mesmo <strong>núcleo Arduino da Espressif</strong>
|
||||||
|
que você instalaria na IDE, então o código que funciona aqui é o mesmo que vai para a placa.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Os três arquivos de um projeto</h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th style="width:28%">Arquivo</th><th>Para que serve</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="destaque">sketch.ino</td><td>O programa, exatamente como na IDE do Arduino.</td></tr>
|
||||||
|
<tr><td class="destaque">diagram.json</td><td>As peças, a posição de cada uma, os fios e as propriedades, como a temperatura inicial do DHT22.</td></tr>
|
||||||
|
<tr><td class="destaque">libraries.txt</td><td>Uma biblioteca por linha. O Wokwi baixa todas antes de compilar.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Abrindo o projeto deste módulo</h3>
|
||||||
|
|
||||||
|
<div class="passo">
|
||||||
|
<div class="passo-num">1</div>
|
||||||
|
<div><h3>Crie um projeto vazio</h3><p>Acesse <strong>wokwi.com/projects/new/esp32-s3</strong>. Não é preciso criar conta para simular.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="passo">
|
||||||
|
<div class="passo-num">2</div>
|
||||||
|
<div><h3>Substitua o programa e o circuito</h3><p>Cole o conteúdo de <code>sketch.ino</code> na aba de mesmo nome e o de <code>diagram.json</code> na aba <em>diagram.json</em>, apagando o que já estava lá.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="passo">
|
||||||
|
<div class="passo-num">3</div>
|
||||||
|
<div><h3>Adicione as bibliotecas</h3><p>Abra a aba <em>Library Manager</em>, clique em <strong>+</strong> e adicione <strong>DHT sensor library</strong> e <strong>LiquidCrystal I2C</strong>. O Wokwi cria o <code>libraries.txt</code> sozinho.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="passo">
|
||||||
|
<div class="passo-num">4</div>
|
||||||
|
<div><h3>Simule</h3><p>Clique no botão verde. A primeira compilação leva alguns segundos; depois o display acende e o monitor serial aparece embaixo.</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="caixa dica">
|
||||||
|
<div class="caixa-titulo">Mude o clima com um clique</div>
|
||||||
|
<p>
|
||||||
|
Com a simulação rodando, clique no DHT22: aparecem dois controles deslizantes, um de
|
||||||
|
temperatura e outro de umidade. Mova-os e veja o display acompanhar na leitura seguinte.
|
||||||
|
É o jeito mais rápido de testar o que o programa faz com valores extremos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="caixa atencao">
|
||||||
|
<div class="caixa-titulo">Salvar exige conta</div>
|
||||||
|
<p>
|
||||||
|
Sem login a simulação funciona normalmente, mas o projeto se perde ao fechar a aba. Para
|
||||||
|
guardar o seu trabalho, entre com uma conta gratuita e clique em <strong>Save</strong>,
|
||||||
|
ou mantenha os três arquivos no seu computador.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════ CAPÍTULO 5 ═══════════════════════════ -->
|
||||||
|
<section class="capitulo" id="cap5">
|
||||||
|
<div class="capitulo-abertura">
|
||||||
|
<div class="capitulo-numero">5</div>
|
||||||
|
<div>
|
||||||
|
<h2>Montagem</h2>
|
||||||
|
<p class="subtitulo">Nove ligações e três resistores.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Todos os pinos usados ficam na <strong>coluna esquerda</strong> da placa, o que deixa a
|
||||||
|
fiação curta e sem cruzamentos. As cores seguem uma convenção que vale a pena adotar
|
||||||
|
também na bancada: vermelho para alimentação, preto para terra, e uma cor por sinal.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th style="width:38%">De</th><th style="width:22%">Para</th><th>Cor e função</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="destaque">DHT22 VCC</td><td>3V3</td><td>Vermelho · alimentação</td></tr>
|
||||||
|
<tr><td class="destaque">DHT22 DATA</td><td>GPIO12</td><td>Amarelo · dados do sensor</td></tr>
|
||||||
|
<tr><td class="destaque">DHT22 GND</td><td>GND</td><td>Preto · terra</td></tr>
|
||||||
|
<tr><td class="destaque">Resistor 10 kΩ</td><td>GPIO12 ↔ 3V3</td><td>Pull-up do DHT22</td></tr>
|
||||||
|
<tr><td class="destaque">LCD VCC</td><td>5V</td><td>Vermelho · o display precisa de 5 V</td></tr>
|
||||||
|
<tr><td class="destaque">LCD GND</td><td>GND</td><td>Preto · terra</td></tr>
|
||||||
|
<tr><td class="destaque">LCD SDA</td><td>GPIO8</td><td>Azul · dados do I2C</td></tr>
|
||||||
|
<tr><td class="destaque">LCD SCL</td><td>GPIO9</td><td>Roxo · relógio do I2C</td></tr>
|
||||||
|
<tr><td class="destaque">2 resistores de 4,7 kΩ</td><td>GPIO8 e GPIO9 ↔ 3V3</td><td>Pull-ups do I2C</td></tr>
|
||||||
|
</tbody>
|
||||||
|
<caption>GPIO8 e GPIO9 são os pinos I2C padrão do ESP32-S3 no núcleo Arduino.</caption>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<img src="circuito-wokwi.png" alt="Fiação no Wokwi: um barramento vermelho de 3V3 no alto alimenta o DHT22 e os três resistores; do resistor de 10 kΩ desce um fio amarelo até o GPIO12, e dos dois resistores de 4,7 kΩ descem os fios azul e roxo até o GPIO8 e o GPIO9, que seguem para o SDA e o SCL do LCD." style="max-height:92mm">
|
||||||
|
<figcaption>
|
||||||
|
O fio vermelho no alto funciona como um barramento de 3V3: dele saem a alimentação do
|
||||||
|
DHT22 e o lado de cima dos três resistores. O lado de baixo de cada resistor encontra a
|
||||||
|
linha de sinal correspondente.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<div class="caixa curiosidade">
|
||||||
|
<div class="caixa-titulo">Por que pull-up para 3V3, e não para 5V</div>
|
||||||
|
<p>
|
||||||
|
Os pinos do ESP32-S3 trabalham em 3,3 V e <strong>não toleram 5 V</strong>. Um resistor
|
||||||
|
de pull-up define a tensão de repouso da linha, então ligá-lo ao 5V colocaria 5 V
|
||||||
|
direto no GPIO. O display é alimentado em 5 V, mas as linhas de sinal precisam ficar
|
||||||
|
em 3,3 V. Na bancada isso exige um cuidado a mais, explicado no capítulo 7.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════ CAPÍTULO 6 ═══════════════════════════ -->
|
||||||
|
<section class="capitulo" id="cap6">
|
||||||
|
<div class="capitulo-abertura">
|
||||||
|
<div class="capitulo-numero">6</div>
|
||||||
|
<div>
|
||||||
|
<h2>O programa</h2>
|
||||||
|
<p class="subtitulo">Um programa curto, lido em três partes.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O programa é lido aqui em três partes, exatamente como foi compilado e executado no Wokwi.
|
||||||
|
O arquivo completo, pronto para colar, está em <a href="wokwi/sketch.ino">wokwi/sketch.ino</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>1. Configuração</h3>
|
||||||
|
<pre class="miudo"><code><span class="c-pre">#include</span> <span class="c-str"><Wire.h></span>
|
||||||
|
<span class="c-pre">#include</span> <span class="c-str"><DHT.h></span>
|
||||||
|
<span class="c-pre">#include</span> <LiquidCrystal_I2C.h>
|
||||||
|
|
||||||
|
<span class="c-pre">#define</span> PINO_DHT <span class="c-num">12</span> <span class="c-com">// fio amarelo</span>
|
||||||
|
<span class="c-pre">#define</span> TIPO_DHT DHT22
|
||||||
|
<span class="c-pre">#define</span> PINO_SDA <span class="c-num">8</span> <span class="c-com">// I2C padrão do ESP32-S3</span>
|
||||||
|
<span class="c-pre">#define</span> PINO_SCL <span class="c-num">9</span>
|
||||||
|
|
||||||
|
DHT <span class="c-fun">dht</span>(PINO_DHT, TIPO_DHT);
|
||||||
|
LiquidCrystal_I2C <span class="c-fun">lcd</span>(<span class="c-num">0x27</span>, <span class="c-num">16</span>, <span class="c-num">2</span>); <span class="c-com">// endereço, colunas, linhas</span>
|
||||||
|
|
||||||
|
<span class="c-com">// Símbolo de grau: 8 linhas de 5 pixels</span>
|
||||||
|
<span class="c-key">byte</span> grau[<span class="c-num">8</span>] = { <span class="c-num">0b01100</span>, <span class="c-num">0b10010</span>, <span class="c-num">0b10010</span>, <span class="c-num">0b01100</span>, <span class="c-num">0</span>, <span class="c-num">0</span>, <span class="c-num">0</span>, <span class="c-num">0</span> };</code></pre>
|
||||||
|
<p>
|
||||||
|
As três bibliotecas cuidam do trabalho pesado: <code>Wire</code> é o I2C do ESP32,
|
||||||
|
<code>DHT</code> fala o protocolo do sensor e <code>LiquidCrystal_I2C</code> traduz
|
||||||
|
comandos como "escreva na linha 2" para o que a plaquinha PCF8574 entende. Os
|
||||||
|
<code>#define</code> reúnem no topo tudo o que depende da montagem: se você mudar um fio de
|
||||||
|
pino, só precisa mudar uma linha.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
O vetor <code>grau</code> desenha o símbolo de grau. Cada número é uma linha do caractere,
|
||||||
|
e cada bit <code>1</code> acende um dos cinco pontos daquela linha:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<figure>
|
||||||
|
<svg class="diagrama" viewBox="0 0 640 150" role="img" aria-label="Grade de 5 por 8 pontos mostrando o desenho do símbolo de grau, com o valor binário de cada linha ao lado.">
|
||||||
|
<g font-family="JetBrains Mono, monospace" font-size="11" fill="#1d2b33">
|
||||||
|
<text x="250" y="26">0b01100</text><text x="250" y="42">0b10010</text><text x="250" y="58">0b10010</text><text x="250" y="74">0b01100</text>
|
||||||
|
<text x="250" y="90" fill="#9aa9b3">0</text><text x="250" y="106" fill="#9aa9b3">0</text><text x="250" y="122" fill="#9aa9b3">0</text><text x="250" y="138" fill="#9aa9b3">0</text>
|
||||||
|
</g>
|
||||||
|
<g fill="#003049">
|
||||||
|
<rect x="177" y="14" width="14" height="14"/><rect x="193" y="14" width="14" height="14"/>
|
||||||
|
<rect x="161" y="30" width="14" height="14"/><rect x="209" y="30" width="14" height="14"/>
|
||||||
|
<rect x="161" y="46" width="14" height="14"/><rect x="209" y="46" width="14" height="14"/>
|
||||||
|
<rect x="177" y="62" width="14" height="14"/><rect x="193" y="62" width="14" height="14"/>
|
||||||
|
</g>
|
||||||
|
<g fill="none" stroke="#c9d3d9" stroke-width="0.8">
|
||||||
|
<rect x="160" y="13" width="80" height="128"/>
|
||||||
|
<path d="M176 13V141M192 13V141M208 13V141M224 13V141M160 29H240M160 45H240M160 61H240M160 77H240M160 93H240M160 109H240M160 125H240"/>
|
||||||
|
</g>
|
||||||
|
<rect x="330" y="40" width="180" height="50" rx="6" fill="#7fae1a"/><text x="420" y="74" font-family="JetBrains Mono, monospace" font-size="26" fill="#1d2b0a" text-anchor="middle">26.5°C</text>
|
||||||
|
<text x="420" y="108" font-family="Inter, sans-serif" font-size="10" fill="#5b6b75" text-anchor="middle">o que aparece no display</text>
|
||||||
|
</svg>
|
||||||
|
<figcaption>
|
||||||
|
<code>lcd.createChar(0, grau)</code> guarda o desenho na posição 0 da memória do display,
|
||||||
|
e <code>lcd.write(0)</code> o imprime como qualquer outro caractere.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<h3>2. setup()</h3>
|
||||||
|
<pre class="miudo"><code><span class="c-key">void</span> <span class="c-fun">setup</span>() {
|
||||||
|
Serial.<span class="c-fun">begin</span>(<span class="c-num">115200</span>);
|
||||||
|
Wire.<span class="c-fun">begin</span>(PINO_SDA, PINO_SCL);
|
||||||
|
dht.<span class="c-fun">begin</span>();
|
||||||
|
|
||||||
|
lcd.<span class="c-fun">init</span>();
|
||||||
|
lcd.<span class="c-fun">backlight</span>();
|
||||||
|
lcd.<span class="c-fun">createChar</span>(<span class="c-num">0</span>, grau);
|
||||||
|
lcd.<span class="c-fun">setCursor</span>(<span class="c-num">0</span>, <span class="c-num">0</span>);
|
||||||
|
lcd.<span class="c-fun">print</span>(<span class="c-str">"LASER ESP32-S3"</span>);
|
||||||
|
lcd.<span class="c-fun">setCursor</span>(<span class="c-num">0</span>, <span class="c-num">1</span>);
|
||||||
|
lcd.<span class="c-fun">print</span>(<span class="c-str">"Iniciando..."</span>);
|
||||||
|
<span class="c-fun">delay</span>(<span class="c-num">2000</span>); <span class="c-com">// o DHT22 precisa de ~2 s para a primeira leitura</span>
|
||||||
|
lcd.<span class="c-fun">clear</span>();
|
||||||
|
}</code></pre>
|
||||||
|
<p>
|
||||||
|
<code>Wire.begin(8, 9)</code> inicia o I2C nos pinos da montagem. Em seguida o display é
|
||||||
|
inicializado, a luz de fundo é ligada e o caractere de grau é registrado. A mensagem
|
||||||
|
"Iniciando..." não é só enfeite: ela cobre os <strong>dois segundos</strong> que o DHT22
|
||||||
|
precisa depois de ligado para entregar a primeira medição válida.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>3. loop()</h3>
|
||||||
|
<pre class="miudo"><code><span class="c-key">void</span> <span class="c-fun">loop</span>() {
|
||||||
|
<span class="c-key">float</span> umidade = dht.<span class="c-fun">readHumidity</span>();
|
||||||
|
<span class="c-key">float</span> temperatura = dht.<span class="c-fun">readTemperature</span>();
|
||||||
|
|
||||||
|
<span class="c-key">if</span> (<span class="c-fun">isnan</span>(umidade) || <span class="c-fun">isnan</span>(temperatura)) {
|
||||||
|
lcd.<span class="c-fun">setCursor</span>(<span class="c-num">0</span>, <span class="c-num">0</span>);
|
||||||
|
lcd.<span class="c-fun">print</span>(<span class="c-str">"Falha no sensor "</span>);
|
||||||
|
Serial.<span class="c-fun">println</span>(<span class="c-str">"Falha na leitura do DHT22"</span>);
|
||||||
|
} <span class="c-key">else</span> {
|
||||||
|
lcd.<span class="c-fun">setCursor</span>(<span class="c-num">0</span>, <span class="c-num">0</span>);
|
||||||
|
lcd.<span class="c-fun">print</span>(<span class="c-str">"Temp: "</span>);
|
||||||
|
lcd.<span class="c-fun">print</span>(temperatura, <span class="c-num">1</span>);
|
||||||
|
lcd.<span class="c-fun">write</span>(<span class="c-num">0</span>); <span class="c-com">// o caractere de grau criado acima</span>
|
||||||
|
lcd.<span class="c-fun">print</span>(<span class="c-str">"C "</span>);
|
||||||
|
|
||||||
|
lcd.<span class="c-fun">setCursor</span>(<span class="c-num">0</span>, <span class="c-num">1</span>);
|
||||||
|
lcd.<span class="c-fun">print</span>(<span class="c-str">"Umid: "</span>);
|
||||||
|
lcd.<span class="c-fun">print</span>(umidade, <span class="c-num">1</span>);
|
||||||
|
lcd.<span class="c-fun">print</span>(<span class="c-str">" % "</span>);
|
||||||
|
|
||||||
|
Serial.<span class="c-fun">printf</span>(<span class="c-str">"Temperatura: %.1f C | Umidade: %.1f %%\n"</span>, temperatura, umidade);
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="c-fun">delay</span>(<span class="c-num">2000</span>); <span class="c-com">// o DHT22 aceita no máximo uma leitura a cada 2 s</span>
|
||||||
|
}</code></pre>
|
||||||
|
<p>
|
||||||
|
A cada volta, o programa lê umidade e temperatura. Se qualquer uma vier como
|
||||||
|
<code>NaN</code>, a sigla de <em>not a number</em> com que a biblioteca avisa que a leitura
|
||||||
|
falhou, o display mostra "Falha no sensor". Caso contrário, os valores são escritos com
|
||||||
|
uma casa decimal. O <code>delay(2000)</code> respeita o intervalo mínimo do DHT22.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="caixa erro">
|
||||||
|
<div class="caixa-titulo">Restos de texto na tela</div>
|
||||||
|
<p>
|
||||||
|
O LCD só sobrescreve as posições em que você escreve; o resto continua como estava.
|
||||||
|
Se a temperatura cair de "26.5" para "-5.2" e depois para "8.0", o texto fica mais
|
||||||
|
curto e sobram caracteres da leitura anterior. Os espaços em <code>"C "</code> e
|
||||||
|
<code>" % "</code> existem para apagar essas sobras. Chamar <code>lcd.clear()</code> a
|
||||||
|
cada leitura também resolveria, mas faz a tela piscar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Os três testes feitos no Wokwi</h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th style="width:36%">Situação</th><th>O que o display mostrou</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="destaque">Sala, 26,5 °C e 62 %</td><td><img src="lcd-funcionando.png" alt="LCD mostrando Temp: 26.5°C e Umid: 62.0 %" style="height:24mm"></td></tr>
|
||||||
|
<tr><td class="destaque">Frio, −5,2 °C e 88,4 %</td><td><img src="lcd-frio.png" alt="LCD mostrando Temp: -5.2°C e Umid: 88.4 %" style="height:24mm"></td></tr>
|
||||||
|
<tr><td class="destaque">Fio de dados desconectado</td><td><img src="lcd-falha.png" alt="LCD mostrando Falha no sensor" style="height:24mm"></td></tr>
|
||||||
|
</tbody>
|
||||||
|
<caption>O terceiro teste confirma que o programa detecta a falha em vez de mostrar números inventados.</caption>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════ CAPÍTULO 7 ═══════════════════════════ -->
|
||||||
|
<section class="capitulo" id="cap7">
|
||||||
|
<div class="capitulo-abertura">
|
||||||
|
<div class="capitulo-numero">7</div>
|
||||||
|
<div>
|
||||||
|
<h2>Da simulação para a bancada</h2>
|
||||||
|
<p class="subtitulo">O que o Wokwi perdoa e o hardware de verdade não.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
O simulador é ideal para aprender e testar a lógica, mas ele modela os componentes como
|
||||||
|
peças perfeitas. Na bancada, três detalhes fazem diferença.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>1. Os pull-ups escondidos na plaquinha I2C</h3>
|
||||||
|
<p>
|
||||||
|
Quase toda plaquinha PCF8574 vendida no Brasil já traz <strong>resistores de pull-up
|
||||||
|
ligados ao VCC dela</strong>. Com o display alimentado em 5 V, esses resistores puxam SDA e
|
||||||
|
SCL para 5 V. Somar os nossos 4,7 kΩ para 3V3 não resolve: os dois resistores formam um
|
||||||
|
divisor, e a linha fica em torno de 4 V, ainda acima do que o ESP32-S3 suporta.
|
||||||
|
</p>
|
||||||
|
<p>Há duas saídas corretas:</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th style="width:32%">Solução</th><th>Como fazer</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="destaque">Conversor de nível (recomendado)</td>
|
||||||
|
<td>Um módulo bidirecional com BSS138, de quatro canais. O lado <strong>LV</strong> vai no 3V3 e nos GPIO8/9; o lado <strong>HV</strong> vai no 5V e no SDA/SCL do display. O módulo já tem pull-ups dos dois lados, então os 4,7 kΩ externos saem do circuito.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="destaque">Remover os pull-ups da plaquinha</td>
|
||||||
|
<td>Dessoldar os dois resistores ligados a SDA e SCL na plaquinha e manter os 4,7 kΩ para 3V3, exatamente como no Wokwi. O PCF8574 reconhece 3,3 V como nível alto mesmo alimentado em 5 V.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="caixa dica">
|
||||||
|
<div class="caixa-titulo">Como saber se a sua plaquinha tem pull-ups</div>
|
||||||
|
<p>
|
||||||
|
Com tudo desligado, meça com o multímetro a resistência entre o pino <strong>SDA</strong>
|
||||||
|
e o <strong>VCC</strong> da plaquinha. Algo entre 2 kΩ e 10 kΩ indica um pull-up. Se não
|
||||||
|
houver continuidade, ela não tem, e os 4,7 kΩ para 3V3 bastam.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="caixa atencao">
|
||||||
|
<div class="caixa-titulo">"Mas eu liguei direto e funcionou"</div>
|
||||||
|
<p>
|
||||||
|
É comum funcionar: o resistor limita a corrente e o ESP32 tem diodos de proteção que
|
||||||
|
desviam o excesso. Funcionar não é o mesmo que estar dentro da especificação. A Espressif
|
||||||
|
não garante os pinos acima de 3,6 V, e um circuito fora da especificação pode falhar
|
||||||
|
com o tempo ou em outra placa. Em projeto que vai ficar ligado, use o conversor.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>2. Descubra o endereço do display</h3>
|
||||||
|
<p>
|
||||||
|
Se o display acende mas não mostra nada mesmo com o contraste ajustado, o endereço pode
|
||||||
|
ser <code>0x3F</code> em vez de <code>0x27</code>. Este programa varre o barramento e
|
||||||
|
imprime o endereço de cada dispositivo que responder. No Wokwi ele encontra o display em
|
||||||
|
<code>0x27</code>:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre class="miudo"><code><span class="c-com">// Scanner I2C: lista o endereço de cada dispositivo no barramento.</span>
|
||||||
|
<span class="c-com">// Use antes do programa principal se o LCD não mostrar nada.</span>
|
||||||
|
|
||||||
|
<span class="c-pre">#include</span> <span class="c-str"><Wire.h></span>
|
||||||
|
|
||||||
|
<span class="c-key">void</span> <span class="c-fun">setup</span>() {
|
||||||
|
Serial.<span class="c-fun">begin</span>(<span class="c-num">115200</span>);
|
||||||
|
Wire.<span class="c-fun">begin</span>(<span class="c-num">8</span>, <span class="c-num">9</span>); <span class="c-com">// SDA, SCL</span>
|
||||||
|
<span class="c-fun">delay</span>(<span class="c-num">500</span>);
|
||||||
|
|
||||||
|
Serial.<span class="c-fun">println</span>(<span class="c-str">"Procurando dispositivos I2C..."</span>);
|
||||||
|
<span class="c-key">int</span> encontrados = <span class="c-num">0</span>;
|
||||||
|
<span class="c-key">for</span> (<span class="c-key">byte</span> endereco = <span class="c-num">1</span>; endereco < <span class="c-num">127</span>; endereco++) {
|
||||||
|
Wire.<span class="c-fun">beginTransmission</span>(endereco);
|
||||||
|
<span class="c-key">if</span> (Wire.<span class="c-fun">endTransmission</span>() == <span class="c-num">0</span>) {
|
||||||
|
Serial.<span class="c-fun">printf</span>(<span class="c-str">"Dispositivo em 0x%02X\n"</span>, endereco);
|
||||||
|
encontrados++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<span class="c-key">if</span> (encontrados == <span class="c-num">0</span>) Serial.<span class="c-fun">println</span>(<span class="c-str">"Nenhum dispositivo. Confira SDA, SCL e alimentacao."</span>);
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="c-key">void</span> <span class="c-fun">loop</span>() {}</code></pre>
|
||||||
|
|
||||||
|
<pre><code>Procurando dispositivos I2C...
|
||||||
|
Dispositivo em 0x27</code></pre>
|
||||||
|
|
||||||
|
<h3>3. Gravando na placa</h3>
|
||||||
|
<p>
|
||||||
|
Na IDE do Arduino, instale o suporte ao ESP32 e as bibliotecas <strong>DHT sensor
|
||||||
|
library</strong> (aceitando a dependência <strong>Adafruit Unified Sensor</strong>) e
|
||||||
|
<strong>LiquidCrystal I2C</strong>, de Frank de Brabander. Selecione a placa
|
||||||
|
<strong>ESP32S3 Dev Module</strong> e lembre do detalhe do módulo 1: na porta marcada
|
||||||
|
<strong>USB</strong>, a opção <em>USB CDC On Boot</em> precisa estar em
|
||||||
|
<strong>Enabled</strong> para o monitor serial funcionar; na porta <strong>UART</strong>,
|
||||||
|
em <strong>Disabled</strong>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════ CAPÍTULO 8 ═══════════════════════════ -->
|
||||||
|
<section class="capitulo" id="cap8">
|
||||||
|
<div class="capitulo-abertura">
|
||||||
|
<div class="capitulo-numero">8</div>
|
||||||
|
<div>
|
||||||
|
<h2>Problemas comuns e desafios</h2>
|
||||||
|
<p class="subtitulo">O que fazer quando não funciona, e para onde ir quando funcionar.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th style="width:34%">Sintoma</th><th>Causa provável</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td class="destaque">"Falha no sensor" o tempo todo</td><td>Fio de dados fora do GPIO12, falta do pull-up de 10 kΩ ou VCC e GND invertidos.</td></tr>
|
||||||
|
<tr><td class="destaque">Display aceso e sem texto</td><td>Contraste desajustado, endereço errado ou SDA e SCL trocados.</td></tr>
|
||||||
|
<tr><td class="destaque">Retângulos cheios na linha de cima</td><td>O display recebeu energia mas não foi inicializado: o I2C não está chegando.</td></tr>
|
||||||
|
<tr><td class="destaque">Display apagado</td><td>Falta o 5V no VCC do display ou o <code>lcd.backlight()</code>.</td></tr>
|
||||||
|
<tr><td class="destaque">Caracteres estranhos no fim da linha</td><td>Sobras da leitura anterior; veja a caixa "Restos de texto na tela".</td></tr>
|
||||||
|
<tr><td class="destaque">Erro de compilação com <code>DHT.h</code></td><td>Biblioteca não instalada, ou instalada sem a Adafruit Unified Sensor.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Desafios</h3>
|
||||||
|
<div class="passo">
|
||||||
|
<div class="passo-num">1</div>
|
||||||
|
<div><h3>Mínima e máxima</h3><p>Guarde a menor e a maior temperatura desde que a placa ligou e alterne a segunda linha entre a umidade e esses dois valores.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="passo">
|
||||||
|
<div class="passo-num">2</div>
|
||||||
|
<div><h3>Alerta de calor</h3><p>Ligue um LED, ou use o LED RGB que já existe no GPIO38 da placa, quando a temperatura passar de 30 °C. Faça a luz de fundo do display piscar junto.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="passo">
|
||||||
|
<div class="passo-num">3</div>
|
||||||
|
<div><h3>Sensação térmica</h3><p>A biblioteca DHT tem a função <code>computeHeatIndex()</code>. Mostre o índice de calor no lugar da umidade quando o ar estiver quente e úmido.</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="passo">
|
||||||
|
<div class="passo-num">4</div>
|
||||||
|
<div><h3>Sem <code>delay()</code></h3><p>Reescreva o <code>loop()</code> usando <code>millis()</code>, para que o programa possa fazer outras coisas, como ler um botão, entre uma leitura e outra.</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="caixa dica">
|
||||||
|
<div class="caixa-titulo">Próximo módulo</div>
|
||||||
|
<p>
|
||||||
|
A estação já mede e mostra. O passo natural é publicar esses dados: o ESP32-S3 tem
|
||||||
|
Wi-Fi, e com poucas linhas a mais a mesma leitura pode ir para uma página web ou para
|
||||||
|
um servidor MQTT.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="caixa dica">
|
||||||
|
<div class="caixa-titulo">Dúvidas, correções e contato</div>
|
||||||
|
<p>
|
||||||
|
Correções e sugestões são bem-vindas e ajudam as próximas turmas. Fale direto com o
|
||||||
|
autor por <a href="mailto:contato@lucasrguerra.dev.br">contato@lucasrguerra.dev.br</a>
|
||||||
|
ou pelo LinkedIn, em
|
||||||
|
<a href="https://linkedin.com/in/lucasrguerra">linkedin.com/in/lucasrguerra</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:12mm;padding-top:4mm;border-top:0.8pt solid var(--linha);font-size:8.5pt;color:var(--tinta-3);text-align:center">
|
||||||
|
LASER · Liga Acadêmica de Sistemas Embarcados · Centro de Informática · UFPE<br>
|
||||||
|
Formação em ESP32 · Módulo 2: Estação de Temperatura e Umidade
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.PagedConfig = {
|
||||||
|
before: () => document.querySelector(".aviso-carregando")?.remove(),
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script src="../assets/paged.polyfill.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"modulo": 2,
|
||||||
|
"titulo": ["Estação de", "temperatura e umidade"],
|
||||||
|
"subtitulo": "Um DHT22, um display LCD 16x2 e um ESP32-S3, simulados no Wokwi com o framework Arduino. Dois protocolos de comunicação, um programa curto e os cuidados elétricos que fazem o circuito funcionar também fora da tela.",
|
||||||
|
"descricao": "Slides, apostila, código e projeto do Wokwi do módulo 2 da formação em ESP32 da LASER: DHT22, LCD 16x2 I2C e ESP32-S3.",
|
||||||
|
"fichas": [["Duração", "2 horas"], ["Formato", "presencial"], ["Simulador", "Wokwi"], ["Framework", "Arduino"]],
|
||||||
|
"pratica": {
|
||||||
|
"titulo": "Projeto no Wokwi",
|
||||||
|
"texto": "O circuito e o código desta formação, testados no simulador: leitura normal, temperatura negativa e sensor desconectado.",
|
||||||
|
"imagem": "circuito-wokwi.png",
|
||||||
|
"meta": "4 arquivos · simule sem instalar nada",
|
||||||
|
"acoes": [["Baixar o projeto", "modulo-2-wokwi.zip", true], ["Abrir o Wokwi", "https://wokwi.com/projects/new/esp32-s3", false]]
|
||||||
|
},
|
||||||
|
"grupos": [
|
||||||
|
{ "titulo": "Material de aula", "arquivos": [
|
||||||
|
{ "arquivo": "slide.html", "tipo": "slides", "descricao": "Os slides da formação. Abrem no navegador, em tela cheia, prontos para projetar.", "abrir": true },
|
||||||
|
{ "arquivo": "apostila.html", "tipo": "apostila", "descricao": "A apostila completa, paginada em A4 no próprio navegador, com o passo a passo e o código comentado.", "abrir": true }
|
||||||
|
]},
|
||||||
|
{ "titulo": "Para baixar e imprimir", "arquivos": [
|
||||||
|
{ "arquivo": "LASER-ESP32-Modulo-2-Slides.pdf", "tipo": "pdf", "descricao": "Os slides em PDF 16:9, um slide por página.", "abrir": true, "baixar": true },
|
||||||
|
{ "arquivo": "LASER-ESP32-Modulo-2-Apostila.pdf", "tipo": "pdf", "descricao": "A apostila em PDF A4, pronta para imprimir.", "abrir": true, "baixar": true }
|
||||||
|
]},
|
||||||
|
{ "titulo": "Projeto do Wokwi", "arquivos": [
|
||||||
|
{ "arquivo": "modulo-2-wokwi.zip", "tipo": "zip", "descricao": "Os quatro arquivos abaixo num só pacote.", "baixar": true },
|
||||||
|
{ "arquivo": "wokwi/sketch.ino", "tipo": "codigo", "descricao": "O programa da estação: lê o DHT22 e escreve no LCD a cada 2 segundos.", "abrir": true, "baixar": true },
|
||||||
|
{ "arquivo": "wokwi/diagram.json", "tipo": "codigo", "descricao": "O circuito: peças, posições, fios e os três resistores de pull-up.", "abrir": true, "baixar": true },
|
||||||
|
{ "arquivo": "wokwi/libraries.txt", "tipo": "codigo", "descricao": "As bibliotecas que o Wokwi instala antes de compilar.", "abrir": true, "baixar": true },
|
||||||
|
{ "arquivo": "wokwi/scanner-i2c.ino", "tipo": "codigo", "descricao": "Diagnóstico: lista o endereço de cada dispositivo no barramento I2C.", "abrir": true, "baixar": true }
|
||||||
|
]},
|
||||||
|
{ "titulo": "Imagens da simulação", "arquivos": [
|
||||||
|
{ "arquivo": "circuito-wokwi.png", "tipo": "imagem", "descricao": "O circuito completo rodando no Wokwi.", "abrir": true },
|
||||||
|
{ "arquivo": "lcd-funcionando.png", "tipo": "imagem", "descricao": "Teste 1: leitura normal, 26,5 °C e 62 %.", "abrir": true },
|
||||||
|
{ "arquivo": "lcd-frio.png", "tipo": "imagem", "descricao": "Teste 2: temperatura negativa, −5,2 °C e 88,4 %.", "abrir": true },
|
||||||
|
{ "arquivo": "lcd-falha.png", "tipo": "imagem", "descricao": "Teste 3: fio de dados desconectado, erro detectado.", "abrir": true }
|
||||||
|
]}
|
||||||
|
],
|
||||||
|
"guias": [
|
||||||
|
{ "titulo": "Rodando no Wokwi", "itens": [
|
||||||
|
"Abra <a href=\"https://wokwi.com/projects/new/esp32-s3\">wokwi.com/projects/new/esp32-s3</a>. Não precisa de conta.",
|
||||||
|
"Cole o <code>sketch.ino</code> e o <code>diagram.json</code> nas abas de mesmo nome.",
|
||||||
|
"Em <i>Library Manager</i>, adicione <b>DHT sensor library</b> e <b>LiquidCrystal I2C</b>.",
|
||||||
|
"Clique no botão verde. Com a simulação rodando, clique no DHT22 para mudar temperatura e umidade."
|
||||||
|
]},
|
||||||
|
{ "titulo": "Montando na bancada", "itens": [
|
||||||
|
"Pull-up de <b>10 kΩ</b> no DATA do DHT22 e de <b>4,7 kΩ</b> no SDA e no SCL, todos para o <b>3V3</b>.",
|
||||||
|
"A plaquinha I2C do LCD costuma trazer pull-ups para 5 V: use um <b>conversor de nível BSS138</b> ou retire esses resistores.",
|
||||||
|
"Tela vazia? Gire o trimpot de contraste e rode o <code>scanner-i2c.ino</code> para conferir o endereço.",
|
||||||
|
"Os detalhes estão no capítulo 7 da apostila."
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,572 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>LASER · Formação em ESP32 · Módulo 2: Estação de temperatura e umidade</title>
|
||||||
|
<meta name="description" content="Slides, apostila, código e projeto do Wokwi do módulo 2 da formação em ESP32 da LASER: DHT22, LCD 16x2 I2C e ESP32-S3.">
|
||||||
|
<meta name="author" content="Lucas Rayan Guerra">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/marca/espdocs-marca.svg">
|
||||||
|
<link rel="stylesheet" href="/formacoes/assets/viewer.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Gerado por scripts/formacoes-viewer.mjs a partir do formacao.json. Não edite à mão. -->
|
||||||
|
|
||||||
|
<header class="topo">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<a href="/formacoes"><img src="/formacoes/assets/laser.svg" alt="LASER"></a>
|
||||||
|
<span class="topo-liga">Liga Acadêmica de Sistemas Embarcados<br>Centro de Informática · UFPE</span>
|
||||||
|
<nav><a href="#arquivos">Arquivos</a><a href="#como-usar">Como usar</a><a href="/formacoes">Formações</a></nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="abertura">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<div>
|
||||||
|
<div class="selo">Formação em ESP32 · Módulo 2</div>
|
||||||
|
<h1>Estação de<strong>temperatura e umidade</strong></h1>
|
||||||
|
<p class="abertura-sub">Um DHT22, um display LCD 16x2 e um ESP32-S3, simulados no Wokwi com o framework Arduino. Dois protocolos de comunicação, um programa curto e os cuidados elétricos que fazem o circuito funcionar também fora da tela.</p>
|
||||||
|
<ul class="fichas"><li>Duração: <b>2 horas</b></li><li>Formato: <b>presencial</b></li><li>Simulador: <b>Wokwi</b></li><li>Framework: <b>Arduino</b></li></ul>
|
||||||
|
<div class="acoes">
|
||||||
|
<a class="botao primario" href="/formacoes/2/slide.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 5v14l11-7z"/></svg>Apresentar os slides</a>
|
||||||
|
<a class="botao" href="/formacoes/2/apostila.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 6c-2-1.5-5-2-8-1.5v14c3-.5 6 0 8 1.5 2-1.5 5-2 8-1.5v-14c-3-.5-6 0-8 1.5zM12 6v14"/></svg>Ler a apostila</a>
|
||||||
|
<a class="botao" href="#baixar"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v12M6 10l6 6 6-6M5 20h14"/></svg>Baixar em PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="previas">
|
||||||
|
<a class="p-apostila" href="/formacoes/2/apostila.html" aria-label="Abrir a apostila"><img src="/formacoes/2/previa-apostila.jpg" alt="Capa da apostila do módulo 2" width="640" height="905"></a>
|
||||||
|
<a class="p-slides" href="/formacoes/2/slide.html" aria-label="Abrir os slides"><img src="/formacoes/2/previa-slides.jpg" alt="Capa dos slides do módulo 2" width="960" height="540"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<hr class="feixe">
|
||||||
|
|
||||||
|
<section class="bloco" id="baixar">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<p class="rotulo">Comece por aqui</p>
|
||||||
|
<h2>Três formas de usar este material</h2>
|
||||||
|
<p class="bloco-sub">Para apresentar, para estudar e para pôr a mão no circuito. Os PDFs são a versão final, para quem só quer o material pronto.</p>
|
||||||
|
<div class="portas">
|
||||||
|
<article class="porta">
|
||||||
|
<a class="porta-imagem" href="/formacoes/2/slide.html"><img src="/formacoes/2/previa-slides.jpg" alt="" loading="lazy"></a>
|
||||||
|
<div class="porta-corpo">
|
||||||
|
<h3>Slides</h3>
|
||||||
|
<p>31 slides para projetar direto do navegador, com navegação por teclado e panorama.</p>
|
||||||
|
<div class="porta-meta">PDF · 31 págs · 1,6 MB</div>
|
||||||
|
<div class="acoes">
|
||||||
|
<a class="botao pequeno primario" href="/formacoes/2/slide.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 5v14l11-7z"/></svg>Apresentar</a>
|
||||||
|
<a class="botao pequeno" href="/formacoes/2/LASER-ESP32-Modulo-2-Slides.pdf" download><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v12M6 10l6 6 6-6M5 20h14"/></svg>PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="porta">
|
||||||
|
<a class="porta-imagem" href="/formacoes/2/apostila.html"><img src="/formacoes/2/previa-apostila.jpg" alt="" loading="lazy"></a>
|
||||||
|
<div class="porta-corpo">
|
||||||
|
<h3>Apostila</h3>
|
||||||
|
<p>O conteúdo completo, com explicações, tabelas e o código comentado, paginado em A4.</p>
|
||||||
|
<div class="porta-meta">PDF · 20 págs · 761 KB</div>
|
||||||
|
<div class="acoes">
|
||||||
|
<a class="botao pequeno primario" href="/formacoes/2/apostila.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 6c-2-1.5-5-2-8-1.5v14c3-.5 6 0 8 1.5 2-1.5 5-2 8-1.5v-14c-3-.5-6 0-8 1.5zM12 6v14"/></svg>Ler</a>
|
||||||
|
<a class="botao pequeno" href="/formacoes/2/LASER-ESP32-Modulo-2-Apostila.pdf" download><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v12M6 10l6 6 6-6M5 20h14"/></svg>PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="porta">
|
||||||
|
<a class="porta-imagem clara" href="/formacoes/2/circuito-wokwi.png" target="_blank" rel="noopener"><img src="/formacoes/2/circuito-wokwi.png" alt="" loading="lazy"></a>
|
||||||
|
<div class="porta-corpo">
|
||||||
|
<h3>Projeto no Wokwi</h3>
|
||||||
|
<p>O circuito e o código desta formação, testados no simulador: leitura normal, temperatura negativa e sensor desconectado.</p>
|
||||||
|
<div class="porta-meta">4 arquivos · simule sem instalar nada</div>
|
||||||
|
<div class="acoes"><a class="botao pequeno primario" href="/formacoes/2/modulo-2-wokwi.zip" download><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v12M6 10l6 6 6-6M5 20h14"/></svg>Baixar o projeto</a><a class="botao pequeno" href="https://wokwi.com/projects/new/esp32-s3" rel="noopener"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>Abrir o Wokwi</a></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="bloco" id="roteiro">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<p class="rotulo">Roteiro da aula</p>
|
||||||
|
<h2>6 partes, 31 slides</h2>
|
||||||
|
<p class="bloco-sub">Cada parte abre os slides direto no ponto em que ela começa.</p>
|
||||||
|
<ol class="roteiro">
|
||||||
|
<li><a href="/formacoes/2/slide.html#4"><b>1</b><span>O DHT22<small>slide 4 · 5 slides</small></span></a></li>
|
||||||
|
<li><a href="/formacoes/2/slide.html#9"><b>2</b><span>LCD e I2C<small>slide 9 · 5 slides</small></span></a></li>
|
||||||
|
<li><a href="/formacoes/2/slide.html#14"><b>3</b><span>Wokwi<small>slide 14 · 3 slides</small></span></a></li>
|
||||||
|
<li><a href="/formacoes/2/slide.html#17"><b>4</b><span>Montagem<small>slide 17 · 2 slides</small></span></a></li>
|
||||||
|
<li><a href="/formacoes/2/slide.html#19"><b>5</b><span>O programa<small>slide 19 · 7 slides</small></span></a></li>
|
||||||
|
<li><a href="/formacoes/2/slide.html#26"><b>6</b><span>Para a bancada<small>slide 26 · 6 slides</small></span></a></li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="bloco" id="arquivos">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<p class="rotulo">Todos os arquivos</p>
|
||||||
|
<h2>O que tem nesta formação</h2>
|
||||||
|
<p class="bloco-sub">Tudo o que foi usado na aula, com o que é cada arquivo e para que serve.</p>
|
||||||
|
|
||||||
|
<div class="grupo-arquivos">
|
||||||
|
<h3>Material de aula</h3>
|
||||||
|
<ul class="arquivos">
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone "><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5h18v12H3z"/><path d="M8 21h8M12 17v4"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">slide.html</div>
|
||||||
|
<div class="arquivo-desc">Os slides da formação. Abrem no navegador, em tela cheia, prontos para projetar.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">72 KB</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/slide.html">Abrir</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone "><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4.5A1.5 1.5 0 0 1 5.5 3H20v16H5.5A1.5 1.5 0 0 0 4 20.5z"/><path d="M4 20.5A1.5 1.5 0 0 0 5.5 22H20"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">apostila.html</div>
|
||||||
|
<div class="arquivo-desc">A apostila completa, paginada em A4 no próprio navegador, com o passo a passo e o código comentado.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">43 KB</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/apostila.html">Abrir</a></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="grupo-arquivos">
|
||||||
|
<h3>Para baixar e imprimir</h3>
|
||||||
|
<ul class="arquivos">
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone pdf"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H6v18h12V7z"/><path d="M14 3v4h4"/><path d="M9 13h6M9 17h4"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">LASER-ESP32-Modulo-2-Slides.pdf</div>
|
||||||
|
<div class="arquivo-desc">Os slides em PDF 16:9, um slide por página.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">31 págs · 1,6 MB</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/LASER-ESP32-Modulo-2-Slides.pdf" target="_blank" rel="noopener">Abrir</a><a href="/formacoes/2/LASER-ESP32-Modulo-2-Slides.pdf" download>Baixar</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone pdf"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H6v18h12V7z"/><path d="M14 3v4h4"/><path d="M9 13h6M9 17h4"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">LASER-ESP32-Modulo-2-Apostila.pdf</div>
|
||||||
|
<div class="arquivo-desc">A apostila em PDF A4, pronta para imprimir.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">20 págs · 761 KB</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/LASER-ESP32-Modulo-2-Apostila.pdf" target="_blank" rel="noopener">Abrir</a><a href="/formacoes/2/LASER-ESP32-Modulo-2-Apostila.pdf" download>Baixar</a></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="grupo-arquivos">
|
||||||
|
<h3>Projeto do Wokwi</h3>
|
||||||
|
<ul class="arquivos">
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone codigo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H6v18h12V7z"/><path d="M14 3v4h4"/><path d="M11 6h1M11 9h1M11 12h1M10.5 15h2v3h-2z"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">modulo-2-wokwi.zip</div>
|
||||||
|
<div class="arquivo-desc">Os quatro arquivos abaixo num só pacote.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">2 KB</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/modulo-2-wokwi.zip" download>Baixar</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone codigo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 8-4 4 4 4M16 8l4 4-4 4M13.5 5l-3 14"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">sketch.ino</div>
|
||||||
|
<div class="arquivo-desc">O programa da estação: lê o DHT22 e escreve no LCD a cada 2 segundos.</div>
|
||||||
|
<details class="codigo-fonte"><summary>Ver código</summary><pre><code>// Formação em ESP32 · LASER · Módulo 2
|
||||||
|
// Estação de temperatura e umidade: DHT22 + LCD 16x2 I2C + ESP32-S3
|
||||||
|
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <DHT.h>
|
||||||
|
#include <LiquidCrystal_I2C.h>
|
||||||
|
|
||||||
|
#define PINO_DHT 12 // fio amarelo
|
||||||
|
#define TIPO_DHT DHT22
|
||||||
|
#define PINO_SDA 8 // I2C padrão do ESP32-S3
|
||||||
|
#define PINO_SCL 9
|
||||||
|
|
||||||
|
DHT dht(PINO_DHT, TIPO_DHT);
|
||||||
|
LiquidCrystal_I2C lcd(0x27, 16, 2); // endereço, colunas, linhas
|
||||||
|
|
||||||
|
// Símbolo de grau: 8 linhas de 5 pixels
|
||||||
|
byte grau[8] = { 0b01100, 0b10010, 0b10010, 0b01100, 0, 0, 0, 0 };
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
Wire.begin(PINO_SDA, PINO_SCL);
|
||||||
|
dht.begin();
|
||||||
|
|
||||||
|
lcd.init();
|
||||||
|
lcd.backlight();
|
||||||
|
lcd.createChar(0, grau);
|
||||||
|
lcd.setCursor(0, 0);
|
||||||
|
lcd.print("LASER ESP32-S3");
|
||||||
|
lcd.setCursor(0, 1);
|
||||||
|
lcd.print("Iniciando...");
|
||||||
|
delay(2000); // o DHT22 precisa de ~2 s para a primeira leitura
|
||||||
|
lcd.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
float umidade = dht.readHumidity();
|
||||||
|
float temperatura = dht.readTemperature();
|
||||||
|
|
||||||
|
if (isnan(umidade) || isnan(temperatura)) {
|
||||||
|
lcd.setCursor(0, 0);
|
||||||
|
lcd.print("Falha no sensor ");
|
||||||
|
Serial.println("Falha na leitura do DHT22");
|
||||||
|
} else {
|
||||||
|
lcd.setCursor(0, 0);
|
||||||
|
lcd.print("Temp: ");
|
||||||
|
lcd.print(temperatura, 1);
|
||||||
|
lcd.write(0); // o caractere de grau criado acima
|
||||||
|
lcd.print("C ");
|
||||||
|
|
||||||
|
lcd.setCursor(0, 1);
|
||||||
|
lcd.print("Umid: ");
|
||||||
|
lcd.print(umidade, 1);
|
||||||
|
lcd.print(" % ");
|
||||||
|
|
||||||
|
Serial.printf("Temperatura: %.1f C | Umidade: %.1f %%\n", temperatura, umidade);
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(2000); // o DHT22 aceita no máximo uma leitura a cada 2 s
|
||||||
|
}</code></pre></details>
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">2 KB</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/wokwi/sketch.ino" download>Baixar</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone codigo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 8-4 4 4 4M16 8l4 4-4 4M13.5 5l-3 14"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">diagram.json</div>
|
||||||
|
<div class="arquivo-desc">O circuito: peças, posições, fios e os três resistores de pull-up.</div>
|
||||||
|
<details class="codigo-fonte"><summary>Ver código</summary><pre><code>{
|
||||||
|
"version": 1,
|
||||||
|
"author": "Lucas Rayan Guerra",
|
||||||
|
"editor": "wokwi",
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "board-esp32-s3-devkitc-1",
|
||||||
|
"id": "esp",
|
||||||
|
"top": 0,
|
||||||
|
"left": 0,
|
||||||
|
"attrs": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "wokwi-dht22",
|
||||||
|
"id": "dht",
|
||||||
|
"top": 15,
|
||||||
|
"left": -250,
|
||||||
|
"attrs": {
|
||||||
|
"temperature": "26.5",
|
||||||
|
"humidity": "62"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "wokwi-lcd1602",
|
||||||
|
"id": "lcd",
|
||||||
|
"top": 320,
|
||||||
|
"left": -60,
|
||||||
|
"attrs": {
|
||||||
|
"pins": "i2c"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "wokwi-resistor",
|
||||||
|
"id": "r_dht",
|
||||||
|
"top": 9,
|
||||||
|
"left": -173,
|
||||||
|
"rotate": 90,
|
||||||
|
"attrs": {
|
||||||
|
"value": "10000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "wokwi-resistor",
|
||||||
|
"id": "r_sda",
|
||||||
|
"top": 9,
|
||||||
|
"left": -83,
|
||||||
|
"rotate": 90,
|
||||||
|
"attrs": {
|
||||||
|
"value": "4700"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "wokwi-resistor",
|
||||||
|
"id": "r_scl",
|
||||||
|
"top": 9,
|
||||||
|
"left": -63,
|
||||||
|
"rotate": 90,
|
||||||
|
"attrs": {
|
||||||
|
"value": "4700"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
[
|
||||||
|
"esp:TX",
|
||||||
|
"$serialMonitor:RX",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"esp:RX",
|
||||||
|
"$serialMonitor:TX",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"lcd:VCC",
|
||||||
|
"esp:5V",
|
||||||
|
"red",
|
||||||
|
[
|
||||||
|
"h-20",
|
||||||
|
"v-100"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"lcd:GND",
|
||||||
|
"esp:GND.1",
|
||||||
|
"black",
|
||||||
|
[
|
||||||
|
"h-30",
|
||||||
|
"v-90"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"lcd:SDA",
|
||||||
|
"esp:8",
|
||||||
|
"blue",
|
||||||
|
[
|
||||||
|
"h-40",
|
||||||
|
"v-200"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"lcd:SCL",
|
||||||
|
"esp:9",
|
||||||
|
"purple",
|
||||||
|
[
|
||||||
|
"h-50",
|
||||||
|
"v-170"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dht:VCC",
|
||||||
|
"r_dht:1",
|
||||||
|
"red",
|
||||||
|
[
|
||||||
|
"v12",
|
||||||
|
"h-25",
|
||||||
|
"v-159",
|
||||||
|
"h100"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dht:SDA",
|
||||||
|
"esp:12",
|
||||||
|
"gold",
|
||||||
|
[
|
||||||
|
"v77"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dht:GND",
|
||||||
|
"esp:GND.1",
|
||||||
|
"black",
|
||||||
|
[
|
||||||
|
"v118"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_dht:1",
|
||||||
|
"esp:3V3.1",
|
||||||
|
"red",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_dht:2",
|
||||||
|
"esp:12",
|
||||||
|
"gold",
|
||||||
|
[
|
||||||
|
"v181"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_sda:1",
|
||||||
|
"esp:3V3.1",
|
||||||
|
"red",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_sda:2",
|
||||||
|
"esp:8",
|
||||||
|
"blue",
|
||||||
|
[
|
||||||
|
"v124"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_scl:1",
|
||||||
|
"esp:3V3.1",
|
||||||
|
"red",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_scl:2",
|
||||||
|
"esp:9",
|
||||||
|
"purple",
|
||||||
|
[
|
||||||
|
"v152"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"dependencies": {}
|
||||||
|
}</code></pre></details>
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">2 KB</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/wokwi/diagram.json" download>Baixar</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone codigo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 8-4 4 4 4M16 8l4 4-4 4M13.5 5l-3 14"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">libraries.txt</div>
|
||||||
|
<div class="arquivo-desc">As bibliotecas que o Wokwi instala antes de compilar.</div>
|
||||||
|
<details class="codigo-fonte"><summary>Ver código</summary><pre><code>DHT sensor library
|
||||||
|
Adafruit Unified Sensor
|
||||||
|
LiquidCrystal I2C</code></pre></details>
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">61 B</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/wokwi/libraries.txt" download>Baixar</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone codigo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 8-4 4 4 4M16 8l4 4-4 4M13.5 5l-3 14"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">scanner-i2c.ino</div>
|
||||||
|
<div class="arquivo-desc">Diagnóstico: lista o endereço de cada dispositivo no barramento I2C.</div>
|
||||||
|
<details class="codigo-fonte"><summary>Ver código</summary><pre><code>// Scanner I2C: lista o endereço de cada dispositivo no barramento.
|
||||||
|
// Use antes do programa principal se o LCD não mostrar nada.
|
||||||
|
|
||||||
|
#include <Wire.h>
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
Wire.begin(8, 9); // SDA, SCL
|
||||||
|
delay(500);
|
||||||
|
|
||||||
|
Serial.println("Procurando dispositivos I2C...");
|
||||||
|
int encontrados = 0;
|
||||||
|
for (byte endereco = 1; endereco < 127; endereco++) {
|
||||||
|
Wire.beginTransmission(endereco);
|
||||||
|
if (Wire.endTransmission() == 0) {
|
||||||
|
Serial.printf("Dispositivo em 0x%02X\n", endereco);
|
||||||
|
encontrados++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (encontrados == 0) Serial.println("Nenhum dispositivo. Confira SDA, SCL e alimentacao.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {}</code></pre></details>
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">664 B</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/wokwi/scanner-i2c.ino" download>Baixar</a></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="grupo-arquivos">
|
||||||
|
<h3>Imagens da simulação</h3>
|
||||||
|
<ul class="arquivos">
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone imagem"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="2"/><path d="m21 17-5-5-9 8"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">circuito-wokwi.png</div>
|
||||||
|
<div class="arquivo-desc">O circuito completo rodando no Wokwi.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">44 KB</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/circuito-wokwi.png" target="_blank" rel="noopener">Abrir</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone imagem"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="2"/><path d="m21 17-5-5-9 8"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">lcd-funcionando.png</div>
|
||||||
|
<div class="arquivo-desc">Teste 1: leitura normal, 26,5 °C e 62 %.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">8 KB</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/lcd-funcionando.png" target="_blank" rel="noopener">Abrir</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone imagem"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="2"/><path d="m21 17-5-5-9 8"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">lcd-frio.png</div>
|
||||||
|
<div class="arquivo-desc">Teste 2: temperatura negativa, −5,2 °C e 88,4 %.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">8 KB</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/lcd-frio.png" target="_blank" rel="noopener">Abrir</a></div>
|
||||||
|
</li>
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone imagem"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="2"/><path d="m21 17-5-5-9 8"/></svg></div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">lcd-falha.png</div>
|
||||||
|
<div class="arquivo-desc">Teste 3: fio de dados desconectado, erro detectado.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">7 KB</div>
|
||||||
|
<div class="arquivo-acoes"><a href="/formacoes/2/lcd-falha.png" target="_blank" rel="noopener">Abrir</a></div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="bloco" id="como-usar">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<p class="rotulo">Como usar</p>
|
||||||
|
<h2>Passo a passo</h2>
|
||||||
|
<div class="guias">
|
||||||
|
<div class="guia">
|
||||||
|
<h3>Apresentando os slides</h3>
|
||||||
|
<p>Abra <a href="/formacoes/2/slide.html">slide.html</a> no navegador. O palco é 16:9 e se ajusta a qualquer tela ou projetor.</p>
|
||||||
|
<dl class="atalhos">
|
||||||
|
<dt><kbd>→</kbd><kbd>←</kbd></dt><dd>avançar e voltar (ou clique nas metades da tela)</dd>
|
||||||
|
<dt><kbd>F</kbd></dt><dd>tela cheia</dd>
|
||||||
|
<dt><kbd>O</kbd></dt><dd>panorama com todos os slides</dd>
|
||||||
|
<dt><kbd>B</kbd></dt><dd>tela preta, para pausar a projeção</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class="guia">
|
||||||
|
<h3>Lendo e imprimindo a apostila</h3>
|
||||||
|
<p>A <a href="/formacoes/2/apostila.html">versão online</a> se pagina em A4 sozinha ao abrir, o que leva alguns segundos.</p>
|
||||||
|
<p>Para papel, prefira o <a href="/formacoes/2/LASER-ESP32-Modulo-2-Apostila.pdf" download>PDF pronto</a>: ele já sai com margens, cores e quebras de página certas, sem configurar a impressora.</p>
|
||||||
|
</div>
|
||||||
|
<div class="guia">
|
||||||
|
<h3>Rodando no Wokwi</h3>
|
||||||
|
<ol><li>Abra <a href="https://wokwi.com/projects/new/esp32-s3">wokwi.com/projects/new/esp32-s3</a>. Não precisa de conta.</li><li>Cole o <code>sketch.ino</code> e o <code>diagram.json</code> nas abas de mesmo nome.</li><li>Em <i>Library Manager</i>, adicione <b>DHT sensor library</b> e <b>LiquidCrystal I2C</b>.</li><li>Clique no botão verde. Com a simulação rodando, clique no DHT22 para mudar temperatura e umidade.</li></ol>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="guia">
|
||||||
|
<h3>Montando na bancada</h3>
|
||||||
|
<ol><li>Pull-up de <b>10 kΩ</b> no DATA do DHT22 e de <b>4,7 kΩ</b> no SDA e no SCL, todos para o <b>3V3</b>.</li><li>A plaquinha I2C do LCD costuma trazer pull-ups para 5 V: use um <b>conversor de nível BSS138</b> ou retire esses resistores.</li><li>Tela vazia? Gire o trimpot de contraste e rode o <code>scanner-i2c.ino</code> para conferir o endereço.</li><li>Os detalhes estão no capítulo 7 da apostila.</li></ol>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="rodape">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<span><strong>LASER</strong> · Liga Acadêmica de Sistemas Embarcados · CIn · UFPE</span>
|
||||||
|
<span>Material de <strong>Lucas Rayan Guerra</strong> · uso livre para fins educacionais · <a href="mailto:contato@lucasrguerra.dev.br">contato@lucasrguerra.dev.br</a></span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,183 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"author": "Lucas Rayan Guerra",
|
||||||
|
"editor": "wokwi",
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"type": "board-esp32-s3-devkitc-1",
|
||||||
|
"id": "esp",
|
||||||
|
"top": 0,
|
||||||
|
"left": 0,
|
||||||
|
"attrs": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "wokwi-dht22",
|
||||||
|
"id": "dht",
|
||||||
|
"top": 15,
|
||||||
|
"left": -250,
|
||||||
|
"attrs": {
|
||||||
|
"temperature": "26.5",
|
||||||
|
"humidity": "62"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "wokwi-lcd1602",
|
||||||
|
"id": "lcd",
|
||||||
|
"top": 320,
|
||||||
|
"left": -60,
|
||||||
|
"attrs": {
|
||||||
|
"pins": "i2c"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "wokwi-resistor",
|
||||||
|
"id": "r_dht",
|
||||||
|
"top": 9,
|
||||||
|
"left": -173,
|
||||||
|
"rotate": 90,
|
||||||
|
"attrs": {
|
||||||
|
"value": "10000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "wokwi-resistor",
|
||||||
|
"id": "r_sda",
|
||||||
|
"top": 9,
|
||||||
|
"left": -83,
|
||||||
|
"rotate": 90,
|
||||||
|
"attrs": {
|
||||||
|
"value": "4700"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "wokwi-resistor",
|
||||||
|
"id": "r_scl",
|
||||||
|
"top": 9,
|
||||||
|
"left": -63,
|
||||||
|
"rotate": 90,
|
||||||
|
"attrs": {
|
||||||
|
"value": "4700"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": [
|
||||||
|
[
|
||||||
|
"esp:TX",
|
||||||
|
"$serialMonitor:RX",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"esp:RX",
|
||||||
|
"$serialMonitor:TX",
|
||||||
|
"",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"lcd:VCC",
|
||||||
|
"esp:5V",
|
||||||
|
"red",
|
||||||
|
[
|
||||||
|
"h-20",
|
||||||
|
"v-100"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"lcd:GND",
|
||||||
|
"esp:GND.1",
|
||||||
|
"black",
|
||||||
|
[
|
||||||
|
"h-30",
|
||||||
|
"v-90"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"lcd:SDA",
|
||||||
|
"esp:8",
|
||||||
|
"blue",
|
||||||
|
[
|
||||||
|
"h-40",
|
||||||
|
"v-200"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"lcd:SCL",
|
||||||
|
"esp:9",
|
||||||
|
"purple",
|
||||||
|
[
|
||||||
|
"h-50",
|
||||||
|
"v-170"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dht:VCC",
|
||||||
|
"r_dht:1",
|
||||||
|
"red",
|
||||||
|
[
|
||||||
|
"v12",
|
||||||
|
"h-25",
|
||||||
|
"v-159",
|
||||||
|
"h100"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dht:SDA",
|
||||||
|
"esp:12",
|
||||||
|
"gold",
|
||||||
|
[
|
||||||
|
"v77"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"dht:GND",
|
||||||
|
"esp:GND.1",
|
||||||
|
"black",
|
||||||
|
[
|
||||||
|
"v118"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_dht:1",
|
||||||
|
"esp:3V3.1",
|
||||||
|
"red",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_dht:2",
|
||||||
|
"esp:12",
|
||||||
|
"gold",
|
||||||
|
[
|
||||||
|
"v181"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_sda:1",
|
||||||
|
"esp:3V3.1",
|
||||||
|
"red",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_sda:2",
|
||||||
|
"esp:8",
|
||||||
|
"blue",
|
||||||
|
[
|
||||||
|
"v124"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_scl:1",
|
||||||
|
"esp:3V3.1",
|
||||||
|
"red",
|
||||||
|
[]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"r_scl:2",
|
||||||
|
"esp:9",
|
||||||
|
"purple",
|
||||||
|
[
|
||||||
|
"v152"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
DHT sensor library
|
||||||
|
Adafruit Unified Sensor
|
||||||
|
LiquidCrystal I2C
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Scanner I2C: lista o endereço de cada dispositivo no barramento.
|
||||||
|
// Use antes do programa principal se o LCD não mostrar nada.
|
||||||
|
|
||||||
|
#include <Wire.h>
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
Wire.begin(8, 9); // SDA, SCL
|
||||||
|
delay(500);
|
||||||
|
|
||||||
|
Serial.println("Procurando dispositivos I2C...");
|
||||||
|
int encontrados = 0;
|
||||||
|
for (byte endereco = 1; endereco < 127; endereco++) {
|
||||||
|
Wire.beginTransmission(endereco);
|
||||||
|
if (Wire.endTransmission() == 0) {
|
||||||
|
Serial.printf("Dispositivo em 0x%02X\n", endereco);
|
||||||
|
encontrados++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (encontrados == 0) Serial.println("Nenhum dispositivo. Confira SDA, SCL e alimentacao.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// Formação em ESP32 · LASER · Módulo 2
|
||||||
|
// Estação de temperatura e umidade: DHT22 + LCD 16x2 I2C + ESP32-S3
|
||||||
|
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <DHT.h>
|
||||||
|
#include <LiquidCrystal_I2C.h>
|
||||||
|
|
||||||
|
#define PINO_DHT 12 // fio amarelo
|
||||||
|
#define TIPO_DHT DHT22
|
||||||
|
#define PINO_SDA 8 // I2C padrão do ESP32-S3
|
||||||
|
#define PINO_SCL 9
|
||||||
|
|
||||||
|
DHT dht(PINO_DHT, TIPO_DHT);
|
||||||
|
LiquidCrystal_I2C lcd(0x27, 16, 2); // endereço, colunas, linhas
|
||||||
|
|
||||||
|
// Símbolo de grau: 8 linhas de 5 pixels
|
||||||
|
byte grau[8] = { 0b01100, 0b10010, 0b10010, 0b01100, 0, 0, 0, 0 };
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
Wire.begin(PINO_SDA, PINO_SCL);
|
||||||
|
dht.begin();
|
||||||
|
|
||||||
|
lcd.init();
|
||||||
|
lcd.backlight();
|
||||||
|
lcd.createChar(0, grau);
|
||||||
|
lcd.setCursor(0, 0);
|
||||||
|
lcd.print("LASER ESP32-S3");
|
||||||
|
lcd.setCursor(0, 1);
|
||||||
|
lcd.print("Iniciando...");
|
||||||
|
delay(2000); // o DHT22 precisa de ~2 s para a primeira leitura
|
||||||
|
lcd.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
float umidade = dht.readHumidity();
|
||||||
|
float temperatura = dht.readTemperature();
|
||||||
|
|
||||||
|
if (isnan(umidade) || isnan(temperatura)) {
|
||||||
|
lcd.setCursor(0, 0);
|
||||||
|
lcd.print("Falha no sensor ");
|
||||||
|
Serial.println("Falha na leitura do DHT22");
|
||||||
|
} else {
|
||||||
|
lcd.setCursor(0, 0);
|
||||||
|
lcd.print("Temp: ");
|
||||||
|
lcd.print(temperatura, 1);
|
||||||
|
lcd.write(0); // o caractere de grau criado acima
|
||||||
|
lcd.print("C ");
|
||||||
|
|
||||||
|
lcd.setCursor(0, 1);
|
||||||
|
lcd.print("Umid: ");
|
||||||
|
lcd.print(umidade, 1);
|
||||||
|
lcd.print(" % ");
|
||||||
|
|
||||||
|
Serial.printf("Temperatura: %.1f C | Umidade: %.1f %%\n", temperatura, umidade);
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(2000); // o DHT22 aceita no máximo uma leitura a cada 2 s
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
/* =========================================================================
|
||||||
|
Página de entrada de cada formação (/formacoes/N).
|
||||||
|
|
||||||
|
Mesma identidade dos slides: base escura tirada do prussiano da marca, o
|
||||||
|
feixe como traço de assinatura e as três famílias tipográficas da LASER.
|
||||||
|
A página é gerada por scripts/formacoes-viewer.mjs a partir do
|
||||||
|
formacao.json de cada pasta; não edite o index.html à mão.
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
|
--breu: #041d2b;
|
||||||
|
--breu-2: #06283a;
|
||||||
|
--breu-3: #0a3a53;
|
||||||
|
--prussiano: #003049;
|
||||||
|
--vermelho: #D62828;
|
||||||
|
--laranja: #F77F00;
|
||||||
|
--ambar: #FCBF49;
|
||||||
|
--creme: #EAE2B7;
|
||||||
|
|
||||||
|
--gelo: #f2f8fb;
|
||||||
|
--texto: #cddde6;
|
||||||
|
--texto-2: #93aebd;
|
||||||
|
--linha: rgba(146, 187, 207, 0.18);
|
||||||
|
--linha-forte: rgba(146, 187, 207, 0.38);
|
||||||
|
--painel: rgba(255, 255, 255, 0.04);
|
||||||
|
--painel-2: rgba(255, 255, 255, 0.07);
|
||||||
|
|
||||||
|
--feixe: linear-gradient(90deg, #1b6d92, var(--creme), var(--ambar), var(--laranja), var(--vermelho));
|
||||||
|
|
||||||
|
--display: "Outfit", system-ui, sans-serif;
|
||||||
|
--corpo: "Inter", system-ui, sans-serif;
|
||||||
|
--mono: "JetBrains Mono", ui-monospace, monospace;
|
||||||
|
|
||||||
|
--largura: 1120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--breu);
|
||||||
|
color: var(--texto);
|
||||||
|
font-family: var(--corpo);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
background:
|
||||||
|
radial-gradient(50% 40% at 85% 0%, rgba(252, 191, 73, 0.10), transparent 70%),
|
||||||
|
radial-gradient(45% 45% at 0% 100%, rgba(0, 48, 73, 0.6), transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
a { color: var(--ambar); text-decoration-thickness: 1px; text-underline-offset: 3px; }
|
||||||
|
a:hover { color: var(--creme); }
|
||||||
|
:focus-visible { outline: 2px solid var(--ambar); outline-offset: 3px; border-radius: 6px; }
|
||||||
|
|
||||||
|
code, kbd {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.86em;
|
||||||
|
padding: 0.1em 0.42em;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--painel-2);
|
||||||
|
border: 1px solid var(--linha);
|
||||||
|
color: var(--gelo);
|
||||||
|
}
|
||||||
|
kbd { border-bottom-width: 2px; }
|
||||||
|
|
||||||
|
.envoltorio { max-width: var(--largura); margin: 0 auto; padding: 0 24px; }
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------
|
||||||
|
Barra do topo
|
||||||
|
------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.topo { border-bottom: 1px solid var(--linha); }
|
||||||
|
.topo .envoltorio { display: flex; align-items: center; gap: 16px; min-height: 68px; }
|
||||||
|
.topo img { height: 26px; display: block; }
|
||||||
|
.topo-liga { font-family: var(--mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--texto-2); line-height: 1.4; border-left: 1px solid var(--linha-forte); padding-left: 16px; }
|
||||||
|
.topo nav { margin-left: auto; display: flex; gap: 20px; }
|
||||||
|
.topo nav a { color: var(--texto); text-decoration: none; font-size: 14px; }
|
||||||
|
.topo nav a:hover { color: var(--ambar); }
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------
|
||||||
|
Abertura
|
||||||
|
------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.abertura { padding: 64px 0 56px; }
|
||||||
|
.abertura .envoltorio { display: grid; grid-template-columns: 1.25fr 0.75fr; gap: 48px; align-items: center; }
|
||||||
|
|
||||||
|
.selo {
|
||||||
|
display: inline-flex; gap: 10px; align-items: center;
|
||||||
|
font-family: var(--mono); font-size: 12px; font-weight: 600; letter-spacing: 0.2em; text-transform: uppercase;
|
||||||
|
color: var(--ambar);
|
||||||
|
}
|
||||||
|
.selo::after { content: ""; width: 48px; height: 1px; background: var(--ambar); opacity: 0.5; }
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: var(--display);
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: clamp(38px, 5.4vw, 60px);
|
||||||
|
line-height: 1.04;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--gelo);
|
||||||
|
margin: 18px 0 18px;
|
||||||
|
}
|
||||||
|
h1 strong { display: block; font-weight: 800; }
|
||||||
|
|
||||||
|
.abertura-sub { font-size: 18px; color: var(--texto); max-width: 54ch; margin: 0 0 24px; }
|
||||||
|
|
||||||
|
.fichas { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 30px; padding: 0; list-style: none; }
|
||||||
|
.fichas li { font-size: 13px; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--linha-forte); color: var(--texto); background: var(--painel); }
|
||||||
|
.fichas li b { color: var(--gelo); font-weight: 600; }
|
||||||
|
|
||||||
|
.acoes { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||||
|
|
||||||
|
.botao {
|
||||||
|
display: inline-flex; align-items: center; gap: 10px;
|
||||||
|
padding: 12px 20px; border-radius: 10px;
|
||||||
|
font-family: var(--corpo); font-weight: 600; font-size: 15px; text-decoration: none;
|
||||||
|
border: 1px solid var(--linha-forte); color: var(--gelo); background: var(--painel);
|
||||||
|
transition: background 0.15s, border-color 0.15s, transform 0.15s;
|
||||||
|
}
|
||||||
|
.botao:hover { background: var(--painel-2); border-color: var(--ambar); color: var(--gelo); transform: translateY(-1px); }
|
||||||
|
.botao svg { width: 18px; height: 18px; flex: none; }
|
||||||
|
.botao.primario { background: var(--ambar); border-color: var(--ambar); color: #1d1300; }
|
||||||
|
.botao.primario:hover { background: var(--creme); border-color: var(--creme); color: #1d1300; }
|
||||||
|
.botao.pequeno { padding: 8px 14px; font-size: 14px; }
|
||||||
|
|
||||||
|
/* Pilha das duas prévias: o slide na frente, a apostila atrás. */
|
||||||
|
.previas { position: relative; aspect-ratio: 4 / 3.3; }
|
||||||
|
.previas a { position: absolute; display: block; border-radius: 10px; overflow: hidden; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55); border: 1px solid var(--linha-forte); transition: transform 0.2s; }
|
||||||
|
.previas img { display: block; width: 100%; height: auto; }
|
||||||
|
.previas .p-apostila { width: 46%; right: 0; top: 0; transform: rotate(4deg); }
|
||||||
|
.previas .p-slides { width: 88%; left: 0; bottom: 6%; }
|
||||||
|
.previas a:hover { transform: translateY(-4px) rotate(0deg); z-index: 2; }
|
||||||
|
|
||||||
|
.feixe { height: 3px; background: var(--feixe); border: 0; margin: 0; }
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------
|
||||||
|
Seções
|
||||||
|
------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
section.bloco { padding: 64px 0 8px; }
|
||||||
|
|
||||||
|
.rotulo {
|
||||||
|
font-family: var(--mono); font-size: 12px; font-weight: 600; letter-spacing: 0.2em; text-transform: uppercase;
|
||||||
|
color: var(--ambar); margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
h2 { font-family: var(--display); font-weight: 700; font-size: 32px; line-height: 1.15; color: var(--gelo); margin: 0 0 8px; letter-spacing: -0.01em; }
|
||||||
|
.bloco-sub { color: var(--texto-2); margin: 0 0 28px; max-width: 70ch; }
|
||||||
|
h3 { font-family: var(--display); font-weight: 700; font-size: 20px; color: var(--gelo); margin: 0 0 6px; }
|
||||||
|
|
||||||
|
/* Três portas de entrada. */
|
||||||
|
.portas { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
|
||||||
|
.porta { display: flex; flex-direction: column; border: 1px solid var(--linha); border-radius: 14px; background: var(--painel); overflow: hidden; }
|
||||||
|
.porta-imagem { aspect-ratio: 16 / 9; background: var(--breu-2); border-bottom: 1px solid var(--linha); overflow: hidden; display: grid; place-items: center; }
|
||||||
|
.porta-imagem img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }
|
||||||
|
.porta-imagem.clara img { object-fit: contain; background: #2f2f2f; }
|
||||||
|
.porta-corpo { padding: 20px 20px 22px; display: flex; flex-direction: column; gap: 10px; flex: 1; }
|
||||||
|
.porta-corpo p { margin: 0; color: var(--texto); font-size: 15px; }
|
||||||
|
.porta-meta { font-family: var(--mono); font-size: 12px; color: var(--texto-2); letter-spacing: 0.04em; }
|
||||||
|
.porta .acoes { margin-top: auto; padding-top: 6px; gap: 8px; }
|
||||||
|
|
||||||
|
/* Lista de arquivos. */
|
||||||
|
.grupo-arquivos { margin: 0 0 30px; }
|
||||||
|
.grupo-arquivos h3 { font-size: 14px; font-family: var(--mono); letter-spacing: 0.16em; text-transform: uppercase; color: var(--texto-2); font-weight: 600; margin: 0 0 10px; }
|
||||||
|
.arquivos { list-style: none; margin: 0; padding: 0; border: 1px solid var(--linha); border-radius: 12px; overflow: hidden; }
|
||||||
|
.arquivo { display: grid; grid-template-columns: 44px minmax(0, 1fr) auto auto; gap: 16px; align-items: center; padding: 14px 18px; background: var(--painel); }
|
||||||
|
.arquivo + .arquivo { border-top: 1px solid var(--linha); }
|
||||||
|
.arquivo:hover { background: var(--painel-2); }
|
||||||
|
.icone { width: 44px; height: 44px; border-radius: 10px; display: grid; place-items: center; background: var(--breu-3); color: var(--ambar); }
|
||||||
|
.icone svg { width: 22px; height: 22px; }
|
||||||
|
.icone.pdf { color: #ff8b8b; background: rgba(214, 40, 40, 0.14); }
|
||||||
|
.icone.codigo { color: #6fdcb0; background: rgba(111, 220, 176, 0.1); }
|
||||||
|
.icone.imagem { color: #7cc4ff; background: rgba(78, 168, 255, 0.12); }
|
||||||
|
.arquivo-nome { font-family: var(--mono); font-size: 14.5px; color: var(--gelo); font-weight: 600; word-break: break-all; }
|
||||||
|
.arquivo-desc { font-size: 14px; color: var(--texto-2); margin-top: 2px; }
|
||||||
|
.arquivo-tam { font-family: var(--mono); font-size: 12.5px; color: var(--texto-2); white-space: nowrap; text-align: right; }
|
||||||
|
.arquivo-acoes { display: flex; gap: 6px; }
|
||||||
|
.arquivo-acoes a { font-size: 13px; font-weight: 600; text-decoration: none; padding: 6px 12px; border-radius: 8px; border: 1px solid var(--linha-forte); color: var(--gelo); white-space: nowrap; }
|
||||||
|
.arquivo-acoes a:hover { border-color: var(--ambar); color: var(--ambar); }
|
||||||
|
|
||||||
|
/* Código embutido: .ino e .txt costumam baixar em vez de abrir, então o
|
||||||
|
conteúdo vem na própria página. */
|
||||||
|
.codigo-fonte { margin-top: 8px; }
|
||||||
|
.codigo-fonte summary { cursor: pointer; font-size: 13px; font-weight: 600; color: var(--ambar); width: max-content; }
|
||||||
|
.codigo-fonte pre { margin: 10px 0 0; max-height: 420px; overflow: auto; padding: 14px 16px; border-radius: 10px; background: rgba(0, 14, 22, 0.7); border: 1px solid var(--linha); border-left: 3px solid var(--laranja); font-family: var(--mono); font-size: 13px; line-height: 1.55; color: #dbeaf2; tab-size: 2; }
|
||||||
|
.codigo-fonte pre code { background: none; border: 0; padding: 0; font-size: inherit; }
|
||||||
|
|
||||||
|
/* Como usar. */
|
||||||
|
.guias { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
|
||||||
|
.guia { border: 1px solid var(--linha); border-radius: 14px; padding: 22px 22px 18px; background: var(--painel); }
|
||||||
|
.guia ol, .guia ul { margin: 10px 0 0; padding-left: 20px; }
|
||||||
|
.guia li { margin: 6px 0; font-size: 15px; }
|
||||||
|
.guia li::marker { color: var(--ambar); font-family: var(--mono); }
|
||||||
|
.guia p { margin: 8px 0 0; font-size: 15px; }
|
||||||
|
.atalhos { display: grid; grid-template-columns: auto 1fr; gap: 8px 14px; margin-top: 12px; font-size: 14.5px; align-items: center; }
|
||||||
|
.atalhos dt { display: flex; gap: 4px; }
|
||||||
|
.atalhos dd { margin: 0; color: var(--texto); }
|
||||||
|
|
||||||
|
/* Roteiro da aula. */
|
||||||
|
.roteiro { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; counter-reset: parte; }
|
||||||
|
.roteiro a { display: flex; gap: 14px; align-items: baseline; padding: 14px 16px; border-radius: 12px; border: 1px solid var(--linha); background: var(--painel); text-decoration: none; color: var(--texto); height: 100%; }
|
||||||
|
.roteiro a:hover { border-color: var(--ambar); background: var(--painel-2); }
|
||||||
|
.roteiro b { font-family: var(--display); font-size: 26px; line-height: 1; color: var(--ambar); min-width: 1.4em; }
|
||||||
|
.roteiro span { font-weight: 600; color: var(--gelo); }
|
||||||
|
.roteiro small { display: block; font-family: var(--mono); font-size: 12px; color: var(--texto-2); font-weight: 400; margin-top: 2px; }
|
||||||
|
|
||||||
|
/* Hub /formacoes. */
|
||||||
|
.formacoes { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 22px; }
|
||||||
|
|
||||||
|
/* Rodapé. */
|
||||||
|
.rodape { margin-top: 80px; border-top: 1px solid var(--linha); padding: 28px 0 40px; font-size: 14px; color: var(--texto-2); }
|
||||||
|
.rodape .envoltorio { display: flex; flex-wrap: wrap; gap: 12px 32px; justify-content: space-between; }
|
||||||
|
.rodape strong { color: var(--texto); }
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------
|
||||||
|
Telas menores
|
||||||
|
------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.abertura { padding: 40px 0; }
|
||||||
|
.abertura .envoltorio { grid-template-columns: 1fr; gap: 40px; }
|
||||||
|
.previas { max-width: 520px; }
|
||||||
|
.portas, .guias { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.envoltorio { padding: 0 16px; }
|
||||||
|
.topo-liga { display: none; }
|
||||||
|
.topo nav { gap: 14px; }
|
||||||
|
.arquivo { grid-template-columns: 40px minmax(0, 1fr); row-gap: 10px; padding: 14px; }
|
||||||
|
.icone { width: 40px; height: 40px; }
|
||||||
|
.arquivo-tam { grid-column: 2; text-align: left; }
|
||||||
|
.arquivo-acoes { grid-column: 2; }
|
||||||
|
h2 { font-size: 26px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html { scroll-behavior: auto; }
|
||||||
|
.botao, .previas a { transition: none; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>LASER · Formação em ESP32</title>
|
||||||
|
<meta name="description" content="Slides, apostilas e arquivos de prática da formação em ESP32 da LASER, Liga Acadêmica de Sistemas Embarcados da UFPE.">
|
||||||
|
<meta name="author" content="Lucas Rayan Guerra">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/marca/espdocs-marca.svg">
|
||||||
|
<link rel="stylesheet" href="/formacoes/assets/viewer.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Gerado por scripts/formacoes-viewer.mjs a partir do formacao.json. Não edite à mão. -->
|
||||||
|
|
||||||
|
<header class="topo">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<a href="/formacoes"><img src="/formacoes/assets/laser.svg" alt="LASER"></a>
|
||||||
|
<span class="topo-liga">Liga Acadêmica de Sistemas Embarcados<br>Centro de Informática · UFPE</span>
|
||||||
|
<nav><a href="#formacoes">Módulos</a></nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="abertura">
|
||||||
|
<div class="envoltorio" style="grid-template-columns:1fr">
|
||||||
|
<div>
|
||||||
|
<div class="selo">Formação em ESP32</div>
|
||||||
|
<h1>Material das<strong>formações da LASER</strong></h1>
|
||||||
|
<p class="abertura-sub">Slides, apostilas e arquivos de prática de cada módulo, para acompanhar a aula, revisar depois ou imprimir.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<hr class="feixe">
|
||||||
|
<section class="bloco" id="formacoes">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<p class="rotulo">Módulos</p>
|
||||||
|
<h2>2 formações disponíveis</h2>
|
||||||
|
<div class="formacoes">
|
||||||
|
<article class="porta">
|
||||||
|
<a class="porta-imagem" href="/formacoes/1"><img src="/formacoes/1/previa-slides.jpg" alt="" loading="lazy"></a>
|
||||||
|
<div class="porta-corpo">
|
||||||
|
<div class="porta-meta">MÓDULO 1 · 3 HORAS</div>
|
||||||
|
<h3>Conhecendo a família ESP32</h3>
|
||||||
|
<p>Do silício à bancada: como esses microcontroladores funcionam por dentro, o que cada uma das doze séries faz e não faz, e como escolher o chip certo. Termina com um DHT11 lendo temperatura e umidade num ESP32-S3.</p>
|
||||||
|
<div class="acoes"><a class="botao pequeno primario" href="/formacoes/1"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>Ver o material</a></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="porta">
|
||||||
|
<a class="porta-imagem" href="/formacoes/2"><img src="/formacoes/2/previa-slides.jpg" alt="" loading="lazy"></a>
|
||||||
|
<div class="porta-corpo">
|
||||||
|
<div class="porta-meta">MÓDULO 2 · 2 HORAS</div>
|
||||||
|
<h3>Estação de temperatura e umidade</h3>
|
||||||
|
<p>Um DHT22, um display LCD 16x2 e um ESP32-S3, simulados no Wokwi com o framework Arduino. Dois protocolos de comunicação, um programa curto e os cuidados elétricos que fazem o circuito funcionar também fora da tela.</p>
|
||||||
|
<div class="acoes"><a class="botao pequeno primario" href="/formacoes/2"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>Ver o material</a></div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="rodape">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<span><strong>LASER</strong> · Liga Acadêmica de Sistemas Embarcados · CIn · UFPE</span>
|
||||||
|
<span>Material de <strong>Lucas Rayan Guerra</strong> · uso livre para fins educacionais · <a href="mailto:contato@lucasrguerra.dev.br">contato@lucasrguerra.dev.br</a></span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
/**
|
||||||
|
* Gera a página de entrada de cada formação e a lista geral de formações.
|
||||||
|
*
|
||||||
|
* npm run formacoes-viewer
|
||||||
|
*
|
||||||
|
* Cada pasta public/formacoes/N/ descreve o próprio material num
|
||||||
|
* formacao.json. Daqui saem:
|
||||||
|
* public/formacoes/N/index.html a página servida em /formacoes/N
|
||||||
|
* public/formacoes/index.html a lista servida em /formacoes
|
||||||
|
*
|
||||||
|
* O que dá para medir não fica no JSON: tamanho de cada arquivo, número de
|
||||||
|
* páginas dos PDFs e o roteiro da aula, lido das aberturas de seção do
|
||||||
|
* slide.html. Assim a página não desatualiza quando o material muda.
|
||||||
|
*
|
||||||
|
* Os links são absolutos (/formacoes/N/...) porque o middleware serve a
|
||||||
|
* página em /formacoes/N, sem barra final, e um link relativo apontaria para
|
||||||
|
* a pasta de cima.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const raiz = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const pastaFormacoes = path.join(raiz, "public", "formacoes");
|
||||||
|
|
||||||
|
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||||
|
|
||||||
|
function tamanho(bytes) {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
||||||
|
return `${(bytes / 1024 / 1024).toFixed(1).replace(".", ",")} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conta os objetos /Type /Page de um PDF. Basta para os PDFs do Chrome, que
|
||||||
|
não comprimem a árvore de páginas. */
|
||||||
|
function paginasPdf(arquivo) {
|
||||||
|
const bruto = fs.readFileSync(arquivo, "latin1");
|
||||||
|
return (bruto.match(/\/Type\s*\/Page(?![s\w])/g) || []).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* As aberturas de seção do slide.html viram o roteiro da aula, cada uma com
|
||||||
|
link para o slide em que começa. */
|
||||||
|
function roteiro(pasta) {
|
||||||
|
const html = fs.readFileSync(path.join(pasta, "slide.html"), "utf8");
|
||||||
|
const slides = [...html.matchAll(/<section class="slide([^"]*)"[^>]*data-titulo="([^"]*)"/g)];
|
||||||
|
const partes = [];
|
||||||
|
slides.forEach((m, i) => {
|
||||||
|
if (!/\bsecao\b/.test(m[1])) return;
|
||||||
|
partes.push({ indice: i + 1, titulo: m[2].replace(/^§\d+\s*·\s*/, "") });
|
||||||
|
});
|
||||||
|
partes.forEach((p, i) => {
|
||||||
|
const fim = i + 1 < partes.length ? partes[i + 1].indice - 1 : slides.length;
|
||||||
|
p.quantos = fim - p.indice + 1;
|
||||||
|
});
|
||||||
|
return { partes, total: slides.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ICONES = {
|
||||||
|
slides: '<path d="M3 5h18v12H3z"/><path d="M8 21h8M12 17v4"/>',
|
||||||
|
apostila: '<path d="M4 4.5A1.5 1.5 0 0 1 5.5 3H20v16H5.5A1.5 1.5 0 0 0 4 20.5z"/><path d="M4 20.5A1.5 1.5 0 0 0 5.5 22H20"/>',
|
||||||
|
pdf: '<path d="M14 3H6v18h12V7z"/><path d="M14 3v4h4"/><path d="M9 13h6M9 17h4"/>',
|
||||||
|
codigo: '<path d="m8 8-4 4 4 4M16 8l4 4-4 4M13.5 5l-3 14"/>',
|
||||||
|
zip: '<path d="M14 3H6v18h12V7z"/><path d="M14 3v4h4"/><path d="M11 6h1M11 9h1M11 12h1M10.5 15h2v3h-2z"/>',
|
||||||
|
imagem: '<rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="2"/><path d="m21 17-5-5-9 8"/>',
|
||||||
|
fritzing: '<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M8 8h3v3H8zM13 13h3v3h-3zM11 9.5h3.5V13"/>',
|
||||||
|
seta: '<path d="M5 12h14M13 6l6 6-6 6"/>',
|
||||||
|
baixar: '<path d="M12 4v12M6 10l6 6 6-6M5 20h14"/>',
|
||||||
|
tocar: '<path d="M7 5v14l11-7z"/>',
|
||||||
|
livro: '<path d="M12 6c-2-1.5-5-2-8-1.5v14c3-.5 6 0 8 1.5 2-1.5 5-2 8-1.5v-14c-3-.5-6 0-8 1.5zM12 6v14"/>',
|
||||||
|
};
|
||||||
|
const icone = (nome) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONES[nome]}</svg>`;
|
||||||
|
const CLASSE_ICONE = { pdf: "pdf", codigo: "codigo", zip: "codigo", imagem: "imagem" };
|
||||||
|
|
||||||
|
const cabecalho = (titulo, descricao) => `<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>${esc(titulo)}</title>
|
||||||
|
<meta name="description" content="${esc(descricao)}">
|
||||||
|
<meta name="author" content="Lucas Rayan Guerra">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/marca/espdocs-marca.svg">
|
||||||
|
<link rel="stylesheet" href="/formacoes/assets/viewer.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Gerado por scripts/formacoes-viewer.mjs a partir do formacao.json. Não edite à mão. -->
|
||||||
|
`;
|
||||||
|
|
||||||
|
const topo = (nav) => `
|
||||||
|
<header class="topo">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<a href="/formacoes"><img src="/formacoes/assets/laser.svg" alt="LASER"></a>
|
||||||
|
<span class="topo-liga">Liga Acadêmica de Sistemas Embarcados<br>Centro de Informática · UFPE</span>
|
||||||
|
<nav>${nav}</nav>
|
||||||
|
</div>
|
||||||
|
</header>`;
|
||||||
|
|
||||||
|
const rodape = `
|
||||||
|
<footer class="rodape">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<span><strong>LASER</strong> · Liga Acadêmica de Sistemas Embarcados · CIn · UFPE</span>
|
||||||
|
<span>Material de <strong>Lucas Rayan Guerra</strong> · uso livre para fins educacionais · <a href="mailto:contato@lucasrguerra.dev.br">contato@lucasrguerra.dev.br</a></span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
function paginaFormacao(pasta, dados) {
|
||||||
|
const n = dados.modulo;
|
||||||
|
const base = `/formacoes/${n}`;
|
||||||
|
const url = (arq) => (/^https?:/.test(arq) ? arq : `${base}/${arq}`);
|
||||||
|
const info = (arq) => {
|
||||||
|
const caminho = path.join(pasta, arq);
|
||||||
|
if (!fs.existsSync(caminho)) throw new Error(`formacao ${n}: não achei ${arq}`);
|
||||||
|
const bytes = fs.statSync(caminho).size;
|
||||||
|
const paginas = arq.endsWith(".pdf") ? paginasPdf(caminho) : 0;
|
||||||
|
return { bytes, paginas };
|
||||||
|
};
|
||||||
|
const { partes, total } = roteiro(pasta);
|
||||||
|
const pdfSlides = `LASER-ESP32-Modulo-${n}-Slides.pdf`;
|
||||||
|
const pdfApostila = `LASER-ESP32-Modulo-${n}-Apostila.pdf`;
|
||||||
|
const iSlides = info(pdfSlides);
|
||||||
|
const iApostila = info(pdfApostila);
|
||||||
|
|
||||||
|
const fichas = dados.fichas.map(([k, v]) => `<li>${esc(k)}: <b>${esc(v)}</b></li>`).join("");
|
||||||
|
const acao = ([rotulo, arq, baixar], primario) =>
|
||||||
|
`<a class="botao pequeno${primario ? " primario" : ""}" href="${url(arq)}"${baixar ? " download" : ""}${/^https?:/.test(arq) ? ' rel="noopener"' : ""}>${icone(baixar ? "baixar" : "seta")}${esc(rotulo)}</a>`;
|
||||||
|
|
||||||
|
const grupos = dados.grupos.map((g) => {
|
||||||
|
const itens = g.arquivos.map((a) => {
|
||||||
|
const { bytes, paginas } = info(a.arquivo);
|
||||||
|
const nome = path.basename(a.arquivo);
|
||||||
|
const meta = paginas ? `${paginas} págs · ${tamanho(bytes)}` : tamanho(bytes);
|
||||||
|
const botoes = [];
|
||||||
|
if (a.tipo === "codigo") {
|
||||||
|
botoes.push(`<a href="${url(a.arquivo)}" download>Baixar</a>`);
|
||||||
|
} else {
|
||||||
|
if (a.abrir) botoes.push(`<a href="${url(a.arquivo)}"${a.tipo === "imagem" || a.tipo === "pdf" ? ' target="_blank" rel="noopener"' : ""}>Abrir</a>`);
|
||||||
|
if (a.baixar) botoes.push(`<a href="${url(a.arquivo)}" download>Baixar</a>`);
|
||||||
|
}
|
||||||
|
let codigo = "";
|
||||||
|
if (a.tipo === "codigo") {
|
||||||
|
const fonte = fs.readFileSync(path.join(pasta, a.arquivo), "utf8");
|
||||||
|
codigo = `<details class="codigo-fonte"><summary>Ver código</summary><pre><code>${esc(fonte.trimEnd())}</code></pre></details>`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<li class="arquivo">
|
||||||
|
<div class="icone ${CLASSE_ICONE[a.tipo] || ""}">${icone(a.tipo)}</div>
|
||||||
|
<div>
|
||||||
|
<div class="arquivo-nome">${esc(nome)}</div>
|
||||||
|
<div class="arquivo-desc">${a.descricao}</div>
|
||||||
|
${codigo}
|
||||||
|
</div>
|
||||||
|
<div class="arquivo-tam">${meta}</div>
|
||||||
|
<div class="arquivo-acoes">${botoes.join("")}</div>
|
||||||
|
</li>`;
|
||||||
|
}).join("");
|
||||||
|
return `
|
||||||
|
<div class="grupo-arquivos">
|
||||||
|
<h3>${esc(g.titulo)}</h3>
|
||||||
|
<ul class="arquivos">${itens}
|
||||||
|
</ul>
|
||||||
|
</div>`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
const guias = [
|
||||||
|
`<div class="guia">
|
||||||
|
<h3>Apresentando os slides</h3>
|
||||||
|
<p>Abra <a href="${base}/slide.html">slide.html</a> no navegador. O palco é 16:9 e se ajusta a qualquer tela ou projetor.</p>
|
||||||
|
<dl class="atalhos">
|
||||||
|
<dt><kbd>→</kbd><kbd>←</kbd></dt><dd>avançar e voltar (ou clique nas metades da tela)</dd>
|
||||||
|
<dt><kbd>F</kbd></dt><dd>tela cheia</dd>
|
||||||
|
<dt><kbd>O</kbd></dt><dd>panorama com todos os slides</dd>
|
||||||
|
<dt><kbd>B</kbd></dt><dd>tela preta, para pausar a projeção</dd>
|
||||||
|
</dl>
|
||||||
|
</div>`,
|
||||||
|
`<div class="guia">
|
||||||
|
<h3>Lendo e imprimindo a apostila</h3>
|
||||||
|
<p>A <a href="${base}/apostila.html">versão online</a> se pagina em A4 sozinha ao abrir, o que leva alguns segundos.</p>
|
||||||
|
<p>Para papel, prefira o <a href="${url(pdfApostila)}" download>PDF pronto</a>: ele já sai com margens, cores e quebras de página certas, sem configurar a impressora.</p>
|
||||||
|
</div>`,
|
||||||
|
...dados.guias.map((g) => `<div class="guia">
|
||||||
|
<h3>${esc(g.titulo)}</h3>
|
||||||
|
${g.itens ? `<ol>${g.itens.map((i) => `<li>${i}</li>`).join("")}</ol>` : ""}
|
||||||
|
${g.texto ? `<p>${g.texto}</p>` : ""}
|
||||||
|
</div>`),
|
||||||
|
].join("\n\t\t");
|
||||||
|
|
||||||
|
const listaRoteiro = partes.map((p, i) => `
|
||||||
|
<li><a href="${base}/slide.html#${p.indice}"><b>${i + 1}</b><span>${esc(p.titulo)}<small>slide ${p.indice} · ${p.quantos} slide${p.quantos > 1 ? "s" : ""}</small></span></a></li>`).join("");
|
||||||
|
|
||||||
|
const titulo = `LASER · Formação em ESP32 · Módulo ${n}: ${dados.titulo.join(" ")}`;
|
||||||
|
|
||||||
|
return cabecalho(titulo, dados.descricao) + topo(`<a href="#arquivos">Arquivos</a><a href="#como-usar">Como usar</a><a href="/formacoes">Formações</a>`) + `
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="abertura">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<div>
|
||||||
|
<div class="selo">Formação em ESP32 · Módulo ${n}</div>
|
||||||
|
<h1>${esc(dados.titulo[0])}<strong>${esc(dados.titulo[1])}</strong></h1>
|
||||||
|
<p class="abertura-sub">${esc(dados.subtitulo)}</p>
|
||||||
|
<ul class="fichas">${fichas}</ul>
|
||||||
|
<div class="acoes">
|
||||||
|
<a class="botao primario" href="${base}/slide.html">${icone("tocar")}Apresentar os slides</a>
|
||||||
|
<a class="botao" href="${base}/apostila.html">${icone("livro")}Ler a apostila</a>
|
||||||
|
<a class="botao" href="#baixar">${icone("baixar")}Baixar em PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="previas">
|
||||||
|
<a class="p-apostila" href="${base}/apostila.html" aria-label="Abrir a apostila"><img src="${base}/previa-apostila.jpg" alt="Capa da apostila do módulo ${n}" width="640" height="905"></a>
|
||||||
|
<a class="p-slides" href="${base}/slide.html" aria-label="Abrir os slides"><img src="${base}/previa-slides.jpg" alt="Capa dos slides do módulo ${n}" width="960" height="540"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<hr class="feixe">
|
||||||
|
|
||||||
|
<section class="bloco" id="baixar">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<p class="rotulo">Comece por aqui</p>
|
||||||
|
<h2>Três formas de usar este material</h2>
|
||||||
|
<p class="bloco-sub">Para apresentar, para estudar e para pôr a mão no circuito. Os PDFs são a versão final, para quem só quer o material pronto.</p>
|
||||||
|
<div class="portas">
|
||||||
|
<article class="porta">
|
||||||
|
<a class="porta-imagem" href="${base}/slide.html"><img src="${base}/previa-slides.jpg" alt="" loading="lazy"></a>
|
||||||
|
<div class="porta-corpo">
|
||||||
|
<h3>Slides</h3>
|
||||||
|
<p>${total} slides para projetar direto do navegador, com navegação por teclado e panorama.</p>
|
||||||
|
<div class="porta-meta">PDF · ${iSlides.paginas} págs · ${tamanho(iSlides.bytes)}</div>
|
||||||
|
<div class="acoes">
|
||||||
|
<a class="botao pequeno primario" href="${base}/slide.html">${icone("tocar")}Apresentar</a>
|
||||||
|
<a class="botao pequeno" href="${url(pdfSlides)}" download>${icone("baixar")}PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="porta">
|
||||||
|
<a class="porta-imagem" href="${base}/apostila.html"><img src="${base}/previa-apostila.jpg" alt="" loading="lazy"></a>
|
||||||
|
<div class="porta-corpo">
|
||||||
|
<h3>Apostila</h3>
|
||||||
|
<p>O conteúdo completo, com explicações, tabelas e o código comentado, paginado em A4.</p>
|
||||||
|
<div class="porta-meta">PDF · ${iApostila.paginas} págs · ${tamanho(iApostila.bytes)}</div>
|
||||||
|
<div class="acoes">
|
||||||
|
<a class="botao pequeno primario" href="${base}/apostila.html">${icone("livro")}Ler</a>
|
||||||
|
<a class="botao pequeno" href="${url(pdfApostila)}" download>${icone("baixar")}PDF</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="porta">
|
||||||
|
<a class="porta-imagem clara" href="${url(dados.pratica.imagem)}" target="_blank" rel="noopener"><img src="${url(dados.pratica.imagem)}" alt="" loading="lazy"></a>
|
||||||
|
<div class="porta-corpo">
|
||||||
|
<h3>${esc(dados.pratica.titulo)}</h3>
|
||||||
|
<p>${esc(dados.pratica.texto)}</p>
|
||||||
|
<div class="porta-meta">${esc(dados.pratica.meta)}</div>
|
||||||
|
<div class="acoes">${dados.pratica.acoes.map((a, i) => acao(a, i === 0)).join("")}</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="bloco" id="roteiro">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<p class="rotulo">Roteiro da aula</p>
|
||||||
|
<h2>${partes.length} partes, ${total} slides</h2>
|
||||||
|
<p class="bloco-sub">Cada parte abre os slides direto no ponto em que ela começa.</p>
|
||||||
|
<ol class="roteiro">${listaRoteiro}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="bloco" id="arquivos">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<p class="rotulo">Todos os arquivos</p>
|
||||||
|
<h2>O que tem nesta formação</h2>
|
||||||
|
<p class="bloco-sub">Tudo o que foi usado na aula, com o que é cada arquivo e para que serve.</p>
|
||||||
|
${grupos}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="bloco" id="como-usar">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<p class="rotulo">Como usar</p>
|
||||||
|
<h2>Passo a passo</h2>
|
||||||
|
<div class="guias">
|
||||||
|
${guias}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
` + rodape;
|
||||||
|
}
|
||||||
|
|
||||||
|
function paginaLista(formacoes) {
|
||||||
|
const cartoes = formacoes.map(({ dados }) => {
|
||||||
|
const base = `/formacoes/${dados.modulo}`;
|
||||||
|
const duracao = (dados.fichas.find(([k]) => k === "Duração") || [])[1];
|
||||||
|
return `
|
||||||
|
<article class="porta">
|
||||||
|
<a class="porta-imagem" href="${base}"><img src="${base}/previa-slides.jpg" alt="" loading="lazy"></a>
|
||||||
|
<div class="porta-corpo">
|
||||||
|
<div class="porta-meta">MÓDULO ${dados.modulo}${duracao ? ` · ${esc(duracao.toUpperCase())}` : ""}</div>
|
||||||
|
<h3>${esc(dados.titulo.join(" "))}</h3>
|
||||||
|
<p>${esc(dados.subtitulo)}</p>
|
||||||
|
<div class="acoes"><a class="botao pequeno primario" href="${base}">${icone("seta")}Ver o material</a></div>
|
||||||
|
</div>
|
||||||
|
</article>`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return cabecalho("LASER · Formação em ESP32", "Slides, apostilas e arquivos de prática da formação em ESP32 da LASER, Liga Acadêmica de Sistemas Embarcados da UFPE.") + topo(`<a href="#formacoes">Módulos</a>`) + `
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="abertura">
|
||||||
|
<div class="envoltorio" style="grid-template-columns:1fr">
|
||||||
|
<div>
|
||||||
|
<div class="selo">Formação em ESP32</div>
|
||||||
|
<h1>Material das<strong>formações da LASER</strong></h1>
|
||||||
|
<p class="abertura-sub">Slides, apostilas e arquivos de prática de cada módulo, para acompanhar a aula, revisar depois ou imprimir.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<hr class="feixe">
|
||||||
|
<section class="bloco" id="formacoes">
|
||||||
|
<div class="envoltorio">
|
||||||
|
<p class="rotulo">Módulos</p>
|
||||||
|
<h2>${formacoes.length} formações disponíveis</h2>
|
||||||
|
<div class="formacoes">${cartoes}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
` + rodape;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formacoes = fs.readdirSync(pastaFormacoes)
|
||||||
|
.filter((d) => /^\d+$/.test(d) && fs.existsSync(path.join(pastaFormacoes, d, "formacao.json")))
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
.map((d) => {
|
||||||
|
const pasta = path.join(pastaFormacoes, d);
|
||||||
|
return { pasta, dados: JSON.parse(fs.readFileSync(path.join(pasta, "formacao.json"), "utf8")) };
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const { pasta, dados } of formacoes) {
|
||||||
|
fs.writeFileSync(path.join(pasta, "index.html"), paginaFormacao(pasta, dados));
|
||||||
|
console.log(`public/formacoes/${dados.modulo}/index.html`);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(path.join(pastaFormacoes, "index.html"), paginaLista(formacoes));
|
||||||
|
console.log("public/formacoes/index.html");
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Exporta os slides de uma formação para PDF em 16:9 exato.
|
* Exporta o material de uma formação para PDF: os slides em 16:9 exato e a
|
||||||
|
* apostila em A4, já paginada pelo Paged.js.
|
||||||
*
|
*
|
||||||
* npm run slides-pdf → módulo 1
|
* npm run slides-pdf → módulo 1
|
||||||
* npm run slides-pdf -- 2 → módulo 2
|
* npm run slides-pdf -- 2 → módulo 2
|
||||||
*
|
*
|
||||||
* A página do PDF tem 1280 x 720 px (338,667 x 190,5 mm), a mesma caixa do
|
* A página do PDF dos slides tem 1280 x 720 px (338,667 x 190,5 mm), a mesma
|
||||||
* palco do slide.html, então nada é reescalado nem cortado. Sai um arquivo por
|
* caixa do palco do slide.html, então nada é reescalado nem cortado. Os dois
|
||||||
* formação, ao lado do próprio slide.html.
|
* arquivos saem ao lado do slide.html e da apostila.html da formação.
|
||||||
*
|
*
|
||||||
* Existe porque a caixa de impressão do navegador depende de o usuário lembrar
|
* Existe porque a caixa de impressão do navegador depende de o usuário lembrar
|
||||||
* de marcar "gráficos de segundo plano" e "margens: nenhuma". Aqui isso é fixo.
|
* de marcar "gráficos de segundo plano" e "margens: nenhuma". Aqui isso é fixo.
|
||||||
@@ -22,6 +23,7 @@ const raiz = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|||||||
const modulo = (process.argv[2] || "1").replace(/\D/g, "") || "1";
|
const modulo = (process.argv[2] || "1").replace(/\D/g, "") || "1";
|
||||||
const pastaModulo = path.join(raiz, "public", "formacoes", modulo);
|
const pastaModulo = path.join(raiz, "public", "formacoes", modulo);
|
||||||
const saida = path.join(pastaModulo, `LASER-ESP32-Modulo-${modulo}-Slides.pdf`);
|
const saida = path.join(pastaModulo, `LASER-ESP32-Modulo-${modulo}-Slides.pdf`);
|
||||||
|
const saidaApostila = path.join(pastaModulo, `LASER-ESP32-Modulo-${modulo}-Apostila.pdf`);
|
||||||
|
|
||||||
if (!fs.existsSync(path.join(pastaModulo, "slide.html"))) {
|
if (!fs.existsSync(path.join(pastaModulo, "slide.html"))) {
|
||||||
console.error(`Não achei public/formacoes/${modulo}/slide.html`);
|
console.error(`Não achei public/formacoes/${modulo}/slide.html`);
|
||||||
@@ -38,6 +40,7 @@ const TIPOS = {
|
|||||||
".png": "image/png",
|
".png": "image/png",
|
||||||
".jpg": "image/jpeg",
|
".jpg": "image/jpeg",
|
||||||
".json": "application/json",
|
".json": "application/json",
|
||||||
|
".woff2": "font/woff2",
|
||||||
};
|
};
|
||||||
|
|
||||||
const servidor = http.createServer((req, res) => {
|
const servidor = http.createServer((req, res) => {
|
||||||
@@ -55,7 +58,7 @@ await new Promise((r) => servidor.listen(0, "127.0.0.1", r));
|
|||||||
const porta = servidor.address().port;
|
const porta = servidor.address().port;
|
||||||
const url = `http://127.0.0.1:${porta}/formacoes/${modulo}/slide.html`;
|
const url = `http://127.0.0.1:${porta}/formacoes/${modulo}/slide.html`;
|
||||||
|
|
||||||
console.log(`[1/3] Abrindo ${url}`);
|
console.log(`[1/4] Abrindo ${url}`);
|
||||||
const navegador = await puppeteer.launch({ headless: "new", args: ["--no-sandbox"] });
|
const navegador = await puppeteer.launch({ headless: "new", args: ["--no-sandbox"] });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -63,7 +66,7 @@ try {
|
|||||||
await pagina.goto(url, { waitUntil: "networkidle0", timeout: 60000 });
|
await pagina.goto(url, { waitUntil: "networkidle0", timeout: 60000 });
|
||||||
await pagina.evaluate(() => document.fonts.ready);
|
await pagina.evaluate(() => document.fonts.ready);
|
||||||
|
|
||||||
console.log("[2/3] Conferindo o encaixe de cada slide no 16:9");
|
console.log("[2/4] Conferindo o encaixe de cada slide no 16:9");
|
||||||
await pagina.emulateMediaType("print");
|
await pagina.emulateMediaType("print");
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
|
||||||
@@ -80,7 +83,7 @@ try {
|
|||||||
console.log(` ${caixas.length} slides, todos em 1280x720`);
|
console.log(` ${caixas.length} slides, todos em 1280x720`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[3/3] Gerando o PDF");
|
console.log("[3/4] Gerando o PDF dos slides");
|
||||||
await pagina.pdf({
|
await pagina.pdf({
|
||||||
path: saida,
|
path: saida,
|
||||||
printBackground: true,
|
printBackground: true,
|
||||||
@@ -89,7 +92,30 @@ try {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const kb = (fs.statSync(saida).size / 1024).toFixed(0);
|
const kb = (fs.statSync(saida).size / 1024).toFixed(0);
|
||||||
console.log(`\nPronto: ${path.relative(raiz, saida)} (${kb} KB, ${caixas.length} páginas em 16:9)`);
|
console.log(` ${path.relative(raiz, saida)} (${kb} KB, ${caixas.length} páginas em 16:9)`);
|
||||||
|
|
||||||
|
/* A apostila é paginada no navegador pelo Paged.js, que não avisa quando
|
||||||
|
termina. Espera-se o número de folhas parar de crescer. */
|
||||||
|
console.log("[4/4] Paginando e gerando o PDF da apostila");
|
||||||
|
const apostila = await navegador.newPage();
|
||||||
|
await apostila.goto(`http://127.0.0.1:${porta}/formacoes/${modulo}/apostila.html`, { waitUntil: "networkidle0", timeout: 120000 });
|
||||||
|
await apostila.waitForFunction(() => document.querySelectorAll(".pagedjs_page").length > 1, { timeout: 120000 });
|
||||||
|
let folhas = 0;
|
||||||
|
for (;;) {
|
||||||
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
|
const agora = await apostila.evaluate(() => document.querySelectorAll(".pagedjs_page").length);
|
||||||
|
if (agora === folhas) break;
|
||||||
|
folhas = agora;
|
||||||
|
}
|
||||||
|
await apostila.evaluate(() => document.fonts.ready);
|
||||||
|
await apostila.pdf({
|
||||||
|
path: saidaApostila,
|
||||||
|
printBackground: true,
|
||||||
|
preferCSSPageSize: true,
|
||||||
|
margin: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||||
|
});
|
||||||
|
const kbA = (fs.statSync(saidaApostila).size / 1024).toFixed(0);
|
||||||
|
console.log(` ${path.relative(raiz, saidaApostila)} (${kbA} KB, ${folhas} páginas em A4)`);
|
||||||
} finally {
|
} finally {
|
||||||
await navegador.close();
|
await navegador.close();
|
||||||
servidor.close();
|
servidor.close();
|
||||||
|
|||||||
@@ -59,5 +59,49 @@ for (const loc of idiomas) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Consulta por pino físico: "pino 32 esp32-s3" traz o cartão do pino 32, com nome, funções e avisos.
|
||||||
|
const comoDizer = {
|
||||||
|
pt: ["pino 32 esp32-s3", "pino 32", "pino 999 s3"],
|
||||||
|
en: ["pin 32 esp32-s3", "pin 32", "pin 999 s3"],
|
||||||
|
es: ["pin 32 esp32-s3", "pin 32", "pin 999 s3"],
|
||||||
|
fr: ["broche 32 esp32-s3", "broche 32", "broche 999 s3"],
|
||||||
|
de: ["pin 32 esp32-s3", "pin 32", "pin 999 s3"],
|
||||||
|
"zh-CN": ["esp32-s3 32号引脚", "引脚 32", "s3 999号引脚"],
|
||||||
|
};
|
||||||
|
for (const loc of idiomas) {
|
||||||
|
const res = await fetch(`${base}/api/busca/${loc}`);
|
||||||
|
if (!res.ok) continue;
|
||||||
|
const indice = preparar(await res.json());
|
||||||
|
const [comChip, semChip, inexistente] = comoDizer[loc];
|
||||||
|
const a = buscar(indice, comChip);
|
||||||
|
const cartao = a.resultados.find((r) => r.tipo === "resposta" && r.linhas?.[0]?.v === "32");
|
||||||
|
if (!cartao || !cartao.linhas.some((l) => l.k && l.v) || cartao.linhas.length < 3) {
|
||||||
|
console.log(`FALHOU [${loc}] "${comChip}": sem cartão do pino 32`);
|
||||||
|
falhas++;
|
||||||
|
} else if (!cartao.href.startsWith(`/${loc}/`)) {
|
||||||
|
console.log(`FALHOU [${loc}] "${comChip}": link fora do idioma: ${cartao.href}`);
|
||||||
|
falhas++;
|
||||||
|
}
|
||||||
|
if (!a.interpretacao.pinos?.length) {
|
||||||
|
console.log(`FALHOU [${loc}] "${comChip}": o pino não aparece em "entendi"`);
|
||||||
|
falhas++;
|
||||||
|
}
|
||||||
|
const b = buscar(indice, semChip);
|
||||||
|
const geral = b.resultados.find((r) => r.tipo === "resposta" && (r.linhas?.length ?? 0) >= 8);
|
||||||
|
if (!geral) {
|
||||||
|
console.log(`FALHOU [${loc}] "${semChip}": sem o quadro "pino 32 em cada série"`);
|
||||||
|
falhas++;
|
||||||
|
}
|
||||||
|
const c = buscar(indice, inexistente);
|
||||||
|
if (!c.resultados.some((r) => r.tipo === "pino" && r.titulo.includes("999"))) {
|
||||||
|
console.log(`FALHOU [${loc}] "${inexistente}": sem aviso de pino inexistente`);
|
||||||
|
falhas++;
|
||||||
|
}
|
||||||
|
if (loc !== "pt") {
|
||||||
|
for (const r of [...a.resultados, ...b.resultados, ...c.resultados]) {
|
||||||
|
for (const s of strings(r)) if (SO_PT.test(s)) { console.log(`FALHOU [${loc}]: português em "${s.slice(0, 80)}"`); falhas++; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (falhas === 0) console.log(` ✔ Busca: respostas e links corretos em ${idiomas.join(", ")}.`);
|
if (falhas === 0) console.log(` ✔ Busca: respostas e links corretos em ${idiomas.join(", ")}.`);
|
||||||
process.exit(falhas > 0 ? 1 : 0);
|
process.exit(falhas > 0 ? 1 : 0);
|
||||||