\
// motor_3vel_fader_auto.ino
// Arduino UNO (5V) — Motor 220V AC com 3 velocidades por relés (pinos 9,6,5)
// + Dimmer AC (TRIAC com zero-cross) para fader (rampa suave)
// + Modo AUTOMÁTICO com troca de tap por faixas de temperatura (com histerese)
// + Ajustes fáceis de velocidade de rampa e piso mínimo de potência
//
// ⚠️ Segurança: 220 V AC é perigoso. Use módulos isolados, fusível, MOV, caixa, aterramento.
// ⚠️ Dimmer AC por ângulo de fase (ex.: RobotDyn AC Dimmer) com Zero-Cross (ZC) e Gate.
// ⚠️ Nunca energize mais de um tap (relé) ao mesmo tempo.
// ⚠️ Sempre reduza a potência (rampa para baixo) antes de trocar o relé do tap.
#include <Arduino.h>
#include "Button.h"
#include "ACI_10K_an.h"
#include <Wire.h>
#include "LiquidCrystal_I2C.h"
#include "RBDdimmer.h" // Biblioteca do módulo Dimmer AC (RobotDyn ou compatível)
// ========================= CONFIGURAÇÕES GERAIS =========================
// ---- Pins (UNO) ----
#define PIN_DIMMER_ZC 2 // Zero-cross (precisa interrupção INT0 no UNO)
#define PIN_DIMMER_GATE 3 // Gate do TRIAC (saída digital)
// Relés das 3 velocidades (taps do motor) — conforme solicitado: 9, 6, 5
#define REL_VEL_BAIXA 9
#define REL_VEL_MEDIA 6
#define REL_VEL_ALTA 5
// Sensores/botões
#define PIN_SENSOR_NIVEL 4 // INPUT_PULLUP (LOW=OK, HIGH=VAZIO)
Button button2(7); // AMARELO - alterar velocidade (1..6)
Button button5(8); // PRETO - alterna Modo (Manual/Auto)
Button button1(A2); // AZUL - bombeamento (toggle)
Button button3(A3); // VERMELHO - ionizador (toggle)
Button button4(A0); // VERDE - oscilador (toggle)
// Atuadores auxiliares
#define PIN_BOMBA 10
#define PIN_OSCILADOR 11
#define PIN_IONIZADOR 12
// Buzzer (LED onboard pisca junto)
#define PIN_BUZZER 13
// LCD I2C (endereço comum 0x27)
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Termistor NTC 10k (divisor 10k/10k) no A1, Vref 5V, ADC 10 bits
Aci_10K an10k(5.0, 10);
// ========================= AJUSTES DO SISTEMA =========================
// ---- AUTO: faixas de temperatura para seleção de TAP ----
// < T_LOW -> BAIXA
// entre T_LOW e T_HIGH -> MEDIA
// >= T_HIGH -> ALTA
const float T_LOW_C = 25.0f; // °C
const float T_HIGH_C = 29.0f; // °C
const float T_HYST_C = 0.6f; // histerese para evitar oscilações
// ---- AUTO: mapeamentos de potência (percentual) por faixa ----
// Você pode ajustar os intervalos alvo por tap conforme sua necessidade
const int AUTO_LOW_MIN_PCT = 30; // piso recomendado p/ PSC
const int AUTO_LOW_MAX_PCT = 60;
const int AUTO_MED_MIN_PCT = 50;
const int AUTO_MED_MAX_PCT = 85;
const int AUTO_HIGH_MIN_PCT = 70;
const int AUTO_HIGH_MAX_PCT = 100;
// ---- Piso mínimo de potência no MODO AUTO (passo 3) ----
const int MIN_POWER_PCT_AUTO = 30; // % (ajuste conforme seu motor)
// ---- Rampas (passo 2): ajuste de velocidade da rampa ----
// Descendo (antes da troca de tap)
const int RAMP_DOWN_STEP = 3; // incremento por tick (maior = mais rápido)
const unsigned long RAMP_DOWN_DT = 12; // ms por tick (menor = mais rápido)
// Subindo (após a troca de tap)
const int RAMP_UP_STEP = 2;
const unsigned long RAMP_UP_DT = 15;
// Efeitos manuais (modos 4–6)
const int EFFECT_STEP = 1;
const unsigned long EFFECT_DT = 20;
// ---- Auto: tempo mínimo entre trocas de tap ----
const unsigned long AUTO_MIN_DWELL_MS = 5000; // 5 s
// ========================= ESTADOS GERAIS =========================
int alterVeloc = 1; // modos 1..6
int nivelEstado = HIGH; // HIGH=VAZIO (pull-up)
bool modoAutomatico = false;
float temperaturaAtual = NAN;
unsigned long tLeitura = 0;
const unsigned long dtLeitura = 100; // ms
// ========================= DIMMER + RAMPAS =========================
dimmerLamp dimmer(PIN_DIMMER_GATE); // (gate, zc)
struct Ramp {
int target; // 0..100 %
int current; // 0..100 %
int step; // incremento por tick
unsigned long interval; // ms por tick
unsigned long last;
bool active;
} ramp = {0, 0, 1, 15, 0, false};
enum SwapState { IDLE, RAMP_DOWN, OPEN_RELAY, GUARD, CLOSE_RELAY, RAMP_UP };
SwapState swapState = IDLE;
unsigned long swapT0 = 0;
const unsigned long guardMs = 80; // guarda entre abrir/fechar
int velAtual = 0; // 0=desligado, 1=baixa, 2=media, 3=alta
int velAlvo = 0;
int postSwapTargetPct = 100; // alvo de potência logo após a troca de tap
unsigned long lastAutoTapChangeMs = 0; // dwell para AUTO
// ========================= PROTÓTIPOS =========================
void configurarPinos();
float lerTemperatura();
void atualizarDisplay();
void bombeamento();
void oscilador();
void ionizador();
void alertaTemperatura();
void controlePrincipal();
void motorSetup();
void setRelesOff();
void setRelayForSpeed(int v);
void setDimmerPower(int pct);
void startRampTo(int pct, int step, unsigned long intervalMs);
bool updateRamp();
void fadeToSpeed(int novaVel, int pisoPct, int topoPctTarget);
void updateMotorSwap();
void motorSetManualMode(int modo);
int mapFloatToPct(float x, float in_min, float in_max);
// AUTO helpers
int selectAutoTap(float T); // retorna 1..3 conforme T com histerese
int computeAutoTargetPct(float T, int tap); // mapeia T->% na faixa do tap
// ========================= SETUP =========================
void setup() {
Serial.begin(9600);
analogReference(DEFAULT); // ~5V no UNO
configurarPinos();
button1.begin();
button2.begin();
button3.begin();
button4.begin();
button5.begin();
randomSeed((unsigned long)(micros() ^ (analogRead(A1) << 10)));
// LCD
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0); lcd.print("Iniciando...");
lcd.setCursor(0, 1); lcd.print("Aguarde...");
delay(1200);
lcd.clear();
temperaturaAtual = lerTemperatura();
nivelEstado = digitalRead(PIN_SENSOR_NIVEL);
motorSetup();
}
// ========================= LOOP =========================
void loop() {
// Atualiza temperatura periodicamente
if (millis() - tLeitura >= dtLeitura) {
tLeitura = millis();
temperaturaAtual = lerTemperatura();
}
// Alterna Manual/Auto
if (button5.pressed()) {
modoAutomatico = !modoAutomatico;
tone(PIN_BUZZER, 440, 180);
}
// Controle principal
controlePrincipal();
// Auxiliares
alertaTemperatura();
bombeamento();
oscilador();
ionizador();
atualizarDisplay();
// Atualiza máquina de estados do motor (rampa/troca de tap)
updateMotorSwap();
}
// ========================= IMPLEMENTAÇÕES =========================
void configurarPinos() {
pinMode(PIN_SENSOR_NIVEL, INPUT_PULLUP);
pinMode(PIN_BOMBA, OUTPUT);
pinMode(PIN_OSCILADOR, OUTPUT);
pinMode(PIN_IONIZADOR, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(REL_VEL_BAIXA, OUTPUT);
pinMode(REL_VEL_MEDIA, OUTPUT);
pinMode(REL_VEL_ALTA, OUTPUT);
digitalWrite(PIN_BOMBA, LOW);
digitalWrite(PIN_OSCILADOR, LOW);
digitalWrite(PIN_IONIZADOR, LOW);
setRelesOff();
}
float lerTemperatura() {
delayMicroseconds(400);
int leituraADC = analogRead(A1);
return an10k.getTemp(leituraADC); // °C
}
void alertaTemperatura() {
static unsigned long t0 = 0;
static bool on = false;
const unsigned long intervalo = 120; // ms
float T = temperaturaAtual;
if (!isnan(T) && T >= 30.0f) {
if (millis() - t0 >= intervalo) {
t0 = millis();
on = !on;
if (on) tone(PIN_BUZZER, 262, 80);
else noTone(PIN_BUZZER);
}
} else {
noTone(PIN_BUZZER);
}
}
void controlePrincipal() {
// Botão muda modo 1..6 (apenas no Manual)
if (!modoAutomatico && button2.pressed()) {
tone(PIN_BUZZER, 262, 150);
alterVeloc++;
if (alterVeloc > 6) alterVeloc = 1;
}
if (modoAutomatico) {
// ====== AUTO com troca de TAP por temperatura (passo 1) ======
int desiredTap = selectAutoTap(temperaturaAtual);
int targetPct = computeAutoTargetPct(temperaturaAtual, desiredTap);
targetPct = max(targetPct, MIN_POWER_PCT_AUTO); // aplica piso (passo 3)
// Se é necessário trocar tap e já passou dwell
if (desiredTap != velAtual && swapState == IDLE && (millis() - lastAutoTapChangeMs >= AUTO_MIN_DWELL_MS)) {
lastAutoTapChangeMs = millis();
fadeToSpeed(desiredTap, /*piso*/ 10, /*topo (após troca)*/ targetPct);
} else if (swapState == IDLE) {
// Mesmo tap: apenas rampa ao target
startRampTo(targetPct, RAMP_UP_STEP, RAMP_UP_DT);
}
} else {
// ====== MANUAL ======
// 1..3 = taps fixos, 4..6 = efeitos de rampa dentro do tap atual
motorSetManualMode(alterVeloc);
}
}
void atualizarDisplay() {
static unsigned long ultimoUpdate = 0;
if (millis() - ultimoUpdate >= 1000) {
ultimoUpdate = millis();
lcd.clear();
// Linha 0: Temperatura e Nível
lcd.setCursor(0, 0);
lcd.print("T:");
if (isnan(temperaturaAtual)) lcd.print("--.-");
else lcd.print(temperaturaAtual, 1);
lcd.print("C");
int nv = digitalRead(PIN_SENSOR_NIVEL);
lcd.setCursor(10, 0);
lcd.print((nv == LOW) ? "H2O:OK" : "VAZIO!");
// Linha 1: Modo
lcd.setCursor(0, 1);
if (modoAutomatico) {
lcd.print("AUTO: ");
lcd.print(ramp.current);
lcd.print("% ");
lcd.print("TAP:");
lcd.print(velAtual);
} else {
lcd.print("MANUAL: M-");
lcd.print(alterVeloc);
lcd.print(" TAP:");
lcd.print(velAtual);
}
}
}
void bombeamento() {
// Botão AZUL alterna a bomba (respeitando o nível)
if (button1.toggled() && button1.read() == Button::PRESSED) {
tone(PIN_BUZZER, 262, 120);
bool nivelOK = (digitalRead(PIN_SENSOR_NIVEL) == LOW);
if (nivelOK) {
digitalWrite(PIN_BOMBA, !digitalRead(PIN_BOMBA));
} else {
digitalWrite(PIN_BOMBA, LOW);
}
}
// Monitora mudança de nível e aplica segurança
int novoValor = digitalRead(PIN_SENSOR_NIVEL);
if (novoValor != nivelEstado) {
nivelEstado = novoValor;
if (nivelEstado == HIGH) {
digitalWrite(PIN_BOMBA, LOW);
tone(PIN_BUZZER, 262, 150);
Serial.println("Nivel: VAZIO");
} else {
Serial.println("Nivel: OK");
}
}
}
void oscilador() {
if (button4.toggled() && button4.read() == Button::PRESSED) {
tone(PIN_BUZZER, 262, 120);
digitalWrite(PIN_OSCILADOR, !digitalRead(PIN_OSCILADOR));
}
}
void ionizador() {
if (button3.toggled() && button3.read() == Button::PRESSED) {
tone(PIN_BUZZER, 262, 120);
digitalWrite(PIN_IONIZADOR, !digitalRead(PIN_IONIZADOR));
}
}
// ========================= DIMMER + RELÉS =========================
void motorSetup() {
setRelesOff();
dimmer.begin(NORMAL_MODE, ON); // NORMAL_MODE permite setPower(0..100)
ramp.current = 0;
setDimmerPower(0);
velAtual = 0;
velAlvo = 0;
swapState = IDLE;
}
void setRelesOff() {
digitalWrite(REL_VEL_BAIXA, LOW);
digitalWrite(REL_VEL_MEDIA, LOW);
digitalWrite(REL_VEL_ALTA, LOW);
}
void setRelayForSpeed(int v) {
setRelesOff();
if (v == 1) digitalWrite(REL_VEL_BAIXA, HIGH);
else if (v == 2) digitalWrite(REL_VEL_MEDIA, HIGH);
else if (v == 3) digitalWrite(REL_VEL_ALTA, HIGH);
}
void setDimmerPower(int pct) { // 0..100
pct = constrain(pct, 0, 100);
dimmer.setPower(pct);
ramp.current = pct; // mantemos current coerente com o set realizado
}
void startRampTo(int pct, int step, unsigned long intervalMs) {
ramp.target = constrain(pct, 0, 100);
ramp.step = max(1, step);
ramp.interval = max(5, intervalMs);
ramp.last = millis();
ramp.active = true;
}
bool updateRamp() {
if (!ramp.active) return false;
unsigned long now = millis();
if (now - ramp.last >= ramp.interval) {
ramp.last = now;
int tgt = ramp.target;
if (ramp.current < tgt) ramp.current = min(ramp.current + ramp.step, tgt);
else if (ramp.current > tgt) ramp.current = max(ramp.current - ramp.step, tgt);
setDimmerPower(ramp.current);
if (ramp.current == tgt) ramp.active = false;
}
return ramp.active;
}
// Solicita troca de velocidade com rampa (piso->troca->rampa ao alvo solicitado)
void fadeToSpeed(int novaVel, int pisoPct, int topoPctTarget) {
novaVel = constrain(novaVel, 0, 3);
velAlvo = novaVel;
postSwapTargetPct = constrain(topoPctTarget, 0, 100);
if (velAlvo == velAtual) {
// Mesmo tap: apenas rampa ao alvo
startRampTo(postSwapTargetPct, RAMP_UP_STEP, RAMP_UP_DT);
swapState = IDLE;
return;
}
// Rampa para baixo antes da troca
startRampTo(constrain(pisoPct, 0, 100), RAMP_DOWN_STEP, RAMP_DOWN_DT);
swapState = RAMP_DOWN;
}
void updateMotorSwap() {
switch (swapState) {
case IDLE:
// nada
break;
case RAMP_DOWN:
if (!updateRamp()) {
setRelesOff();
swapT0 = millis();
swapState = OPEN_RELAY;
}
break;
case OPEN_RELAY:
if (millis() - swapT0 >= guardMs) {
swapState = GUARD;
swapT0 = millis();
}
break;
case GUARD:
if (millis() - swapT0 >= guardMs) {
if (velAlvo > 0) setRelayForSpeed(velAlvo);
swapState = CLOSE_RELAY;
swapT0 = millis();
}
break;
case CLOSE_RELAY:
if (millis() - swapT0 >= guardMs) {
// Rampa para o alvo definido por fadeToSpeed()
startRampTo(postSwapTargetPct, RAMP_UP_STEP, RAMP_UP_DT);
swapState = RAMP_UP;
}
break;
case RAMP_UP:
if (!updateRamp()) {
velAtual = velAlvo;
swapState = IDLE;
}
break;
}
}
void motorSetManualMode(int modo) {
switch (modo) {
case 1: // BAIXA (tap 1), sobe ao topo (100%)
fadeToSpeed(1, /*piso*/10, /*alvo pós-troca*/ 100);
break;
case 2: // MÉDIA (tap 2)
fadeToSpeed(2, 10, 100);
break;
case 3: // ALTA (tap 3)
fadeToSpeed(3, 10, 100);
break;
case 4: // efeito no tap atual (ou BAIXA se desligado): 40..80%
if (velAtual == 0 && swapState == IDLE) { fadeToSpeed(1, 10, 60); break; }
if (swapState == IDLE && !ramp.active) {
static bool up4 = false;
int alvo = up4 ? 80 : 40;
startRampTo(alvo, EFFECT_STEP, EFFECT_DT);
up4 = !up4;
}
break;
case 5: // efeito mais cheio: 50..90%
if (velAtual == 0 && swapState == IDLE) { fadeToSpeed(2, 10, 70); break; }
if (swapState == IDLE && !ramp.active) {
static bool up5 = true;
int alvo = up5 ? 90 : 50;
startRampTo(alvo, EFFECT_STEP, EFFECT_DT);
up5 = !up5;
}
break;
case 6: // efeito alto: 60..100%
if (velAtual == 0 && swapState == IDLE) { fadeToSpeed(3, 10, 80); break; }
if (swapState == IDLE && !ramp.active) {
static bool up6 = true;
int alvo = up6 ? 100 : 60;
startRampTo(alvo, EFFECT_STEP, EFFECT_DT);
up6 = !up6;
}
break;
default:
fadeToSpeed(0, 0, 0);
break;
}
}
int mapFloatToPct(float x, float in_min, float in_max) {
if (isnan(x)) return 0;
if (x < in_min) x = in_min;
if (x > in_max) x = in_max;
float frac = (x - in_min) / (in_max - in_min); // 0..1
int pct = (int)round(frac * 100.0f);
return constrain(pct, 0, 100);
}
// ========================= AUTO HELPERS =========================
int selectAutoTap(float T) {
// Histerese baseada no tap atual
// Se já estamos em ALTA, só desce para MÉDIA abaixo de (T_HIGH - T_HYST)
// Se já estamos em BAIXA, só sobe para MÉDIA acima de (T_LOW + T_HYST)
// Caso contrário usa thresholds centrais
if (velAtual == 3) {
if (T <= (T_HIGH_C - T_HYST_C)) return 2; // desce para MED
else return 3; // mantém ALTA
}
if (velAtual == 1) {
if (T >= (T_LOW_C + T_HYST_C)) return 2; // sobe para MED
else return 1; // mantém BAIXA
}
// Se estamos em MEDIA, decida pelos thresholds centrais
if (T < T_LOW_C) return 1;
if (T > T_HIGH_C) return 3;
return 2;
}
int computeAutoTargetPct(float T, int tap) {
// Mapeia T em um range de % por tap
if (tap == 1) {
// 22..T_LOW -> AUTO_LOW_MIN..AUTO_LOW_MAX
int pct = mapFloatToPct(T, 22.0f, T_LOW_C);
int out = AUTO_LOW_MIN_PCT + (int)round(pct * (AUTO_LOW_MAX_PCT - AUTO_LOW_MIN_PCT) / 100.0f);
return constrain(out, AUTO_LOW_MIN_PCT, AUTO_LOW_MAX_PCT);
} else if (tap == 2) {
// T_LOW..T_HIGH -> AUTO_MED_MIN..AUTO_MED_MAX
int pct = mapFloatToPct(T, T_LOW_C, T_HIGH_C);
int out = AUTO_MED_MIN_PCT + (int)round(pct * (AUTO_MED_MAX_PCT - AUTO_MED_MIN_PCT) / 100.0f);
return constrain(out, AUTO_MED_MIN_PCT, AUTO_MED_MAX_PCT);
} else {
// T_HIGH..32 -> AUTO_HIGH_MIN..AUTO_HIGH_MAX
int pct = mapFloatToPct(T, T_HIGH_C, 32.0f);
int out = AUTO_HIGH_MIN_PCT + (int)round(pct * (AUTO_HIGH_MAX_PCT - AUTO_HIGH_MIN_PCT) / 100.0f);
return constrain(out, AUTO_HIGH_MIN_PCT, AUTO_HIGH_MAX_PCT);
}
}