/* Hello Wokwi! */
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 20, 4);
/*************************************************************************************
Ioquir Sotile
Auxiliar no processo de fabricação de cerveja caseira
Curitiba - Paraná - Brasil
Início da programação start of project: 26/out/2015.
This program is designed to help home brewing proccess
Prepared to work with a 4x20 display.
For English messages, uncomment #define ENGLISH
-- not yet prepared for fahrenheit . Sorry! See DisplayTemperature function.
Notas da subversão / Subversion Notes:
- Corrected time display when time is greater than 59 minutes
- Intermitent active buzzer to alert when temperature is over desired
- ESC button functions: mute alarm (intermitent buzzer), go to step
- Use interrupt for Ok button
- extend boil time when user takes too long to add a hop, change total boil time displayed.
- process whirflock as a hop
buggy / not working or implemented or implementing:
- customize mash and boil profiles // won't be done in this version because of UNO memory limitations
- ESC button function go to substep
- adjust functions behavor after ESC button function go to step
- add whirflock alert is not working
- measure water temperature before warming up to calculate how may degrees it may be at startup
Units are:
seconds (millis()/1000 or
minutes (profiles, whirflock time, recirculate time definitions)
Celsius
some sources:
http://www.pjrc.com/teensy/td_libs_OneWire.html
http://henrysbench.capnfatz.com/henrys-bench/arduino-sensors-and-input/keyes-ky-040-arduino-rotary-encoder-user-manual/
www.zonamaker.com.br/simbolos-personalizados-no-display-lcd-com-i2c/
**************************************************************************************/
#define On 1
#define Off 0
//#define TEST 1 //uncomment for code test
//#define ENGLISH 1 //uncomment for english messages. If not, it will be BR portuguese.
#define COMMON_ANODE // comment if using RGB common catode LEDs
//#define POTENTIOMETER //uncomment if using a 5K potentiometer to input values and menus, instead will use a rotary encoder
// display inicial definitions
/* if you use a 20x4 with parallel interface, you shoud set:
#include <LiquidCrystal.h>
LiquidCrystal lcd(7, 6, 5, 4, 3, 2); // select the pins used on the LCD panel
*/
// for a display with a serial interface adapter:
#include <Wire.h>
// set the LCD address to 0x27 for a 20 chars and 4 line display
// temperature sensor initial definitions
#include <OneWire.h>
OneWire ONE_WIRE_BUS(8); // select the digital pin used to connect sensor(s)
/*-----( Declare Variables )-----*/
// special characters
byte Cedilha[8] = {0x0, 0x0, 0xe, 0x11, 0x10, 0x15, 0xe, 0x8}; // character ç
byte ATil [8] = {0xd, 0x16, 0xe, 0x1, 0xf, 0x11, 0xf}; //character ã
byte AAgudo [8] = {0x2, 0x4, 0xe, 0x1, 0xf, 0x11, 0xf}; //character á
/*byte Thermo [8] = {B00100,B01010,B01010,B01110,B01110,B11111,B11111,B01110}; //icon for termometer
uint8_t bell[8] = {0x4,0xe,0xe,0xe,0x1f,0x0,0x4};
uint8_t note[8] = {0x2,0x3,0x2,0xe,0x1e,0xc,0x0};
uint8_t clock[8] = {0x0,0xe,0x15,0x17,0x11,0xe,0x0};
uint8_t heart[8] = {0x0,0xa,0x1f,0x1f,0xe,0x4,0x0};
uint8_t duck[8] = {0x0,0xc,0x1d,0xf,0xf,0x6,0x0};
uint8_t check[8] = {0x0,0x1,0x3,0x16,0x1c,0x8,0x0};
uint8_t cross[8] = {0x0,0x1b,0xe,0x4,0xe,0x1b,0x0};
uint8_t retarrow[8] = { 0x1,0x1,0x5,0x9,0x1f,0x8,0x4};
*/
// #constants
#define Choose 0
#define Prepare 1
#define Mash 2
#define Lauther 3
#define Boil 4
#define Cool 5
#define Whirlpool 6
#define End 7
#define BoilUp 0
#define BoilOn 1
#define BoilOk 2
#define BoilRest 3
const int MashOutTemperature = 76;
#ifdef TEST
const int MashOutTime = 1;
#else
const int MashOutTime = 10;
#endif
const int ClarificationTime = 10; // time recirculating the worth before Lauther
const int RestTime = 5;
const int CoolToTemperature = 25; // final temperature of cooling step
const int Recirculate = 0;
const int WhirflockTime = 14; // 14 not to overlap hop at 15' Should not be greater than 14 and lower than 11, or rearrange boil profiles.
const int WhirlpoolTime = 5;
const int Sparge = 1;
const int TemperatureTolerance= 1; // mash temperature tolerance acepted, + or -
const int DebouncingTime = 1;
// PINS digital
#define Encoder0PinA 0
#define Encoder0PinB 1
//#define EscButton 2 // Interrupt pin in NANO. Used by ESC but no need to define.
#define OkButton 3 // Interrupt pin in NANO. Used by OK without interruption, so that need to define. Used also as an interrupt.
// parallel lcd 7,6,5,4,3,2 // only if you use a parallel display, note the conflicting ports!!
// free 4
// free 5
// free 6
// free 7
// ONE_WIRE_BUS 8 // definitiron above. Here only for documentation
// temperature sensor conector pins:
// 1 = +5V
// 2 = DATA
// 3 = GND
#define Buzzer 9
#define IntermitentBuzzer 10
#define RGBLedRedPin 11
#define RGBLedGreenPin 12
#define RGBLedBluePin 13
// PINS analogic
#define SensorTemp 0
#define Potentiometer 1
// vars
boolean WaitForOkButton, OkButtonWasPressed, EscButtonWasPressed, SoundAlarm, SoundAlarmCanceled,
HeatIsOn, YellowAlert, RedAlert;
byte MashOption, BoilOption,
CurrentStage, PreviousStage,
CurrentMashStep, CurrentBoilStep,
HopNumber,
Option, LastOption,
Red, Green, Blue,
present = 0, type_s, data[12], addr[8],
Encoder0PinALast, AuxNumberLastMessage, NumberLastMessage;
int Minutes, Seconds, BoilTotalDelaySeconds, BoilDelaySeconds, Aux, i;
volatile int EscButtonState;
volatile boolean StepHasSubsteps;
long Now;
unsigned long HalfSecondAgo, TimeStageStarted, TimeMashStepStarted, CurrentBoilStepStarted,
TimeLastOkInterrupt, TimeLastEscInterrupt;
float CurrentTemperature;
String StrAddHop = "";
String ProgramString = "Sotile Brewing v1.1";
struct StructSteps {
String Name;
int InitialTemperature;
int NumberOfSteps;
int Temperature[4]; // 3 steps plus mashout
int Time[4]; // 3 steps plus mashout
};
struct StructBoil {
int MinutesTotal;
int NumberOfHops;
int TimeOfHop[5]; // max of 4 hoppings during boil + whirflock
int WhichIsWhirflock;
};
struct StructSteps StepProfile;
struct StructBoil BoilProfile;
void CleanDisplay() {
lcd.clear();
}
int Message(int Which, int B) {
for (i = 0; i < 20; i++) {
lcd.setCursor(i, 3);
lcd.print(" ");
}
lcd.setCursor(0, 3);
#ifdef ENGLISH
switch (Which) {
case 0 : lcd.print(F(" ")); break; // F function loads strings into flash rather than in SRAM
case 1 : lcd.print(F(" ** warm up!!! ** ")); break;
case 2 : lcd.print(F(" Recirculate wort ")); break;
case 3 : lcd.print(F(" Sparge (OK) ")); break;
case 4 : lcd.print(F(" Do whirlpool ")); break;
case 5 : lcd.print(F(" Heat the wather ")); break;
case 6 : lcd.print(F(" Add grains (OK) ")); break;
case 7 : lcd.print(F(" Mash complete (OK) ")); break;
case 8 : lcd.print(F("Warm up til ramp(OK)")); break;
case 9 : lcd.print(F(" Recirculate ")); break;
case 10 : lcd.print(F("Heat to boiling!(OK)")); break;
case 11 : lcd.print(F(" Boil completed (OK)")); break;
case 12 : lcd.print(F(" Add hop " ));
lcd.print( StrAddHop ); break;
case 13 : lcd.print(F(" Add whirflock (OK) ")); break;
case 14 : {
lcd.print(F(" Rest for "));
lcd.print(RestTime);
lcd.print(F(" min. "));
} break;
case 15 : lcd.print(F(" Cool the wort ")); break;
case 16 : lcd.print(F(" Remove chiller (OK)")); break;
case 17 : lcd.print(F(" Press (OK) to start")); break;
case 18 : lcd.print(F("Exit with no change!")); break;
case 19 : lcd.print(F("Delayed minutes")); break;
}
#else
switch (Which) {
case 0 : lcd.print(F(" ")); break;
case 1 : lcd.print(F(" ** Aquecer!!! ** ")); break;
case 2 : lcd.print(F(" Recircular mosto ")); break;
case 3 : {
lcd.print(F(" Lavar os graos (OK)"));
lcd.setCursor(12, 3); lcd.write(char(1)); break; // writes an "ã"
}
case 4 : lcd.print(F(" Fazer whirlpool ")); break;
case 5 : lcd.print(F(" Aquecer a agua "));
lcd.setCursor(12, 3); lcd.write(char(2)); break; // writes an "á"
case 6 : {
lcd.print(F("Adicionar graos (OK)"));
lcd.setCursor(12, 3); lcd.write(char(1)); break; // writes an "ã"
}
case 7 : lcd.print(F(" Mash completo (OK) ")); break;
case 8 : lcd.print(F("Aquecer p/ rampa(OK)")); break;
case 9 : lcd.print(F(" Recircular ")); break;
case 10 : lcd.print(F("Aquecer p/ferver(OK)")); break;
case 11 : lcd.print(F(" Fim da fervura (OK)")); break;
case 12 : lcd.print(F("Ad.lupulo " ));
lcd.print( StrAddHop ); break;
case 13 : lcd.print(F("Junte whirflock (OK)")); break;
case 14 : lcd.print(F(" Descansar 10 min. ")); break;
case 15 : lcd.print(F(" Resfriar o mosto ")); break;
case 16 : lcd.print(F("Retirar chiller (OK)")); break;
case 17 : lcd.print(F(" Pressione (OK) ")); break;
case 18 : lcd.print(F("Volta sem alteracao!"));
lcd.setCursor(16, 3); lcd.print(char(0)); // writes a "ç"
lcd.print(char(1)); // writes an "ã"
break;
case 19 : lcd.print(F("Atrasou minutos")); break;
}
#endif
if (Which < 18)
AuxNumberLastMessage = Which;
if (B) {
tone(Buzzer, 2000);
delay(200);
noTone(Buzzer);
}
}
void ShowAlert () {
/* RGB common catode for display
R G B
255, 0, 0 = red
0, 255, 0 = green
0, 0, 255 = blue
255, 255, 0 = yellow
80, 0, 80 = purple
0, 255, 255 = aqua
*/
if (RedAlert) {
Red = 255;
Green = 0;
Blue = 0;
if (!SoundAlarm) {
SoundAlarm = true;
SoundAlarmCanceled = false;
digitalWrite(IntermitentBuzzer, HIGH);
}
} else {
if (YellowAlert) { // blue seems better than yellow!
Red = 0;
Green = 0;
Blue = 255;
} else if ((CurrentStage == Choose) || (WaitForOkButton && !OkButtonWasPressed)) { // waiting for an input = aqua color
Red = 0;
Green = 255;
Blue = 255;
} else { // green color
Red = 0;
Green = 255;
Blue = 0;
}
if (SoundAlarm) {
SoundAlarm = false;
SoundAlarmCanceled = false;
digitalWrite(IntermitentBuzzer, LOW);
}
}
#ifdef COMMON_ANODE
// for common anode, we need to make digital pin near to 0, so the color is:
// color = 255 - color;
Red = (255 - Red );
Green = (255 - Green);
Blue = (255 - Blue );
#endif
analogWrite(RGBLedRedPin , Red );
analogWrite(RGBLedGreenPin, Green);
analogWrite(RGBLedBluePin , Blue );
}
void TurnMotor(int OnOff) {
// to be developed
}
void TurnHeat(int OnOff) {
// to be adapted
if (OnOff == On) {
Message( 1, true); //" ** warm up!!! **"
HeatIsOn = true;
YellowAlert = true;
} else {
Message ( 0, false); // cleans message
HeatIsOn = false;
YellowAlert = false;
}
}
void TurnPump (int Witch, int OnOff) {
if (OnOff = On) {
switch (Witch) {
case Recirculate : Message( 2, false); break; //"Recirculate wort"
case Sparge : Message( 3, false); break; //"Sparge (OK)"
case Whirlpool : Message( 4, false); break; //"Do whirlpool"
}
YellowAlert = true;
} else {
Message( 0, false); // cleans message
YellowAlert = false;
}
}
void MessageStageName (int WhichName) {
#ifdef ENGLISH
switch (WhichName) {
case Choose: {
lcd.print(F("Choose" )); // translate here
break;
}
case Prepare: {
lcd.print(F("Prepare" ));
break;
}
case Mash: {
lcd.print(F("Mash" ));
break;
}
case Lauther: {
lcd.print(F("Sparge" ));
break;
}
case Boil: {
lcd.print(F("Boil" ));
break;
}
case Cool: {
lcd.print(F("Cooling" ));
break;
}
case Whirlpool: {
lcd.print(F("Whirlpool" ));
break;
}
default : {
lcd.print(F("Process end "));
break;
}
}
#else
switch (WhichName) {
case Choose: {
lcd.print(F("Escolha" ));
break;
}
case Prepare: {
lcd.print(F("Prepara"));
lcd.print(char(0)); // writea a "ç"
lcd.print(char(1)); // writes an "ã"
lcd.print(F("o" )); break;
}
case Mash: {
lcd.print(F("Mostura" ));
break;
}
case Lauther: {
lcd.print(F("Filtragem" ));
break;
}
case Boil: {
lcd.print(F("Fervura" ));
break;
}
case Cool: {
lcd.print(F("Resfriamento"));
break;
}
case Whirlpool: {
lcd.print(F("Whirlpool" ));
break;
}
default : {
lcd.print(F("Fim" ));
break;
}
}
#endif
}
void MessageStage(int Which) {
for (i = 0; i < 20; i++) { // 20 = lcd rows
lcd.setCursor(i, 0);
lcd.print(" ");
}
lcd.setCursor(0, 0);
#ifdef ENGLISH
lcd.print(F("Stage: "));
#else
lcd.print(F("Etapa: "));
#endif
switch (Which) {
case Choose :
case Prepare: MessageStageName (Which); break; // translate here
case Mash: MessageStageName (Which);
lcd.setCursor(15, 0); lcd.print("1/");
lcd.print(StepProfile.NumberOfSteps);
break;
case Lauther: MessageStageName (Which); break;
case Boil: MessageStageName (Which);
lcd.print(F(" "));
lcd.print(BoilProfile.MinutesTotal + (BoilTotalDelaySeconds / 60));
lcd.print("\'");
break;
case Cool:
case Whirlpool: MessageStageName (Which); break;
}
}
void DisplayATime(int X, int Min, int Sec) {
if (Min >= 60) { // if time is more than one hour, shows using format hh:mm (or 12h00 if ENGLISH is not deffined)
Aux = Min / 60; // calculate hours
lcd.setCursor(X , 1);
if (Aux < 10)
lcd.print(" ");
lcd.print(Aux); // prints hours
Aux = Min % 60; // calculate minutes that are not full hours
#ifdef ENGLISH
lcd.print(":");
#else
lcd.print("h");
#endif
if ((int)Aux < 10)
lcd.print("0");
lcd.print(Aux);
} else { // else shows using format mm'ss
if (Min >= 10) lcd.setCursor(X , 1);
else lcd.setCursor(X + 1, 1);
lcd.print(Min); lcd.print("\'");
if ((int)Sec < 10)
lcd.print("0");
lcd.print(Sec);
}
}
void DisplayTimes() {
// displays total time:
int aux;
Now = millis() / 1000;
Minutes = (int) (Now / 60);
Seconds = (int) (Now - (Minutes * 60));
DisplayATime(0, Minutes, Seconds);
// displays stage time:
Minutes = (int) ((Now - TimeStageStarted) / 60);
Seconds = (int) ((Now - TimeStageStarted) - (Minutes * 60));
DisplayATime(6, Minutes, Seconds);
switch (CurrentStage) {
case Mash :
// if ((StepProfile.NumberOfSteps == 1) && (CurrentMashStep ==1)) {
// lcd.setCursor(11,1); lcd.print("/"); lcd.print(StepProfile.Time[0]); lcd.print("\'");
// } else {
if (CurrentMashStep <= StepProfile.NumberOfSteps + 2) {
Minutes = (int) ((Now - TimeMashStepStarted) / 60 );
Seconds = (int) ((Now - TimeMashStepStarted) - (Minutes * 60));
DisplayATime(12, Minutes, Seconds);
if (CurrentMashStep <= StepProfile.NumberOfSteps+1) {
lcd.setCursor(17, 1); lcd.print("/"); lcd.print(StepProfile.Time[CurrentMashStep - 1]);
}
}
// }
break;
case Boil :
// aqui!
if (CurrentBoilStep == BoilOn) {
lcd.setCursor(12, 1);
//Serial.print("HopNumber: "); Serial.println(HopNumber);
Aux = HopNumber;
if (WaitForOkButton && (Aux > 1))
Aux --; // adjusts hopnumber not to screw display
if (HopNumber < BoilProfile.NumberOfHops) { // there's still hop(s)/whirflock to add
Now = (BoilProfile.MinutesTotal * 60 - BoilProfile.TimeOfHop[Aux] * 60) // gets hop n time to end
- (millis() / 1000 - CurrentBoilStepStarted); // time elapsed since boilOn started.
if (HopNumber == BoilProfile.WhichIsWhirflock) { // it's whirflock, not a hop
if (Now < 0) {
Now = (Now * -1)-1; // compiler didn't like abs function!
lcd.print("w+: ");
} else {
lcd.print(" w: ");
}
} else { // hop time
#ifdef ENGLISH
lcd.print("h");
#else
lcd.print("L");
#endif
if (Now < 0) {
Now = (Now * -1)-1; // compiler didn't like abs function!
lcd.print("+: ");
} else {
lcd.print(Aux + 1); lcd.print(": ");
}
}
Minutes = (int) ((Now) / 60) ;
Seconds = (int) ((Now) - (Minutes * 60));
DisplayATime(15, Minutes, Seconds);
} else {
lcd.print(F(" "));
}
}
break;
}
}
float GetONE_WIRE_BUSTemperature() {
ONE_WIRE_BUS.reset();
ONE_WIRE_BUS.select(addr);
ONE_WIRE_BUS.write(0x44, 1); // start conversion, with parasite power on at the end
delay(800); // maybe 750ms is enough, maybe not
ONE_WIRE_BUS.reset();
ONE_WIRE_BUS.select(addr);
ONE_WIRE_BUS.write(0xBE); // Read Scratchpad
for ( i = 0; i < 9; i++) { // we need 9 bytes
data[i] = ONE_WIRE_BUS.read();
//Serial.print(data[i], HEX);
//Serial.print(" ");
}
// Convert the data to Current temperature because the result is a 16 bit signed integer, it should
// be stored to an "int16_t" type, which is always 16 bits even when compiled on a 32 bit processor.
int16_t raw = (data[1] << 8) | data[0];
if (type_s) {
raw = raw << 3; // 9 bit resolution default
if (data[7] == 0x10) { // "count remain" gives full 12 bit resolution
raw = (raw & 0xFFF0) + 12 - data[6];
}
} else {
byte cfg = (data[4] & 0x60);
// at lower res, the low bits are undefined, so let's zero them
if (cfg == 0x00) raw = raw & ~7; // 9 bit resolution, 93.75 ms
else if (cfg == 0x20) raw = raw & ~3; // 10 bit res, 187.5 ms
else if (cfg == 0x40) raw = raw & ~1; // 11 bit res, 375 ms
//// default is 12 bit resolution, 750 ms conversion time
}
return (float)raw / 16.0;
}
void DisplayTemperature() {
Now = millis() / 1000;
if (Now - HalfSecondAgo > 0.3) {
HalfSecondAgo = Now;
#ifdef TEST // A 5K potentiometer is used to mimic the sensor
Aux = analogRead (SensorTemp);
CurrentTemperature = (map((Aux), 1000, 0, 0, 100));
#else
CurrentTemperature = GetONE_WIRE_BUSTemperature(); // a digital read, first sensor in the bus.
#endif
/* All code is to work with Celsius
If you want, change sensors funcion by sensors.getTempFByIndex(0) to read Fahreinheit
and adapt code if necessary.
*/
lcd.setCursor( 0, 2);
lcd.print("T:");
if (CurrentTemperature < 10)
lcd.print("0");
lcd.print(CurrentTemperature, 1); lcd.write(B11011111); lcd.print("C");
switch (CurrentStage) {
case Prepare :
lcd.print("/ ");
lcd.print(StepProfile.InitialTemperature); lcd.write(B11011111); lcd.print("C");
if ((CurrentTemperature) > StepProfile.InitialTemperature + TemperatureTolerance)
RedAlert = true ;
else RedAlert = false;
if ((CurrentTemperature) < StepProfile.InitialTemperature - TemperatureTolerance)
YellowAlert = true ;
else YellowAlert = false;
break;
case Mash :
if (WaitForOkButton) {
if (CurrentMashStep <= StepProfile.NumberOfSteps) {
lcd.setCursor(16, 2); lcd.print(StepProfile.Temperature[CurrentMashStep]);
if ((CurrentTemperature) > StepProfile.Temperature[CurrentMashStep] + TemperatureTolerance)
RedAlert = true ;
else RedAlert = false;
if ((CurrentTemperature) < StepProfile.Temperature[CurrentMashStep] - TemperatureTolerance)
YellowAlert = true ;
else YellowAlert = false;
} else {
lcd.setCursor(16, 2); lcd.print(StepProfile.Temperature[CurrentMashStep - 2]);
if ((CurrentTemperature) > 76 + TemperatureTolerance)
RedAlert = true ;
else RedAlert = false;
if ((CurrentTemperature) < 76 - TemperatureTolerance)
YellowAlert = true ;
else YellowAlert = false;
}
} else {
lcd.setCursor(16, 2); lcd.print(StepProfile.Temperature[CurrentMashStep - 1]);
if ((CurrentTemperature) > StepProfile.Temperature[CurrentMashStep - 1] + TemperatureTolerance)
RedAlert = true ;
else RedAlert = false;
if ((CurrentTemperature) < StepProfile.Temperature[CurrentMashStep - 1] - TemperatureTolerance)
YellowAlert = true ;
else YellowAlert = false;
}
lcd.setCursor(18, 2); lcd.write(B11011111); lcd.print("C");
lcd.setCursor(13, 2); lcd.print(" / ");
break;
case Lauther :
lcd.setCursor(13, 2); lcd.print(" / 76"); lcd.write(B11011111); lcd.print("C");
if ((CurrentTemperature) > 76 + TemperatureTolerance)
RedAlert = true ;
else RedAlert = false;
if ((CurrentTemperature) < 76 - TemperatureTolerance)
YellowAlert = true ;
else YellowAlert = false;
break;
case Boil :
if ((CurrentTemperature) < 94) // assuming that it will boil less than 100ºC
YellowAlert = true ;
else YellowAlert = false;
break;
case Cool :
lcd.setCursor(13, 2); lcd.print(" / "); lcd.print(CoolToTemperature); lcd.write(B11011111); lcd.print("C");
break;
}
}
}
int InputOption(int Min, int Max, int Encoder0Pos) {
#ifdef POTENTIOMETER // using a potentiometer
// Current has no use when reading a potentiometer.
Aux = analogRead (Potentiometer);
return (map((Aux), 5, 890, Min, Max)); // converts potentiometer read to value, adjusted for a 5K linear potentiometer
#else
// http://henrysbench.capnfatz.com/henrys-bench/arduino-sensors-and-input/keyes-ky-040-arduino-rotary-encoder-user-manual/
i = digitalRead(Encoder0PinA);
if ((Encoder0PinALast == LOW) && (i == HIGH)) {
if (digitalRead(Encoder0PinB) == LOW) {
if (Encoder0Pos > Min)
Encoder0Pos--;
else
Encoder0Pos = Max;
} else {
if (Encoder0Pos < Max)
Encoder0Pos++;
else
Encoder0Pos = Min;
}
}
Encoder0PinALast = i;
return (Encoder0Pos);
#endif
}
void ProcessChooseMash() {
const int NumberOfProfiles = 7;
MashOption--;
LastOption = MashOption - 2;
CleanDisplay;
Encoder0PinALast = LOW;
lcd.setCursor( 0, 0);
#ifdef ENGLISH
lcd.print(F("Select mash profile:"));
#else
lcd.print(F("Selecione o perfil: "));
#endif
do {
MashOption = InputOption(1, NumberOfProfiles, MashOption);
// steps template - position: 01234561789ABCDEFGHIJ
// 50C/20'+64C/30'+70/30
if (MashOption != LastOption) {
switch (MashOption) {
case 1 :
lcd.setCursor( 0, 1); lcd.print(F("TM 1 step light body"));
lcd.setCursor( 0, 2); lcd.print(F("64C/75' "));
break;
case 2 :
lcd.setCursor( 0, 1); lcd.print(F("TM 1 step medium bdy"));
lcd.setCursor( 0, 2); lcd.print(F("67C/60' "));
break;
case 3 :
lcd.setCursor( 0, 1); lcd.print(F("TM 1 step full body "));
lcd.setCursor( 0, 2); lcd.print(F("69C/40' "));
break;
case 4 :
lcd.setCursor( 0, 1); lcd.print(F("TM 2 step light body"));
lcd.setCursor( 0, 2); lcd.print(F("50C/30'+64C/75' "));
break;
case 5 :
lcd.setCursor( 0, 1); lcd.print(F("TM 2 step medium bdy"));
lcd.setCursor( 0, 2); lcd.print(F("50C/30'+67C/45' "));
break;
case 6 :
lcd.setCursor( 0, 1); lcd.print(F("TM 2 step full body "));
lcd.setCursor( 0, 2); lcd.print(F("50C/30'+69C/30' "));
break;
case 7 :
lcd.setCursor( 0, 1); lcd.print(F("TM 3 step medium bdy"));
lcd.setCursor( 0, 2); lcd.print(F("50C/20'+62C/20+67/50"));
break;
}
//lcd.setCursor( 2,2); lcd.print("C/");
lcd.setCursor( 3, 3); lcd.print(MashOption); lcd.print("/"); lcd.print(NumberOfProfiles); lcd.print(" Ok?");
// Aux = digitalRead(OkButton);
}
LastOption = MashOption;
Aux = digitalRead (OkButton);
} while (Aux); // OK button pressed goes to false/low/zero
// initiale fields
StepProfile.Temperature[0] = 0; StepProfile.Time[0] = 0;
StepProfile.Temperature[1] = 0; StepProfile.Time[1] = 0;
StepProfile.Temperature[2] = 0; StepProfile.Time[2] = 0;
StepProfile.Temperature[3] = 0; StepProfile.Time[3] = 0;
switch (MashOption) {
case 1 : {
StepProfile.Name = F("TM 1 step light body");
StepProfile.NumberOfSteps = 1;
#ifdef TEST
StepProfile.InitialTemperature = 30; // just for initial tests.
StepProfile.Time[0] = 1;
#else
StepProfile.InitialTemperature = 68;
StepProfile.Time[0] = 75;
#endif
StepProfile.Temperature[0] = 64;
break;
}
case 2 : {
StepProfile.Name = F("TM 1 step medium bdy");
StepProfile.NumberOfSteps = 1;
StepProfile.InitialTemperature = 71;
StepProfile.Temperature[0] = 67; StepProfile.Time[0] = 60;
break;
}
case 3 : {
StepProfile.Name = F("TM 1 step full body ");
StepProfile.NumberOfSteps = 1;
StepProfile.InitialTemperature = 73;
StepProfile.Temperature[0] = 69; StepProfile.Time[0] = 40;
break;
}
case 4 :
StepProfile.Name = F("TM 2 step light body");
StepProfile.NumberOfSteps = 2;
StepProfile.InitialTemperature = 55;
StepProfile.Temperature[0] = 50; StepProfile.Time[0] = 30;
StepProfile.Temperature[1] = 64; StepProfile.Time[1] = 75;
break;
case 5 :
StepProfile.Name = F("TM 2 step medium bdy");
StepProfile.NumberOfSteps = 2;
StepProfile.InitialTemperature = 54;
StepProfile.Temperature[0] = 50; StepProfile.Time[0] = 30;
StepProfile.Temperature[1] = 67; StepProfile.Time[1] = 45;
break;
case 6 :
StepProfile.Name = F("TM 2 step full body ");
StepProfile.NumberOfSteps = 2;
StepProfile.InitialTemperature = 54;
StepProfile.Temperature[0] = 50; StepProfile.Time[0] = 30;
StepProfile.Temperature[1] = 69; StepProfile.Time[1] = 30;
break;
case 7 :
case 8 :
StepProfile.Name = F("TM 3 step medium bdy");
StepProfile.NumberOfSteps = 3;
#ifdef TEST
StepProfile.InitialTemperature = 30; // just for initial tests.
StepProfile.Temperature[0] = 50; StepProfile.Time[0] = 1;
StepProfile.Temperature[1] = 62; StepProfile.Time[1] = 2;
StepProfile.Temperature[2] = 67; StepProfile.Time[2] = 1;
#else
StepProfile.InitialTemperature = 54;
StepProfile.Temperature[0] = 50; StepProfile.Time[0] = 20;
StepProfile.Temperature[1] = 62; StepProfile.Time[1] = 25;
StepProfile.Temperature[2] = 67; StepProfile.Time[2] = 50;
#endif
break;
}
// set mash out temperature and time:
// MashOutTemperature = 76;
// MashOutTime = 1; defined at program's start
StepProfile.Temperature[StepProfile.NumberOfSteps] = MashOutTemperature;
StepProfile.Time [StepProfile.NumberOfSteps] = MashOutTime;
}
void ProcessChooseBoil() {
const int NumberOfBoilProfiles = 18;
BoilOption--;
if (BoilOption == 0)
BoilOption = 1;
LastOption = BoilOption - 2;
Encoder0PinALast = LOW;
CleanDisplay;
lcd.setCursor( 0, 0);
#ifdef ENGLISH
lcd.print(F("Select boil profile"));
#else
lcd.print(F("Selecione boil: "));
#endif
do {
BoilOption = InputOption(1, NumberOfBoilProfiles, BoilOption);
if (BoilOption != LastOption) {
switch (BoilOption) {
case 1 :
lcd.setCursor( 0, 1); lcd.print(F(" 60 min, 1 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' "));
break;
case 2 :
lcd.setCursor( 0, 1); lcd.print(F(" 60 min, 2 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 0' "));
break;
case 3 :
lcd.setCursor( 0, 1); lcd.print(F(" 60 min, 2 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 5' "));
break;
case 4 :
lcd.setCursor( 0, 1); lcd.print(F(" 60 min, 3 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 10' 0' "));
break;
case 5 :
lcd.setCursor( 0, 1); lcd.print(F(" 60 min, 3 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 10' 5' "));
break;
case 6 :
lcd.setCursor( 0, 1); lcd.print(F(" 60 min, 3 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 20' 0' "));
break;
case 7 :
lcd.setCursor( 0, 1); lcd.print(F(" 60 min, 3 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 20' 5' "));
break;
case 8 :
lcd.setCursor( 0, 1); lcd.print(F(" 60 min, 3 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 20' 10' "));
break;
case 9 :
lcd.setCursor( 0, 1); lcd.print(F(" 75 min, 2 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 15' "));
break;
case 10 :
lcd.setCursor( 0, 1); lcd.print(F(" 75 min, 2 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 20' "));
break;
case 11:
lcd.setCursor( 0, 1); lcd.print(F(" 75 min, 3 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 20' 0' "));
break;
case 12 :
lcd.setCursor( 0, 1); lcd.print(F(" 90 min, 2 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 15' "));
break;
case 13 :
lcd.setCursor( 0, 1); lcd.print(F(" 90 min, 2 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 20' "));
break;
case 14 :
lcd.setCursor( 0, 1); lcd.print(F(" 90 min, 3 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 20' 0' "));
break;
case 15 :
lcd.setCursor( 0, 1); lcd.print(F("120 min, 2 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 15' "));
break;
case 16 :
lcd.setCursor( 0, 1); lcd.print(F("120 min, 2 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 20' "));
break;
case 17 :
lcd.setCursor( 0, 1); lcd.print(F("120 min, 3 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 20' 0' "));
break;
case 18 :
lcd.setCursor( 0, 1); lcd.print(F("120 min, 4 hops "));
lcd.setCursor( 0, 2); lcd.print(F(" 60' 20' 15' 0' "));
break;
}
if (BoilOption < 10) {
lcd.setCursor( 2, 3); lcd.print(" ");
//lcd.setCursor( 3,3);
} else
lcd.setCursor( 2, 3);
lcd.print(BoilOption);
//lcd.setCursor( 4,3);
lcd.print("/");
//lcd.setCursor( 5,3);
lcd.print(NumberOfBoilProfiles);
lcd.print(" Ok?");
}
LastOption = BoilOption;
Aux = digitalRead (OkButton);
} while (Aux); // OK button pressed goes to false/low/zero
// to avoid repeated code
BoilProfile.TimeOfHop[1] = -1; // -1 indicates no hop at this time
BoilProfile.TimeOfHop[2] = -1;
BoilProfile.TimeOfHop[3] = -1;
BoilProfile.TimeOfHop[4] = -1;
switch (BoilOption) {
case 1 :
// 60 min, 1 hops
// hop at 60'
BoilProfile.MinutesTotal = 60;
BoilProfile.NumberOfHops = 2;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = WhirflockTime; // WhirflockTime is defined at code top and should be between 11 and 14.
BoilProfile.WhichIsWhirflock= 1; // if u use different value, change logic here.
break;
case 2 :
//60 min, 2 hops
// hops at 60' 0'
BoilProfile.MinutesTotal = 60;
BoilProfile.NumberOfHops = 3;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = WhirflockTime; BoilProfile.WhichIsWhirflock= 1;
BoilProfile.TimeOfHop[2] = 0;
break;
case 3 :
// 60 min, 2 hops
// hops at 60' 5'
BoilProfile.MinutesTotal = 60;
BoilProfile.NumberOfHops = 3;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = WhirflockTime; BoilProfile.WhichIsWhirflock= 1;
BoilProfile.TimeOfHop[2] = 5;
break;
case 4 :
//60 min, 3 hops
// hops at 60' 10' 0'
BoilProfile.MinutesTotal = 60;
BoilProfile.NumberOfHops = 4;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = WhirflockTime; BoilProfile.WhichIsWhirflock= 1;
BoilProfile.TimeOfHop[2] = 10;
BoilProfile.TimeOfHop[3] = 0;
break;
case 5 :
// 60 min, 3 hops
// hops at 60' 10' 5'
BoilProfile.MinutesTotal = 60;
BoilProfile.NumberOfHops = 4;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = WhirflockTime; BoilProfile.WhichIsWhirflock= 1;
BoilProfile.TimeOfHop[2] = 10;
BoilProfile.TimeOfHop[3] = 5;
break;
case 6 :
// 60 min, 3 hops
// hops at 60' 20' 0'
BoilProfile.MinutesTotal = 60;
BoilProfile.NumberOfHops = 4;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 20;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
BoilProfile.TimeOfHop[3] = 0;
break;
case 7 :
// 60 min, 3 hops
// hops at 60' 20' 5'
BoilProfile.MinutesTotal = 60;
BoilProfile.NumberOfHops = 4;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 20;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
BoilProfile.TimeOfHop[3] = 5;
break;
case 8 :
// 60 min, 3 hops
// hops at 60' 20' 10'
BoilProfile.MinutesTotal = 60;
BoilProfile.NumberOfHops = 4;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 20;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
BoilProfile.TimeOfHop[3] = 10;
break;
case 9 :
// 75 min, 2 hops
// hops at 60' 15'
BoilProfile.MinutesTotal = 75;
BoilProfile.NumberOfHops = 3;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 15;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
break;
case 10 :
// 75 min, 2 hops
// hops at 60' 20'
BoilProfile.MinutesTotal = 75;
BoilProfile.NumberOfHops = 3;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 20;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
break;
case 11:
// 75 min, 3 hops
// hops at 60' 20' 0'
BoilProfile.MinutesTotal = 75;
BoilProfile.NumberOfHops = 4;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 20;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
BoilProfile.TimeOfHop[3] = 0;
break;
case 12 :
// 90 min, 2 hops
// hops at 60' 15'
BoilProfile.MinutesTotal = 90;
BoilProfile.NumberOfHops = 3;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 15;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
break;
case 13 :
// 90 min, 2 hops
// hops at 60' 20'
BoilProfile.MinutesTotal = 90;
BoilProfile.NumberOfHops = 3;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 20;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
break;
case 14 :
// 90 min, 3 hops
// hops at 60' 20' 0'
BoilProfile.MinutesTotal = 90;
BoilProfile.NumberOfHops = 4;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 20;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
BoilProfile.TimeOfHop[3] = 0;
break;
case 15 :
// 120 min, 2 hops
// hops at 60' 15'
BoilProfile.MinutesTotal =120;
BoilProfile.NumberOfHops = 3;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 15;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
break;
case 16 :
// 120 min, 2 hops
// hops at 120' 60' 20'
BoilProfile.MinutesTotal =120;
BoilProfile.NumberOfHops = 3;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 20;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
break;
case 17 :
// 120 min, 3 hops
// hops at 60' 20' 0'
BoilProfile.MinutesTotal =120;
BoilProfile.NumberOfHops = 4;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 20;
BoilProfile.TimeOfHop[2] = WhirflockTime; BoilProfile.WhichIsWhirflock= 2;
BoilProfile.TimeOfHop[3] = 0;
break;
case 18 :
// 120 min, 4 hops
// hops at 60' 20' 15' 0'
#ifdef TEST
BoilProfile.MinutesTotal = 6;
BoilProfile.NumberOfHops = 5;
BoilProfile.TimeOfHop[0] = 5;
BoilProfile.TimeOfHop[1] = 4;
BoilProfile.TimeOfHop[2] = 3;
BoilProfile.TimeOfHop[3] = 2;
BoilProfile.TimeOfHop[4] = 1; BoilProfile.WhichIsWhirflock= 4;
#else
BoilProfile.MinutesTotal =120;
BoilProfile.NumberOfHops = 5;
BoilProfile.TimeOfHop[0] = 60;
BoilProfile.TimeOfHop[1] = 20;
BoilProfile.TimeOfHop[2] = 15;
BoilProfile.TimeOfHop[3] = WhirflockTime; BoilProfile.WhichIsWhirflock= 3;
BoilProfile.TimeOfHop[4] = 0;
#endif
break;
}
}
void ProcessChoose() {
delay(200);
CleanDisplay();
ProcessChooseMash();
delay(1000);
ProcessChooseBoil();
CurrentStage = Prepare;
PreviousStage = Choose ;
#ifdef TEST
CurrentStage = Boil; // jumps to boil stage, for debuggin'
#endif
}
/*
void ProcessPrepare() {
if (CurrentStage != PreviousStage) {
CleanDisplay();
PreviousStage = CurrentStage;
MessageStage(Prepare);
TimeStageStarted = millis() / 1000;
TurnHeat(On);
Message( 5, true); //"Heat the wather"
WaitForOkButton = false;
OkButtonWasPressed = false;
}
if ((CurrentTemperature >= StepProfile.InitialTemperature) && (WaitForOkButton == false)) {
TurnHeat (Off);
TurnMotor(On );
Message( 6, true); //"Add grains (ok)"
WaitForOkButton = true;
}
if (WaitForOkButton && OkButtonWasPressed)
CurrentStage++; //ok, let's go to next stage
}
*/
void ProcessMash() {
if (CurrentStage != PreviousStage) {
CleanDisplay();
PreviousStage = CurrentStage;
TimeStageStarted = millis() / 1000;
CurrentMashStep = 1;
TimeMashStepStarted = millis() / 1000;
WaitForOkButton = false;
OkButtonWasPressed = false;
MessageStage(Mash);
Message(0, false);
}
// Current mash step time is over, tests once; add 1 more step, the mash out
// same stage, next step
if ( (WaitForOkButton == false)
&& (((millis() / 1000) - TimeMashStepStarted) > StepProfile.Time[CurrentMashStep - 1] * 60)
&& (CurrentMashStep <= StepProfile.NumberOfSteps + 2))
if (CurrentMashStep == StepProfile.NumberOfSteps + 1) {
TurnMotor(Off);
TurnHeat (Off);
CurrentMashStep++;
Message( 7, true); //"Mash complete (Ok)"
WaitForOkButton = true;
} else {
TurnMotor(On);
TurnHeat (On);
Message( 8, true); //"Warm up til ramp (Ok)"
WaitForOkButton = true;
}
// same step, but temperature too low.
if (WaitForOkButton == false) {
if (CurrentTemperature < StepProfile.Temperature[CurrentMashStep - 1]) {
if (!HeatIsOn) {
TurnMotor(On);
TurnHeat (On);
}
} else {
if (HeatIsOn) {
TurnMotor(Off);
TurnHeat (Off);
}
}
} else { // go to next mash step, and it's temperature has been reached - no need to wait for OK press
if ((CurrentMashStep < StepProfile.NumberOfSteps + 1) && (CurrentTemperature >= StepProfile.Temperature[CurrentMashStep])) {
TurnMotor(Off);
TurnHeat (Off);
CurrentMashStep++;
lcd.setCursor(15, 0);
if (CurrentMashStep <= StepProfile.NumberOfSteps)
lcd.print(CurrentMashStep);
else lcd.print("M-out"); // last "step" is mash out, it's not choosen, but defined in choose function.
WaitForOkButton = false;
OkButtonWasPressed = false;
TimeMashStepStarted = millis() / 1000;
lcd.setCursor(0, 1); lcd.print(F(" ")); // best place in the code to clean line in display.
}
}
if (WaitForOkButton && OkButtonWasPressed) {
if (CurrentMashStep > StepProfile.NumberOfSteps + 1) {
CurrentStage++; //ok, let's go to next stage
TurnMotor(Off);
TurnHeat (Off);
} else { // same stage, next step.
Message( 0, false); // cleans message
CurrentMashStep++;
lcd.setCursor(15, 0);
if (CurrentMashStep <= StepProfile.NumberOfSteps)
lcd.print(CurrentMashStep);
else lcd.print("M-out");
WaitForOkButton = false;
OkButtonWasPressed = false;
TurnMotor(On);
TurnHeat (On);
lcd.setCursor(0, 1); lcd.print(" "); // best place in the code to clean line in display. :-)
TimeMashStepStarted = millis() / 1000;
if (CurrentTemperature < StepProfile.Temperature[CurrentMashStep - 1]) {
TurnMotor(On);
TurnHeat (On);
}
}
}
}
void ProcessLauther() {
if (CurrentStage != PreviousStage) {
CleanDisplay();
PreviousStage = CurrentStage;
MessageStage(Lauther);
TimeStageStarted = millis() / 1000;
WaitForOkButton = false ;
OkButtonWasPressed = false;
TurnPump(Recirculate, On);
Message( 9, true); //"Recirculate"
lcd.setCursor(11, 1); lcd.print("/"); lcd.print(10); lcd.print("\'");
}
#ifdef TEST
if ((WaitForOkButton == false) && (((millis() / 1000) - TimeStageStarted) > 1 * 60)) {
#else
if ((WaitForOkButton == false) && (((millis() / 1000) - TimeStageStarted) > ClarificationTime * 60)) {
#endif
TurnPump(Recirculate, Off);
TurnPump(Sparge , On );
delay(100);
lcd.setCursor(11, 1); lcd.print(" ");
delay (100);
WaitForOkButton = true;
}
if (WaitForOkButton && OkButtonWasPressed) {
TurnPump(Sparge , Off );
CurrentStage++; //ok, let's go to next stage
}
}
void ProcessBoil() {
if (CurrentStage != PreviousStage) {
CleanDisplay();
PreviousStage = CurrentStage;
MessageStage(Boil);
TimeStageStarted = millis() / 1000; // saves the time when boil up started
CurrentBoilStepStarted = TimeStageStarted;
WaitForOkButton = true;
OkButtonWasPressed = false;
RedAlert = false;
YellowAlert = false;
CurrentBoilStep = BoilUp;
TurnHeat(On);
HopNumber = 0;
BoilTotalDelaySeconds = 0;
BoilDelaySeconds = 0;
Message(10, true); //"Heat to boiling!(ok)"
lcd.setCursor(13, 2); lcd.print(" ");
}
switch (CurrentBoilStep) {
case BoilUp :
//Serial.println("BoilUp"); delay (1000);
if (WaitForOkButton && OkButtonWasPressed) {
CurrentBoilStep++;
CurrentBoilStepStarted = millis() / 1000; // saves the time when boil on started
TimeStageStarted = CurrentBoilStepStarted; // it makes time restart when boil is set to on
WaitForOkButton = false;
Message( 0, false); // cleans message
}
break;
case BoilOn :
// Proccess Hops:
if (!WaitForOkButton && // If this is the first time for this hop (not WaitForOk)
(HopNumber < BoilProfile.NumberOfHops) && // and There's still hops to add
(((millis() / 1000) - CurrentBoilStepStarted) + BoilProfile.TimeOfHop[HopNumber] * 60 >= (BoilProfile.MinutesTotal * 60) )) { // and it's time to add a hop/whirflock (BoilMinutesTotal - TimeElapsed) <= time to add Hop
if (HopNumber == BoilProfile.WhichIsWhirflock) { // it's not a hop, it's the whirflock.
Message(13, true); //"Add whirflock (Ok)"
} else {
StrAddHop = HopNumber + 1; // StrAddHop is a string used to show message 12
StrAddHop += "/";
StrAddHop += BoilProfile.TimeOfHop[HopNumber];
StrAddHop += "\'(OK)";
Message(12, true); //"Add hop n/mm'(Ok)"
}
WaitForOkButton = true;
OkButtonWasPressed = false;
}
// boil total time is over:
if (!WaitForOkButton &&
(((millis() / 1000) - CurrentBoilStepStarted) > ((BoilProfile.MinutesTotal * 60)))) { // Time of Boil (boilOn) is greater than Total Boil time for the profile + delays in the hop additions
CurrentBoilStep++; //ok, let's go to next step
CurrentBoilStepStarted = millis() / 1000; // saves the time when boil ok
WaitForOkButton = true;
OkButtonWasPressed = false;
TurnHeat(Off);
Message(11, true); //"Boil completed (Ok)"
}
if (WaitForOkButton && OkButtonWasPressed) {
BoilDelaySeconds = ((millis() / 1000) - CurrentBoilStepStarted - (BoilProfile.MinutesTotal - BoilProfile.TimeOfHop[HopNumber]) * 60);
BoilTotalDelaySeconds += BoilDelaySeconds;
HopNumber++;
CurrentBoilStepStarted = CurrentBoilStepStarted + BoilDelaySeconds;
if (BoilDelaySeconds > 60) {
Message(19, false); // "Delayed minutes"
lcd.setCursor(9, 3); lcd.print(BoilDelaySeconds / 60);
delay(700);
}
MessageStage(Boil);
WaitForOkButton = false;
OkButtonWasPressed = false;
Message(0, false); // cleans line 4
}
break;
case BoilOk :
if (WaitForOkButton && OkButtonWasPressed) {
Message(14, true); //"Rest for 10 min."
CurrentBoilStep++; //ok, let's go to next step
CurrentBoilStepStarted = millis() / 1000; // saves the time when boil ok
TimeStageStarted = CurrentBoilStepStarted;
WaitForOkButton = false;
OkButtonWasPressed = false;
}
//delay(100);
break;
case BoilRest:
#ifdef TEST
if (((millis() / 1000) - CurrentBoilStepStarted) > 67) { // for development purposes
#else
if (((millis() / 1000) - CurrentBoilStepStarted) > RestTime * 60) { // rest for ten minutes
#endif
CurrentStage++; //ok, let's go to next stage
}
break;
}
}
void ProcessCool() {
if (CurrentStage != PreviousStage) {
CleanDisplay();
PreviousStage = CurrentStage;
MessageStage(Cool);
TimeStageStarted = millis() / 1000;
WaitForOkButton = false ;
OkButtonWasPressed = false;
RedAlert = false;
YellowAlert = false;
Message(15, true); //"Cool the wort "
}
if ((CurrentTemperature <= CoolToTemperature) && !WaitForOkButton) {
WaitForOkButton = true;
Message (16, false); //"Remove chiller (OK)"
}
if (WaitForOkButton && OkButtonWasPressed) {
CurrentStage++; //ok, let's go to next stage
}
}
void ProcessWhirlpool() {
if (CurrentStage != PreviousStage) {
CleanDisplay();
PreviousStage = CurrentStage;
MessageStage(Whirlpool);
TimeStageStarted = millis() / 1000;
WaitForOkButton = false ;
OkButtonWasPressed = false;
lcd.setCursor(11, 1); lcd.print("/"); lcd.print(5); lcd.print("\'");
lcd.setCursor(12, 2); lcd.print(" ");
TurnMotor(On);
TurnPump(Whirlpool, On);
}
#ifdef TEST
if (((millis() / 1000) - TimeStageStarted) > 60) { // for development purposes
#else
if (((millis() / 1000) - TimeStageStarted) >= WhirlpoolTime * 60) {
#endif
TurnMotor(Off);
TurnPump (Whirlpool, Off);
CurrentStage++;
}
}
void ProcessEnd() {
if (CurrentStage != PreviousStage) {
CleanDisplay();
PreviousStage = CurrentStage;
TimeStageStarted = millis() / 1000;
lcd.setCursor(0, 0); lcd.print (ProgramString); // "Sotile Brewing vn.n
#ifdef ENGLISH
lcd.setCursor(0, 1); lcd.print (F("It took" ));
lcd.setCursor(0, 2); lcd.print (F("to complete brewing" ));
#else
lcd.setCursor(0, 1); lcd.print (F("Foram" ));
lcd.setCursor(0, 2); lcd.print (F("em todo o processo." ));
#endif
Minutes = (int) ((TimeStageStarted) / 60) ;
Seconds = (int) ((TimeStageStarted) - (Minutes * 60));
DisplayATime(8, Minutes, Seconds);
lcd.setCursor(0, 3); lcd.print ("(ESC)" );
WaitForOkButton = false ;
}
}
/*ProcessCancelButton();
This function should allow user to:
- Restart whole proccess
- Restart current Stage
- Restart current step within a stage
This can be done changing values of status variables like
CurrentStage, PreviousStage,
CurrentMashStep, CurrentBoilStep,
*/
//------------------------------------------------------ ESC button Routines -----------------------------
void EscButtonChooseOption() {
int MinOption, MaxOption;
boolean StepHasSubsteps;
//Serial.println("Entrou ESC - choose");
CleanDisplay();
lcd.setCursor( 0, 0);
#ifdef ENGLISH
lcd.print(F("(!) Select:"));
#else
lcd.print(F("(!) Selecione:"));
#endif
switch (CurrentStage) {
case Choose:
case Prepare:
case Lauther:
case Cool:
case Whirlpool:
case End: StepHasSubsteps = false;
MaxOption = 2; break;
case Mash:
case Boil: StepHasSubsteps = true;
MaxOption = 3; break;
}
if (SoundAlarm && !SoundAlarmCanceled)
MinOption = 0; // option 0 = mute the alarm
else MinOption = 1; // will not show the option to mute an alarm that is not on!
Option = MinOption;
LastOption = Option - 1;
//Serial.print("Option: "); Serial.println(Option);
//Serial.print("MinOption: "); Serial.println(MinOption);
//Serial.print("MaxOption: "); Serial.println(MaxOption);
//Serial.print("HasSubStep:"); Serial.println(StepHasSubsteps);
do {
Option = InputOption(MinOption, MaxOption, Option);
if (Option != LastOption) {
switch (Option) {
#ifdef ENGLISH
case 0 : lcd.setCursor( 0, 2); lcd.print(F(" Mute the alarm ")); break;
case 1 : lcd.setCursor( 0, 2); lcd.print(F(" Go to step ... ")); break;
case 2 : if (!StepHasSubsteps) {
Option++;
} else {
lcd.setCursor( 0, 2); lcd.print(F(" Go to substep ... ")); break;
}
case 3 : lcd.setCursor( 0, 2); lcd.print(F(" Exit with no change")); break;
#else
case 0 : lcd.setCursor( 0, 2); lcd.print(F(" Desliga o alarme ")); break;
case 1 : lcd.setCursor( 0, 2); lcd.print(F(" Vai para a etapa...")); break;
case 2 :
case 3 : if (Option == 2 && StepHasSubsteps) {
lcd.setCursor( 0, 2); lcd.print(F(" Vai p/ sub-etapa...")); break;
} else {
lcd.setCursor( 0, 2); lcd.print(F(" Sai sem alteracao "));
lcd.setCursor(15, 2); lcd.print(char(0)); // writes a "ç"
lcd.print(char(1)); break; // writes an "ã"
}
#endif
}
lcd.setCursor(10, 3); lcd.print(" Ok?");
}
LastOption = Option;
Aux = digitalRead (OkButton);
} while (Aux); // OK button pressed goes to false/low/zero
//Serial.print("Option apos:"); Serial.println(Option);
if (!StepHasSubsteps && Option == 2)
Option++; //if no substeps, must set option from 2 to 3 that is "exit with no change"
// return Option;
}
void EscButtonChangeStep() {
CleanDisplay();
if (HeatIsOn) {
TurnMotor(Off);
TurnHeat (Off);
HeatIsOn = false;
}
lcd.setCursor( 0, 0);
#ifdef ENGLISH
lcd.print(F("(!) Select stage:")); // translate here
lcd.setCursor( 0, 1); lcd.print(F("Current:")); MessageStageName(CurrentStage);
lcd.setCursor( 0, 2); lcd.print(F("New : ")); MessageStageName(CurrentStage);
#else
lcd.print(F("(!) Selec. estapa:"));
lcd.setCursor( 0, 1); lcd.print(F("Atual : ")); MessageStageName(CurrentStage);
lcd.setCursor( 0, 2); lcd.print(F("Nova : ")); MessageStageName(CurrentStage);
#endif
lcd.setCursor(10, 3); lcd.print(" Ok?");
Option = CurrentStage;
LastOption = CurrentStage;
delay (100); // kinda debounce ok button.
do {
Option = InputOption(Choose, Whirlpool + 1, Option);
if (Option != LastOption) {
lcd.setCursor(8, 2); lcd.print(F(" ")); // clear previous message
lcd.setCursor(8, 2); MessageStageName(Option);
LastOption = Option;
}
Aux = digitalRead (OkButton);
} while (Aux); // OK button pressed goes to false/low/zero
if (Option == CurrentStage) {
//Serial.println("exit with no change");
Message(18, true); // "Exit with no change!"
delay(1500);
} else { // a different stage was selected
switch (CurrentStage) {
case Lauther : TurnPump(0, Off); break;
case Prepare :
case Mash :
case Boil : TurnHeat (Off);
TurnMotor(Off); break;
case Whirlpool: TurnPump(0, Off);
TurnMotor(Off); break;
}
SoundAlarmCanceled = false;
CurrentStage = Option;
PreviousStage = CurrentStage - 1; //forces the code to restart selected stage
}
}
void EscButtonChangeSubStep() {
CleanDisplay();
lcd.setCursor( 0, 0);
/* Mash and Boil have substeps.
options are get back to the substep, even if it is single
or exit without changes
if mash it will be:
"now: STAGE STEP TIME
"new: STAGE step zero
*** if same step, ask if it's to set reset time or exit without changes
if boil it will be :
"now: hop 1/3 time"
"new: hop n/3 time " -->> rotary encoder hop number - calculate remaining time
*** if same step, ask if it's to set reset time or exit without changes
struct StructSteps {
String Name;
int InitialTemperature;
int NumberOfSteps; <<<<<<<<
int Temperature[4]; // 3 steps plus mashout
int Time[4]; // 3 steps plus mashout
};
struct StructBoil {
int MinutesTotal;
int NumberOfHops; <<<<<<<<
int TimeOfHop[4]; // max of 4 hoppings during boil
};
*/
#ifdef ENGLISH
lcd.print(F("(Esc!) Select step: ")); // translate here
// Max message size, in English: 01234567890123456789 , considering possible stages are Mash and Boil
// lcd.print(F("Now: Boil 59:00 1/4"));
lcd.setCursor( 0, 1); lcd.print(F("Now : ")); MessageStageName(CurrentStage);
lcd.setCursor( 0, 2); lcd.print(F("New : "));
#else
lcd.print(F("(Esc!) Selec. passo:"));
// Max message size, in Portug.: 01234567890123456789 , considering possible stages are Mostura and Fervura
// lcd.print(F("A: Fervura 59:00 1/4"));
lcd.setCursor( 0, 1); lcd.print(F("A: ")); MessageStageName(CurrentStage);
lcd.setCursor( 0, 2); lcd.print(F("N: "));
#endif
lcd.setCursor(17, 1);
//struct StructSteps StepProfile;
//struct StructBoil BoilProfile;
if (CurrentStage == Mash) {
lcd.print(CurrentMashStep); lcd.print("/"); lcd.print(StepProfile.NumberOfSteps);
lcd.setCursor( 0, 3); lcd.print(" <rampa>");
Option = CurrentMashStep;
} else { // CurrentStage == Boil
lcd.print(HopNumber); lcd.print("/"); lcd.print(BoilProfile.NumberOfHops);
lcd.setCursor( 0, 3); lcd.print(" <hop>");
Option = HopNumber;
}
lcd.setCursor(10, 3); lcd.print(" Ok?");
LastOption = Option;
do {
Minutes = (int) ((Now - TimeStageStarted) / 60);
Seconds = (int) ((Now - TimeStageStarted) - (Minutes * 60));
DisplayATime(11, Minutes, Seconds);
Option = InputOption(Choose, Whirlpool + 1, Option);
if (Option != LastOption) {
lcd.setCursor(11, 2); MessageStageName(CurrentStage);
}
LastOption = Option;
Aux = digitalRead (OkButton);
} while (Aux); // OK button pressed goes to false/low/zero
}
void(* resetFunc) (void) = 0; //declare reset function @ address 0
// http://www.instructables.com/id/two-ways-to-reset-arduino-in-software/?ALLSTEPS
void ProcessEscButton() {
/*
This function should allow user to:
- silent alarm buzzer
- go to step
- go to substep
- exit with no change
This can be done changing values of status variables like
CurrentStage, PreviousStage,
CurrentMashStep, CurrentBoilStep,
*/
/* This code forces an arduino reset if ESC is pressed at end of proccess / stage END
if (CurrentStage > Whirlpool) // Process already ended
resetFunc(); //call reset
// else:
*/
NumberLastMessage = AuxNumberLastMessage;
EscButtonChooseOption();
switch (Option) {
case 0 : // mute alarm
SoundAlarmCanceled = true;
digitalWrite(IntermitentBuzzer, LOW);
break;
case 1 : EscButtonChangeStep(); break;
case 2 : EscButtonChangeSubStep(); break;
case 3 : break; // Exit with no change
}
CleanDisplay();
MessageStage(CurrentStage);
//Serial.print("currentstage: "); Serial.println(CurrentStage);
if (CurrentStage == Mash) {
//Serial.print("1currentstage: "); Serial.println(CurrentStage);
lcd.setCursor(15, 0);
if (CurrentMashStep <= StepProfile.NumberOfSteps) {
lcd.print(CurrentMashStep);
} else
lcd.print("M-out");
}
//Serial.print("NumberLastMessage: "); Serial.println(NumberLastMessage);
Message(NumberLastMessage, false);
TimeLastOkInterrupt = millis();
OkButtonWasPressed = false;
}
// ################################ SETUP ########################################
void setup() {
/* Parallel display:
lcd.begin(20, 4); // start the library
*/
//i2c serial display:
lcd.init();
lcd.backlight();
CleanDisplay();
// starting message
lcd.setCursor(0, 0); lcd.print (ProgramString); //"Sotile Homebrewing v.1n"
#ifdef ENGLISH
lcd.setCursor(0, 1); lcd.print (F(" 2017 April "));
lcd.setCursor(0, 2); lcd.print (F(" by: Ioquir Sotile "));
lcd.setCursor(0, 3); lcd.print (F(" ...setting up... ")); // nothing! just to keep name and version on the display
#else
lcd.setCursor(0, 1); lcd.print (F(" Abril de 2017 "));
lcd.setCursor(0, 2); lcd.print (F(" de: Ioquir Sotile "));
lcd.setCursor(0, 3); lcd.print (F(" ...configurando..."));
lcd.createChar(0, Cedilha); // creates "ç" char
lcd.createChar(1, ATil ); // creates "ã" char
lcd.createChar(2, AAgudo ); // creates "á" char
#endif
#ifdef TEST
Serial.begin(9600); // open the serial port at 9600 bps:
#endif
pinMode(Buzzer, OUTPUT);
pinMode(IntermitentBuzzer, OUTPUT);
pinMode(RGBLedBluePin, OUTPUT);
pinMode(RGBLedGreenPin, OUTPUT);
pinMode(RGBLedRedPin, OUTPUT);
//pinMode(OkButton, INPUT );
attachInterrupt(1, debounceOkButtonInterrupt, CHANGE); //OkButton
//pinMode(EscButton, INPUT_PULLUP);
attachInterrupt(0, debounceEscButtonInterrupt, CHANGE); //EscButton
pinMode(Encoder0PinA, INPUT );
pinMode(Encoder0PinB, INPUT );
CurrentStage = Choose;
PreviousStage = CurrentStage;
MashOption = 1;
BoilOption = 0;
WaitForOkButton = false;
OkButtonWasPressed = false;
EscButtonWasPressed = false;
Now = millis();
HalfSecondAgo = Now;
TimeStageStarted = Now;
TimeLastOkInterrupt = Now;
TimeLastEscInterrupt = Now;
RedAlert = false;
YellowAlert = false;
delay(1500);
// one wire temperature sensor setup:
if ( !ONE_WIRE_BUS.search(addr)) {
//Serial.println("No sensor found - No more addresses.");
ONE_WIRE_BUS.reset_search();
delay(250);
return;
}
if (OneWire::crc8(addr, 7) != addr[7]) {
//Serial.println("CRC is not valid!");
return;
}
//Serial.println();
// the first ROM byte indicates which chip
switch (addr[0]) {
case 0x10: //Serial.println(" Chip = DS18S20"); // or old DS1820
type_s = 1; break;
case 0x28: //Serial.println(" Chip = DS18B20");
type_s = 0; break;
case 0x22: //Serial.println(" Chip = DS1822");
type_s = 0; break;
default: //Serial.println("Device is not a DS18x20 family device.");
return;
}
Message(17, true); //" Press (OK) to start "
delay(1000);
do
Aux = digitalRead(OkButton);
while (Aux); // OK button pressed goes to false/low/zero
CleanDisplay();
}
// ################################ LOOP ########################################
void loop() {
/* if (WaitForOkButton){ // see INTERRUPT bellow
Aux = digitalRead(OkButton);
//Serial.print("Aux: "); Serial.print(Aux); Serial.print("-- Ok pressed: "); Serial.println(OkButtonWasPressed);
if (Aux == LOW) {
OkButtonWasPressed = 1;
}
}
*/
if (EscButtonWasPressed) {
if (CurrentStage > Choose)
ProcessEscButton();
EscButtonWasPressed = false;
}
switch (CurrentStage) {
case Choose : ProcessChoose (); break;
//case Prepare : ProcessPrepare (); break;
//case Mash : ProcessMash (); break;
// case Lauther : ProcessLauther (); break;
//case Boil : ProcessBoil (); break;
//case Cool : ProcessCool (); break;
// case Whirlpool : ProcessWhirlpool(); break;
// default : ProcessEnd (); break;
}
ShowAlert ();
if ((CurrentStage <= Whirlpool) && (PreviousStage != Choose)) {
DisplayTimes();
DisplayTemperature();
}
}
// ################################ INTERRUPS ########################################
void debounceOkButtonInterrupt() {
if ((long)(millis() - TimeLastOkInterrupt) >= DebouncingTime * 500) {
OkButtonWasPressed = true;
TimeLastOkInterrupt = millis();
}
}
void debounceEscButtonInterrupt() {
if ((long)(millis() - TimeLastEscInterrupt) >= DebouncingTime * 500) {
EscButtonWasPressed = true; // I tried to call ProcessEscButton() from here, but compilator didn't like the idea, so I had to move it to loop.
TimeLastEscInterrupt = millis();
}
}