



Em projetos de IoT baseados em ESP32, é comum precisar de um Hub central — um servidor local responsável por receber dados, exibir dashboards e coordenar a comunicação entre diversos módulos sensores e atuadores. Entretanto, quando esse Hub está atrás de um roteador doméstico, surge uma limitação: como acessá-lo remotamente pela Internet sem depender de IP fixo ou configurações complexas de NAT/port forwarding? Soluções tradicionais como DDNS ou VPNs podem funcionar, mas envolvem custos, dependências externas ou configurações avançadas. Com o Cloudflare Tunnel, e em particular o Quick Tunnel, torna-se possível publicar o servidor local com pouco esforço, sem expor a rede doméstica e sem exigir autenticação prévia com a conta Cloudflare. Cada inicialização gera automaticamente uma URL pública temporária no domínio trycloudflare.com. Para tornar essa arquitetura realmente prática, o projeto acrescenta um segundo componente essencial: o serviço de notificação ntfy.sh, que envia a nova URL pública diretamente ao usuário, sempre que o sistema é reiniciado.
A motivação deste trabalho surgiu da necessidade de monitorar e controlar múltiplas estações ESP32 de forma remota, utilizando um Hub central com interface web local (via Nginx e HTML). Nos testes iniciais, verificou-se que o Quick Tunnel atende perfeitamente para uso doméstico, laboratorial e educacional, porém sua principal limitação é a não persistência da URL pública — ela muda a cada reinicialização. A solução foi criar um serviço automatizado (systemd) que:
Antes de iniciar a configuração do túnel e das notificações automáticas, é importante garantir que o ambiente de base esteja pronto e funcional. A seguir, são listados os componentes de hardware e software necessários, bem como as etapas iniciais para acesso remoto via mDNS — um recurso que facilita o uso do Putty a partir de outro computador na mesma rede local.
ssh dailton@domlinux.local
sudo apt update
sudo apt install avahi-daemon avahi-utils -y
sudo systemctl enable --now avahi-daemon
systemctl status avahi-daemon
sudo hostnamectl set-hostname <hostname>
<hostname>pelo nome de sua escolha (por exemplo, meu-hub ou raspberrypi).
Atualize o arquivo /etc/hosts (opcional, mas recomendado)
sudo nano /etc/hosts
127.0.1.1 <hostname>
127.0.0.1 localhost.
Reinicie o serviço Avahi para aplicar as alterações:
sudo systemctl restart avahi-daemon
ssh <user>@<hostname>.local
<hostname>.local via mDNS, conectando ao seu Linux sem precisar saber o IP.
💡 Dica:sudo apt install nginx curl systemd net-tools -y
A figura a seguir apresenta a arquitetura geral da solução, destacando o papel de cada componente e o fluxo de comunicação entre eles:
Figura 1 - Componentes da Arquitetura o Sistema
🌐 Visão geral O sistema foi projetado para permitir acesso remoto seguro ao Hub ESP32 hospedado em um servidor Linux ou Raspberry Pi, utilizando o Cloudflare Quick Tunnel como ponte entre a rede local e a Internet. Todo o processo ocorre automaticamente a cada inicialização do sistema, garantindo que o Hub permaneça acessível mesmo que o IP local ou público varie. Componentes principais 🖥️ 1. Servidor Linux / Raspberry Pi (Hub Local) É o núcleo da solução. Nele residem:[Hub ESP32 - Cloudflare Tunnel]
Nova URL: https://josh-commerce-ann-humidity.trycloudflare.com
O administrador, ao receber a notificação, pode clicar diretamente no link informado e acessar a interface web do Hub, mesmo que o servidor esteja por trás de NAT ou firewall residencial.
O acesso pode ser feito a partir de qualquer dispositivo conectado à Internet — notebook, smartphone ou desktop.
🔌 5. Dispositivos ESP32 (Coletoras e Sensores)
As estações ESP32 comunicam-se com o Hub via rede local (Wi-Fi ou Ethernet).
Cada uma possui um proxy configurado no servidor (via Nginx) que permite redirecionar requisições e monitorar status individualmente.
Assim, o Hub centraliza dados de sensores, gráficos e comandos de controle — enquanto o túnel expõe apenas a interface do Hub, mantendo os dispositivos internos protegidos.
🔁 6. Fluxo de inicialização resumido
login as: <seu usuário>
password: ******
Figura 2 - Tela Inicial do Putty
Figura 3 - Tela de Login do Putty
Figura 4 - Tela do Putty Logado
🔹 Ativando o SSH e o mDNS no Linux/Raspberry Pi Habilitar o serviço SSHsudo systemctl enable ssh
sudo systemctl start ssh
ssh usuario@meu-linux.local
Host: meu-linux.local
Port: 22
ping raspberrypi.local
ssh usuario@raspberrypi.local
ssh-keygen -t ed25519
ssh-copy-id usuario@meu-linux.local
|
Função |
Comando principal |
|
Instalar mDNS |
sudo apt install avahi-daemon -y |
|
Habilitar SSH |
sudo systemctl enable --now ssh |
|
Testar mDNS |
ping raspberrypi.local |
|
Acessar via PuTTY |
Host: raspberrypi.local, Port: 22, Type: SSH |
|
Login |
usuario / senha do Linux |
Nesta seção faremos a configuração do Hub para rodar em um Linux ou Raspberry PI fazendo o apontamento para duas aplicações rodando em ESP32's diferentes na mesma rede local. As duas aplicações ficarão disponíveis para o acesso direto através da Internet sem ter IP público, sem domínio próprio registrado, sem liberação de portas pelo provedor de Internet, sem DNS dinâmico, etc. As duas aplicações já foram publicadas no Blog da Eletrogate. Sugerimos ao leitor dar uma olhada em cada projeto mencionado para melhor entendimento do tutorial aqui apresentado. São elas:
function pathBase() {
// se a URL tiver /d/algumaCoisa no começo, preserve; senão, vazio
const m = location.pathname.match(/^\/d\/\d+(?=\/|$)/);
return m ? m[0] : ''; // ex.: "/d/1" ou ""
}
function inicializaWebSocket() {
//const proto = location.protocol === 'https:' ? 'wss' : 'ws';
//ws = new WebSocket(`${proto}://${location.host}/ws`);
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const base = pathBase(); // "/d/1" no túnel, "" na LAN
ws = new WebSocket(`${proto}://${location.host}${base}/ws`);
ws.onmessage = (ev)=>{
let msg;
try { msg = JSON.parse(ev.data); } catch(e){ return; }
if (msg.type === 'snapshot') {
applySnapshot(msg.nodes);
} else if (msg.type === 'update') {
renderCard(msg);
} else if (msg.type === 'remove') {
removeCard(msg.mac);
}
};
}
Com o ambiente básico preparado e a arquitetura compreendida, passamos agora à configuração prática do servidor local.
Nesta etapa, instalaremos e validaremos os componentes que formam o núcleo da comunicação: o servidor Nginx, o serviço Cloudflared, e os testes de conectividade local e remota.
🧩 1. Instalação e configuração do Nginx
O Nginx será o servidor web local responsável por hospedar o painel do Hub ESP32.
Ele também atuará como ponto de entrada para os proxies que redirecionam as requisições para cada ESP32 coletor da rede local.
Instale com:
sudo apt update
sudo apt install nginx -y
sudo systemctl status nginx
sudo mkdir -p /var/www/hubesp32
sudo chown -R $USER:$USER /var/www/hubesp32
cat <<'EOF' > /var/www/hubesp32/index.html <!doctype html> <html lang="pt-br"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"> <title>Hub ESP32 - Teste</title> </head> <body> <h2>Hub ESP32 ativo!</h2> <p>Este é o painel principal hospedado no Nginx local.</p> <!-- ESP1 --> <a class="card" href="/d/1/">Rede ESP-NOW — ESP01</a> <br><br> <!-- ESP2 --> <a class="card" href="/d/2/">Notificações via ntfy.sh — ESP02</a> </body> </html> EOFOu crie um arquivo index.html inicial mais elaborado como opção 2:
cat <<'EOF' > /var/www/hubesp32/index.html
<!doctype html>
<html lang="pt-BR" data-theme="system">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>Hub ESP32</title>
<style>
/* ---------- Tema (variáveis) ---------- */
:root{
--bg:#0b0c10; --card:#11151b; --text:#e6e8eb; --muted:#9aa4b2; --accent:#00d2ff;
--radius:18px; --shadow:0 8px 24px rgba(0,0,0,.25); --size:18px;
}
@media (prefers-color-scheme: light){
:root{ --bg:#f7f7fb; --card:#ffffff; --text:#262933; --muted:#5d6677; --accent:#0077ff; --shadow:0 8px 24px rgba(0,0,0,.08); }
}
/* Força claro/escuro quando usuário escolhe */
[data-theme="light"]{
--bg:#f7f7fb; --card:#ffffff; --text:#262933; --muted:#5d6677; --accent:#0077ff; --shadow:0 8px 24px rgba(0,0,0,.08);
}
[data-theme="dark"]{
--bg:#0b0c10; --card:#11151b; --text:#e6e8eb; --muted:#9aa4b2; --accent:#00d2ff; --shadow:0 8px 24px rgba(0,0,0,.25);
}
/* ---------- Layout ---------- */
html,body{height:100%;margin:0;}
body{
font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Arial,sans-serif;
background:var(--bg); color:var(--text); font-size:var(--size);
display:flex; align-items:center; justify-content:center; padding:24px;
}
.wrap{ width:100%; max-width:720px; }
.card{
background:var(--card); border-radius:var(--radius); box-shadow:var(--shadow);
padding:20px 18px; position:relative;
}
.title{ display:flex; align-items:center; gap:12px; margin:6px 6px 14px; }
.title .logo{ width:36px; height:36px; display:grid; place-items:center;
border-radius:12px; background:linear-gradient(135deg,var(--accent),#7f5af0); color:white; font-weight:800; }
h1{ font-size:1.4rem; line-height:1.2; margin:0; }
p.sub{ margin:2px 0 0; color:var(--muted); font-size:.95rem; }
.grid{ display:grid; gap:14px; grid-template-columns:1fr; margin:12px 6px 4px; }
@media (min-width:560px){ .grid{ grid-template-columns:1fr 1fr; } }
.app{
border:1px solid rgba(127,127,127,.18); border-radius:16px; padding:14px 12px;
display:flex; flex-direction:column; gap:10px;
background:linear-gradient(180deg, color-mix(in oklab, var(--card), #000 2%), var(--card));
}
.app h2{ margin:0; font-size:1.05rem; }
.badges{ display:flex; gap:8px; flex-wrap:wrap; color:var(--muted); font-size:.9rem;}
.btn{
appearance:none; -webkit-appearance:none; cursor:pointer; text-decoration:none;
display:inline-flex; align-items:center; justify-content:center; gap:.6em;
padding:12px 14px; border-radius:12px; font-weight:600; border:0;
color:#fff; background:linear-gradient(135deg,var(--accent),#7f5af0);
box-shadow:0 6px 16px rgba(0,0,0,.25);
}
.btn:active{ transform:translateY(1px); }
.footer{ text-align:center; color:var(--muted); font-size:.9rem; margin-top:14px; }
/* ---------- Alternador de tema ---------- */
.theme-toggle{
position:absolute; top:12px; right:12px;
display:flex; align-items:center; gap:8px;
background:transparent; border:1px dashed rgba(127,127,127,.35);
color:var(--muted); border-radius:12px; padding:6px 10px; cursor:pointer;
font-weight:600;
}
.theme-toggle:hover{ border-style:solid; }
.theme-toggle .dot{ width:8px; height:8px; border-radius:999px; background:var(--accent); }
/* Tamanho bom em telas pequenas */
@media (max-width:400px){
:root{ --size:17px; }
.btn{ padding:14px 16px; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<button id="themeBtn" class="theme-toggle" aria-label="Alternar tema">
<span class="dot"></span><span id="themeLabel">🖥️ Sistema</span>
</button>
<div class="title">
<div class="logo">ESP</div>
<div>
<h1>Hub ESP32</h1>
<p class="sub">Selecione uma aplicação</p>
</div>
</div>
<div class="grid">
<div class="app">
<h2>Rede ESP-NOW de ESP01</h2>
<div class="badges">
<span>WebSocket ✔</span>
<span>Painéis dinâmicos</span>
</div>
<a class="btn" href="/d/1/">Abrir aplicação</a>
</div>
<div class="app">
<h2>Notificações via Ntfy.sh</h2>
<div class="badges">
<span>Formulário de envio</span>
<span>Proxy /send</span>
</div>
<a class="btn" href="/d/2/">Abrir aplicação</a>
</div>
</div>
<div class="footer">Protegido por autenticação • Cloudflare Quick Tunnel</div>
</div>
</div>
<script>
// -------- Alternador de tema --------
const root = document.documentElement;
const btn = document.getElementById('themeBtn');
const lab = document.getElementById('themeLabel');
// Estados possíveis: 'system' | 'light' | 'dark'
function applyTheme(mode){
root.setAttribute('data-theme', mode);
localStorage.setItem('hubTheme', mode);
lab.textContent = mode === 'light' ? '☀️ Claro' : mode === 'dark' ? '🌙 Escuro' : '🖥️ Sistema';
}
// Inicializa com preferência salva (ou 'system')
applyTheme(localStorage.getItem('hubTheme') || 'system');
// Alterna ciclicamente: system -> light -> dark -> system...
btn.addEventListener('click', ()=>{
const now = root.getAttribute('data-theme') || 'system';
applyTheme(now === 'system' ? 'light' : now === 'light' ? 'dark' : 'system');
});
// Ajuste fino para telas com densidade alta
if (window.devicePixelRatio && devicePixelRatio > 2) {
document.documentElement.style.setProperty('--size','19px');
}
</script>
</body>
</html>
EOF
cat <<'EOF' > /var/www/hubesp32/index.html
<!doctype html>
<html lang="pt-br">
<head>
<meta charset="utf-8">
<title>Hub ESP32 - Teste</title>
</head>
<body>
<h2>Hub ESP32 ativo!</h2>
<p>Este é o painel principal hospedado no Nginx local.</p>
</body>
</html>
EOF
🌐 3. Configuração do servidor virtual (site do Hub)
Crie o arquivo de configuração do Nginx:
sudo nano /etc/nginx/sites-available/hubesp32
map $http_upgrade $connection_upgrade { default upgrade; "" close; }
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
auth_basic "Hub ESP32";
auth_basic_user_file /etc/nginx/.htpasswd;
root /var/www/hubesp32;
index index.html;
# Garante que a raiz SEMPRE cai no nosso index.html
location = / { try_files /index.html =404; }
location / { try_files $uri /index.html; }
# Proxy ESP32 #1 (WebSocket/HTTP)
location ^~ /d/1/ {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
rewrite ^/d/1/(.*)$ /$1 break;
proxy_pass http://192.168.18.15:80/;
}
# Proxy ESP32 #2 (ajuste o IP)
location ^~ /d/2/ {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
rewrite ^/d/2/(.*)$ /$1 break;
proxy_pass http://192.168.18.33:80/;
}
# Rota exata /d/2/send -> envia POST direto ao ESP32-2
location = /d/2/send {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.18.33:80/send;
}
# Rota exata /send (quando o HTML usa action="/send")
location = /send {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.18.33:80/send;
}
location ~* \.(js|css|png|jpg|ico)$ {
add_header Cache-Control "public, max-age=3600";
}
}
Ative o site e reinicie o Nginx:
sudo ln -s /etc/nginx/sites-available/hubesp32 /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl -I http://127.0.0.1/
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
sudo mkdir -p /etc/cloudflared
cd /usr/local/bin
sudo wget -O cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
sudo chmod +x cloudflared
cd ~
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64 -o cloudflared
chmod +x cloudflared
sudo mv cloudflared /usr/local/bin/cloudflared
cloudflared tunnel --url http://127.0.0.1:80
sudo nano /etc/systemd/system/cloudflared.service
[Unit] Description=cloudflared (Quick Tunnel - 127.0.0.1:80) After=network.target [Service] ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --url http://127.0.0.1:80 Restart=always RestartSec=10 User=root [Install] WantedBy=multi-user.targetAtive e inicie o serviço:
sudo systemctl daemon-reload sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared
journalctl -u cloudflared -n 30 --no-pager | grep trycloudflare
🧩 6. Definição de Credenciais do Hub
Como o Hub vai ser acessado direto pela Internet é fundamental definir credenciais para acesso ao Hub. Estamos definindo a autenticação BASIC mas dentro do contexto https e do tunnel seguro da Cloudflare. As credenciais do Hub (usuário/senha) são definidas no arquivo de autenticação usado pelo Nginx:/etc/nginx/.htpasswd
sudo htpasswd -c /etc/nginx/.htpasswd <nome_usuario>
sudo apt update
sudo apt install apache2-utils
sudo htpasswd /etc/nginx/.htpasswd <novo_usuario>
cat /etc/nginx/.htpasswd
cat /etc/nginx/sites-available/hubesp32
auth_basic "Hub ESP32";
auth_basic_user_file /etc/nginx/.htpasswd;
Ou seja:
O Quick Tunnel oferece acesso instantâneo à rede local, mas sua URL muda a cada inicialização. Para evitar a necessidade de consultar manualmente os logs, criaremos um serviço auxiliar (cf-url-notify) que:
cloudflared.service
Isso vai deixar o Quick Tunnel subindo automaticamente no boot:
sudo tee /etc/systemd/system/cloudflared.service >/dev/null <<'EOF' [Unit] Description=cloudflared (Quick Tunnel - 127.0.0.1:80) After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/cloudflared tunnel --url http://127.0.0.1:80 Restart=always RestartSec=5 User=root [Install] WantedBy=multi-user.target EOFRecarregue o systemd e já habilite o serviço:
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared.service
sudo systemctl status cloudflared.service --no-pager -n 20
status mostrar active (running) e no log aparecer uma URL https://...trycloudflare.com, o túnel está ok.
Passo 2: Criando o script de monitoramento e notificação
Crie o arquivo:
sudo nano /usr/local/bin/cf-url-notify.sh
#!/bin/bash
# cf-url-notify.sh - Monitora logs do Cloudflared e envia a URL pública via ntfy.sh
# Autor: Dailton de Oliveira Menezes (projeto Hub ESP32 com Cloudflare Tunnel)
set -e
TOPIC="dom_07c2_07e9_alerts" # Tópico de destino no ntfy.sh
SERVICE="cloudflared.service"
TMPFILE="/tmp/cf_url_current"
NTFY_URL="https://ntfy.sh/${TOPIC}"
echo "[cf-url-notify] Aguardando URL pública do serviço ${SERVICE} (boot atual)..."
# Aguarda até o Cloudflared gerar a URL
URL=""
for i in {1..60}; do
URL=$(journalctl -u ${SERVICE} -b --no-pager | grep -o 'https://[a-zA-Z0-9.-]*\.trycloudflare\.com' | tail -n1)
if [ -n "$URL" ]; then
echo "[cf-url-notify] URL detectada: $URL"
break
fi
sleep 5
done
if [ -z "$URL" ]; then
echo "[cf-url-notify] Nenhuma URL detectada após o tempo limite."
exit 1
fi
# Evita reenvio da mesma URL
if [ -f "$TMPFILE" ] && grep -q "$URL" "$TMPFILE"; then
echo "[cf-url-notify] Mesma URL já enviada neste boot: $URL (nada a fazer)"
exit 0
fi
# Testa a disponibilidade da URL
for i in {1..10}; do
if curl -s --max-time 3 -I "$URL" | grep -q "200"; then
echo "[cf-url-notify] URL acessível."
break
fi
echo "[cf-url-notify] Aguardando URL ficar acessível..."
sleep 10
done
# Envia notificação
MESSAGE="Nova URL pública do Hub ESP32 disponível:"
curl -s -X POST \
-H "Title: 🌐 Hub ESP32 Online" \
-H "Priority: high" \
-H "Tags: globe_with_meridians,rocket" \
-d "${MESSAGE} ${URL}" \
"${NTFY_URL}"
echo "$URL" > "$TMPFILE"
echo "[cf-url-notify] Notificação enviada: $URL"
Permita execução:
sudo chmod +x /usr/local/bin/cf-url-notify.sh
sudo nano /etc/systemd/system/cf-url-notify.service
[Unit] Description=Enviar URL do Quick Tunnel para ntfy.sh After=cloudflared.service Requires=cloudflared.service [Service] ExecStart=/usr/local/bin/cf-url-notify.sh Type=oneshot User=root RemainAfterExit=true [Install] WantedBy=multi-user.targetPasso 4: Ativando e testando o serviço Ative e rode manualmente para testar:
sudo systemctl daemon-reload
sudo systemctl enable cf-url-notify.service
sudo systemctl start cf-url-notify.service
journalctl -u cf-url-notify.service -n 30 --no-pager
[cf-url-notify] Aguardando URL pública do serviço cloudflared (boot atual)...
[cf-url-notify] URL detectada: https://lucky-wavey-puma.trycloudflare.com
[cf-url-notify] URL acessível.
[cf-url-notify] Notificação enviada: https://lucky-wavey-puma.trycloudflare.com
sudo reboot
curl http://127.0.0.1
<h2>Hub ESP32 ativo!</h2>
http://<IP_DO_LINUX_LOCAL>
http://192.168.18.18
journalctl -u cloudflared -n 30 --no-pager
INF | https://josh-commerce-ann-humidity.trycloudflare.com
journalctl -u cf-url-notify.service -n 20 --no-pager
[cf-url-notify] URL detectada: https://josh-commerce-ann-humidity.trycloudflare.com
[cf-url-notify] URL acessível.
[cf-url-notify] Notificação enviada: https://josh-commerce-ann-humidity.trycloudflare.com
sudo reboot
sudo journalctl -u cloudflared -n 20 --no-pager
| Problema observado | Possível causa | Ação recomendada |
| ❌ Sem resposta do túnel público | Cloudflared não inicializou corretamente | Verificar systemctl status cloudflared |
| 🔕 Notificação não chegou ao ntfy.sh | Falha de rede ou token incorreto | Testar manualmente com curl e revisar tópico do ntfy |
| 🌐 Página do Hub inacessível | Erro no Nginx ou conflito de porta | Verificar sudo nginx -t e logs em /var/log/nginx/error.log |
| 🚫 URL antiga enviada | Script cf-url-notify.sh não detectou novo boot | Verificar data e conteúdo de /tmp/cf_url_current |
Para adicionar mais ESP32 ao Hub, basicamente, será necessário alterar os arquivos de configuração do Hub e o index.html. Para ver o arquivo do Hub:
cat /etc/nginx/sites-available/hubesp32
Caminhos estáveis no Hub:
https://<URL_PUBLICA>/d/1/ → encaminha para http://192.168.18.15/
https://<URL_PUBLICA>/d/2/ → encaminha para http://192.168.18.33/
(adicione quantos precisar: /d/3/, /d/4/ …)
Mantemos o sufixo / no final para facilitar “caminhos relativos” das páginas do ESP.
Dica: fixe IPs dos ESP32 via DHCP reservation no seu roteador para não perder o mapeamento.
Configuração do Nginx (reverse proxy + WebSocket)
Edite o arquivo do site do Hub:sudo nano /etc/nginx/sites-available/hubesp32
server { ... }, adicione um location por ESP
Exemplo para ESP3 (IP local 192.168.18.50), adicione o bloco a seguir ao arquivo :
# Proxy ESP32 #3 (ajuste o IP)
location ^~ /d/3/ {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
rewrite ^/d/3/(.*)$ /$1 break;
proxy_pass http://192.168.18.50:80/;
}
Altere o index.html para adicionar ao html:
sudo nano /var/www/hubesp32/index.html
<!-- ESP3 -->
<a class="card" href="/d/3/">Nova Aplicação — ESP03</a>
sudo nginx -t
sudo systemctl reload nginx
Neste ponto a referência ao novo ESP3 já estará disponível.
Suporte a outras rotas que o ESP possa ter
Como a aplicação o ESP está sob um PROXY, uma atenção especial deve ser tomada para outras rotas que por acaso a aplicação utiliza.
Se a aplicação do ESP faz, por exemplo, fetch('/send', {method: 'POST', ...}) e o ESP está sob o PROXY, a requisição vai ser passada para o PROXY e não para o ESP dando o erro 404 ou 405 porque o servidor web do Hub só conhece a entrada de cada ESP. Portanto, precisamos criar uma regra adicional para cada requisição que o ESP envie informando ao PROXY para devolver a requisição para o ESP.
No nosso exemplo, a aplicação ESP2 envia a requisição /send para o envio da notificação e por isso criamos uma regra adicional para garantir que a requisição retorne do PROXY para o ESP. Veja como ficou o bloco:
# Rota exata /send (quando o HTML usa action="/send")
location = /send {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.18.33:80/send;
}
Caminhos relativos no ESP: com proxy_pass http://IP/; o ESP “acha” que está na raiz.
Evite hardcode de caminhos absolutos; prefira relativos.
WebSocket no ESP: padronize caminho (/ws) e não force wss:// no código do ESP; deixe o Nginx/Cloudflare fazer TLS.
CORS: como o Hub e o proxy usam o mesmo host/porta, normalmente não precisa de CORS.
Se for chamar APIs externas, aí sim ajuste CORS no Nginx ou no fetch.
Tempo de leitura: dashboards com SSE/WS podem precisar de proxy_read_timeout 3600s.
IPs fixos: reserve IP dos ESP32 no DHCP para que /espN/ nunca quebre.
Fixe o IP do novo ESP (ex.: 192.168.18.53);
Duplique um bloco location /d/x/ { ... } no /etc/nginx/sites-available/hubesp32 trocando IP e caminho;
sudo nginx -t && sudo systemctl reload nginx;
Adicione o card correspondente em /var/www/hubesp32/index.html;
Teste curl -I http://127.0.0.1/d/x/ e, por fim, pela URL pública do Cloudflare.
Checagens rápidas (sanity checks) A) Nginx local
# Teste de sintaxe sudo nginx -t # Está ativo? systemctl is-active nginx && systemctl is-enabled nginx # Responde na 80? curl -I --max-time 3 http://127.0.0.1/B) Cloudflared (Quick Tunnel)
# Status do serviço sudo systemctl status cloudflared --no-pager -n 30 # Últimas linhas de log (boot atual) journalctl -b -u cloudflared -n 80 --no-pager # Ver URL pública atual (a partir do log) journalctl -b -u cloudflared --no-pager \ | grep -Eo 'https://[a-z0-9-]+\.trycloudflare\.com' \ | tail -1C) Proxies para ESPs
# Deve retornar HTTP 200/401/301 (dependendo do seu setup)
curl -I --max-time 3 http://127.0.0.1/d/1/
curl -I --max-time 3 http://127.0.0.1/d/2/
-u usuario:senha no curl.
Logs úteis (onde olhar e o que procurar)
A) Nginx
# Acessos sudo tail -n 200 /var/log/nginx/access.log # Erros (proxy, upstream, WebSocket, etc.) sudo tail -n 200 /var/log/nginx/error.log # Filtrar um ESP específico sudo grep '/esp1/' -n /var/log/nginx/access.log | tail -n 50B) Cloudflared
journalctl -b -u cloudflared -n 200 --no-pager
systemctl list-timers | grep cf-url-notify
journalctl -u cf-url-notify.service -n 80 --no-pager
Network → WS: confirme que o upgrade do WebSocket virou 101 Switching Protocols.
Frames: verifique se chegam mensagens snapshot/update.
Console: erros de CORS, Mixed Content, TypeError: ws is undefined, etc.
|
Sintoma / Log |
Causa provável |
Correção |
|
405 Not Allowed vindo do Hub |
Método não roteado no proxy encurtador (ex.: POST /esp2/send) |
Crie location = /esp2/send { proxy_pass http://IP/send; } ou deixe o ESP aceitar o caminho “sem encurtador” via location /esp2/ |
|
502/504 no Nginx |
ESP desligado/fora do ar; IP mudou; timeout curto |
Verifique IP (reserva DHCP), aumente proxy_read_timeout e proxy_send_timeout |
|
WS não conecta (fica “pending”) |
Upgrade/Connection não propagados |
Garanta no server do Nginx: proxy_http_version 1.1, proxy_set_header Upgrade $http_upgrade, proxy_set_header Connection $connection_upgrade |
|
WS fecha após alguns segundos |
Timeout do proxy |
Aumente proxy_read_timeout 3600s; |
|
Mixed Content no navegador |
Página HTTPS chamando ws:// explícito |
No front use const proto = location.protocol==='https:'?'wss':'ws' |
|
401 Unauthorized no Hub |
Basic Auth ativo |
No teste use curl -u user:senha … (ou desative na fase de debug) |
|
URL pública “antiga” no push |
Captura antes do Cloudflared “subir” |
Espere alguns segundos e leia a última ocorrência via `journalctl … |
# 1) Nginx sudo nginx -t && sudo systemctl restart nginx # 2) Cloudflared sudo systemctl restart cloudflared sleep 5 journalctl -b -u cloudflared --no-pager \ | grep -Eo 'https://[a-z0-9-]+\.trycloudflare\.com' | tail -1 # 3) ESP(s) — se necessário # (reinicie o dispositivo físico ou via OTA)Testes de ponta a ponta A) Pela URL pública
PUB=$(journalctl -b -u cloudflared --no-pager \ | grep -Eo 'https://[a-z0-9-]+\.trycloudflare\.com' | tail -1) # Hub (página inicial) curl -I --max-time 6 "$PUB/" # ESP1 via Hub curl -I --max-time 6 "$PUB/esp1/" # Envio POST (se tiver encurtador) curl -i --max-time 6 -X POST "$PUB/esp2/send" -d 't=1'B) WebSocket pelo navegador
Abra https://URL_PUBLICA/d/1/, pressione F12 > Network > WS, confirme 101 e frames chegando.
# (se estiver usando htpasswd)
sudo htpasswd /etc/nginx/.htpasswd seu_usuario
sudo systemctl reload nginx
O Hub abre em http://127.0.0.1/?
A URL pública aparece no journalctl -b -u cloudflared?
O card do ESP abre localmente (curl -I http://127.0.0.1/esp1/)?
O mesmo card abre pela URL pública?
Para WS: status 101 e frames?
POSTs retornam 2xx?
Logs do Nginx/Cloudflared mostram erro? Qual?
IPs dos ESPs estão fixos e respondendo na LAN?
Figura 5 - Tela do Hub no Desktop
Figura 6 - Tela do Hub no Celular
Figura 7 - Tela Simples do Hub no Celular
Figura 8 - Tela App1 do Hub no Desktop
Figura 9 - Tela App1 do Hub no Celular
Figura 10 - Tela App2 do Hub no Desktop
Figura 11 - Tela App2 do Hub no Celular
Figura 12 - Tela Web do Serviço de Notificação Ntfy.sh
Figura 13- Tela do App do Serviço de Notificação Ntfy.sh
meu-linux.local em vez de IP.Este projeto buscou demonstrar que é possível construir uma infraestrutura segura e totalmente automatizada para hospedar dashboards e serviços locais de IoT sem depender de IPs válidos na Internet, ou domínio registrado, ou roteamento avançado ou conta cadastrada na Cloudflare com fornecimento de cartão de crédito. A combinação entre o Quick Tunnel da Cloudflare, o servidor Nginx e o sistema de notificações ntfy.sh fornece uma base extremamente flexível, especialmente para ambientes dinâmicos com endereços públicos variáveis. Embora o Quick Tunnel não ofereça persistência de endereço como o Named Tunnel, o uso de um serviço no Linux para monitorar a criação da URL e notificar automaticamente o administrador compensa essa limitação com eficácia. O resultado é um ambiente totalmente funcional, gratuito, que pode ser inicializado, atualizado e acessado remotamente com mínimo esforço. Além disso, a estrutura modular adotada — com o hub local servindo como ponto central de acesso aos módulos ESP32 — facilita futuras expansões, como a integração de novos dispositivos, sensores ou painéis de controle. Essa arquitetura distribui a complexidade, mas mantém o controle dentro da rede local. Buscamos também demonstrar que os mesmos princípios poderão ser aplicados, sem alterações significativas, para o Raspberry Pi 4. Isso demonstra a portabilidade e a escalabilidade da solução, tornando-a ideal para laboratórios, sistemas de automação residencial e projetos didáticos. Por fim, o projeto mostra como tecnologias de código aberto podem ser combinadas para entregar resultados profissionais. Espera-se que este guia sirva de base para novas ideias e inspire a criação de soluções ainda mais inteligentes e acessíveis. Com poucos recursos e um pouco de curiosidade, é possível transformar um simples servidor local em uma janela segura para o acesso de qualquer lugar do mundo.
|
|
Este projeto apresenta uma forma prática e segura de acessar remotamente um Hub de dispositivos ESP32 hospedado em um servidor Linux ou Raspberry Pi, utilizando Cloudflare Quick Tunnel para exposição pública dinâmica e ntfy.sh para notificações automáticas da URL pública atual.
Encontre tudo na Loja Eletrogate com frete grátis para compras acima de R$ 200