// ============================================================
// PROJETO SMARTPRESS 2026 — CÓDIGO COMENTADO PARA AVALIAÇÃO
// ============================================================
// --- IDENTIFICAÇÃO DO PROJETO NO BLYNK ---
// Estas 3 linhas são obrigatórias para o ESP32 se ligar à plataforma Blynk na cloud.
// O TEMPLATE_ID e TEMPLATE_NAME identificam o "molde" do dashboard que criaste no Blynk.
// O AUTH_TOKEN é como uma palavra-passe única — garante que só o teu ESP32 acede ao teu dashboard.
#define BLYNK_TEMPLATE_ID "TMPL5HHILaq_W"
#define BLYNK_TEMPLATE_NAME "PROJETO SMARTPRESS 2026"
#define BLYNK_AUTH_TOKEN "ktQ54HO051H4FBsRleEx8DCB2beP5Zpq"
// --- BIBLIOTECAS ---
// #include carrega código já escrito por outros para reutilizar funcionalidades.
#include <WiFi.h> // Ativa o chip Wi-Fi que está dentro do ESP32
#include <WiFiClient.h> // Permite ao ESP32 abrir ligações de rede (tipo cliente TCP/IP)
#include <BlynkSimpleEsp32.h> // "Traduz" a comunicação entre o ESP32 e os servidores Blynk
#include <ESP32Servo.h> // Permite controlar servomotores com o ESP32
#include <LiquidCrystal.h> // Permite enviar texto para um ecrã LCD ligado via fios (paralelo)
#include <Preferences.h> // Permite gravar dados na memória Flash interna do ESP32
// (como um mini disco rígido — não perde dados quando desligas)
// --- CREDENCIAIS WI-FI ---
// O ESP32 usa estes dados para se ligar automaticamente à rede quando arranca.
char ssid[] = "iPhone"; // Nome da rede Wi-Fi (SSID)
char pass[] = "portoporto"; // Password da rede Wi-Fi
// --- OBJETOS GLOBAIS ---
// Um "objeto" é uma variável especial que representa um componente físico ou funcionalidade.
Servo meuServo;
// Representa o servomotor físico ligado ao projeto.
// Com este objeto conseguimos dar ordens ao motor: parar, rodar para a frente, etc.
Preferences preferences;
// Representa o acesso à memória Flash interna do ESP32.
// É aqui que guardamos o contador de peças — mesmo que desliguemos o sistema,
// quando voltar a ligar, o número não se perde.
// --- CONFIGURAÇÃO DO LCD ---
// O LCD está ligado ao ESP32 através de 6 fios de dados (interface paralela).
// O construtor recebe os números dos pinos nesta ordem: RS, EN, D4, D5, D6, D7
// RS (Register Select) — diz ao LCD se o que estás a enviar é um comando (ex: limpar ecrã, mover cursor) ou texto (um caractere para mostrar)
// EN (Enable) — funciona como um "confirmar" — o LCD só lê os dados quando este pino faz um pulso. É como dizer "agora podes ler o que está nos outros pinos"
// D4 a D7 — os 4 fios que transportam os bits de dados (modo 4-bit)
LiquidCrystal lcd(0, 1, 2, 20, 15, 19);
// --- MAPEAMENTO DOS PINOS DE HARDWARE ---
// "const int" garante que estes valores nunca mudam acidentalmente no código.
const int pinoLED_R = 10;
const int pinoLED_G = 11;
const int pinoBuzzer = 18; // PWM — gera frequências sonoras
const int pinoSensorOtico = 3;
const int pinoEN_Sensor = 8;
const int pinoSensorVibracao = 5; // ADC — lê valor analógico (0 a 4095)
const int pinoBotaoEmergencia= 6;
const int pinoMotor = 7; // PWM — controla velocidade/direção do servo
// --- PARÂMETROS DO PROCESSO ---
// Constantes que definem os limites e a lógica do processo de fabrico.
const float VELOCIDADE_TAPETE = 1.5; // Velocidade do tapete em cm/s (medida experimentalmente)
// Usada para calcular o comprimento da peça: comprimento = tempo × velocidade
const int LIMITE_MIN_VIBRACAO = 50; // Valor mínimo do sensor de vibração para confirmar que a prensa funcionou
const int LIMITE_MAX_VIBRACAO = 10000;// Valor máximo aceitável de vibração (acima pode ser erro/avaria)
const int TEMPO_MIN_PECA = 400; // Tempo mínimo em ms que a peça deve demorar a passar no sensor
// (peças demasiado pequenas = inválidas)
const int TIMEOUT_MEDICAO = 6000; // Se a peça não terminar de passar em 6 segundos → erro
// Evita que o sistema fique preso à espera indefinidamente
// --- VARIÁVEIS GLOBAIS ---
// Variáveis que precisam de existir durante todo o programa e ser partilhadas entre funções.
String tipoErroMaterial = ""; // Guarda o texto do erro de material (ex: "INSUF." ou "EXCED.")
// Usado para mostrar no LCD e enviar para o Blynk
String tipoErroVibracao = ""; // Guarda o texto do erro de vibração
unsigned long tempoInicio = 0; // Guarda o momento em que a peça entrou no sensor
unsigned long tempoFinal = 0; // Guarda quanto tempo a peça demorou a atravessar
// "unsigned long" é usado porque millis() devolve números muito grandes (ms desde o boot)
// Um "int" normal só aguenta até ~32.000 — um "unsigned long" aguenta até ~4.000.000.000
int valorVibracao = 0; // Valor lido do sensor de vibração (0 a 4095 no ESP32)
int contadorPecas = 0; // Número total de peças produzidas com sucesso
// --- CONSTANTES DO SERVO 360º ---
// Um servo de 360º não controla ângulo, controla VELOCIDADE e DIREÇÃO.
// A posição 90 é o ponto neutro (parado). Valores acima ou abaixo fazem rodar em sentidos opostos.
const int SERVO_PARADO = 90; // Envia sinal PWM de 90° → motor fica parado
const int SERVO_FRENTE = 180; // Envia sinal PWM de 180° → motor roda para a frente (tapete avança)
// --- MÁQUINA DE ESTADOS (FSM — Finite State Machine) ---
// O sistema funciona como um semáforo: só pode estar num estado de cada vez.
// "enum" cria um tipo personalizado com nomes para cada estado possível.
// "volatile" avisa o compilador que esta variável pode ser alterada FORA do fluxo normal
// (por exemplo, pela interrupção do botão de emergência) — sem volatile poderia ser ignorada.
enum Estados { TRANSPORTE, MEDICAO, PRENSAGEM, ERRO_MAT, ERRO_VIB, EMERGENCIA };
volatile Estados estadoAtual = TRANSPORTE; // O sistema começa sempre no estado TRANSPORTE
// ============================================================
// FUNÇÕES BLYNK — Chamadas automaticamente quando o
// utilizador interage com a App no telemóvel
// ============================================================
// V1 é o "pino virtual 1" do Blynk — está ligado a um botão de emergência na App.
// Esta função é chamada automaticamente quando o utilizador prime esse botão.
BLYNK_WRITE(V1) {
int valorBotao = param.asInt(); // Lê o valor enviado pela App (1 = premido, 0 = solto)
if (valorBotao == 1) { // Se o botão foi premido (não apenas solto)
estadoAtual = EMERGENCIA; // Muda o estado do sistema para EMERGÊNCIA imediatamente
Serial.println("[BLYNK] Emergência ativada via App!");
}
}
// V4 é o "pino virtual 4" do Blynk — botão de reset do contador na App.
BLYNK_WRITE(V4) {
int resetBotao = param.asInt(); // Lê se o botão foi premido
if (resetBotao == 1) {
contadorPecas = 0; // Zera o contador na memória RAM (temporária)
preferences.begin("smartpress", false); // Abre o acesso à memória Flash com o nome "smartpress"
// (false = modo leitura e escrita)
preferences.putInt("pecas", 0); // Grava o valor 0 na Flash com a chave "pecas"
preferences.end(); // Fecha o acesso à Flash (boa prática — liberta recursos)
Blynk.virtualWrite(3, 0); // Atualiza o display do contador na App para 0
lcd.clear(); // Apaga tudo o que estava escrito no LCD
lcd.print("ZERADA!!"); // Mostra mensagem de confirmação no LCD
delay(1000); // Espera 1 segundo para o utilizador ver a mensagem
}
}
// --- PROTÓTIPOS DAS FUNÇÕES ---
// Em C++, se chamas uma função antes de a definir, tens de a declarar aqui primeiro.
// O compilador precisa de conhecer a "assinatura" (nome, parâmetros, tipo de retorno) antes de usar.
// aqui estás a declarar as funções
// dizes ao compilador "esta função existe e vai ser definida mais abaixo"
void faseTransporte();
void faseMedicao();
void fasePrensagem();
void faseErroMaterial();
void faseErroVibracao();
void faseEmergencia();
void imprimirComprimento(unsigned long ms);
void IRAM_ATTR tratarEmergencia(); // ISR = Interrupt Service Routine (função de interrupção)
boolean esperarComSeguranca(unsigned long tempoMs);
// ============================================================
// SETUP — Corre UMA vez quando o ESP32 liga ou reinicia
// ============================================================
void setup() {
Serial.begin(115200); // Inicia comunicação série para debug no computador (115200 baud = velocidade)
lcd.begin(8, 2); // Inicializa o LCD com 8 colunas e 2 linhas
lcd.setCursor(0, 0); // Coloca o cursor no início: coluna 0, linha 0 (canto superior esquerdo)
// Configura cada pino como saída (OUTPUT) ou entrada (INPUT)
pinMode(pinoLED_R, OUTPUT); // LED vermelho: o ESP32 controla-o (envia sinal)
pinMode(pinoLED_G, OUTPUT); // LED verde: o ESP32 controla-o
pinMode(pinoEN_Sensor, OUTPUT); // Pino de enable do sensor: o ESP32 liga/desliga o sensor
pinMode(pinoSensorOtico, INPUT); // Sensor ótico: o ESP32 lê o sinal que ele envia
pinMode(pinoSensorVibracao, INPUT); // Sensor de vibração: o ESP32 lê o valor analógico
pinMode(pinoBotaoEmergencia, INPUT_PULLUP);
// INPUT_PULLUP ativa um resistor interno de pull-up.
// Sem ele, o pino "flutua" e pode ter leituras erráticas/aleatórias.
// Com pull-up: sem pressão = HIGH (3.3V), com pressão = LOW (0V)
meuServo.attach(pinoMotor, 500, 2400);
// Liga o objeto servo ao pino físico do motor.
// 500 e 2400 são os limites do pulso PWM em microssegundos (µs):
// 500µs = posição/velocidade mínima
// 2400µs = posição/velocidade máxima
ledcAttach(pinoBuzzer, 2000, 8);
// Configura o pino do buzzer para gerar PWM (sinal que produz som).
// 2000 = frequência base em Hz, 8 = resolução de 8 bits (0 a 255)
attachInterrupt(digitalPinToInterrupt(pinoBotaoEmergencia), tratarEmergencia, FALLING);
// Configura uma INTERRUPÇÃO HARDWARE no botão de emergência.
// Interrupção = o ESP32 para tudo o que está a fazer e executa "tratarEmergencia" imediatamente.
// FALLING = ativa quando o sinal cai de HIGH (3.3V) para LOW (0V) — ou seja, quando o botão é premido.
// Isto garante resposta IMEDIATA, independentemente de onde o programa estiver.
Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
// Liga o ESP32 ao servidor Blynk na cloud usando o token, o nome e a password do Wi-Fi.
// Este processo inclui ligar ao Wi-Fi e autenticar na plataforma Blynk.
preferences.begin("smartpress", false); // Abre a memória Flash no "namespace" smartpress
contadorPecas = preferences.getInt("pecas", 0); // Lê o contador guardado; se não existir, usa 0
preferences.end(); // Fecha a Flash — sempre bom fechar depois de ler
}
// ============================================================
// LOOP — Corre em CICLO INFINITO depois do setup()
// ============================================================
void loop() {
Blynk.run(); // OBRIGATÓRIO em cada ciclo — mantém a ligação ao Blynk ativa
// e processa eventos da App (como os botões V1 e V4)
// FSM (Máquina de Estados): com base no estado atual, executa a função correspondente.
// O "switch" é mais eficiente que vários "if/else" para múltiplos casos.
switch (estadoAtual) {
case TRANSPORTE: faseTransporte(); break; // Tapete a mover, à espera de peça
case MEDICAO: faseMedicao(); break; // Peça detetada — a medir o comprimento
case PRENSAGEM: fasePrensagem(); break; // Peça no lugar — ativa a prensa
case ERRO_MAT: faseErroMaterial(); break; // Erro no material (tamanho inválido)
case ERRO_VIB: faseErroVibracao(); break; // Erro na vibração (prensa não atuou)
case EMERGENCIA: faseEmergencia(); break; // Paragem de emergência ativada
}
delay(50); // Pequena pausa de 50ms entre ciclos para não sobrecarregar o processador
}
// ============================================================
// FUNÇÃO DE ESPERA "INTELIGENTE"
// ============================================================
// Problema com delay() normal: bloqueia o ESP32 completamente.
// → Enquanto espera, não consegue atender o Blynk, nem detetar a emergência.
// Esta função substitui o delay() e continua a processar o Blynk durante a espera.
// Retorna "true" se a espera completou normalmente, "false" se foi interrompida por emergência.
boolean esperarComSeguranca(unsigned long tempoMs) {
unsigned long inicioDessaEspera = millis(); // Guarda o momento em que a espera começou
while (millis() - inicioDessaEspera < tempoMs) { // Enquanto não passou o tempo desejado...
Blynk.run(); // ...continua a atender a App Blynk
if (estadoAtual == EMERGENCIA) return false; // Se emergência foi ativada, para imediatamente
delay(5); // Pausa curta de 5ms para não "fritar" o CPU
}
return true; // A espera completou sem interrupções
}
// ============================================================
// IMPLEMENTAÇÃO DOS ESTADOS DA FSM
// ============================================================
// ESTADO 1: TRANSPORTE
// O tapete está a andar. O sistema aguarda que uma peça chegue ao sensor ótico.
void faseTransporte() {
if (!meuServo.attached()) meuServo.attach(pinoMotor, 500, 2400);
// '!' é o operador NOT (negação). Se o servo NÃO está ligado, liga-o.
// O servo pode ter sido desligado (detach) no estado anterior — aqui garantimos que está ativo.
meuServo.write(SERVO_FRENTE); // Envia o sinal PWM de 180° → o tapete avança
digitalWrite(pinoEN_Sensor, HIGH); // Liga o sensor ótico (HIGH = ativado)
if (digitalRead(pinoSensorOtico) == LOW) {
// O sensor ótico devolve LOW quando deteta um objeto (a peça bloqueia a luz).
tempoInicio = millis(); // Regista o instante exato em que a peça entrou
estadoAtual = MEDICAO; // Avança para o próximo estado
}
}
// ESTADO 2: MEDIÇÃO
// A peça está a passar no sensor. Medimos quanto tempo demora (para calcular o comprimento).
void faseMedicao() {
if (millis() - tempoInicio > TIMEOUT_MEDICAO) {
// Se já passou mais de 6 segundos e a peça ainda não saiu → erro
// (pode estar presa, ou ser grande demais, ou o sensor avariou)
tipoErroMaterial = "EXCED."; // Define o texto do erro
estadoAtual = ERRO_MAT; // Muda para o estado de erro de material
}
else if (digitalRead(pinoSensorOtico) == HIGH) {
// O sensor voltou a HIGH → a peça passou completamente
tempoFinal = millis() - tempoInicio; // Calcula a duração: agora - quando começou
meuServo.detach(); // Desliga o sinal PWM do servo (para o tapete)
imprimirComprimento(tempoFinal); // Calcula e envia o comprimento para o Blynk
if (tempoFinal < TEMPO_MIN_PECA) {
// Se demorou menos de 400ms, a peça é demasiado pequena → inválida
tipoErroMaterial = "INSUF."; // Texto do erro: "insuficiente"
estadoAtual = ERRO_MAT;
} else {
estadoAtual = PRENSAGEM; // Tamanho válido → avança para prensar
}
}
}
// ESTADO 3: PRENSAGEM
// A peça chegou à prensa. O servo para, a prensa atua, e verificamos a vibração para confirmar.
void fasePrensagem() {
meuServo.detach(); // Garante que o motor está parado durante a prensagem
// ... (aqui estaria a sequência de espera de 3 segundos para a prensa baixar e subir) ...
valorVibracao = analogRead(pinoSensorVibracao);
// Lê o valor analógico do sensor de vibração (0 a 4095 no ESP32 com ADC de 12 bits).
// Uma prensagem real gera vibração — se o valor estiver no intervalo esperado, a prensa funcionou.
if (valorVibracao > LIMITE_MIN_VIBRACAO && valorVibracao < LIMITE_MAX_VIBRACAO) {
// Vibração dentro dos limites aceitáveis → prensagem bem-sucedida!
contadorPecas++; // Incrementa o contador: contadorPecas = contadorPecas + 1
// Grava o novo valor do contador na memória Flash para não se perder se houver corte de energia:
preferences.begin("smartpress", false); // Abre a Flash
preferences.putInt("pecas", contadorPecas); // Guarda o valor atualizado
preferences.end(); // Fecha a Flash
estadoAtual = TRANSPORTE; // Volta ao início para processar a próxima peça
} else {
estadoAtual = ERRO_VIB; // Vibração fora do esperado → a prensa pode não ter atuado
}
}
// ESTADO 4: ERRO DE MATERIAL
// A peça tinha tamanho inválido (muito pequena ou demorou tempo de mais).
// O sistema emite 5 bips de aviso e depois volta ao estado TRANSPORTE.
void faseErroMaterial() {
// ... (aqui estaria a sinalização visual com os LEDs) ...
for (int i = 0; i < 5; i++) { // Repete 5 vezes o ciclo de bip
if (estadoAtual == EMERGENCIA) return; // Se emergência foi ativada, sai imediatamente da função
ledcWriteTone(pinoBuzzer, 1000); // Liga o buzzer com frequência de 1000 Hz (tom médio)
esperarComSeguranca(300); // Espera 300ms com o buzzer ligado (sem bloquear o Blynk)
ledcWriteTone(pinoBuzzer, 0); // Desliga o buzzer (frequência 0 = silêncio)
esperarComSeguranca(300); // Espera 300ms em silêncio
}
if (estadoAtual != EMERGENCIA) estadoAtual = TRANSPORTE; // Só volta ao início se não houve emergência
}
// ESTADO 5: ERRO DE VIBRAÇÃO
// A vibração não foi detetada na gama esperada → a prensa pode não ter funcionado corretamente.
void faseErroVibracao() {
// ... (sinalização de erro similar à anterior, com aviso diferente) ...
}
// ESTADO 6: EMERGÊNCIA
// Paragem total do sistema. Tudo para. O sistema fica neste estado até ser reiniciado manualmente.
void faseEmergencia() {
meuServo.detach(); // Desliga imediatamente o motor do tapete
ledcWriteTone(pinoBuzzer, 0); // Garante que o buzzer fica em silêncio
// ... (aqui estaria a sinalização visual de emergência e o bloqueio total) ...
}
// ============================================================
// INTERRUPÇÃO DE EMERGÊNCIA (ISR)
// ============================================================
// Esta função é chamada AUTOMATICAMENTE pelo hardware quando o botão de emergência é premido.
// "IRAM_ATTR" garante que esta função fica carregada na RAM (memória rápida) do ESP32,
// para ser executada o mais rápido possível — as ISRs têm de ser ultra-rápidas.
// REGRA: numa ISR nunca uses delay(), Serial.print() lento, ou operações complexas.
// Apenas muda variáveis simples, como fazemos aqui.
void IRAM_ATTR tratarEmergencia() {
estadoAtual = EMERGENCIA; // Muda o estado — o loop() vai detetar na próxima iteração
}
// ============================================================
// FUNÇÃO AUXILIAR: Calcular e enviar comprimento para Blynk
// ============================================================
// Recebe o tempo em milissegundos que a peça demorou a atravessar o sensor
// e converte para centímetros usando a velocidade calibrada do tapete.
void imprimirComprimento(unsigned long ms) {
float segundos = ms / 1000.0; // Converte ms para segundos
// (divide por 1000.0 com ponto para forçar divisão decimal)
float comprimentoCm = segundos * VELOCIDADE_TAPETE; // Fórmula: comprimento = velocidade × tempo
Blynk.virtualWrite(2, String(comprimentoCm, 1) + " cm");
// Envia o valor formatado para o pino virtual V2 da App Blynk.
// String(comprimentoCm, 1) converte o float para texto com 1 casa decimal (ex: "12.4 cm")
}