#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include <OneWire.h>
#include <DallasTemperature.h>
const unsigned long multiplicadorTempo = 1000; // 1000 para simular rápido. Use 60000 para a panela real.
struct Receita {
char nome[16];
float tempStrike; // Usado no mashIn()
float tempRampa[3]; // Índice 0 e 1: mash() | Índice 2: mashOut()
int tempoRampa[3];
int tempoLupulo; // Usado na fervura()
};
#define PINO_CLK 2
#define PINO_DT 3
#define PINO_SW 4
#define PINO_DS18B20 5
#define PINO_SSR 6
#define PINO_BOMBA 7
#define PINO_BUZZER 8
LiquidCrystal_I2C lcd(0x27, 20, 4);
OneWire oneWire(PINO_DS18B20);
DallasTemperature sensorTemp(&oneWire);
const float offsetInercia = 1.5;
const float histerese = 0.5;
// --- ESTADOS REVISADOS PARA REFLETIR O DOCUMENTO ---
enum EstadoMaquina { MODO_MENU, MODO_MASH_IN, MODO_MASH, MODO_MASH_OUT, MODO_FERVURA, MODO_FIM };
EstadoMaquina estadoAtual = MODO_MENU;
volatile int contadorMenu = 0;
volatile bool atualizaDisplay = false;
Receita receitaAtual;
unsigned long tempoAnterior = 0;
unsigned long tempoInicioFase = 0;
int rampaAtual = 0;
bool contandoTempo = false;
bool aguardandoMalte = false; // Trava do Mash In
// --- FUNÇÕES DE HARDWARE ---
void lerEncoder() {
if (estadoAtual == MODO_MENU) {
if (digitalRead(PINO_DT) == HIGH) contadorMenu++;
else contadorMenu--;
if (contadorMenu > 9) contadorMenu = 0;
if (contadorMenu < 0) contadorMenu = 9;
atualizaDisplay = true;
}
}
void bip(int vezes) {
for(int i=0; i<vezes; i++){
digitalWrite(PINO_BUZZER, HIGH); delay(200);
digitalWrite(PINO_BUZZER, LOW); delay(200);
}
}
// =========================================================================
// AS 4 FUNÇÕES PRINCIPAIS DO PROCESSO CERVEJEIRO
// =========================================================================
void mashIn(float tempAtual, bool lerSensorAgora) {
if (!aguardandoMalte) {
// 1. Aquece a água até a temperatura de Strike
digitalWrite(PINO_BOMBA, HIGH);
controlarTemperatura(receitaAtual.tempStrike, tempAtual);
if (lerSensorAgora) atualizarTelaProcesso("1. MASH IN (Arreio)", receitaAtual.tempStrike, tempAtual, 0, 0);
// 2. Atingiu a temperatura? Para e chama o cervejeiro
if (tempAtual >= receitaAtual.tempStrike) {
digitalWrite(PINO_SSR, LOW);
aguardandoMalte = true;
bip(5);
lcd.clear();
}
} else {
// 3. Aguarda a adição dos grãos
digitalWrite(PINO_BOMBA, LOW);
if (lerSensorAgora) {
lcd.setCursor(0, 0); lcd.print("AGUA PRONTA! ");
lcd.setCursor(0, 1); lcd.print("Add Malte e Misture ");
lcd.setCursor(0, 2); lcd.print("para evitar pelotas.");
lcd.setCursor(0, 3); lcd.print("Clique p/ Iniciar >>");
}
// 4. Cervejeiro clicou, avança para o Mash
if (digitalRead(PINO_SW) == LOW) {
estadoAtual = MODO_MASH;
aguardandoMalte = false;
rampaAtual = 0;
contandoTempo = false;
bip(1);
lcd.clear();
delay(500); // debounce
}
}
}
void mash(float tempAtual, bool lerSensorAgora) {
// Limita o Mash às duas primeiras rampas (índices 0 e 1). O índice 2 é o Mash Out.
if (rampaAtual >= 2 || receitaAtual.tempoRampa[rampaAtual] == 0) {
estadoAtual = MODO_MASH_OUT;
contandoTempo = false;
bip(3);
lcd.clear();
return;
}
digitalWrite(PINO_BOMBA, HIGH);
float alvoRampa = receitaAtual.tempRampa[rampaAtual];
controlarTemperatura(alvoRampa, tempAtual);
if (!contandoTempo) {
if (tempAtual >= alvoRampa) {
contandoTempo = true;
tempoInicioFase = millis();
bip(2);
}
if (lerSensorAgora) atualizarTelaProcesso("2. MASH (Aquecendo) ", alvoRampa, tempAtual, 0, 0);
} else {
unsigned long decorrido = millis() - tempoInicioFase;
unsigned long totalAlvo = receitaAtual.tempoRampa[rampaAtual] * multiplicadorTempo;
int minRestantes = (totalAlvo - decorrido) / multiplicadorTempo;
if (lerSensorAgora) {
lcd.setCursor(0, 0); lcd.print("2. MASH (Sacarific.)");
atualizarTelaProcesso("", alvoRampa, tempAtual, minRestantes, receitaAtual.tempoRampa[rampaAtual]);
}
if (decorrido >= totalAlvo) {
rampaAtual++;
contandoTempo = false;
bip(1);
}
}
}
void mashOut(float tempAtual, bool lerSensorAgora) {
// Usamos o índice 2 da matriz de rampas exclusivamente para o Mash Out
float alvoMashOut = receitaAtual.tempRampa[2];
int tempoMashOut = receitaAtual.tempoRampa[2];
// Se não houver Mash Out configurado na receita, pula direto pra fervura
if (tempoMashOut == 0) {
estadoAtual = MODO_FERVURA;
contandoTempo = false;
bip(3);
lcd.clear();
return;
}
digitalWrite(PINO_BOMBA, HIGH);
controlarTemperatura(alvoMashOut, tempAtual);
if (!contandoTempo) {
if (tempAtual >= alvoMashOut) {
contandoTempo = true;
tempoInicioFase = millis();
bip(2);
}
if (lerSensorAgora) atualizarTelaProcesso("3. MASH OUT (Aquece)", alvoMashOut, tempAtual, 0, 0);
} else {
unsigned long decorrido = millis() - tempoInicioFase;
unsigned long totalAlvo = tempoMashOut * multiplicadorTempo;
int minRestantes = (totalAlvo - decorrido) / multiplicadorTempo;
if (lerSensorAgora) {
lcd.setCursor(0, 0); lcd.print("3. MASH OUT (Inativa");
atualizarTelaProcesso("", alvoMashOut, tempAtual, minRestantes, tempoMashOut);
}
if (decorrido >= totalAlvo) {
estadoAtual = MODO_FERVURA;
contandoTempo = false;
bip(3);
lcd.clear();
}
}
}
void fervura(float tempAtual, bool lerSensorAgora) {
// Aquece sem parar. Bomba desligada para não oxidar.
digitalWrite(PINO_SSR, HIGH);
digitalWrite(PINO_BOMBA, LOW);
if (!contandoTempo) {
// Alerta sonoro especial pois é o momento de remover os grãos antes de ferver
bip(4);
tempoInicioFase = millis();
contandoTempo = true;
}
unsigned long decorrido = millis() - tempoInicioFase;
unsigned long alvoFervura = receitaAtual.tempoLupulo * multiplicadorTempo;
int minRestantes = (alvoFervura - decorrido) / multiplicadorTempo;
if (lerSensorAgora) {
lcd.setCursor(0, 0); lcd.print("4. FERVURA (Boil) ");
lcd.setCursor(0, 1); lcd.print("Risco: Alta Temp! ");
lcd.setCursor(0, 2); lcd.print("Add Lupulo em: "); lcd.print(minRestantes); lcd.print("m ");
lcd.setCursor(0, 3); lcd.print("Aquecedor: LIGADO ");
}
if (decorrido >= alvoFervura) {
estadoAtual = MODO_FIM;
digitalWrite(PINO_SSR, LOW);
bip(10);
lcd.clear();
}
}
// =========================================================================
// SETUP E LOOP PRINCIPAL
// =========================================================================
void setup() {
pinMode(PINO_CLK, INPUT); pinMode(PINO_DT, INPUT); pinMode(PINO_SW, INPUT_PULLUP);
pinMode(PINO_SSR, OUTPUT); pinMode(PINO_BOMBA, OUTPUT); pinMode(PINO_BUZZER, OUTPUT);
lcd.init(); lcd.backlight();
sensorTemp.begin(); sensorTemp.setWaitForConversion(false);
attachInterrupt(digitalPinToInterrupt(PINO_CLK), lerEncoder, RISING);
carregarReceita(0);
if (receitaAtual.nome[0] == 255 || receitaAtual.nome[0] == 0) {
// Configura a Lager Clássica mapeada perfeitamente:
// Rampa 0: Mash (65C/60m) | Rampa 1: Vazia (0C/0m) | Rampa 2: Mash Out (76C/10m)
Receita r0 = {"Lager Classica", 68.0, {65.0, 0.0, 76.0}, {60, 0, 10}, 60};
salvarReceita(0, r0);
Receita rVazia = {"Vazia... ", 0.0, {0.0, 0.0, 0.0}, {0, 0, 0}, 0};
for(int i = 1; i < 10; i++) salvarReceita(i, rVazia);
carregarReceita(0);
}
bip(1);
}
void loop() {
unsigned long tempoAtual = millis();
float tempAtual = 0.0;
bool lerSensorAgora = false;
if (tempoAtual - tempoAnterior >= 1000) {
tempoAnterior = tempoAtual;
sensorTemp.requestTemperatures();
lerSensorAgora = true;
}
if (lerSensorAgora) tempAtual = sensorTemp.getTempCByIndex(0);
// O cérebro limpo usando a Máquina de Estados e as Funções Principais
switch (estadoAtual) {
case MODO_MENU:
digitalWrite(PINO_BOMBA, LOW); digitalWrite(PINO_SSR, LOW);
if (atualizaDisplay) { carregarReceita(contadorMenu); atualizaDisplay = false; }
if (lerSensorAgora) atualizarTelaMenu(tempAtual);
if (digitalRead(PINO_SW) == LOW && receitaAtual.tempStrike > 0) {
estadoAtual = MODO_MASH_IN; bip(2); lcd.clear(); delay(500);
}
break;
case MODO_MASH_IN: mashIn(tempAtual, lerSensorAgora); break;
case MODO_MASH: mash(tempAtual, lerSensorAgora); break;
case MODO_MASH_OUT: mashOut(tempAtual, lerSensorAgora); break;
case MODO_FERVURA: fervura(tempAtual, lerSensorAgora); break;
case MODO_FIM:
digitalWrite(PINO_SSR, LOW); digitalWrite(PINO_BOMBA, LOW);
if (lerSensorAgora) {
lcd.setCursor(0, 0); lcd.print("PROCESSO FINALIZADO!");
lcd.setCursor(0, 1); lcd.print("Desligue a maquina ");
lcd.setCursor(0, 2); lcd.print("e resfrie o mosto. ");
}
break;
}
}
// --- FUNÇÕES AUXILIARES TÉRMICAS E DISPLAY ---
void controlarTemperatura(float alvo, float atual) {
if (atual >= (alvo - offsetInercia)) digitalWrite(PINO_SSR, LOW);
else if (atual < (alvo - offsetInercia - histerese)) digitalWrite(PINO_SSR, HIGH);
}
void salvarReceita(int indice, Receita r) { EEPROM.put(indice * sizeof(Receita), r); }
void carregarReceita(int indice) { EEPROM.get(indice * sizeof(Receita), receitaAtual); }
void atualizarTelaMenu(float temp) {
lcd.setCursor(0, 0); lcd.print("Rec["); lcd.print(contadorMenu); lcd.print("] "); lcd.print(receitaAtual.nome);
lcd.setCursor(0, 1); lcd.print("Strike: "); lcd.print(receitaAtual.tempStrike, 1); lcd.print("C ");
lcd.setCursor(0, 2); lcd.print("Mosto: "); lcd.print(temp, 1); lcd.print("C ");
lcd.setCursor(0, 3); lcd.print("Pressione p/ Iniciar");
}
void atualizarTelaProcesso(const char* titulo, float alvo, float atual, int minRest, int minTotal) {
if (titulo[0] != '\0') { lcd.setCursor(0, 0); lcd.print(titulo); }
lcd.setCursor(0, 1); lcd.print("Alvo: "); lcd.print(alvo, 1); lcd.print("C ");
lcd.setCursor(0, 2); lcd.print("Real: "); lcd.print(atual, 1); lcd.print("C ");
lcd.setCursor(0, 3);
if (minTotal > 0) { lcd.print("Tempo Restante: "); lcd.print(minRest); lcd.print("m "); }
else {
if (digitalRead(PINO_SSR) == HIGH) lcd.print("Aquecedor: LIGADO ");
else lcd.print("Aquecedor: DESLIG. ");
}
}🔥
🔄️