/*
Arduino Electronic-Dreno v4.0 (PID Evolution)
Controle Dinâmico de Drenagem por Turbidez usando PID
*/
// Inclusão de bibliotecas necessárias para o funcionamento do sistema
#include <Wire.h> // Comunicação I2C (usada pelo display LCD)
#include <LiquidCrystal_I2C.h> // Controle do display LCD via adaptador I2C
#include <PID_v1.h> // Biblioteca oficial para o cálculo matemático do algoritmo PID
// ================= MAPEAMENTO DE PINOS HARDWARE =================
#define BUZZER_PIN 8 // Pino digital onde o sinalizador sonoro (Buzzer) está conectado
#define RELAY_PIN 9 // Pino digital que aciona o Relé principal (Válvula de Dreno)
#define RELAY_NC_PIN 7 // Pino de feedback conectado ao terminal Normal Fechado (NC) do relé para monitorar falhas
#define LDR_PIN A0 // Pino analógico que lê o sensor óptico (LDR) para medir a turbidez
#define LED_PWM_PIN 11 // Pino digital PWM que controla a intensidade do LED de iluminação/sinalização
// ================= INICIALIZAÇÃO DO LCD =================
// Configura o display no endereço I2C 0x27, com 16 colunas e 2 linhas
LiquidCrystal_I2C lcd(0x27, 16, 2);
// ================= VARIÁVEIS & CONFIGURAÇÕES DO PID =================
double Setpoint; // Armazena o valor alvo de turbidez (NTU) que o sistema tentará manter estável
double Input; // Armazena a leitura atualizada do sensor (NTU) que entra no cálculo do PID
double Output; // Armazena a resposta calculada pelo PID (tempo em milissegundos que a válvula deve abrir)
// Constantes de Sintonia (Tuning) do PID - Definem a agressividade da correção
double Kp = 3.5; // Ganho Proporcional: Reage ao erro atual (quanto maior o erro, maior a reação)
double Ki = 1.5; // Ganho Integral: Acumula erros passados (elimina pequenos desvios ao longo do tempo)
double Kd = 0.5; // Ganho Derivativo: Preve o erro futuro com base na velocidade de variação atual
// Inicialização do objeto PID.
// ATENÇÃO: Está como DIRECT no código, mas o comentário original alertava para REVERSE.
// No modo DIRECT, se o Input (NTU) subir acima do Setpoint, o Output aumenta.
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
// Variáveis para converter o controle analógico do PID em pulsos de tempo no Relé
unsigned long windowStartTime; // Armazena o momento exato em que a janela de tempo atual começou
const unsigned long WindowSize = 5000; // Define o tamanho total da janela de controle em 5 segundos (5000ms)
// ================= VARIÁVEIS DE CONTROLE DO SISTEMA =================
unsigned long lastSensorRead = 0; // Armazena a última vez que o sensor foi lido (para temporização não-bloqueante)
unsigned long lastDisplayTime = 0; // Armazena o momento em que o display entrou em estado estável (para timeout)
const unsigned long DISPLAY_TIMEOUT = 5000UL; // Tempo máximo (5 segundos) para a tela permanecer acesa se o sistema estiver OK
long ldrFiltrado = 0; // Armazena o valor do LDR após passar pelo filtro de estabilização
float ntu = 0; // Armazena o valor bruto convertido para a escala NTU (Turbidez)
float Tntu = 0; // Variável auxiliar de conversão/escala
float Dntu = 0; // Armazena o valor final de NTU formatado para exibição visual
bool drenando = false; // Flag/Indicador lógico se a válvula de dreno está fisicamente aberta
bool displayOn = true; // Flag/Indicador lógico se o backlight (luz de fundo) do LCD está ligado
// ================= LEITURA DO SENSOR COM MÉDIA MÓVEL =================
float readLDR() {
long soma = 0; // Zera a variável que acumulará as leituras
for (byte i = 0; i < 20; i++) {
soma += analogRead(LDR_PIN); // Soma 20 leituras analógicas consecutivas do LDR
delay(2); // Pequena pausa de 2ms entre leituras para estabilização do ADC do Arduino
}
return soma / 20.0; // Retorna a média aritmética simples das 20 amostras
}
void atualizarSensor() {
float leitura = readLDR(); // Obtém a média atual das leituras do sensor
// Filtro Digital Passa-Baixas Exponencial: suaviza ruídos e variações bruscas na leitura
// Mantém 85% do valor anterior e adiciona apenas 15% da nova leitura
ldrFiltrado = (ldrFiltrado * 0.85) + (leitura * 0.15);
// Converte a leitura analógica (0 a 1023) inversamente para NTU (3000 a 0)
ntu = map((int)ldrFiltrado, 1023, 0, 0, 3000);
ntu = constrain(ntu, 0, 3000); // Garante que o valor de NTU nunca escape do limite seguro de 0 a 3000
// Mapeamento secundário para simplificação decimal da turbidez
Tntu = map((int)ntu, 3000, 0, 0, 300);
Dntu = Tntu / 10; // Divide por 10 para criar uma casa decimal para exibição
}
// ================= CONTROLE DE SINALIZAÇÃO ACÚSTICA =================
void beep(int freq, int tempo) {
tone(BUZZER_PIN, freq, tempo); // Gera uma frequência sonora no pino do Buzzer pelo tempo determinado
}
void controlarLEDProporcional() {
// Ajusta o brilho do LED baseado na turbidez (quanto mais sujo/crítico, mais forte o brilho)
int brilho = map((int)ntu, 2900, Setpoint, 25, 255);
brilho = constrain(brilho, 0, 255); // Limita o valor para a faixa aceita pelo PWM (0 a 255)
analogWrite(LED_PWM_PIN, brilho); // Aplica o sinal PWM no pino do LED
}
// ================= GERENCIAMENTO DO LCD =================
void atualizarLCD() {
// Se a tela está desligada e a ação do dreno é insignificante (<= 50ms), não faz nada
if (!displayOn && Output <= 50.00) {
return;
}
// Se a tela estava desligada mas o PID exige drenagem (> 50ms), acorda o visor imediatamente
if (!displayOn && Output > 50.00) {
lcd.backlight(); // Liga a luz de fundo do display
displayOn = true;
}
// Escrita de informações na primeira linha do LCD
lcd.setCursor(0, 0);
lcd.print("NTU:");
lcd.print(Dntu, 1); // Exibe a turbidez atual com 1 casa decimal
lcd.setCursor(8, 0);
lcd.print(" Alvo:");
lcd.print((int)(Setpoint / 300.0), 1); // Exibe o alvo de forma simplificada e compatível com o espaço
// Escrita de informações na segunda linha do LCD
lcd.setCursor(0, 1);
if (Output > 50.00) { // Verifica se há uma demanda ativa e relevante de drenagem pelo PID
lcd.print("Dreno PID:");
lcd.print((int)Output); // Mostra o tempo ativo da janela que a válvula ficará aberta
lcd.print("ms ");
lastDisplayTime = millis(); // Reseta o cronômetro de timeout para evitar que o LCD apague durante o dreno
} else {
lcd.print("Status: AGUA OK "); // Informa que a turbidez está dentro do esperado
// O lastDisplayTime NÃO é atualizado aqui, permitindo que a contagem para apagar o LCD continue correndo
}
}
void controlarDisplay() {
// Se o LCD estiver ligado, o tempo limite estourar e o dreno estiver em repouso, desliga o backlight para economizar energia
if (displayOn && (millis() - lastDisplayTime > DISPLAY_TIMEOUT) && (Output <= 50.00)) {
displayOn = false;
lcd.noBacklight(); // Desliga a luz de fundo do display
}
}
// ================= SEGURANÇA E TRATAMENTO DE FALHAS CRÍTICAS =================
void dispararAlarmeErro(const char* linha1, const char* linha2, int freq) {
digitalWrite(RELAY_PIN, LOW); // Medida de Segurança: Corta a energia do relé do dreno imediatamente
lcd.clear();
lcd.backlight(); // Força o acendimento do display para alertar o operador
lcd.setCursor(0, 0);
lcd.print(linha1); // Mostra a mensagem de erro principal
lcd.setCursor(0, 1);
lcd.print(linha2); // Mostra o complemento do erro ou instrução de manutenção
while (1) { // Trava o Arduino em um loop infinito de segurança (Interrupção total do sistema)
tone(BUZZER_PIN, freq, 300); // Soa o alarme de erro intermitente
delay(600);
}
}
void verificarSegurancaDreno() {
static unsigned long inicioDrenoMaximo = 0; // Mantém o registro de quando o dreno máximo começou
// Se o PID estiver exigindo dreno máximo (100% do tempo da janela aberto)
if (Output >= WindowSize) {
if (inicioDrenoMaximo == 0) {
inicioDrenoMaximo = millis(); // Inicia a contagem do tempo de dreno ininterrupto
} else if (millis() - inicioDrenoMaximo > 120000UL) { // Caso permaneça assim por mais de 2 minutos (120000 ms)
// Dispara o travamento de segurança por suspeita de vazamento, entupimento ou sensor quebrado
dispararAlarmeErro("LIMITE EXCEDIDO", " MANUTENCAO ", 1800);
}
} else {
inicioDrenoMaximo = 0; // Reseta o cronômetro caso o PID recue e feche parcialmente o dreno (sistema respondendo)
}
}
// ================= LOGICA DE CHAVEAMENTO DO RELÉ POR PWM TEMPORAL =================
void controlarRelePID() {
unsigned long now = millis(); // Captura o tempo de execução atual do Arduino
// Se o tempo decorrido passar do tamanho da janela (5s), move o início da janela para o tempo atual
if (now - windowStartTime > WindowSize) {
windowStartTime += WindowSize;
}
// Compara o tempo necessário de dreno (Output em ms) com o quanto de tempo já passou dentro da janela atual
if (Output > (now - windowStartTime)) {
if (!drenando) { // Se a válvula deveria estar aberta mas está marcada como fechada
digitalWrite(RELAY_PIN, HIGH); // Ativa fisicamente o pino do relé para abrir a válvula
drenando = true;
beep(1500, 50); // Sinal sonoro curto indicando abertura da válvula
Serial.println("-> VALVULA ABERTA VIA PID");
}
} else {
if (drenando) { // Se o tempo do PID terminou para esta janela, mas a válvula ainda está aberta
digitalWrite(RELAY_PIN, LOW); // Desativa fisicamente o pino do relé para fechar a válvula
drenando = false;
beep(1000, 50); // Sinal sonoro curto de fechamento da válvula
Serial.println("-> VALVULA FECHADA VIA PID");
// --- PROTEÇÃO DE HARDWARE INTERNA ---
delay(15); // Aguarda 15ms para dar tempo ao mecanismo físico do relé se mover e desmagnetizar
// Lê o pino de feedback NC. Se o relé fechou, o NC deve ir para LOW (ou vice-versa dependendo da ligação elétrica).
// Se a condição abaixo for verdadeira, o relé colou os contatos ou quebrou fisicamente.
if (digitalRead(RELAY_NC_PIN) == HIGH) {
dispararAlarmeErro(" ERRO: RELE NO ", " TRAVADO/ALERTA ", 2000);
}
}
}
}
// ================= ENVIO DE TELEMETRIA =================
void enviarDadosSerial() {
// Envia os dados formatados para plotagem no Serial Plotter do Arduino ou diagnóstico
Serial.print("Setpoint:"); Serial.print(ntu);
Serial.print(",NTU_Filtrado:"); Serial.print(ntu);
Serial.print(",Janela_Dreno_ms:"); Serial.println(Output);
}
// ================= CONFIGURAÇÃO INICIAL (BOOT) =================
void setup() {
Serial.begin(9600); // Inicializa a comunicação Serial na velocidade de 9600 bps
// Configuração da direção dos pinos digitais
pinMode(RELAY_PIN, OUTPUT);
pinMode(RELAY_NC_PIN, INPUT_PULLUP); // Usa o resistor pull-up interno para ler o feedback do relé com segurança
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_PWM_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW); // Garante que o dreno inicie FECHADO por segurança
// Inicialização e Splash Screen do display LCD
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("ElectronicDreno");
lcd.setCursor(0, 1);
lcd.print(" v4.0 - PID Mode");
delay(3000); // Exibe a tela de abertura por 3 segundos
lcd.clear();
// Configura a meta inicial do sistema (Valor NTU desejado para estabilização)
Setpoint = 1500.0;
// Configura os limites da resposta do PID para coincidir perfeitamente com o tamanho da nossa janela de tempo
myPID.SetOutputLimits(0, WindowSize);
myPID.SetMode(AUTOMATIC); // Ativa o cálculo automático do PID
windowStartTime = millis(); // Captura o tempo inicial para a janela do relé
lastDisplayTime = millis(); // Garante que a contagem do timeout do visor comece a contar a partir do fim do boot
}
// ================= LOOP PRINCIPAL (EXECUÇÃO CONTÍNUA) =================
void loop() {
// 1. EXECUÇÃO TEMPORIZADA NÃO-BLOQUEANTE (Acontece exatamente a cada 500 milissegundos)
if (millis() - lastSensorRead >= 500) {
lastSensorRead = millis(); // Atualiza a marca de tempo da última leitura
atualizarSensor(); // Lê o LDR, aplica o filtro exponencial e calcula o NTU
// Passa a medição atualizada da turbidez para a entrada do algoritmo
Input = ntu;
myPID.Compute(); // Executa as equações matemáticas do PID e atualiza o valor da variável 'Output'
// Atualiza os periféricos de saída de informação com base no novo cálculo do PID
atualizarLCD();
controlarLEDProporcional();
enviarDadosSerial();
}
// 2. CONTROLE DO RELE EM TEMPO REAL (Executado na velocidade máxima do processador)
// Verifica constantemente se deve ligar ou desligar o relé baseado no tempo calculado pelo PID
controlarRelePID();
// 3. ROTINAS DE SUPERVISÃO E SEGURANÇA CONTÍNUA
controlarDisplay(); // Gerencia se a tela deve apagar para economizar energia
verificarSegurancaDreno(); // Fiscaliza se o dreno está aberto há tempo demais (evitando inundações/falhas)
}