Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12480048f8 | ||
|
|
9ecf30dda0 | ||
|
|
f9b64b3354 | ||
|
|
bfc1a73104 | ||
|
|
23576b5c06 | ||
|
|
1f84f1f661 | ||
|
|
6ab83c112d | ||
|
|
900c61df3d | ||
|
|
5fba78308c | ||
|
|
b6fb230328 | ||
|
|
1860e34cbb |
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,47 @@ Tudo o que muda de relevante no ESPDocs está aqui. O formato segue o
|
|||||||
|
|
||||||
## [Não lançado]
|
## [Não lançado]
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
- A apostila de cada formação passou de `index.html` para `apostila.html`; o `index.html`
|
||||||
|
agora é a página de entrada.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
|
||||||
|
- 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
|
## [1.7.0] - 2026-11-02
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<a href={link} target="_blank" rel="noopener noreferrer" className="btn btn-primary w-full">
|
||||||
{isEn ? "View on AliExpress" : isEs ? "Ver en AliExpress" : isFr ? "Voir sur AliExpress" : isZh ? "在 AliExpress 查看" : isDe ? "Auf AliExpress ansehen" : "Ver no AliExpress"}
|
{isEn ? "View on AliExpress" : isEs ? "Ver en AliExpress" : isFr ? "Voir sur AliExpress" : isZh ? "在 AliExpress 查看" : isDe ? "Auf AliExpress ansehen" : "Ver no AliExpress"}
|
||||||
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
<ArrowUpRight className="h-4 w-4" aria-hidden="true" />
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
{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)"}
|
{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>
|
</span>
|
||||||
</a>
|
</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"
|
||||||
@@ -438,11 +438,6 @@ export default function Header() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{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}>
|
<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"}
|
{isPt ? "Sobre" : isEs ? "Acerca de" : isFr ? "À propos" : isZh ? "关于" : isDe ? "Über uns" : "About"}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -455,7 +450,9 @@ export default function Header() {
|
|||||||
</Link>
|
</Link>
|
||||||
<SearchDialog />
|
<SearchDialog />
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
|
<div className="max-[389px]:hidden">
|
||||||
<ThemeToggle isPt={isPt} isEs={isEs} isFr={isFr} isDe={isDe} isZh={isZh} />
|
<ThemeToggle isPt={isPt} isEs={isEs} isFr={isFr} isDe={isDe} isZh={isZh} />
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
ref={menuButtonRef}
|
ref={menuButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -523,13 +520,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 +555,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 +572,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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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]}`;
|
||||||
|
}
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||